Skip to content

Commit 654e924

Browse files
committed
Add ArchRule that checks that the type of exposed bean is not private
Signed-off-by: Dmytro Nosan <[email protected]>
1 parent a5d8eec commit 654e924

File tree

6 files changed

+142
-6
lines changed

6 files changed

+142
-6
lines changed

buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheck.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.gradle.api.file.FileTree;
4444
import org.gradle.api.provider.ListProperty;
4545
import org.gradle.api.provider.Property;
46+
import org.gradle.api.provider.Provider;
4647
import org.gradle.api.tasks.Classpath;
4748
import org.gradle.api.tasks.IgnoreEmptyDirectories;
4849
import org.gradle.api.tasks.Input;
@@ -53,6 +54,7 @@
5354
import org.gradle.api.tasks.PathSensitive;
5455
import org.gradle.api.tasks.PathSensitivity;
5556
import org.gradle.api.tasks.SkipWhenEmpty;
57+
import org.gradle.api.tasks.SourceSet;
5658
import org.gradle.api.tasks.TaskAction;
5759
import org.gradle.api.tasks.VerificationException;
5860

@@ -75,9 +77,17 @@ public ArchitectureCheck() {
7577
getRules().addAll(getProhibitObjectsRequireNonNull().convention(true)
7678
.map(whenTrue(ArchitectureRules::noClassesShouldCallObjectsRequireNonNull)));
7779
getRules().addAll(ArchitectureRules.standard());
80+
getRules().addAll(whenMainSources(
81+
() -> Collections.singletonList(ArchitectureRules.allBeanMethodsShouldReturnNonPrivateType())));
7882
getRuleDescriptions().set(getRules().map(this::asDescriptions));
7983
}
8084

85+
private Provider<List<ArchRule>> whenMainSources(Supplier<List<ArchRule>> rules) {
86+
return getSourceSet().convention(SourceSet.MAIN_SOURCE_SET_NAME)
87+
.map(SourceSet.MAIN_SOURCE_SET_NAME::equals)
88+
.map(whenTrue(rules));
89+
}
90+
8191
private Transformer<List<ArchRule>, Boolean> whenTrue(Supplier<List<ArchRule>> rules) {
8292
return (in) -> (!in) ? Collections.emptyList() : rules.get();
8393
}
@@ -170,6 +180,9 @@ final FileTree getInputClasses() {
170180
@Internal
171181
public abstract Property<Boolean> getProhibitObjectsRequireNonNull();
172182

183+
@Internal
184+
abstract Property<String> getSourceSet();
185+
173186
@Input // Use descriptions as input since rules aren't serializable
174187
abstract ListProperty<String> getRuleDescriptions();
175188

buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitecturePlugin.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ private void registerTasks(Project project) {
4949
TaskProvider<ArchitectureCheck> checkPackageTangles = project.getTasks()
5050
.register("checkArchitecture" + StringUtils.capitalize(sourceSet.getName()), ArchitectureCheck.class,
5151
(task) -> {
52+
task.getSourceSet().set(sourceSet.getName());
5253
task.getCompileClasspath().from(sourceSet.getCompileClasspath());
5354
task.setClasses(sourceSet.getOutput().getClassesDirs());
5455
task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir());

buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
import com.tngtech.archunit.core.domain.JavaClass;
3434
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
3535
import com.tngtech.archunit.core.domain.JavaMethod;
36+
import com.tngtech.archunit.core.domain.JavaModifier;
3637
import com.tngtech.archunit.core.domain.JavaParameter;
3738
import com.tngtech.archunit.core.domain.JavaType;
3839
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
40+
import com.tngtech.archunit.core.domain.properties.HasModifiers;
3941
import com.tngtech.archunit.core.domain.properties.HasName;
4042
import com.tngtech.archunit.core.domain.properties.HasOwner;
4143
import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With;
@@ -93,6 +95,13 @@ static List<ArchRule> standard() {
9395
return List.copyOf(rules);
9496
}
9597

98+
static ArchRule allBeanMethodsShouldReturnNonPrivateType() {
99+
return methodsThatAreAnnotatedWith("org.springframework.context.annotation.Bean").should()
100+
.haveRawReturnType(DescribedPredicate.not(HasModifiers.Predicates.modifier(JavaModifier.PRIVATE)))
101+
.as("@Bean methods must not return types declared with the private modifier, as such types are incompatible with Spring AOT processing")
102+
.allowEmptyShould(true);
103+
}
104+
96105
private static ArchRule allPackagesShouldBeFreeOfTangles() {
97106
return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles();
98107
}

buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.boot.build.architecture;
1818

19+
import java.io.FileNotFoundException;
1920
import java.io.IOException;
21+
import java.nio.charset.StandardCharsets;
2022
import java.nio.file.Files;
2123
import java.nio.file.Path;
2224
import java.util.function.Consumer;
@@ -160,6 +162,17 @@ void whenClassCallsStringToUpperCaseWithLocaleShouldNotFail() throws IOException
160162
runGradleWithCompiledClasses("string/toUpperCaseWithLocale", shouldHaveEmptyFailureReport());
161163
}
162164

165+
@Test
166+
void whenBeanMethodExposePrivateTypeShouldFailAndWriteReport() throws IOException {
167+
runGradleWithCompiledClasses("beans/privatebean", shouldHaveFailureReportWithMessage(
168+
"@Bean methods must not return types declared with the private modifier, as such types are incompatible with Spring AOT processing"));
169+
}
170+
171+
@Test
172+
void whenBeanMethodExposeNonPrivateTypeeShouldNotFail() throws IOException {
173+
runGradleWithCompiledClasses("beans/regular", shouldHaveEmptyFailureReport());
174+
}
175+
163176
@Test
164177
void whenBeanPostProcessorBeanMethodIsNotStaticWithExternalClass() throws IOException {
165178
Files.writeString(this.buildFile, """
@@ -196,17 +209,22 @@ IntegrationMBeanExporter integrationMBeanExporter() {
196209

197210
private Consumer<GradleRunner> shouldHaveEmptyFailureReport() {
198211
return (gradleRunner) -> {
199-
assertThat(gradleRunner.build().getOutput()).contains("BUILD SUCCESSFUL")
200-
.contains("Task :checkArchitectureMain");
201-
assertThat(failureReport()).isEmptyFile();
212+
try {
213+
assertThat(gradleRunner.build().getOutput()).contains("BUILD SUCCESSFUL")
214+
.contains("Task :checkArchitectureMain");
215+
assertThat(failureReport()).isEmpty();
216+
}
217+
catch (Exception ex) {
218+
throw new AssertionError("Expected build to succeed but it failed\n" + failureReport(), ex);
219+
}
202220
};
203221
}
204222

205223
private Consumer<GradleRunner> shouldHaveFailureReportWithMessage(String message) {
206224
return (gradleRunner) -> {
207225
assertThat(gradleRunner.buildAndFail().getOutput()).contains("BUILD FAILED")
208226
.contains("Task :checkArchitectureMain FAILED");
209-
assertThat(failureReport()).content().contains(message);
227+
assertThat(failureReport()).contains(message);
210228
};
211229
}
212230

@@ -235,8 +253,17 @@ private void runGradle(Consumer<GradleRunner> callback) {
235253
.withPluginClasspath());
236254
}
237255

238-
private Path failureReport() {
239-
return this.projectDir.resolve("build/checkArchitectureMain/failure-report.txt");
256+
private String failureReport() {
257+
try {
258+
Path failureReport = this.projectDir.resolve("build/checkArchitectureMain/failure-report.txt");
259+
return Files.readString(failureReport, StandardCharsets.UTF_8);
260+
}
261+
catch (FileNotFoundException ex) {
262+
return "Failure report does not exist";
263+
}
264+
catch (IOException ex) {
265+
return "Failure report could not be read: " + ex.getMessage();
266+
}
240267
}
241268

242269
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.build.architecture.beans.privatebean;
18+
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
21+
22+
@Configuration(proxyBeanMethods = false)
23+
class PrivateBean {
24+
25+
@Bean
26+
static MyBean myBean() {
27+
return new MyBean();
28+
}
29+
30+
private static final class MyBean {
31+
32+
}
33+
34+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.build.architecture.beans.regular;
18+
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
21+
22+
@Configuration(proxyBeanMethods = false)
23+
class RegularBean {
24+
25+
@Bean
26+
static PackagePrivate packagePrivateBean() {
27+
return new PackagePrivate();
28+
}
29+
30+
@Bean
31+
static Protected protectedBean() {
32+
return new Protected();
33+
}
34+
35+
@Bean
36+
static Public publicBean() {
37+
return new Public();
38+
}
39+
40+
static final class PackagePrivate {
41+
42+
}
43+
44+
protected static final class Protected {
45+
46+
}
47+
48+
public static final class Public {
49+
50+
}
51+
52+
}

0 commit comments

Comments
 (0)