Spring Boot 2.x學習筆記:集成EhCache和RedisCache實現緩存

Maven依賴配置

<!-- 引入 spring-boot-starter-web 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 引入 spring-boot-starter-data-jpa 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- 引入 mysql-connector-java 依賴 -->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- 引入 spring-boot-starter-data-redis 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 引入 fastjson 依賴 -->
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
</dependency>

<!-- 引入 spring-boot-starter-cache 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- 引入 spring-context-support 依賴 -->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-context-support</artifactId>
</dependency>

<!-- 引入 ehcache 依賴 -->
<dependency>
	<groupId>net.sf.ehcache</groupId>
	<artifactId>ehcache</artifactId>
</dependency>

<!-- 引入 spring-boot-starter-test 依賴 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

application.properties配置

# DATASOURCE CONFIGURATION
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc\:mysql\://localhost\:3306/spring_boot_demo_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=adm123456

# Tomcat will use the above plus the following to setup connection pooling
spring.datasource.tomcat.max-active=100
spring.datasource.tomcat.max-idle=8
spring.datasource.tomcat.min-idle=8
spring.datasource.tomcat.initial-size=10
spring.datasource.tomcat.init-s-q-l=SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci
# spring.datasource.tomcat.validation-query=
# spring.datasource.tomcat.test-on-borrow=false
# spring.datasource.tomcat.test-on-return=false
# spring.datasource.tomcat.test-while-idle=
# spring.datasource.tomcat.time-between-eviction-runs-millis=
# spring.datasource.tomcat.min-evictable-idle-time-millis=
# spring.datasource.tomcat.max-wait=

# JPA (JpaBaseConfiguration, HibernateJpaAutoConfiguration)
# spring.jpa.properties.*= # properties to set on the JPA connection
spring.jpa.open-in-view=true
spring.jpa.show-sql=true
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.database=MYSQL
spring.jpa.generate-ddl=false
# spring.jpa.hibernate.naming-strategy= # naming classname
# spring.jpa.hibernate.ddl-auto= # defaults to create-drop for embedded dbs
spring.data.jpa.repositories.enabled=true

# REDIS CONFIGURATION
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=adm123456
spring.redis.timeout=5000
spring.redis.jedis.pool.max-active=1000
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=30
spring.redis.jedis.pool.min-idle=10

# Redis緩存屬性
cache.default.expire-time=300
cache.test.name=test
cache.test.expire-time=300

說明:spring.jpa.show-sql=true,開啓sql顯示,方便後期測試的時候通過日誌區分數據是來自緩存還是數據庫。

ehcache.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
	updateCheck="false">
	<diskStore path="Java.io.tmpdir" />
	<defaultCache eternal="false" maxElementsInMemory="900"
		overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
		timeToLiveSeconds="60" memoryStoreEvictionPolicy="LRU" />
		
	<cache name="test" eternal="false" maxElementsInMemory="200"
		overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0"
		timeToLiveSeconds="60" memoryStoreEvictionPolicy="LRU" />
</ehcache>

ehcache.xml配置文件標籤說明

  • diskStore: ehcache分爲內存和磁盤兩級,當內存緩存中對象數量超過maxElementsInMemory時,將緩存對象寫到磁盤緩存中,此標籤定義磁盤緩存相關信息。

標籤屬性說明:

path:磁盤緩存使用的物理路徑,可選值:

user.home – 用戶主目錄

user.dir  – 用戶當前工作目錄

java.io.tmpdir – 默認臨時文件路徑

  • defaultCache:此標籤聲明默認緩存策略,當ehcache找不到定義的緩存時,則使用這個緩存策略(只能定義一個)。
  • cache:此標籤聲明自定緩存策略。

標籤屬性說明:

name:緩存名稱。

maxElementsOnDisk:磁盤緩存中最多可以存放的元素數量,若是0表示無窮大。

maxElementsInMemory:內存緩存中最多可以存放的元素數量,若放入Cache中的元素超過這個數值,則有以下兩種情況:

若overflowToDisk=true,則會將Cache中多出的元素放入磁盤文件中;

若overflowToDisk=false,則根據memoryStoreEvictionPolicy策略替換Cache中原有的元素;

eternal:緩存中對象是否永久有效,即是否永駐內存,爲true時將忽略timeToIdleSeconds和timeToLiveSeconds,默認爲false。

timeToIdleSeconds:緩存數據在失效前的允許閒置時間(單位:秒),僅當eternal=false時使用,默認值是0表示可閒置時間無窮大,此爲可選屬性。即訪問這個cache中元素的最大間隔時間,若超過這個時間沒有訪問此Cache中的某個元素,那麼此元素將被從Cache中清除。

timeToLiveSeconds:緩存數據在失效前的允許存活時間(單位:秒),僅當eternal=false時使用,默認值是0表示可存活時間無窮大。即Cache中的某元素從創建到清楚的生存時間,也就是說從創建開始計時,當超過這個時間時,此元素將從Cache中清除。

overflowToDisk:內存不足時,是否啓用磁盤緩存(即內存中對象數量達到maxElementsInMemory時,Ehcache會將對象寫到磁盤中),會根據diskStore標籤中path值查找對應的屬性值,寫入磁盤的文件會放在path文件夾下,文件的名稱是cache的名稱,後綴名是data。注意:如果緩存的對象要寫入到硬盤中的話,則該對象必須實現了Serializable接口才行。

diskPersistent:是否持久化磁盤緩存,當這個屬性的值爲true時,系統在初始化時會在磁盤中查找文件名爲cache名稱,後綴名爲index的文件。這個文件中存放了已經持久化在磁盤中的cache的index,找到後會把cache加載到內存。要想把cache真正持久化到磁盤,寫程序時注意執行net.sf.ehcache.Cache.put(Element element)後要調用flush()方法。

diskExpiryThreadIntervalSeconds:磁盤緩存的清理線程運行間隔,默認是120秒。

diskSpoolBufferSizeMB:設置DiskStore(磁盤緩存)的緩存區大小,默認是30MB。

memoryStoreEvictionPolicy:內存存儲與釋放策略,即達到maxElementsInMemory限制時,Ehcache會根據指定策略清理內存,共有三種策略:

LRU(Least Recently Used 最近最少使用);

LFU(Less Frequently Used最不常用的);

FIFO(first in first out先進先出);

SpringBoot啓動類

package com.yuanx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.cache.annotation.EnableCaching;

/**
 * 
 * @ClassName: DemoApplication
 * @Description: Demo啓動類
 * @author YuanXu
 * @date 2019年7月5日 下午4:09:33
 *
 */
@SpringBootApplication
@EnableCaching
public class DemoApplication extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(DemoApplication.class);
    }

    /**
     * @Title main
     * @Description Demo啓動入口
     * @author YuanXu
     * @param args
     */
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

EhCacheConfig配置類

package com.yuanx.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;

/**
 * 
 * @ClassName: EhCacheConfig
 * @Description: ehcache緩存Bean配置
 * @author YuanXu
 * @date 2019年7月19日 下午2:41:15
 *
 */
@Configuration
@EnableCaching
public class EhCacheConfig {

    @SuppressWarnings("unused")
    private static Logger log = LoggerFactory.getLogger(EhCacheConfig.class);

    /**
     * 
     * @Title: ehCacheManagerFactoryBean
     * @Description: EhCacheManagerFactoryBean Bean 聲明
     * @author YuanXu
     * @return
     */
    @Bean
    public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() {
        EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean();
        cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
        cacheManagerFactoryBean.setShared(true);
        return cacheManagerFactoryBean;
    }

    /**
     * 
     * @Title: ehCacheCacheManager
     * @Description: EhCacheCacheManager Bean 聲明
     * @author YuanXu
     * @param ehCacheManagerFactoryBean
     * @return
     */
    @Primary
    @Bean(name = "ehCacheCacheManager")
    public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean ehCacheManagerFactoryBean) {
        return new EhCacheCacheManager(ehCacheManagerFactoryBean.getObject());
    }

}

RedisCacheConfig配置類

package com.yuanx.config;

import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * 
 * @ClassName: RedisCacheConfig
 * @Description: redis緩存Bean配置
 * @author YuanXu
 * @date 2019年7月19日 下午2:37:56
 *
 */
@Configuration
public class RedisCacheConfig {

    @SuppressWarnings("unused")
    private static Logger log = LoggerFactory.getLogger(RedisCacheConfig.class);

    @Value("${cache.default.expire-time:1800}")
    private int defaultExpireTime;
    @Value("${cache.test.expire-time:180}")
    private int testExpireTime;
    @Value("${cache.test.name:test}")
    private String testCacheName;

    /**
     * 
     * @Title: redisCacheManager
     * @Description: RedisCacheManager Bean聲明
     * @author YuanXu
     * @param redisConnectionFactory
     * @return
     */
    @Bean(name = "redisCacheManager")
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration defaultCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();

        RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        defaultCacheConfiguration = defaultCacheConfiguration.entryTtl(Duration.ofSeconds(defaultExpireTime)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer)).disableKeyPrefix().disableCachingNullValues();

        Set<String> cacheNames = new HashSet<>();
        cacheNames.add(testCacheName);

        Map<String, RedisCacheConfiguration> cacheConfigMap = new HashMap<>();
        cacheConfigMap.put(testCacheName, defaultCacheConfiguration.entryTtl(Duration.ofSeconds(testExpireTime)));

        return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(defaultCacheConfiguration).initialCacheNames(cacheNames).withInitialCacheConfigurations(cacheConfigMap).build();
    }

}

說明:由於工程中通過@Bean定義了RedisCacheManager和EhCacheCacheManager兩個CacheManager的Bean對象,所以我們需要用@Primary標記出默認CacheManager,否則工程啓動的時候會出現下面的錯誤。

java.lang.IllegalStateException: No CacheResolver specified, and no unique bean of type CacheManager found. Mark one as primary or declare a specific CacheManager to use.
	at org.springframework.cache.interceptor.CacheAspectSupport.afterSingletonsInstantiated(CacheAspectSupport.java:223) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:866) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:127) [spring-boot-test-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:117) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:108) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:246) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:227) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) [junit-4.12.jar:4.12]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:246) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) [junit-4.12.jar:4.12]
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) [junit-4.12.jar:4.12]
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) [junit-4.12.jar:4.12]
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) [junit-4.12.jar:4.12]
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) [junit-4.12.jar:4.12]
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363) [junit-4.12.jar:4.12]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206) [.cp/:na]

2019-07-22 10:34:06.380 ERROR 32156 --- [           main] o.s.test.context.TestContextManager      : Caught exception while allowing TestExecutionListener [org.springframework.test.context.web.ServletTestExecutionListener@791f145a] to prepare test instance [com.yuanx.test.DemoTest@7a689979]

java.lang.IllegalStateException: Failed to load ApplicationContext
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:125) ~[spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:108) ~[spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190) ~[spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132) ~[spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:246) ~[spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.createTest(SpringJUnit4ClassRunner.java:227) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner$1.runReflectiveCall(SpringJUnit4ClassRunner.java:289) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) [junit-4.12.jar:4.12]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.methodBlock(SpringJUnit4ClassRunner.java:291) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:246) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) [junit-4.12.jar:4.12]
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) [junit-4.12.jar:4.12]
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) [junit-4.12.jar:4.12]
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) [junit-4.12.jar:4.12]
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) [junit-4.12.jar:4.12]
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363) [junit-4.12.jar:4.12]
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190) [spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460) [.cp/:na]
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206) [.cp/:na]
Caused by: java.lang.IllegalStateException: No CacheResolver specified, and no unique bean of type CacheManager found. Mark one as primary or declare a specific CacheManager to use.
	at org.springframework.cache.interceptor.CacheAspectSupport.afterSingletonsInstantiated(CacheAspectSupport.java:223) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:866) ~[spring-beans-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) ~[spring-context-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:127) ~[spring-boot-test-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99) ~[spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:117) ~[spring-test-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	... 25 common frames omitted

創建Entity、Repository、Service類

在需要實現緩存的類或方法上添加相關注解

  • 不指定緩存類型,使用默認緩存

  • 指定EhCache作爲緩存

  • 指定Redis作爲緩存

Spring緩存註解@Cacheable、@CacheEvict、@CachePut使用說明

Spring爲我們提供了幾個註解來支持Spring Cache。其核心主要是@Cacheable和@CacheEvict。使用@Cacheable標記的方法在執行後Spring Cache將緩存其返回結果,而使用@CacheEvict標記的方法會在方法執行前或者執行後移除Spring Cache中的某些元素。

  • @Cacheable註解

@Cacheable註解可以標記在一個方法上,也可以標記在一個類上。當標記在一個方法上時表示該方法是支持緩存的,當標記在一個類上時則表示該類所有的方法都是支持緩存的。對於一個支持緩存的方法,Spring會在其被調用後將其返回值緩存起來,以保證下次利用同樣的參數來執行該方法時可以直接從緩存中獲取結果,而不需要再次執行該方法。Spring在緩存方法的返回值時是以鍵值對進行緩存的,值就是方法的返回結果,至於鍵的話,Spring又支持兩種策略,默認策略和自定義策略,這個稍後會進行說明。需要注意的是當一個支持緩存的方法在對象內部被調用時是不會觸發緩存功能的。@Cacheable可以指定三個屬性,value、key和condition。

value屬性指定cache名稱。value屬性是必須指定的,其表示當前方法的返回值是會被緩存在哪個Cache上的,對應Cache的名稱。其可以是一個Cache也可以是多個Cache,當需要指定多個Cache時其是一個數組。

@Cacheable("cache1")//Cache是發生在cache1上的
public User find(Integer id) {
    return null;
}

@Cacheable({"cache1", "cache2"})//Cache是發生在cache1和cache2上的
public User find(Integer id) {
    return null;
}

key屬性用來自定義key。key屬性是用來指定Spring緩存方法的返回結果時對應的key的。該屬性支持SpringEL表達式。當我們沒有指定該屬性時,Spring將使用默認策略生成key。自定義策略是指我們可以通過Spring的EL表達式來指定我們的key。這裏的EL表達式可以使用方法參數及它們對應的屬性。使用方法參數時我們可以直接使用“#參數名”或者“#p參數index”。

@Cacheable(value="users", key="#id")
public User find(Integer id) {
    return null;
}

@Cacheable(value="users", key="#p0")
public User find(Integer id) {
    return null;
}

@Cacheable(value="users", key="#user.id")
public User find(User user) {
    return null;
}

@Cacheable(value="users", key="#p0.id")
public User find(User user) {
    return null;
}

說明:除了上述使用方法參數作爲key之外,Spring還爲我們提供了一個root對象可以用來生成key。通過該root對象我們可以獲取到以下信息:

屬性名稱 描述 示例
methodName 當前方法名 #root.methodName
method 當前方法 #root.method.name
target 當前被調用的對象 #root.target
targetClass 當前被調用的對象的class #root.targetClass
args 當前方法參數組成的數組 #root.args[0]
caches 當前被調用的方法使用的Cache #root.caches[0].name

當我們要使用root對象的屬性作爲key時我們也可以將“#root”省略,因爲Spring默認使用的就是root對象的屬性。

@Cacheable(value={"users", "xxx"}, key="caches[1].name")
public User find(User user) {
    return null;
}

condition屬性指定發生的條件。有的時候我們可能並不希望緩存一個方法所有的返回結果。通過condition屬性可以實現這一功能。condition屬性默認爲空,表示將緩存所有的調用情形。其值是通過SpringEL表達式來指定的,當爲true時表示進行緩存處理;當爲false時表示不進行緩存處理,即每次調用該方法時該方法都會執行一次。如下示例表示只有當user的id爲偶數時纔會進行緩存。

@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
public User find(User user) {
    System.out.println("find user by user " + user);
    return user;
}
  • @CacheEvict註解

@CacheEvict是用來標註在需要清除緩存元素的方法或類上的。當標記在一個類上時表示其中所有的方法的執行都會觸發緩存的清除操作。@CacheEvict可以指定的屬性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的語義與@Cacheable對應的屬性類似。即value表示清除操作是發生在哪些Cache上的(對應Cache的名稱);key表示需要清除的是哪個key,如未指定則會使用默認策略生成的key;condition表示清除操作發生的條件。

說明:下面我們來介紹一下新出現的兩個屬性allEntries和beforeInvocation。

allEntries屬性

allEntries是boolean類型,表示是否需要清除緩存中的所有元素。默認爲false,表示不需要。當指定了allEntries爲true時,Spring Cache將忽略指定的key。有的時候我們需要Cache一下清除所有的元素,這比一個一個清除元素更有效率。

@CacheEvict(value="users", allEntries=true)
public void delete(Integer id) {
    System.out.println("delete user by id: " + id);
}

beforeInvocation屬性

清除操作默認是在對應方法成功執行之後觸發的,即方法如果因爲拋出異常而未能成功返回時也不會觸發清除操作。使用beforeInvocation可以改變觸發清除操作的時間,當我們指定該屬性值爲true時,Spring會在調用該方法之前清除緩存中的指定元素。

@CacheEvict(value="users", beforeInvocation=true)
public void delete(Integer id) {
    System.out.println("delete user by id: " + id);
}
  • CachePut註解

在支持Spring Cache的環境下,對於使用@Cacheable標註的方法,Spring在每次執行前都會檢查Cache中是否存在相同key的緩存元素,如果存在就不再執行該方法,而是直接從緩存中獲取結果進行返回,否則纔會執行並將返回結果存入指定的緩存中。@CachePut也可以聲明一個方法支持緩存功能。與@Cacheable不同的是使用@CachePut標註的方法在執行前不會去檢查緩存中是否存在之前執行過的結果,而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的緩存中。

@CachePut也可以標註在類上和方法上。使用@CachePut時我們可以指定的屬性跟@Cacheable是一樣的。

@CachePut("users")//每次都會執行方法,並將結果存入指定的緩存中
public User find(Integer id) {
    return null;
}
  • @Caching註解

@Caching註解可以讓我們在一個方法或者類上同時指定多個Spring Cache相關的註解。其擁有三個屬性:cacheable、put和evict,分別用於指定@Cacheable、@CachePut和@CacheEvict。

@Caching(cacheable = @Cacheable("users"), evict = { @CacheEvict("cache2"), @CacheEvict(value = "cache3", allEntries = true) })
public User find(Integer id) {
    return null;
}

創建測試Controller類

package com.yuanx.demo.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.yuanx.demo.entity.TDeptEntity;
import com.yuanx.demo.entity.TOrgEntity;
import com.yuanx.demo.entity.TUserEntity;
import com.yuanx.demo.service.ITDeptService;
import com.yuanx.demo.service.ITOrgService;
import com.yuanx.demo.service.ITUserService;

/**
 * 
 * @ClassName: DemoController
 * @Description: demo功能測試Controller
 * @author YuanXu
 * @date 2019年7月19日 下午2:45:43
 *
 */
@Controller
@RequestMapping(value = "/demo")
public class DemoController {

    @SuppressWarnings("unused")
    private static Logger log = LoggerFactory.getLogger(DemoController.class);

    @Autowired
    private ITOrgService orgService;

    @Autowired
    private ITDeptService deptService;

    @Autowired
    private ITUserService userService;

    /**
     * 
     * @Title: doTest
     * @Description: 測試方法
     * @author YuanXu
     * @param request
     * @param response
     */
    @RequestMapping(value = "/doTest", method = { RequestMethod.GET, RequestMethod.POST })
    public void doTest(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("-------------------- 測試方法 執行開始-----------------------");
        System.out.println("部門信息採用Ehcache緩存方式");
        TOrgEntity org = orgService.getOne("4028e3816c097177016c0971952c0001");
        if (org != null) {
            System.out.println(org.getName());
        } else {
            System.out.println("機構不存在!");
        }

        System.out.println("部門信息採用Redis緩存方式");
        TDeptEntity dept = deptService.getOne("4028e3816c097177016c097194650000");
        if (dept != null) {
            System.out.println(dept.getName());
        } else {
            System.out.println("部門不存在!");
        }

        System.out.println("用戶信息採用默認緩存方式");
        TUserEntity user = userService.getOne("4028e3816bd5a547016bd5a55b6e0000");
        if (user != null) {
            System.out.println(user.getName());
        } else {
            System.out.println("用戶不存在!");
        }
        System.out.println("-------------------- 測試方法  執行結束 -----------------------");
    }

}

啓動項目,在瀏覽器訪問http://localhost/cacheDemo/demo/doTest,查看後臺日誌。

  • 第一次訪問
-------------------- 測試方法 執行開始-----------------------
機構信息採用Ehcache緩存方式
Hibernate: select torgentity0_.id as id1_1_0_, torgentity0_.name as name2_1_0_, torgentity0_.p_id as p_id3_1_0_ from t_org torgentity0_ where torgentity0_.id=?
測試機構
部門信息採用Redis緩存方式
Hibernate: select tdeptentit0_.id as id1_0_0_, tdeptentit0_.name as name2_0_0_, tdeptentit0_.org_id as org_id3_0_0_, tdeptentit0_.p_id as p_id4_0_0_, tdeptentit0_.p_ids as p_ids5_0_0_ from t_dept tdeptentit0_ where tdeptentit0_.id=?
測試部門
用戶信息採用默認緩存方式
Hibernate: select tuserentit0_.id as id1_2_0_, tuserentit0_.code as code2_2_0_, tuserentit0_.dept_id as dept_id3_2_0_, tuserentit0_.email as email4_2_0_, tuserentit0_.name as name5_2_0_, tuserentit0_.org_id as org_id6_2_0_, tuserentit0_.phone as phone7_2_0_, tuserentit0_.pwd as pwd8_2_0_ from t_user tuserentit0_ where tuserentit0_.id=?
測試用戶
-------------------- 測試方法  執行結束 -----------------------

所有的數據都來源於數據庫。

  • 第二次訪問
-------------------- 測試方法 執行開始-----------------------
機構信息採用Ehcache緩存方式
測試機構
部門信息採用Redis緩存方式
測試部門
用戶信息採用默認緩存方式
測試用戶
-------------------- 測試方法  執行結束 -----------------------

所有數據都來源於緩存

  • 第三次訪問
-------------------- 測試方法 執行開始-----------------------
機構信息採用Ehcache緩存方式
Hibernate: select torgentity0_.id as id1_1_0_, torgentity0_.name as name2_1_0_, torgentity0_.p_id as p_id3_1_0_ from t_org torgentity0_ where torgentity0_.id=?
測試機構
部門信息採用Redis緩存方式
測試部門
用戶信息採用默認緩存方式
Hibernate: select tuserentit0_.id as id1_2_0_, tuserentit0_.code as code2_2_0_, tuserentit0_.dept_id as dept_id3_2_0_, tuserentit0_.email as email4_2_0_, tuserentit0_.name as name5_2_0_, tuserentit0_.org_id as org_id6_2_0_, tuserentit0_.phone as phone7_2_0_, tuserentit0_.pwd as pwd8_2_0_ from t_user tuserentit0_ where tuserentit0_.id=?
測試用戶
-------------------- 測試方法  執行結束 -----------------------

由於Ehcache緩存過期時間較短,用戶數據和機構數據在緩存中已經過期,數據來源於數據庫,部門數據來源於Redis緩存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章