Skip to content

Commit 083ce7b

Browse files
committed
GH-1098 - Fix performance regression in JavaPackage.
The fix for GH-1039 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 423f54d commit 083ce7b

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

+70-38
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.BiPredicate;
3234
import java.util.function.Predicate;
3335
import java.util.function.Supplier;
@@ -58,32 +60,44 @@ public class JavaPackage implements DescribedIterable<JavaClass>, Comparable<Jav
5860
has(simpleName(PACKAGE_INFO_NAME)).or(is(metaAnnotatedWith(PackageInfo.class)));
5961

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

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

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

76-
this.classes = classes;
77-
this.packageClasses = classes
78-
.that(resideInAPackage(name.asFilter(includeSubPackages)));
95+
this.classes = classes.that(resideInAPackage(name.asFilter(true)));
7996
this.name = name;
80-
81-
this.directSubPackages = () -> detectSubPackages()
97+
this.subPackages = subpackages;
98+
this.directSubPackages = SingletonSupplier.of(() -> subPackages.get().stream()
8299
.filter(this::isDirectParentOf)
83-
.collect(Collectors.toUnmodifiableSet());
84-
85-
this.subPackages = SingletonSupplier.of(() -> detectSubPackages()
86-
.collect(collectingAndThen(toUnmodifiableList(), JavaPackages::new)));
100+
.collect(Collectors.toCollection(TreeSet::new)));
87101
}
88102

89103
/**
@@ -167,7 +181,7 @@ public Collection<JavaPackage> getDirectSubPackages() {
167181
* @return will never be {@literal null}.
168182
*/
169183
public Classes getClasses() {
170-
return packageClasses;
184+
return classes;
171185
}
172186

173187
/**
@@ -177,7 +191,7 @@ public Classes getClasses() {
177191
*/
178192
public Classes getExposedClasses() {
179193

180-
return packageClasses //
194+
return classes //
181195
.that(doNotHave(simpleName(PACKAGE_INFO_NAME))) //
182196
.that(have(modifier(JavaModifier.PUBLIC)));
183197
}
@@ -192,7 +206,7 @@ public Stream<JavaPackage> getSubPackagesAnnotatedWith(Class<? extends Annotatio
192206

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

195-
return packageClasses.that(ARE_PACKAGE_INFOS.and(are(metaAnnotatedWith(annotation)))).stream() //
209+
return classes.that(ARE_PACKAGE_INFOS.and(are(metaAnnotatedWith(annotation)))).stream() //
196210
.map(JavaClass::getPackageName) //
197211
.filter(Predicate.not(name::hasName))
198212
.distinct() //
@@ -226,7 +240,7 @@ public Classes that(DescribedPredicate<? super JavaClass> predicate) {
226240

227241
Assert.notNull(predicate, "Predicate must not be null!");
228242

229-
return packageClasses.that(predicate);
243+
return classes.that(predicate);
230244
}
231245

232246
/**
@@ -238,7 +252,7 @@ public boolean contains(JavaClass type) {
238252

239253
Assert.notNull(type, "Type must not be null!");
240254

241-
return packageClasses.contains(type);
255+
return classes.contains(type);
242256
}
243257

244258
/**
@@ -250,7 +264,7 @@ public boolean contains(String typeName) {
250264

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

253-
return packageClasses.contains(typeName);
267+
return classes.contains(typeName);
254268
}
255269

256270
/**
@@ -259,7 +273,7 @@ public boolean contains(String typeName) {
259273
* @return will never be {@literal null}.
260274
*/
261275
public Stream<JavaClass> stream() {
262-
return packageClasses.stream();
276+
return classes.stream();
263277
}
264278

265279
/**
@@ -273,7 +287,7 @@ public <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationType)
273287

274288
var isPackageInfo = have(simpleName(PACKAGE_INFO_NAME)).or(are(metaAnnotatedWith(PackageInfo.class)));
275289

276-
return packageClasses.that(isPackageInfo.and(are(metaAnnotatedWith(annotationType)))) //
290+
return classes.that(isPackageInfo.and(are(metaAnnotatedWith(annotationType)))) //
277291
.toOptional() //
278292
.map(it -> it.reflect())
279293
.map(it -> AnnotatedElementUtils.getMergedAnnotation(it, annotationType));
@@ -343,7 +357,7 @@ Classes getClasses(Iterable<JavaPackage> exclusions) {
343357
.map(JavaPackage::asFilter)
344358
.toArray(String[]::new);
345359

346-
return packageClasses.that(resideOutsideOfPackages(excludedPackages));
360+
return classes.that(resideOutsideOfPackages(excludedPackages));
347361
}
348362

349363
/**
@@ -387,7 +401,7 @@ public <A extends Annotation> Optional<A> findAnnotation(Class<A> annotationType
387401

388402
var isPackageInfo = have(simpleName(PACKAGE_INFO_NAME)).or(are(metaAnnotatedWith(PackageInfo.class)));
389403

390-
var annotatedTypes = toSingle().packageClasses
404+
var annotatedTypes = toSingle().classes
391405
.that(isPackageInfo.and(are(metaAnnotatedWith(annotationType))))
392406
.stream()
393407
.map(JavaClass::reflect)
@@ -427,17 +441,6 @@ public String getDescription() {
427441
return classes.getDescription();
428442
}
429443

430-
private Stream<JavaPackage> detectSubPackages() {
431-
432-
return packageClasses.stream() //
433-
.map(JavaClass::getPackageName)
434-
.filter(Predicate.not(name::hasName))
435-
.map(PackageName::of)
436-
.flatMap(name::expandUntil)
437-
.distinct()
438-
.map(it -> new JavaPackage(classes, it, true));
439-
}
440-
441444
/*
442445
* (non-Javadoc)
443446
* @see java.lang.Iterable#iterator()
@@ -487,8 +490,7 @@ public boolean equals(Object obj) {
487490

488491
return Objects.equals(this.classes, that.classes) //
489492
&& Objects.equals(this.getDirectSubPackages(), that.getDirectSubPackages()) //
490-
&& Objects.equals(this.name, that.name) //
491-
&& Objects.equals(this.packageClasses, that.packageClasses);
493+
&& Objects.equals(this.name, that.name);
492494
}
493495

494496
/*
@@ -497,10 +499,40 @@ public boolean equals(Object obj) {
497499
*/
498500
@Override
499501
public int hashCode() {
500-
return Objects.hash(classes, directSubPackages.get(), name, packageClasses);
502+
return Objects.hash(classes, directSubPackages.get(), name);
501503
}
502504

503505
static Comparator<JavaPackage> reverse() {
504506
return (left, right) -> -left.compareTo(right);
505507
}
508+
509+
private static JavaPackages detectSubPackages(Classes classes, PackageName name) {
510+
511+
var packages = new TreeSet<PackageName>(Comparator.reverseOrder());
512+
513+
for (JavaClass clazz : classes) {
514+
515+
var candidate = PackageName.of(clazz.getPackageName());
516+
517+
if (candidate.equals(name)) {
518+
continue;
519+
}
520+
521+
name.expandUntil(candidate).forEach(packages::add);
522+
}
523+
524+
var result = new TreeMap<PackageName, JavaPackage>();
525+
526+
for (PackageName packageName : packages) {
527+
528+
Supplier<JavaPackages> subPackages = () -> result.entrySet().stream()
529+
.filter(it -> it.getKey().isSubPackageOf(packageName))
530+
.map(Entry::getValue)
531+
.collect(Collectors.collectingAndThen(Collectors.toList(), JavaPackages::new));
532+
533+
result.put(packageName, new JavaPackage(classes, packageName, SingletonSupplier.of(subPackages)));
534+
}
535+
536+
return new JavaPackages(result.values());
537+
}
506538
}

0 commit comments

Comments
 (0)