Skip to content

Commit 09faa83

Browse files
committed
Use CompileClasspathClassResolver for ArchitectureCheck Gradle Task
Prior to this commit, certain rules, such as `BeanPostProcessor`, did not work with external classes. This commit ensures that `ArchRules` are executed using the `CompileClasspathClassResolver`, which resolves any missing classes. This helps build a Class Graph for external classes. Signed-off-by: Dmytro Nosan <[email protected]>
1 parent 7f02013 commit 09faa83

File tree

8 files changed

+276
-149
lines changed

8 files changed

+276
-149
lines changed

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

+34-16
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,21 @@
2626
import java.util.function.Supplier;
2727
import java.util.stream.Stream;
2828

29+
import com.tngtech.archunit.ArchConfiguration;
2930
import com.tngtech.archunit.core.domain.JavaClasses;
3031
import com.tngtech.archunit.core.importer.ClassFileImporter;
3132
import com.tngtech.archunit.lang.ArchRule;
3233
import com.tngtech.archunit.lang.EvaluationResult;
3334
import org.gradle.api.DefaultTask;
3435
import org.gradle.api.Task;
3536
import org.gradle.api.Transformer;
37+
import org.gradle.api.file.ConfigurableFileCollection;
3638
import org.gradle.api.file.DirectoryProperty;
3739
import org.gradle.api.file.FileCollection;
3840
import org.gradle.api.file.FileTree;
3941
import org.gradle.api.provider.ListProperty;
4042
import org.gradle.api.provider.Property;
43+
import org.gradle.api.tasks.Classpath;
4144
import org.gradle.api.tasks.IgnoreEmptyDirectories;
4245
import org.gradle.api.tasks.Input;
4346
import org.gradle.api.tasks.InputFiles;
@@ -58,6 +61,7 @@
5861
* @author Scott Frederick
5962
* @author Ivan Malutin
6063
* @author Phillip Webb
64+
* @author Dmytro Nosan
6165
*/
6266
public abstract class ArchitectureCheck extends DefaultTask {
6367

@@ -80,14 +84,18 @@ private List<String> asDescriptions(List<ArchRule> rules) {
8084
}
8185

8286
@TaskAction
83-
void checkArchitecture() throws IOException {
84-
JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths());
85-
List<EvaluationResult> violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList();
86-
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
87-
writeViolationReport(violations, outputFile);
88-
if (!violations.isEmpty()) {
89-
throw new VerificationException("Architecture check failed. See '" + outputFile + "' for details.");
90-
}
87+
void checkArchitecture() {
88+
ArchConfiguration.withThreadLocalScope((configuration) -> {
89+
configuration.setClassResolver(CompileClasspathClassResolver.class);
90+
configuration.setProperty(CompileClasspathClassResolver.PROPERTY_NAME, getCompileClasspath().getAsPath());
91+
JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths());
92+
List<EvaluationResult> violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList();
93+
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
94+
writeViolationReport(violations, outputFile);
95+
if (!violations.isEmpty()) {
96+
throw new VerificationException("Architecture check failed. See '" + outputFile + "' for details.");
97+
}
98+
});
9199
}
92100

93101
private List<Path> classFilesPaths() {
@@ -98,15 +106,21 @@ private Stream<EvaluationResult> evaluate(JavaClasses javaClasses) {
98106
return getRules().get().stream().map((rule) -> rule.evaluate(javaClasses));
99107
}
100108

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"));
109+
private void writeViolationReport(List<EvaluationResult> violations, File outputFile) {
110+
try {
111+
Files.createDirectories(outputFile.getParentFile().toPath());
112+
StringBuilder report = new StringBuilder();
113+
for (EvaluationResult violation : violations) {
114+
report.append(violation.getFailureReport());
115+
report.append(String.format("%n"));
116+
}
117+
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
118+
StandardOpenOption.TRUNCATE_EXISTING);
119+
}
120+
catch (IOException ex) {
121+
throw new VerificationException(
122+
"Failed to write violation report to '" + outputFile + "' " + ex.getMessage());
107123
}
108-
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
109-
StandardOpenOption.TRUNCATE_EXISTING);
110124
}
111125

112126
public void setClasses(FileCollection classes) {
@@ -126,6 +140,10 @@ final FileTree getInputClasses() {
126140
return this.classes.getAsFileTree();
127141
}
128142

143+
@InputFiles
144+
@Classpath
145+
public abstract ConfigurableFileCollection getCompileClasspath();
146+
129147
@Optional
130148
@InputFiles
131149
@PathSensitive(PathSensitivity.RELATIVE)

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -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.getCompileClasspath().from(sourceSet.getCompileClasspath());
5253
task.setClasses(sourceSet.getOutput().getClassesDirs());
5354
task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir());
5455
task.dependsOn(sourceSet.getProcessResourcesTaskName());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2012-2025 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;
18+
19+
import java.io.File;
20+
import java.net.MalformedURLException;
21+
import java.net.URISyntaxException;
22+
import java.net.URL;
23+
import java.net.URLClassLoader;
24+
import java.util.Arrays;
25+
import java.util.Optional;
26+
27+
import com.tngtech.archunit.ArchConfiguration;
28+
import com.tngtech.archunit.base.ArchUnitException;
29+
import com.tngtech.archunit.core.domain.JavaClass;
30+
import com.tngtech.archunit.core.importer.resolvers.ClassResolver;
31+
32+
import org.springframework.util.Assert;
33+
import org.springframework.util.StringUtils;
34+
35+
/**
36+
* A {@link ClassResolver} that resolves Java classes from a provided compile classpath.
37+
*
38+
* @author Dmytro Nosan
39+
*/
40+
class CompileClasspathClassResolver implements ClassResolver {
41+
42+
static final String PROPERTY_NAME = CompileClasspathClassResolver.class.getName();
43+
44+
private ClassUriImporter classUriImporter;
45+
46+
private final URLClassLoader classLoader;
47+
48+
CompileClasspathClassResolver() {
49+
this.classLoader = new URLClassLoader(getUrls(), getClass().getClassLoader());
50+
}
51+
52+
@Override
53+
public void setClassUriImporter(ClassUriImporter classUriImporter) {
54+
this.classUriImporter = classUriImporter;
55+
}
56+
57+
@Override
58+
public Optional<JavaClass> tryResolve(String typeName) {
59+
String fileName = typeName.replace(".", "/") + ".class";
60+
URL url = this.classLoader.getResource(fileName);
61+
if (url == null) {
62+
return Optional.empty();
63+
}
64+
try {
65+
return this.classUriImporter.tryImport(url.toURI());
66+
}
67+
catch (URISyntaxException ex) {
68+
throw new ArchUnitException.LocationException(ex);
69+
}
70+
}
71+
72+
private static URL[] getUrls() {
73+
ArchConfiguration configuration = ArchConfiguration.get();
74+
String classpath = configuration.getProperty(PROPERTY_NAME);
75+
Assert.state(classpath != null, () -> PROPERTY_NAME + " property is not set");
76+
return Arrays.stream(StringUtils.tokenizeToStringArray(classpath, File.pathSeparator))
77+
.map(File::new)
78+
.map(CompileClasspathClassResolver::toURL)
79+
.toArray(URL[]::new);
80+
}
81+
82+
private static URL toURL(File file) {
83+
try {
84+
return file.toURI().toURL();
85+
}
86+
catch (MalformedURLException ex) {
87+
throw new ArchUnitException.LocationException(ex);
88+
}
89+
}
90+
91+
}

0 commit comments

Comments
 (0)