【问题标题】:Hibernate second level cache is closed while running a couple of spring testsHibernate 二级缓存在运行几个弹簧测试时关闭
【发布时间】:2019-10-25 22:21:32
【问题描述】:

我正在尝试为基于 Hibernate 5.3 和 Spring Boot 2.1.3 并使用 Hibernate 二级缓存的应用程序编写测试。

当我正在执行一批设置 spring 上下文并尝试更新某些 JPA 实体的测试时,有时会出现这样的异常:

org.springframework.dao.InvalidDataAccessApiUsageException: Cache[default-update-timestamps-region] is closed; nested exception is java.lang.IllegalStateException: Cache[default-update-timestamps-region] is closed

at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:370)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:536)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:746)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:533)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:304)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:135)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy244.save(Unknown Source)

Hibernate 二级缓存的配置如下:

spring.jpa.properties.hibernate.cache.use_second_level_cache=true spring.jpa.properties.hibernate.cache.use_query_cache=true spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.jcache.JCacheRegionFactory spring.jpa.properties.javax.persistence.sharedCache.mode=ENABLE_SELECTIVE

并使用 Hibernate JCache 作为依赖项。

据我了解,org.hibernate.cache.jcache.JCacheRegionFactory 为 Spring Test 创建的所有上下文重用了相同的 EhCache CacheManager 实例,但一段时间后 Spring 关闭缓存的上下文导致关闭 CacheManager 和缓存。

以前,Hibernate(Hibernate EhCache 模块)提供了 org.hibernate.cache.ehcache.EhCacheRegionFactory 工厂,它每次都在创建新的 CacheManager,没有上述问题。

有谁知道如何为每个 Spring 测试上下文创建新的 CacheManager 并避免使用共享一个?

【问题讨论】:

  • 为什么不使用 Spring Boot 的缓存呢?你有@EnableCaching
  • @SimonMartinelli 希望在存储库布局上启用一些查询结果的缓存。我认为 Hibernate 可以比 Spring 更好地管理它,因为缓存将包含 JPA 实体
  • 我不是这个意思。如果你启用缓存,那么 Spring 会处理缓存
  • @AndriiKorovin 你能找到解决方案吗?

标签: spring hibernate ehcache


【解决方案1】:

解决此问题的一种可能的解决方法是将@DirtiesContext 像这样添加到您的课程中:

@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
public class SomeTestClass {
...
}

这将强制 Spring 为此类的所有方法创建一个新的应用程序上下文。就我而言,这解决了问题。

另一种方法是确保 Spring 知道 Hibernate 缓存管理器。这可以像in this blog post 描述的那样实现。但是,在某些情况下这可能是不可能的。

【讨论】:

    【解决方案2】:

    根本的 GC 原因在于 javax.cache.Caching,如果测试在同一个 JVM 中运行,则它包含在所有 Spring 上下文之间共享的 CachingProvider-s 的静态集合。

    在测试运行期间创建的 Spring 上下文共享相同的 CachingProvider,因此共享相同的 CacheManagers。当任何共享 CachingProvider 的上下文关闭时,所有相关的 CacheManager 也将关闭,从而使引用已关闭 CachingProvider 的剩余 Spring 上下文处于不一致状态。

    为了解决这个问题,CacheManager 的每个请求都应该返回一个不与其他上下文共享的全新实例。

    我编写了一个简单的CachingProvider 实现,它就是这样做的,并且依赖于现有的CachingProviders。请在下面找到代码。

    基类:

    import java.net.URI;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.Properties;
    import java.util.WeakHashMap;
    import javax.cache.CacheManager;
    import javax.cache.configuration.OptionalFeature;
    import javax.cache.spi.CachingProvider;
    
    /**
     * The abstract JCache compatible {@link CachingProvider} suitable for test purposes.
     *
     * <p>When using JCache and {@link org.hibernate.cache.jcache.JCacheRegionFactory}, {@link CachingProvider}-s
     * are shared between Spring contexts, which means that {@link CacheManager}-s are shared too. The class responsible
     * for storing loaded {@link CachingProvider}-s is {@link javax.cache.Caching}. If any cached Spring context is closed,
     * then all related {@link CacheManager}-s are closed as well, but since these {@link CacheManager}-s are shared with
     * remaining Spring contexts, we end up with in an inconsistent state.</p>
     *
     * <p>The solution is to make sure that each time a {@link CacheManager} for a particular config URI is requested, a new
     * instance not shared between Spring contexts is created</p>
     *
     * <p>The simplest approach is to create a new instance of {@link CachingProvider} for each {@link CacheManager} request
     * and manage them separately from {@link CachingProvider}-s loaded via {@link javax.cache.Caching}. This approach
     * allows reusing existing required {@link CachingProvider}-s and overcome any sharing issues.</p>
     *
     * <p>Tests relying on caching functionality MUST make sure that for regular caching the properties
     * {@code spring.cache.jcache.provider} and {@code spring.cache.jcache.config} are set and for 2nd-level cache
     * the properties {@code spring.jpa.properties.hibernate.javax.cache.provider} and
     * {@code spring.jpa.properties.hibernate.javax.cache.uri} are set. Please note that classpath URI-s for
     * the {@code spring.jpa.properties.hibernate.javax.cache.uri} property are supported by {@code hibernate-jcache} only
     * since 5.4.1, therefore with earlier versions this property should be set programmatically, for example via
     * {@link System#setProperty(String, String)}.</p>
     *
     * @see <a href="https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#caching-provider-jcache-cache-manager">Hibernate
     * JCache configuration</a>
     * @see org.hibernate.cache.jcache.JCacheRegionFactory
     * @see CachingProvider
     * @see javax.cache.Caching
     */
    public abstract class AbstractTestJCacheCachingProvider implements CachingProvider {
    
        /**
         * The {@link CachingProvider}-s specific for a configuration {@link URI} for a specific {@link ClassLoader}.
         *
         * <p>All access MUST be handled in a <i>synchronized</i> manner.</p>
         */
        private final Map<ClassLoader, Map<URI, List<CachingProvider>>>
                classLoaderToUriToCachingProviders = new WeakHashMap<>();
    
        /**
         * {@inheritDoc}
         */
        @Override
        public CacheManager getCacheManager(URI uri, ClassLoader classLoader, Properties properties) {
            Objects.requireNonNull(uri, "The cache manager configuration URI must not be null.");
            Objects.requireNonNull(classLoader, "The class loader must not be null");
    
            final CachingProvider cachingProvider = createCachingProvider();
            synchronized (classLoaderToUriToCachingProviders) {
                classLoaderToUriToCachingProviders
                        .computeIfAbsent(classLoader, k -> new HashMap<>())
                        .computeIfAbsent(uri, k -> new ArrayList<>())
                        .add(cachingProvider);
            }
            return cachingProvider.getCacheManager(uri, classLoader, properties);
        }
    
        /**
         * Creates a {@link CachingProvider}.
         *
         * @return a created {@link CachingProvider}
         */
        protected abstract CachingProvider createCachingProvider();
    
        /**
         * {@inheritDoc}
         */
        @Override
        public ClassLoader getDefaultClassLoader() {
            return Thread.currentThread().getContextClassLoader();
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public URI getDefaultURI() {
            throw new UnsupportedOperationException("Please specify an explicit cache manager configuration URI.");
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public Properties getDefaultProperties() {
            return new Properties();
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public CacheManager getCacheManager(URI uri, ClassLoader classLoader) {
            return getCacheManager(uri, classLoader, null);
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public CacheManager getCacheManager() {
            throw new UnsupportedOperationException("The cache manager configuration URI must be specified.");
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public void close() {
            synchronized (classLoaderToUriToCachingProviders) {
                classLoaderToUriToCachingProviders.keySet().forEach(this::close);
            }
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public void close(ClassLoader classLoader) {
            Objects.requireNonNull(classLoader, "The class loader must not be null");
    
            synchronized (classLoaderToUriToCachingProviders) {
                // Process all CachingProvider collections regardless of the configuration URI.
                classLoaderToUriToCachingProviders
                        .getOrDefault(classLoader, Collections.emptyMap())
                        .values().stream().flatMap(Collection::stream)
                        // Close all CachingProvider resources since we are sure that CachingProvider-s are not shared
                        // or reused.
                        .forEach(CachingProvider::close);
    
                classLoaderToUriToCachingProviders.remove(classLoader);
            }
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public void close(URI uri, ClassLoader classLoader) {
            Objects.requireNonNull(uri, "The cache manager configuration URI must not be null");
            Objects.requireNonNull(classLoader, "The class loader must not be null");
    
            synchronized (classLoaderToUriToCachingProviders) {
                final Map<URI, List<CachingProvider>> uriToCachingProviders = classLoaderToUriToCachingProviders
                        .getOrDefault(classLoader, Collections.emptyMap());
                uriToCachingProviders
                        .getOrDefault(uri, Collections.emptyList())
                        // Close all CachingProvider resources since we are sure that CachingProvider-s are not shared
                        // or reused.
                        .forEach(CachingProvider::close);
    
                uriToCachingProviders.remove(uri);
            }
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isSupported(OptionalFeature optionalFeature) {
            // Find the first available CachingProvider and delegate the request to it.
            synchronized (classLoaderToUriToCachingProviders) {
                return classLoaderToUriToCachingProviders.values().stream().findFirst()
                        .flatMap(uriToCachingProviders -> uriToCachingProviders.values().stream().findFirst())
                        .flatMap(cachingProviders -> cachingProviders.stream().findFirst())
                        .map(cachingProvider -> cachingProvider.isSupported(optionalFeature))
                        .orElse(false);
            }
        }
    }
    

    基于 Ehcache 的实现:

    import javax.cache.spi.CachingProvider;
    import org.ehcache.jsr107.EhcacheCachingProvider;
    
    /**
     * The test {@link CachingProvider} based on {@link EhcacheCachingProvider}.
     */
    public class TestEhcacheJCacheCachingProvider extends AbstractTestJCacheCachingProvider {
    
        @Override
        protected CachingProvider createCachingProvider() {
            return new EhcacheCachingProvider();
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2014-07-05
      • 2016-02-01
      • 2011-05-09
      • 2015-05-14
      • 1970-01-01
      • 2010-11-16
      • 2014-09-02
      • 2014-11-28
      • 2011-12-18
      相关资源
      最近更新 更多