Skip to content

Commit cf87b25

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 51bf592 commit cf87b25

File tree

8 files changed

+262
-149
lines changed

8 files changed

+262
-149
lines changed

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

Lines changed: 35 additions & 16 deletions
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,19 @@ 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((archConfiguration) -> {
89+
archConfiguration.setClassResolver(CompileClasspathClassResolver.class);
90+
archConfiguration.setClassResolverArguments(
91+
getCompileClasspath().getFiles().stream().map(File::toString).toArray(String[]::new));
92+
JavaClasses javaClasses = new ClassFileImporter().importPaths(classFilesPaths());
93+
List<EvaluationResult> violations = evaluate(javaClasses).filter(EvaluationResult::hasViolation).toList();
94+
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
95+
writeViolationReport(violations, outputFile);
96+
if (!violations.isEmpty()) {
97+
throw new VerificationException("Architecture check failed. See '" + outputFile + "' for details.");
98+
}
99+
});
91100
}
92101

93102
private List<Path> classFilesPaths() {
@@ -98,15 +107,21 @@ private Stream<EvaluationResult> evaluate(JavaClasses javaClasses) {
98107
return getRules().get().stream().map((rule) -> rule.evaluate(javaClasses));
99108
}
100109

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

112127
public void setClasses(FileCollection classes) {
@@ -126,6 +141,10 @@ final FileTree getInputClasses() {
126141
return this.classes.getAsFileTree();
127142
}
128143

144+
@InputFiles
145+
@Classpath
146+
public abstract ConfigurableFileCollection getCompileClasspath();
147+
129148
@Optional
130149
@InputFiles
131150
@PathSensitive(PathSensitivity.RELATIVE)

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

Lines changed: 2 additions & 1 deletion
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());
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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.List;
25+
import java.util.Optional;
26+
27+
import com.tngtech.archunit.base.ArchUnitException;
28+
import com.tngtech.archunit.core.domain.JavaClass;
29+
import com.tngtech.archunit.core.importer.resolvers.ClassResolver;
30+
31+
/**
32+
* A {@link ClassResolver} that resolves Java classes from a provided compile classpath.
33+
*
34+
* @author Dmytro Nosan
35+
*/
36+
public class CompileClasspathClassResolver implements ClassResolver {
37+
38+
private ClassUriImporter classUriImporter;
39+
40+
private final URLClassLoader classLoader;
41+
42+
public CompileClasspathClassResolver(List<String> classpath) {
43+
this.classLoader = new URLClassLoader(
44+
classpath.stream().map(File::new).map(CompileClasspathClassResolver::toURL).toArray(URL[]::new));
45+
}
46+
47+
@Override
48+
public void setClassUriImporter(ClassUriImporter classUriImporter) {
49+
this.classUriImporter = classUriImporter;
50+
}
51+
52+
@Override
53+
public Optional<JavaClass> tryResolve(String typeName) {
54+
String typeFile = typeName.replace(".", "/") + ".class";
55+
URL resource = this.classLoader.getResource(typeFile);
56+
if (resource == null) {
57+
return Optional.empty();
58+
}
59+
try {
60+
return this.classUriImporter.tryImport(resource.toURI());
61+
}
62+
catch (URISyntaxException ex) {
63+
throw new ArchUnitException.LocationException(ex);
64+
}
65+
}
66+
67+
private static URL toURL(File file) {
68+
try {
69+
return file.toURI().toURL();
70+
}
71+
catch (MalformedURLException ex) {
72+
throw new ArchUnitException.LocationException(ex);
73+
}
74+
}
75+
76+
}

0 commit comments

Comments
 (0)