Skip to content

Commit 06e1e3f

Browse files
committed
GH-1100 - Fix performance regression in JavaPackage.
The fix for GH-1041 introduced a performance regression as the calculation of the sub-packages escaped the SingletonSupplier and thus is now included in every hashCode() calculation. Took the chance to significantly revamp the sub-package calculation for nested packages. In other words, the entire sub-package arrangement for a package is calculated once with pre-computed intermediaries held instead of re-computing them per sub-package.
1 parent 7dbe64a commit 06e1e3f

File tree

1 file changed

+70
-38
lines changed
  • spring-modulith-core/src/main/java/org/springframework/modulith/core

1 file changed

+70
-38
lines changed

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

Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@
1818
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
1919
import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.*;
2020
import static com.tngtech.archunit.core.domain.properties.HasModifiers.Predicates.*;
21-
import static java.util.stream.Collectors.*;
2221
import static org.springframework.modulith.core.SyntacticSugar.*;
2322

2423
import java.lang.annotation.Annotation;
2524
import java.util.Collection;
2625
import java.util.Comparator;
2726
import java.util.Iterator;
27+
import java.util.Map.Entry;
2828
import java.util.Objects;
2929
import java.util.Optional;
30-
import java.util.Set;
30+
import java.util.SortedSet;
31+
import java.util.TreeMap;
32+
import java.util.TreeSet;
3133
import java.util.function.Predicate;
3234
import java.util.function.Supplier;
3335
import java.util.stream.Collectors;
@@ -57,32 +59,44 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
5759
has(simpleName(PACKAGE_INFO_NAME)).or(is(metaAnnotatedWith(PackageInfo.class)));
5860

5961
private final PackageName name;
60-
private final Classes classes, packageClasses;
61-
private final Supplier<Set<JavaPackage>> directSubPackages;
62+
private final Classes classes;
63+
private final Supplier<SortedSet<JavaPackage>> directSubPackages;
6264
private final Supplier<JavaPackages> subPackages;
6365

6466
/**
6567
* Creates a new {@link JavaPackage} for the given {@link Classes}, name and whether to include all sub-packages.
6668
*
6769
* @param classes must not be {@literal null}.
6870
* @param name must not be {@literal null}.
69-
* @param includeSubPackages
71+
* @param includeSubPackages whether to include sub-packages.
7072
*/
7173
private JavaPackage(Classes classes, PackageName name, boolean includeSubPackages) {
7274

75+
this(classes.that(resideInAPackage(name.asFilter(includeSubPackages))), name, includeSubPackages
76+
? SingletonSupplier.of(() -> detectSubPackages(classes, name))
77+
: SingletonSupplier.of(JavaPackages.NONE));
78+
}
79+
80+
/**
81+
* Creates a new {@link JavaPackage} for the given {@link Classes}, name and pre-computed sub-packages.
82+
*
83+
* @param classes must not be {@literal null}.
84+
* @param name must not be {@literal null}.
85+
* @param subpackages must not be {@literal null}.
86+
* @see #detectSubPackages(Classes, PackageName)
87+
*/
88+
private JavaPackage(Classes classes, PackageName name, Supplier<JavaPackages> subpackages) {
89+
7390
Assert.notNull(classes, "Classes must not be null!");
91+
Assert.notNull(name, "PackageName must not be null!");
92+
Assert.notNull(subpackages, "Sub-packages must not be null!");
7493

75-
this.classes = classes;
76-
this.packageClasses = classes
77-
.that(resideInAPackage(name.asFilter(includeSubPackages)));
94+
this.classes = classes.that(resideInAPackage(name.asFilter(true)));
7895
this.name = name;
79-
80-
this.directSubPackages = () -> detectSubPackages()
96+
this.subPackages = subpackages;
97+
this.directSubPackages = SingletonSupplier.of(() -> subPackages.get().stream()
8198
.filter(this::isDirectParentOf)
82-
.collect(Collectors.toUnmodifiableSet());
83-
84-
this.subPackages = SingletonSupplier.of(() -> detectSubPackages()
85-
.collect(collectingAndThen(toUnmodifiableList(), JavaPackages::new)));
99+
.collect(Collectors.toCollection(TreeSet::new)));
86100
}
87101

88102
/**
@@ -166,7 +180,7 @@ public Collection<JavaPackage> getDirectSubPackages() {
166180
* @return will never be {@literal null}.
167181
*/
168182
public Classes getClasses() {
169-
return packageClasses;
183+
return classes;
170184
}
171185

172186
/**
@@ -176,7 +190,7 @@ public Classes getClasses() {
176190
*/
177191
public Classes getExposedClasses() {
178192

179-
return packageClasses //
193+
return classes //
180194
.that(doNotHave(simpleName(PACKAGE_INFO_NAME))) //
181195
.that(have(modifier(JavaModifier.PUBLIC)));
182196
}
@@ -191,7 +205,7 @@ public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotatio
191205

192206
Assert.notNull(annotation, "Annotation must not be null!");
193207

194-
return packageClasses.that(ARE_PACKAGE_INFOS.and(are(metaAnnotatedWith(annotation)))).stream() //
208+
return classes.that(ARE_PACKAGE_INFOS.and(are(metaAnnotatedWith(annotation)))).stream() //
195209
.map(JavaClass::getPackageName) //
196210
.filter(Predicate.not(name::hasName))
197211
.distinct() //
@@ -208,7 +222,7 @@ public Classes that(DescribedPredicate<? super JavaClass> predicate) {
208222

209223
Assert.notNull(predicate, "Predicate must not be null!");
210224

211-
return packageClasses.that(predicate);
225+
return classes.that(predicate);
212226
}
213227

214228
/**
@@ -220,7 +234,7 @@ public boolean contains(JavaClass type) {
220234

221235
Assert.notNull(type, "Type must not be null!");
222236

223-
return packageClasses.contains(type);
237+
return classes.contains(type);
224238
}
225239

226240
/**
@@ -232,7 +246,7 @@ public boolean contains(String typeName) {
232246

233247
Assert.hasText(typeName, "Type name must not be null or empty!");
234248

235-
return packageClasses.contains(typeName);
249+
return classes.contains(typeName);
236250
}
237251

238252
/**
@@ -241,7 +255,7 @@ public boolean contains(String typeName) {
241255
* @return will never be {@literal null}.
242256
*/
243257
public Stream<JavaClass> stream() {
244-
return packageClasses.stream();
258+
return classes.stream();
245259
}
246260

247261
/**
@@ -255,7 +269,7 @@ public <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationType)
255269

256270
var isPackageInfo = have(simpleName(PACKAGE_INFO_NAME)).or(are(metaAnnotatedWith(PackageInfo.class)));
257271

258-
return packageClasses.that(isPackageInfo.and(are(metaAnnotatedWith(annotationType)))) //
272+
return classes.that(isPackageInfo.and(are(metaAnnotatedWith(annotationType)))) //
259273
.toOptional() //
260274
.map(it -> it.reflect())
261275
.map(it -> AnnotatedElementUtils.getMergedAnnotation(it, annotationType));
@@ -325,7 +339,7 @@ Classes getClasses(Iterable<JavaPackage> exclusions) {
325339
.map(JavaPackage::asFilter)
326340
.toArray(String[]::new);
327341

328-
return packageClasses.that(resideOutsideOfPackages(excludedPackages));
342+
return classes.that(resideOutsideOfPackages(excludedPackages));
329343
}
330344

331345
/**
@@ -369,7 +383,7 @@ public <A extends Annotation> Optional<A> findAnnotation(Class<A> annotationType
369383

370384
var isPackageInfo = have(simpleName(PACKAGE_INFO_NAME)).or(are(metaAnnotatedWith(PackageInfo.class)));
371385

372-
var annotatedTypes = toSingle().packageClasses
386+
var annotatedTypes = toSingle().classes
373387
.that(isPackageInfo.and(are(metaAnnotatedWith(annotationType))))
374388
.stream()
375389
.map(JavaClass::reflect)
@@ -409,17 +423,6 @@ public String getDescription() {
409423
return classes.getDescription();
410424
}
411425

412-
private Stream<JavaPackage> detectSubPackages() {
413-
414-
return packageClasses.stream() //
415-
.map(JavaClass::getPackageName)
416-
.filter(Predicate.not(name::hasName))
417-
.map(PackageName::of)
418-
.flatMap(name::expandUntil)
419-
.distinct()
420-
.map(it -> new JavaPackage(classes, it, true));
421-
}
422-
423426
/*
424427
* (non-Javadoc)
425428
* @see java.lang.Iterable#iterator()
@@ -469,8 +472,7 @@ public boolean equals(Object obj) {
469472

470473
return Objects.equals(this.classes, that.classes) //
471474
&& Objects.equals(this.getDirectSubPackages(), that.getDirectSubPackages()) //
472-
&& Objects.equals(this.name, that.name) //
473-
&& Objects.equals(this.packageClasses, that.packageClasses);
475+
&& Objects.equals(this.name, that.name);
474476
}
475477

476478
/*
@@ -479,10 +481,40 @@ public boolean equals(Object obj) {
479481
*/
480482
@Override
481483
public int hashCode() {
482-
return Objects.hash(classes, directSubPackages.get(), name, packageClasses);
484+
return Objects.hash(classes, directSubPackages.get(), name);
483485
}
484486

485487
static Comparator<JavaPackage> reverse() {
486488
return (left, right) -> -left.compareTo(right);
487489
}
490+
491+
private static JavaPackages detectSubPackages(Classes classes, PackageName name) {
492+
493+
var packages = new TreeSet<PackageName>(Comparator.reverseOrder());
494+
495+
for (JavaClass clazz : classes) {
496+
497+
var candidate = PackageName.of(clazz.getPackageName());
498+
499+
if (candidate.equals(name)) {
500+
continue;
501+
}
502+
503+
name.expandUntil(candidate).forEach(packages::add);
504+
}
505+
506+
var result = new TreeMap<PackageName, JavaPackage>();
507+
508+
for (PackageName packageName : packages) {
509+
510+
Supplier<JavaPackages> subPackages = () -> result.entrySet().stream()
511+
.filter(it -> it.getKey().isSubPackageOf(packageName))
512+
.map(Entry::getValue)
513+
.collect(Collectors.collectingAndThen(Collectors.toList(), JavaPackages::new));
514+
515+
result.put(packageName, new JavaPackage(classes, packageName, SingletonSupplier.of(subPackages)));
516+
}
517+
518+
return new JavaPackages(result.values());
519+
}
488520
}

0 commit comments

Comments
 (0)