Skip to content

Commit 3f00b08

Browse files
committed
Merge branch '3.3.x' into 3.4.x
2 parents 7664bab + 4326817 commit 3f00b08

File tree

2 files changed

+337
-255
lines changed

2 files changed

+337
-255
lines changed

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

+33-255
Original file line numberDiff line numberDiff line change
@@ -18,41 +18,22 @@
1818

1919
import java.io.File;
2020
import java.io.IOException;
21-
import java.net.URLDecoder;
22-
import java.net.URLEncoder;
2321
import java.nio.file.Files;
22+
import java.nio.file.Path;
2423
import java.nio.file.StandardOpenOption;
2524
import java.util.Collections;
2625
import java.util.List;
27-
import java.util.Map;
28-
import java.util.Objects;
2926
import java.util.function.Supplier;
30-
import java.util.stream.Collectors;
27+
import java.util.stream.Stream;
3128

32-
import com.tngtech.archunit.base.DescribedPredicate;
33-
import com.tngtech.archunit.core.domain.JavaAnnotation;
34-
import com.tngtech.archunit.core.domain.JavaCall;
35-
import com.tngtech.archunit.core.domain.JavaClass;
36-
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
3729
import com.tngtech.archunit.core.domain.JavaClasses;
38-
import com.tngtech.archunit.core.domain.JavaMethod;
39-
import com.tngtech.archunit.core.domain.JavaParameter;
40-
import com.tngtech.archunit.core.domain.JavaType;
41-
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
42-
import com.tngtech.archunit.core.domain.properties.HasName;
43-
import com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With;
44-
import com.tngtech.archunit.core.domain.properties.HasParameterTypes;
4530
import com.tngtech.archunit.core.importer.ClassFileImporter;
46-
import com.tngtech.archunit.lang.ArchCondition;
4731
import com.tngtech.archunit.lang.ArchRule;
48-
import com.tngtech.archunit.lang.ConditionEvents;
4932
import com.tngtech.archunit.lang.EvaluationResult;
50-
import com.tngtech.archunit.lang.SimpleConditionEvent;
51-
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
52-
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
5333
import org.gradle.api.DefaultTask;
5434
import org.gradle.api.GradleException;
5535
import org.gradle.api.Task;
36+
import org.gradle.api.Transformer;
5637
import org.gradle.api.file.DirectoryProperty;
5738
import org.gradle.api.file.FileCollection;
5839
import org.gradle.api.file.FileTree;
@@ -69,264 +50,63 @@
6950
import org.gradle.api.tasks.SkipWhenEmpty;
7051
import org.gradle.api.tasks.TaskAction;
7152

72-
import org.springframework.util.ResourceUtils;
73-
7453
/**
7554
* {@link Task} that checks for architecture problems.
7655
*
7756
* @author Andy Wilkinson
7857
* @author Yanming Zhou
7958
* @author Scott Frederick
8059
* @author Ivan Malutin
60+
* @author Phillip Webb
8161
*/
8262
public abstract class ArchitectureCheck extends DefaultTask {
8363

8464
private FileCollection classes;
8565

8666
public ArchitectureCheck() {
8767
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
88-
getProhibitObjectsRequireNonNull().convention(true);
89-
getRules().addAll(allPackagesShouldBeFreeOfTangles(),
90-
allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization(),
91-
allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters(),
92-
noClassesShouldCallStepVerifierStepVerifyComplete(),
93-
noClassesShouldConfigureDefaultStepVerifierTimeout(), noClassesShouldCallCollectorsToList(),
94-
noClassesShouldCallURLEncoderWithStringEncoding(), noClassesShouldCallURLDecoderWithStringEncoding(),
95-
noClassesShouldLoadResourcesUsingResourceUtils(), noClassesShouldCallStringToUpperCaseWithoutLocale(),
96-
noClassesShouldCallStringToLowerCaseWithoutLocale(),
97-
conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType(),
98-
enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType());
99-
getRules().addAll(getProhibitObjectsRequireNonNull()
100-
.map((prohibit) -> prohibit ? noClassesShouldCallObjectsRequireNonNull() : Collections.emptyList()));
101-
getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList()));
68+
getRules().addAll(getProhibitObjectsRequireNonNull().convention(true)
69+
.map(whenTrue(ArchitectureRules::noClassesShouldCallObjectsRequireNonNull)));
70+
getRules().addAll(ArchitectureRules.standard());
71+
getRuleDescriptions().set(getRules().map(this::asDescriptions));
72+
}
73+
74+
private Transformer<List<ArchRule>, Boolean> whenTrue(Supplier<List<ArchRule>> rules) {
75+
return (in) -> (!in) ? Collections.emptyList() : rules.get();
76+
}
77+
78+
private List<String> asDescriptions(List<ArchRule> rules) {
79+
return rules.stream().map(ArchRule::getDescription).toList();
10280
}
10381

10482
@TaskAction
10583
void checkArchitecture() throws IOException {
106-
JavaClasses javaClasses = new ClassFileImporter()
107-
.importPaths(this.classes.getFiles().stream().map(File::toPath).toList());
108-
List<EvaluationResult> violations = getRules().get()
109-
.stream()
110-
.map((rule) -> rule.evaluate(javaClasses))
111-
.filter(EvaluationResult::hasViolation)
112-
.toList();
84+
JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths());
85+
List<EvaluationResult> violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList();
11386
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
114-
outputFile.getParentFile().mkdirs();
87+
writeViolationReport(violations, outputFile);
11588
if (!violations.isEmpty()) {
116-
StringBuilder report = new StringBuilder();
117-
for (EvaluationResult violation : violations) {
118-
report.append(violation.getFailureReport());
119-
report.append(String.format("%n"));
120-
}
121-
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
122-
StandardOpenOption.TRUNCATE_EXISTING);
12389
throw new GradleException("Architecture check failed. See '" + outputFile + "' for details.");
12490
}
125-
else {
126-
outputFile.createNewFile();
127-
}
128-
}
129-
130-
private ArchRule allPackagesShouldBeFreeOfTangles() {
131-
return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles();
132-
}
133-
134-
private ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization() {
135-
return ArchRuleDefinition.methods()
136-
.that()
137-
.areAnnotatedWith("org.springframework.context.annotation.Bean")
138-
.and()
139-
.haveRawReturnType(Predicates.assignableTo("org.springframework.beans.factory.config.BeanPostProcessor"))
140-
.should(onlyHaveParametersThatWillNotCauseEagerInitialization())
141-
.andShould()
142-
.beStatic()
143-
.allowEmptyShould(true);
144-
}
145-
146-
private ArchCondition<JavaMethod> onlyHaveParametersThatWillNotCauseEagerInitialization() {
147-
DescribedPredicate<CanBeAnnotated> notAnnotatedWithLazy = DescribedPredicate
148-
.not(CanBeAnnotated.Predicates.annotatedWith("org.springframework.context.annotation.Lazy"));
149-
DescribedPredicate<JavaClass> notOfASafeType = DescribedPredicate
150-
.not(Predicates.assignableTo("org.springframework.beans.factory.ObjectProvider")
151-
.or(Predicates.assignableTo("org.springframework.context.ApplicationContext"))
152-
.or(Predicates.assignableTo("org.springframework.core.env.Environment")));
153-
return new ArchCondition<>("not have parameters that will cause eager initialization") {
154-
155-
@Override
156-
public void check(JavaMethod item, ConditionEvents events) {
157-
item.getParameters()
158-
.stream()
159-
.filter(notAnnotatedWithLazy)
160-
.filter((parameter) -> notOfASafeType.test(parameter.getRawType()))
161-
.forEach((parameter) -> events.add(SimpleConditionEvent.violated(parameter,
162-
parameter.getDescription() + " will cause eager initialization as it is "
163-
+ notAnnotatedWithLazy.getDescription() + " and is "
164-
+ notOfASafeType.getDescription())));
165-
}
166-
167-
};
168-
}
169-
170-
private ArchRule allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters() {
171-
return ArchRuleDefinition.methods()
172-
.that()
173-
.areAnnotatedWith("org.springframework.context.annotation.Bean")
174-
.and()
175-
.haveRawReturnType(
176-
Predicates.assignableTo("org.springframework.beans.factory.config.BeanFactoryPostProcessor"))
177-
.should(onlyInjectEnvironment())
178-
.andShould()
179-
.beStatic()
180-
.allowEmptyShould(true);
181-
}
182-
183-
private ArchCondition<JavaMethod> onlyInjectEnvironment() {
184-
return new ArchCondition<>("only inject Environment") {
185-
186-
@Override
187-
public void check(JavaMethod item, ConditionEvents events) {
188-
List<JavaParameter> parameters = item.getParameters();
189-
for (JavaParameter parameter : parameters) {
190-
if (!"org.springframework.core.env.Environment".equals(parameter.getType().getName())) {
191-
events.add(SimpleConditionEvent.violated(item,
192-
item.getDescription() + " should only inject Environment"));
193-
}
194-
}
195-
}
196-
197-
};
19891
}
19992

200-
private ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() {
201-
return ArchRuleDefinition.noClasses()
202-
.should()
203-
.callMethod(String.class, "toLowerCase")
204-
.because("String.toLowerCase(Locale.ROOT) should be used instead");
93+
private List<Path> classFilesPaths() {
94+
return this.classes.getFiles().stream().map(File::toPath).toList();
20595
}
20696

207-
private ArchRule noClassesShouldCallStringToUpperCaseWithoutLocale() {
208-
return ArchRuleDefinition.noClasses()
209-
.should()
210-
.callMethod(String.class, "toUpperCase")
211-
.because("String.toUpperCase(Locale.ROOT) should be used instead");
97+
private Stream<EvaluationResult> evaluate(JavaClasses javaClasses) {
98+
return getRules().get().stream().map((rule) -> rule.evaluate(javaClasses));
21299
}
213100

214-
private ArchRule noClassesShouldCallStepVerifierStepVerifyComplete() {
215-
return ArchRuleDefinition.noClasses()
216-
.should()
217-
.callMethod("reactor.test.StepVerifier$Step", "verifyComplete")
218-
.because("it can block indefinitely and expectComplete().verify(Duration) should be used instead");
219-
}
220-
221-
private ArchRule noClassesShouldConfigureDefaultStepVerifierTimeout() {
222-
return ArchRuleDefinition.noClasses()
223-
.should()
224-
.callMethod("reactor.test.StepVerifier", "setDefaultTimeout", "java.time.Duration")
225-
.because("expectComplete().verify(Duration) should be used instead");
226-
}
227-
228-
private ArchRule noClassesShouldCallCollectorsToList() {
229-
return ArchRuleDefinition.noClasses()
230-
.should()
231-
.callMethod(Collectors.class, "toList")
232-
.because("java.util.stream.Stream.toList() should be used instead");
233-
}
234-
235-
private ArchRule noClassesShouldCallURLEncoderWithStringEncoding() {
236-
return ArchRuleDefinition.noClasses()
237-
.should()
238-
.callMethod(URLEncoder.class, "encode", String.class, String.class)
239-
.because("java.net.URLEncoder.encode(String s, Charset charset) should be used instead");
240-
}
241-
242-
private ArchRule noClassesShouldCallURLDecoderWithStringEncoding() {
243-
return ArchRuleDefinition.noClasses()
244-
.should()
245-
.callMethod(URLDecoder.class, "decode", String.class, String.class)
246-
.because("java.net.URLDecoder.decode(String s, Charset charset) should be used instead");
247-
}
248-
249-
private ArchRule noClassesShouldLoadResourcesUsingResourceUtils() {
250-
return ArchRuleDefinition.noClasses()
251-
.should()
252-
.callMethodWhere(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class)))
253-
.and(JavaCall.Predicates.target(HasName.Predicates.name("getURL")))
254-
.and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class)))
255-
.or(JavaCall.Predicates.target(With.owner(Predicates.type(ResourceUtils.class)))
256-
.and(JavaCall.Predicates.target(HasName.Predicates.name("getFile")))
257-
.and(JavaCall.Predicates.target(HasParameterTypes.Predicates.rawParameterTypes(String.class)))))
258-
.because("org.springframework.boot.io.ApplicationResourceLoader should be used instead");
259-
}
260-
261-
private List<ArchRule> noClassesShouldCallObjectsRequireNonNull() {
262-
return List.of(
263-
ArchRuleDefinition.noClasses()
264-
.should()
265-
.callMethod(Objects.class, "requireNonNull", Object.class, String.class)
266-
.because("org.springframework.utils.Assert.notNull(Object, String) should be used instead"),
267-
ArchRuleDefinition.noClasses()
268-
.should()
269-
.callMethod(Objects.class, "requireNonNull", Object.class, Supplier.class)
270-
.because("org.springframework.utils.Assert.notNull(Object, Supplier) should be used instead"));
271-
}
272-
273-
private ArchRule conditionalOnMissingBeanShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodReturnType() {
274-
return ArchRuleDefinition.methods()
275-
.that()
276-
.areAnnotatedWith("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean")
277-
.should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType())
278-
.allowEmptyShould(true);
279-
}
280-
281-
private ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodReturnType() {
282-
return new ArchCondition<>("not specify only a type that is the same as the method's return type") {
283-
284-
@Override
285-
public void check(JavaMethod item, ConditionEvents events) {
286-
JavaAnnotation<JavaMethod> conditional = item
287-
.getAnnotationOfType("org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean");
288-
Map<String, Object> properties = conditional.getProperties();
289-
if (!properties.containsKey("type") && !properties.containsKey("name")) {
290-
conditional.get("value").ifPresent((value) -> {
291-
JavaType[] types = (JavaType[]) value;
292-
if (types.length == 1 && item.getReturnType().equals(types[0])) {
293-
events.add(SimpleConditionEvent.violated(item, conditional.getDescription()
294-
+ " should not specify only a value that is the same as the method's return type"));
295-
}
296-
});
297-
}
298-
}
299-
300-
};
301-
}
302-
303-
private ArchRule enumSourceShouldNotSpecifyOnlyATypeThatIsTheSameAsMethodParameterType() {
304-
return ArchRuleDefinition.methods()
305-
.that()
306-
.areAnnotatedWith("org.junit.jupiter.params.provider.EnumSource")
307-
.should(notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType())
308-
.allowEmptyShould(true);
309-
}
310-
311-
private ArchCondition<? super JavaMethod> notSpecifyOnlyATypeThatIsTheSameAsTheMethodParameterType() {
312-
return new ArchCondition<>("not specify only a type that is the same as the method's parameter type") {
313-
314-
@Override
315-
public void check(JavaMethod item, ConditionEvents events) {
316-
JavaAnnotation<JavaMethod> conditional = item
317-
.getAnnotationOfType("org.junit.jupiter.params.provider.EnumSource");
318-
Map<String, Object> properties = conditional.getProperties();
319-
if (properties.size() == 1 && item.getParameterTypes().size() == 1) {
320-
conditional.get("value").ifPresent((value) -> {
321-
if (value.equals(item.getParameterTypes().get(0))) {
322-
events.add(SimpleConditionEvent.violated(item, conditional.getDescription()
323-
+ " should not specify only a value that is the same as the method's parameter type"));
324-
}
325-
});
326-
}
327-
}
328-
329-
};
101+
private void writeViolationReport(List<EvaluationResult> violations, File outputFile) throws IOException {
102+
outputFile.getParentFile().mkdirs();
103+
StringBuilder report = new StringBuilder();
104+
for (EvaluationResult violation : violations) {
105+
report.append(violation.getFailureReport());
106+
report.append(String.format("%n"));
107+
}
108+
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
109+
StandardOpenOption.TRUNCATE_EXISTING);
330110
}
331111

332112
public void setClasses(FileCollection classes) {
@@ -360,9 +140,7 @@ final FileTree getInputClasses() {
360140
@Internal
361141
public abstract Property<Boolean> getProhibitObjectsRequireNonNull();
362142

363-
@Input
364-
// The rules themselves can't be an input as they aren't serializable so we use
365-
// their descriptions instead
143+
@Input // Use descriptions as input since rules aren't serializable
366144
abstract ListProperty<String> getRuleDescriptions();
367145

368146
}

0 commit comments

Comments
 (0)