Skip to content

Commit e1c8671

Browse files
committed
DATAJPA-1446 - Clear JpaMetamodel cache when application context is closed.
We now register a dedicated Spring Bean to wipe the static cache in JpaMetamodel to avoid memory from leaking in scenarios where ApplicationContexts are started and closed very often (e.g. integration tests). Heavily inspired by the work Sylvere Richard (@Nowheresly) has done in #301 but the key responsibility of wiping moved away from the MappingCOntext implementation. Original pull request #301.
1 parent d05a755 commit e1c8671

File tree

6 files changed

+121
-3
lines changed

6 files changed

+121
-3
lines changed

src/main/java/org/springframework/data/jpa/repository/Modifying.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@
2222
import java.lang.annotation.Target;
2323

2424
/**
25-
* Indicates a method should be regarded as modifying query.
25+
* Indicates a query method should be considered as modifying query as that changes the way it needs to be executed.
26+
* This annotation is only considered if used on actual query methods (either derived or manually defined through a
27+
* {@link Query} annotation). It's not applied on custom implementation methods as they already have control over the
28+
* underlying data access APIs.
2629
*
2730
* @author Oliver Gierke
2831
* @author Christoph Strobl
2932
* @author Nicolas Cirigliano
33+
* @see Query
3034
*/
3135
@Retention(RetentionPolicy.RUNTIME)
3236
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })

src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
7575
private static final Class<?> PAB_POST_PROCESSOR = PersistenceAnnotationBeanPostProcessor.class;
7676
private static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME = "transactionManager";
7777
private static final String ENABLE_DEFAULT_TRANSACTIONS_ATTRIBUTE = "enableDefaultTransactions";
78+
private static final String JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME = "org.springframework.data.jpa.util.JpaMetamodelCacheCleanup";
7879

7980
/*
8081
* (non-Javadoc)
@@ -187,6 +188,8 @@ public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConf
187188
contextDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
188189

189190
registerIfNotAlreadyRegistered(contextDefinition, registry, JPA_CONTEXT_BEAN_NAME, source);
191+
registerIfNotAlreadyRegistered(new RootBeanDefinition(JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME), registry,
192+
JPA_METAMODEL_CACHE_CLEANUP_CLASSNAME, source);
190193
}
191194

192195
/*

src/main/java/org/springframework/data/jpa/util/JpaMetamodel.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
package org.springframework.data.jpa.util;
1717

1818
import java.util.Collection;
19-
import java.util.HashMap;
2019
import java.util.Map;
2120
import java.util.Optional;
21+
import java.util.concurrent.ConcurrentHashMap;
2222

2323
import javax.persistence.metamodel.EntityType;
2424
import javax.persistence.metamodel.ManagedType;
@@ -34,10 +34,11 @@
3434
*
3535
* @author Oliver Gierke
3636
* @author Mark Paluch
37+
* @author Sylvère Richard
3738
*/
3839
public class JpaMetamodel {
3940

40-
private static final Map<Metamodel, JpaMetamodel> CACHE = new HashMap<>(4);
41+
private static final Map<Metamodel, JpaMetamodel> CACHE = new ConcurrentHashMap<>(4);
4142

4243
private final Metamodel metamodel;
4344

@@ -95,6 +96,13 @@ public boolean isSingleIdAttribute(Class<?> entity, String name, Class<?> attrib
9596
.orElse(false);
9697
}
9798

99+
/**
100+
* Wipes the static cache of {@link Metamodel} to {@link JpaMetamodel}.
101+
*/
102+
static void clear() {
103+
CACHE.clear();
104+
}
105+
98106
/**
99107
* Returns the {@link SingularAttribute} representing the identifier of the given {@link EntityType} if it contains a
100108
* singular one.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.data.jpa.util;
17+
18+
import org.springframework.beans.factory.DisposableBean;
19+
import org.springframework.context.ApplicationContext;
20+
21+
/**
22+
* Simple component to be reigstered as Spring bean to clear the {@link JpaMetamodel} cache to avoid a memory leak in
23+
* applications bootstrapping multiple {@link ApplicationContext}s.
24+
*
25+
* @author Oliver Gierke
26+
* @author Sylvère Richard
27+
* @see org.springframework.data.jpa.repository.config.JpaRepositoryConfigExtension#registerBeansForRoot(org.springframework.beans.factory.support.BeanDefinitionRegistry,
28+
* org.springframework.data.repository.config.RepositoryConfigurationSource)
29+
*/
30+
class JpaMetamodelCacheCleanup implements DisposableBean {
31+
32+
/*
33+
* (non-Javadoc)
34+
* @see org.springframework.beans.factory.DisposableBean#destroy()
35+
*/
36+
@Override
37+
public void destroy() throws Exception {
38+
JpaMetamodel.clear();
39+
}
40+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2018 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+
* http://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.data.jpa.util;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import javax.persistence.metamodel.Metamodel;
21+
22+
import org.junit.Test;
23+
import org.junit.runner.RunWith;
24+
import org.mockito.Mock;
25+
import org.mockito.junit.MockitoJUnitRunner;
26+
import org.springframework.context.support.GenericApplicationContext;
27+
28+
/**
29+
* Integration tests for {@link JpaMetamodelCacheCleanup}.
30+
*
31+
* @author Oliver Gierke
32+
*/
33+
@RunWith(MockitoJUnitRunner.class)
34+
public class JpaMetamodelCacheCleanupIntegrationTests {
35+
36+
@Mock Metamodel metamodel;
37+
38+
@Test // DATAJPA-1446
39+
public void wipesJpaMetamodelCacheOnApplicationContextClose() {
40+
41+
JpaMetamodel model = JpaMetamodel.of(metamodel);
42+
43+
try (GenericApplicationContext context = new GenericApplicationContext()) {
44+
45+
context.registerBean(JpaMetamodelCacheCleanup.class);
46+
context.refresh();
47+
48+
assertThat(model).isSameAs(JpaMetamodel.of(metamodel));
49+
}
50+
51+
assertThat(model).isNotSameAs(JpaMetamodel.of(metamodel));
52+
}
53+
}

src/test/java/org/springframework/data/jpa/util/JpaMetamodelUnitTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,14 @@ public void skipsEntityTypesWithoutJavaTypeForIdentifierLookup() {
4747

4848
assertThat(JpaMetamodel.of(metamodel).isSingleIdAttribute(Object.class, "id", Object.class)).isFalse();
4949
}
50+
51+
@Test // DATAJPA-1446
52+
public void cacheIsEffectiveUnlessCleared() {
53+
54+
JpaMetamodel model = JpaMetamodel.of(metamodel);
55+
assertThat(model).isEqualTo(JpaMetamodel.of(metamodel));
56+
57+
JpaMetamodel.clear();
58+
assertThat(model).isNotEqualTo(JpaMetamodel.of(metamodel));
59+
}
5060
}

0 commit comments

Comments
 (0)