Skip to content

Commit df4d82d

Browse files
committed
GH-1098 - Optimize type selection in Classes.
We now resort to a simple iteration over the types within a Classes arrangement to detect all classes residing in certain packages. This is primarily used during the JavaPackage data structure construction as it's called for every sub-package of a package originally created from a Classes instance. The new simplified algorithm avoids set up of DescribedPredicate instances to eventually only perform simple package name checks.
1 parent 083ce7b commit df4d82d

File tree

3 files changed

+97
-12
lines changed

3 files changed

+97
-12
lines changed

spring-modulith-core/src/main/java/org/springframework/modulith/core/Classes.java

+29-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import com.tngtech.archunit.core.domain.JavaClass;
3939
import com.tngtech.archunit.core.domain.JavaClasses;
4040
import com.tngtech.archunit.core.domain.JavaModifier;
41-
import com.tngtech.archunit.core.domain.JavaType;
4241
import com.tngtech.archunit.core.domain.properties.HasName;
4342

4443
/**
@@ -87,7 +86,8 @@ static Classes of(List<JavaClass> classes) {
8786
}
8887

8988
/**
90-
* Returns a {@link Collector} creating a {@link Classes} instance from a {@link Stream} of {@link JavaType}.
89+
* Returns a {@link Collector} creating a {@link Classes} instance from a {@link Stream} of
90+
* {@link com.tngtech.archunit.core.domain.JavaType}.
9191
*
9292
* @return will never be {@literal null}.
9393
*/
@@ -110,6 +110,26 @@ Classes that(DescribedPredicate<? super JavaClass> predicate) {
110110
.collect(Collectors.collectingAndThen(Collectors.toList(), Classes::new));
111111
}
112112

113+
/**
114+
* Returns all classes that reside the given {@link PackageName}.
115+
*
116+
* @param name must not be {@literal null}.
117+
* @param nested whether to include nested packages
118+
* @return will never be {@literal null}.
119+
*/
120+
Classes thatResideIn(PackageName name, boolean nested) {
121+
122+
var result = new ArrayList<JavaClass>();
123+
124+
for (JavaClass candidate : classes) {
125+
if (residesIn(name, candidate, nested)) {
126+
result.add(candidate);
127+
}
128+
}
129+
130+
return new Classes(result);
131+
}
132+
113133
Classes and(Classes classes) {
114134
return and(classes.classes);
115135
}
@@ -268,6 +288,13 @@ private static String format(JavaClass type) {
268288
return format(type, "");
269289
}
270290

291+
private static boolean residesIn(PackageName reference, JavaClass type, boolean inNested) {
292+
293+
var typesPackage = PackageName.ofType(type.getFullName());
294+
295+
return inNested ? reference.contains(typesPackage) : reference.equals(typesPackage);
296+
}
297+
271298
private static class SameClass extends DescribedPredicate<JavaClass> {
272299

273300
private final JavaClass reference;

spring-modulith-core/src/main/java/org/springframework/modulith/core/JavaPackage.java

+17-10
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import java.util.TreeMap;
3232
import java.util.TreeSet;
3333
import java.util.function.BiPredicate;
34-
import java.util.function.Predicate;
3534
import java.util.function.Supplier;
3635
import java.util.stream.Collectors;
3736
import java.util.stream.Stream;
@@ -73,7 +72,7 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
7372
*/
7473
private JavaPackage(Classes classes, PackageName name, boolean includeSubPackages) {
7574

76-
this(classes.that(resideInAPackage(name.asFilter(includeSubPackages))), name, includeSubPackages
75+
this(classes.thatResideIn(name, includeSubPackages), name, includeSubPackages
7776
? SingletonSupplier.of(() -> detectSubPackages(classes, name))
7877
: SingletonSupplier.of(JavaPackages.NONE));
7978
}
@@ -92,7 +91,7 @@ private JavaPackage(Classes classes, PackageName name, Supplier<JavaPackages> su
9291
Assert.notNull(name, "PackageName must not be null!");
9392
Assert.notNull(subpackages, "Sub-packages must not be null!");
9493

95-
this.classes = classes.that(resideInAPackage(name.asFilter(true)));
94+
this.classes = classes.thatResideIn(name, true);
9695
this.name = name;
9796
this.subPackages = subpackages;
9897
this.directSubPackages = SingletonSupplier.of(() -> subPackages.get().stream()
@@ -206,15 +205,12 @@ public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotatio
206205

207206
Assert.notNull(annotation, "Annotation must not be null!");
208207

209-
return classes.that(ARE_PACKAGE_INFOS.and(are(metaAnnotatedWith(annotation)))).stream() //
210-
.map(JavaClass::getPackageName) //
211-
.filter(Predicate.not(name::hasName))
212-
.distinct() //
213-
.map(it -> of(classes, it));
208+
return getSubPackages().stream()
209+
.filter(it -> it.hasAnnotation(annotation));
214210
}
215211

216212
/**
217-
* Returns all sub-packages that match the given {@link BiPredicate} for the canidate package and its trailing name
213+
* Returns all sub-packages that match the given {@link BiPredicate} for the candidate package and its trailing name
218214
* relative to the current one.
219215
*
220216
* @param filter must not be {@literal null}.
@@ -388,7 +384,7 @@ Optional<JavaPackage> getSubPackage(String localName) {
388384

389385
/**
390386
* Finds the annotation of the given type declared on the package itself or any type located the direct package's
391-
* types .
387+
* types.
392388
*
393389
* @param <A> the type of the annotation.
394390
* @param annotationType must not be {@literal null}.
@@ -502,6 +498,17 @@ public int hashCode() {
502498
return Objects.hash(classes, directSubPackages.get(), name);
503499
}
504500

501+
/**
502+
* Returns whether the current {@link JavaPackage}
503+
*
504+
* @param <A>
505+
* @param annotationType
506+
* @return
507+
*/
508+
private <A extends Annotation> boolean hasAnnotation(Class<A> annotationType) {
509+
return findAnnotation(annotationType).isPresent();
510+
}
511+
505512
static Comparator<JavaPackage> reverse() {
506513
return (left, right) -> -left.compareTo(right);
507514
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 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+
package org.springframework.modulith.core;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import example.Example;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import com.tngtech.archunit.core.domain.JavaClass;
25+
26+
/**
27+
* Unit tests for {@link Classes}.
28+
*
29+
* @author Oliver Drotbohm
30+
*/
31+
class ClassesUnitTests {
32+
33+
@Test // GH-1098
34+
void filtersClassesByPackageName() {
35+
36+
var classes = TestUtils.getClasses(Example.class);
37+
var nestedDirectly = classes.thatResideIn(PackageName.of("example.ni.nested"), false);
38+
39+
assertThat(nestedDirectly)
40+
.extracting(JavaClass::getSimpleName)
41+
.contains("InNested")
42+
.doesNotContain("InNestedA");
43+
44+
var nestedRecursive = classes.thatResideIn(PackageName.of("example.ni.nested"), true);
45+
46+
assertThat(nestedRecursive)
47+
.extracting(JavaClass::getSimpleName)
48+
.contains("InNested", "InNestedA")
49+
.doesNotContain("ApiType");
50+
}
51+
}

0 commit comments

Comments
 (0)