Skip to content

Commit 50b74f6

Browse files
committed
Revise RepositoryInformation and RepositoryComposition caching.
We now use a refined strategy to cache RepositoryInformation and RepositoryComposition. Previously, RepositoryComposition wasn't cached at all and store modules that e.g. contributed a Querydsl (or a different) fragment based on the interface declaration returned a new RepositoryComposition (and thus a different hashCode) each time RepositoryInformation was obtained leading to memory leaks caused by HashMap caching. We now use Fragment's hashCode for the cache key resulting in RepositoryComposition being created only once for a given repository interface and input-fragments arrangement. Closes #3252
1 parent bb59eb4 commit 50b74f6

File tree

4 files changed

+74
-57
lines changed

4 files changed

+74
-57
lines changed

src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java

+5
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,10 @@ private static Method findMethod(InvokedMethod invokedMethod, MethodLookup looku
541541
return null;
542542
}
543543

544+
List<RepositoryFragment<?>> getFragments() {
545+
return fragments;
546+
}
547+
544548
/**
545549
* Returns the number of {@link RepositoryFragment fragments} available.
546550
*
@@ -585,5 +589,6 @@ public int hashCode() {
585589
result = (31 * result) + ObjectUtils.nullSafeHashCode(fragments);
586590
return result;
587591
}
592+
588593
}
589594
}

src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java

+42-56
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public abstract class RepositoryFactorySupport
107107
CONVERSION_SERVICE.removeConvertible(Object.class, Object.class);
108108
}
109109

110-
private final Map<RepositoryInformationCacheKey, RepositoryInformation> repositoryInformationCache;
110+
private final Map<RepositoryInformationCacheKey, RepositoryStub> repositoryInformationCache;
111111
private final List<RepositoryProxyPostProcessor> postProcessors;
112112

113113
private @Nullable Class<?> repositoryBaseClass;
@@ -127,7 +127,7 @@ public abstract class RepositoryFactorySupport
127127
@SuppressWarnings("null")
128128
public RepositoryFactorySupport() {
129129

130-
this.repositoryInformationCache = new HashMap<>(16);
130+
this.repositoryInformationCache = new HashMap<>(8);
131131
this.postProcessors = new ArrayList<>();
132132

133133
this.namedQueries = PropertiesBasedNamedQueries.EMPTY;
@@ -291,16 +291,6 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata
291291
return RepositoryFragments.empty();
292292
}
293293

294-
/**
295-
* Creates {@link RepositoryComposition} based on {@link RepositoryMetadata} for repository-specific method handling.
296-
*
297-
* @param metadata the repository metadata to use.
298-
* @return repository composition.
299-
*/
300-
private RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata) {
301-
return RepositoryComposition.fromMetadata(metadata);
302-
}
303-
304294
/**
305295
* Returns a repository instance for the given interface.
306296
*
@@ -359,8 +349,9 @@ public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fra
359349
repositoryInterface);
360350
repositoryCompositionStep.tag("fragment.count", String.valueOf(fragments.size()));
361351

362-
RepositoryComposition composition = getRepositoryComposition(metadata, fragments);
363-
RepositoryInformation information = getRepositoryInformation(metadata, composition);
352+
RepositoryStub stub = getRepositoryStub(metadata, fragments);
353+
RepositoryComposition composition = stub.composition();
354+
RepositoryInformation information = stub.information();
364355

365356
repositoryCompositionStep.tag("fragments", () -> {
366357

@@ -490,47 +481,35 @@ protected RepositoryMetadata getRepositoryMetadata(Class<?> repositoryInterface)
490481
* @return will never be {@literal null}.
491482
*/
492483
protected RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata, RepositoryFragments fragments) {
493-
return getRepositoryInformation(metadata, getRepositoryComposition(metadata, fragments));
484+
return getRepositoryStub(metadata, fragments).information();
494485
}
495486

496487
/**
497-
* Returns the {@link RepositoryComposition} for the given {@link RepositoryMetadata} and extra
498-
* {@link RepositoryFragments}.
499-
*
500-
* @param metadata must not be {@literal null}.
501-
* @param fragments must not be {@literal null}.
502-
* @return will never be {@literal null}.
503-
*/
504-
private RepositoryComposition getRepositoryComposition(RepositoryMetadata metadata, RepositoryFragments fragments) {
505-
506-
Assert.notNull(metadata, "RepositoryMetadata must not be null");
507-
Assert.notNull(fragments, "RepositoryFragments must not be null");
508-
509-
RepositoryComposition composition = getRepositoryComposition(metadata);
510-
RepositoryFragments repositoryAspects = getRepositoryFragments(metadata);
511-
512-
return composition.append(fragments).append(repositoryAspects);
513-
}
514-
515-
/**
516-
* Returns the {@link RepositoryInformation} for the given repository interface.
488+
* Returns the cached {@link RepositoryStub} for the given repository and composition. {@link RepositoryMetadata} is a
489+
* strong cache key while {@link RepositoryFragments} contributes a light-weight caching component by using only the
490+
* fragments hash code. In a typical Spring scenario, that shouldn't impose issues as one repository factory produces
491+
* only a single repository instance for one repository interface. Things might be different when using various
492+
* fragments for the same repository interface.
517493
*
518494
* @param metadata
519-
* @param composition
495+
* @param fragments
520496
* @return
521497
*/
522-
private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,
523-
RepositoryComposition composition) {
498+
private RepositoryStub getRepositoryStub(RepositoryMetadata metadata, RepositoryFragments fragments) {
524499

525-
RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, composition);
500+
RepositoryInformationCacheKey cacheKey = new RepositoryInformationCacheKey(metadata, fragments);
526501

527502
synchronized (repositoryInformationCache) {
528503

529504
return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {
530505

531-
Class<?> baseClass = repositoryBaseClass != null ? repositoryBaseClass : getRepositoryBaseClass(metadata);
506+
RepositoryComposition composition = RepositoryComposition.fromMetadata(metadata);
507+
RepositoryFragments repositoryAspects = getRepositoryFragments(metadata);
508+
composition = composition.append(fragments).append(repositoryAspects);
532509

533-
return new DefaultRepositoryInformation(metadata, baseClass, composition);
510+
Class<?> baseClass = repositoryBaseClass != null ? repositoryBaseClass : getRepositoryBaseClass(metadata);
511+
512+
return new RepositoryStub(new DefaultRepositoryInformation(metadata, baseClass, composition), composition);
534513
});
535514
}
536515
}
@@ -811,6 +790,18 @@ public List<QueryMethod> getQueryMethods() {
811790
}
812791
}
813792

793+
/**
794+
* Repository stub holding {@link RepositoryInformation} and {@link RepositoryComposition}.
795+
*
796+
* @param information
797+
* @param composition
798+
* @author Mark Paluch
799+
* @since 3.4.4
800+
*/
801+
record RepositoryStub(RepositoryInformation information, RepositoryComposition composition) {
802+
803+
}
804+
814805
/**
815806
* Simple value object to build up keys to cache {@link RepositoryInformation} instances.
816807
*
@@ -820,31 +811,26 @@ public List<QueryMethod> getQueryMethods() {
820811
private static final class RepositoryInformationCacheKey {
821812

822813
private final String repositoryInterfaceName;
823-
private final long compositionHash;
814+
private final long fragmentsHash;
824815

825816
/**
826-
* Creates a new {@link RepositoryInformationCacheKey} for the given {@link RepositoryMetadata} and composition.
817+
* Creates a new {@link RepositoryInformationCacheKey} for the given {@link RepositoryMetadata} and fragments.
827818
*
828819
* @param metadata must not be {@literal null}.
829-
* @param composition must not be {@literal null}.
820+
* @param fragments must not be {@literal null}.
830821
*/
831-
public RepositoryInformationCacheKey(RepositoryMetadata metadata, RepositoryComposition composition) {
822+
public RepositoryInformationCacheKey(RepositoryMetadata metadata, RepositoryFragments fragments) {
832823

833824
this.repositoryInterfaceName = metadata.getRepositoryInterface().getName();
834-
this.compositionHash = composition.hashCode();
835-
}
836-
837-
public RepositoryInformationCacheKey(String repositoryInterfaceName, long compositionHash) {
838-
this.repositoryInterfaceName = repositoryInterfaceName;
839-
this.compositionHash = compositionHash;
825+
this.fragmentsHash = fragments.getFragments().hashCode();
840826
}
841827

842828
public String getRepositoryInterfaceName() {
843829
return this.repositoryInterfaceName;
844830
}
845831

846-
public long getCompositionHash() {
847-
return this.compositionHash;
832+
public long getFragmentsHash() {
833+
return this.fragmentsHash;
848834
}
849835

850836
@Override
@@ -855,7 +841,7 @@ public boolean equals(Object o) {
855841
if (!(o instanceof RepositoryInformationCacheKey that)) {
856842
return false;
857843
}
858-
if (compositionHash != that.compositionHash) {
844+
if (fragmentsHash != that.fragmentsHash) {
859845
return false;
860846
}
861847
return ObjectUtils.nullSafeEquals(repositoryInterfaceName, that.repositoryInterfaceName);
@@ -864,14 +850,14 @@ public boolean equals(Object o) {
864850
@Override
865851
public int hashCode() {
866852
int result = ObjectUtils.nullSafeHashCode(repositoryInterfaceName);
867-
result = 31 * result + (int) (compositionHash ^ (compositionHash >>> 32));
853+
result = 31 * result + Long.hashCode(fragmentsHash);
868854
return result;
869855
}
870856

871857
@Override
872858
public String toString() {
873859
return "RepositoryFactorySupport.RepositoryInformationCacheKey(repositoryInterfaceName="
874-
+ this.getRepositoryInterfaceName() + ", compositionHash=" + this.getCompositionHash() + ")";
860+
+ this.getRepositoryInterfaceName() + ", fragmentsHash=" + this.getFragmentsHash() + ")";
875861
}
876862
}
877863

src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.mockito.ArgumentMatchers;
2525
import org.mockito.Mockito;
26+
2627
import org.springframework.beans.factory.BeanFactory;
2728
import org.springframework.core.metrics.ApplicationStartup;
2829
import org.springframework.core.metrics.StartupStep;
@@ -103,7 +104,7 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata
103104
var fragments = super.getRepositoryFragments(metadata);
104105

105106
return QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()) //
106-
? fragments.append(RepositoryFragments.just(querydsl)) //
107+
? fragments.append(RepositoryFragments.just(querydsl, new Object())) //
107108
: fragments;
108109
}
109110

src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java

+25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.lang.reflect.Method;
2525
import java.util.Collections;
2626
import java.util.List;
27+
import java.util.Map;
2728
import java.util.Optional;
2829
import java.util.Set;
2930
import java.util.concurrent.CompletableFuture;
@@ -293,6 +294,26 @@ record Metadata(RepositoryMethodContext context, MethodInvocation methodInvocati
293294
assertThat(metadata.methodInvocation().getMethod().getName()).isEqualTo("findMetadataByLastname");
294295
}
295296

297+
@Test
298+
void cachesRepositoryInformation() {
299+
300+
var repository1 = factory.getRepository(ObjectAndQuerydslRepository.class, backingRepo);
301+
var repository2 = factory.getRepository(ObjectAndQuerydslRepository.class, backingRepo);
302+
repository1.findByFoo();
303+
repository2.deleteAll();
304+
305+
for (int i = 0; i < 10; i++) {
306+
RepositoryFragments fragments = RepositoryFragments.just(backingRepo);
307+
RepositoryMetadata metadata = factory.getRepositoryMetadata(ObjectAndQuerydslRepository.class);
308+
factory.getRepositoryInformation(metadata, fragments);
309+
}
310+
311+
Map<Object, RepositoryInformation> cache = (Map) ReflectionTestUtils.getField(factory,
312+
"repositoryInformationCache");
313+
314+
assertThat(cache).hasSize(1);
315+
}
316+
296317
@Test // DATACMNS-509, DATACMNS-1764
297318
void convertsWithSameElementType() {
298319

@@ -544,6 +565,10 @@ private void expect(Future<?> future, Object value) throws Exception {
544565

545566
interface SimpleRepository extends Repository<Object, Serializable> {}
546567

568+
interface ObjectAndQuerydslRepository extends ObjectRepository, QuerydslPredicateExecutor<Object> {
569+
570+
}
571+
547572
interface ObjectRepository extends Repository<Object, Object>, ObjectRepositoryCustom {
548573

549574
@Nullable

0 commit comments

Comments
 (0)