spring目前在@Cacheable和@CacheEvict等注解上不支持缓存时效设置,只允许通过配置文件设置全局时效。这样就很不方便设定时间。比如系统参数和业务数据的时效是不一样的,这给程序开发造成很大的困扰。不得已,本人实现了类似Sping的三个注解。以下是具体实现。
1、注解类的实现
package com.example.spring.boot.redis.annotation;
import java.lang.annotation.*;
/**
* Author: 王俊超
* Date: 2017-06-10 05:56
* All Rights Reserved !!!
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RedisCacheEvict {
/**
* 缓存名称
*
* @return
*/
String cacheName();
/**
* 缓存key
*
* @return
*/
String key();
}
package com.example.spring.boot.redis.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* Author: 王俊超
* Date: 2017-06-10 05:56
* All Rights Reserved !!!
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RedisCacheGet {
/**
* 缓存名称
*
* @return
*/
String cacheName();
/**
* 缓存key
*
* @return
*/
String key();
/**
* 缓存过期时间
*
* @return
*/
int expire() default 0;
/**
* 缓存的时间单位
*
* @return
*/
TimeUnit timeUnit();
}
package com.example.spring.boot.redis.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* Author: 王俊超
* Date: 2017-06-10 05:56
* All Rights Reserved !!!
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RedisCachePut {
/**
* 缓存名称
*
* @return
*/
String cacheName();
/**
* 缓存key
*
* @return
*/
String key();
/**
* 缓存过期时间
*
* @return
*/
int expire() default 0;
/**
* 缓存的时间单位
*
* @return
*/
TimeUnit timeUnit();
}
2、切面代码实现
package com.example.spring.boot.redis.aspect;
import com.example.spring.boot.redis.annotation.RedisCacheEvict;
import com.example.spring.boot.redis.annotation.RedisCacheGet;
import com.example.spring.boot.redis.annotation.RedisCachePut;
import com.example.spring.boot.redis.common.RedisClient;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
/**
* 缓存处理时间切面
*
* Author: 王俊超
* Date: 2017-06-10 06:17
* All Rights Reserved !!!
*/
@Aspect
public class RedisCacheAspect {
private final static char DOT = '.';
private final static char SHARP = '#';
private RedisClient redisClient;
public RedisClient getRedisClient() {
return redisClient;
}
public void setRedisClient(RedisClient redisClient) {
this.redisClient = redisClient;
}
/**
* 对象入缓存
* @param pjp
* @param cachePut
* @return
* @throws Throwable
*/
@Around("@annotation(cachePut)")
public Object cachePut(ProceedingJoinPoint pjp, RedisCachePut cachePut) throws Throwable {
Object keyObject = getCacheKey(pjp, cachePut.key());
Object result = pjp.proceed();
// 不为空才保存数据
if (result != null && keyObject != null){
redisClient.set(cachePut.cacheName(), keyObject, result, cachePut.expire());
}
return result;
}
/**
* 优先从缓存中取对象
*
* @param pjp
* @param cacheGet
* @return
* @throws Throwable
*/
@Around("@annotation(cacheGet)")
public Object cacheGet(ProceedingJoinPoint pjp, RedisCacheGet cacheGet) throws Throwable {
Object keyObject = getCacheKey(pjp, cacheGet.key());
Object result = redisClient.get(cacheGet.cacheName(), keyObject);
// 如果从缓存中没有取到数据,就从调用方法获取数据
if (result == null) {
result = pjp.proceed();
// 方法的返回值不是void类型,就要将结果入缓存
Class clz = getAdvicedMethod(pjp).getReturnType();
if (clz != Void.class) {
redisClient.set(cacheGet.cacheName(), keyObject, result, cacheGet.expire());
}
}
return result;
}
/**
* 删除缓存
*
* @param pjp
* @param cacheEvict
* @return
* @throws Throwable
*/
@Around("@annotation(cacheEvict)")
public Object cacheEvict(ProceedingJoinPoint pjp, RedisCacheEvict cacheEvict) throws Throwable {
Object keyObject = getCacheKey(pjp, cacheEvict.key());
redisClient.del(cacheEvict.cacheName(), keyObject);
return pjp.proceed();
}
/**
* 获取缓存的key对象
*
* @param pjp
* @param key
* @return
* @throws Exception
*/
private Object getCacheKey(ProceedingJoinPoint pjp, String key) throws Exception {
// 以#开头
if (key.length() > 0 && key.charAt(0) == SHARP) {
// 去掉#
key = key.substring(1);
// 将key分割成属性和参数名,第一个“.”之前是参数名,之后是属性名称
int dotIdx = key.indexOf(DOT);
String argName = key;
if (dotIdx > 0) {
argName = key.substring(0, dotIdx);
key = key.substring(dotIdx + 1); // 剩下的属性
}
// 取参数值
Object argVal = getArg(pjp, argName);
// 获取参数的属性值
Object objectKey = argVal;
if (dotIdx > 0) {
objectKey = getObjectKey(argVal, key);
}
return objectKey;
} else { // 不是以#开头的就以其值作为参数key
return key;
}
}
/**
* 获取参数对象
*
* @param pjp 连接点
* @param parameterName 参数名称
* @return
*/
private Object getArg(ProceedingJoinPoint pjp, String parameterName) throws NoSuchMethodException {
Method method = getAdvicedMethod(pjp);
ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
if (parameterNames != null) {
int idx = 0;
for (String name : parameterNames) {
if (name.equals(parameterName)) {
return pjp.getArgs()[idx];
}
idx++;
}
}
throw new IllegalArgumentException("no such parameter name: [" + parameterName + "] in method: " + method);
}
/**
* 获取拦截的方法
*
* @param pjp 连接点
* @return
* @throws NoSuchMethodException
*/
private Method getAdvicedMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException {
Signature sig = pjp.getSignature();
MethodSignature msig = null;
if (!(sig instanceof MethodSignature)) {
throw new IllegalArgumentException("annotation can only use to method.");
}
msig = (MethodSignature) sig;
Object target = pjp.getTarget();
return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
}
/**
* 获取从object上获取key所对应的属性对象
* 例如:key: country.province.city.town
* 就相当于调用:object.getCountry().getProvince().getCity.getTown()
*
* @param object
* @param key
* @return
* @throws Exception
*/
private Object getObjectKey(Object object, String key) throws Exception {
// 如果key已经是空了就直接返回
if (StringUtils.isEmpty(key)) {
return object;
}
int dotIdx = key.indexOf(DOT);
// 形如key=aa.bb**的情况
if (dotIdx > 0) {
// 取第一个属性值
String propertyName = key.substring(0, dotIdx);
// 取剩下的key
key = key.substring(dotIdx + 1);
Object property = getProperty(object, getterMethod(propertyName));
return getObjectKey(property, key);
} else { // key=aa
return getProperty(object, getterMethod(key));
}
}
/**
* 获取name的getter方法名称
*
* @param name
* @return
*/
private String getterMethod(String name) {
return "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
/**
* 调用obj对象上的getterMethodName
*
* @param obj
* @param getterMethodName
* @return
* @throws Exception
*/
private Object getProperty(Object obj, String getterMethodName) throws Exception {
return obj.getClass().getMethod(getterMethodName).invoke(obj);
}
}
3、工具类实现
package com.example.spring.boot.redis.common;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
/**
* 序列化和反序列化工具
*
* Author: 王俊超
* Date: 2017-05-31 21:43
* All Rights Reserved !!!
*/
public class KryoRedisSerializer implements RedisSerializer {
private Kryo kryo = new Kryo();
@Override
public byte[] serialize(T t) throws SerializationException {
byte[] buffer = new byte[2048];
Output output = new Output(buffer);
kryo.writeClassAndObject(output, t);
byte[] result = output.toBytes();
output.close();
return result;
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
Input input = new Input(bytes);
@SuppressWarnings("unchecked")
T t = (T) kryo.readClassAndObject(input);
input.close();
return t;
}
}
package com.example.spring.boot.redis.common;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.util.Set;
/**
* Redis客户端实现工具
*
* Author: 王俊超
* Date: 2017-06-04 19:57
* All Rights Reserved !!!
*/
public class RedisClientImpl implements RedisClient {
private RedisTemplate
4、应用配置
package com.example.spring.boot.redis;
import com.example.spring.boot.redis.aspect.RedisCacheAspect;
import com.example.spring.boot.redis.common.KryoRedisSerializer;
import com.example.spring.boot.redis.common.RedisClient;
import com.example.spring.boot.redis.common.RedisClientImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
/**
* Author: 王俊超
* Date: 2017-05-07 10:02
* All Rights Reserved !!!
*/
@Configuration
public class AppConfig {
/**
* Redis连接工厂
*
* @return
*/
@Primary
@Bean("redisConnectionFactory")
public RedisConnectionFactory redisConnectionFactory() {
// Redis集群地址
List clusterNodes = Arrays.asList("192.168.241.150:7110",
"192.168.241.150:7111", "192.168.241.150:7112", "192.168.241.150:7113",
"192.168.241.150:7114", "192.168.241.150:7115", "192.168.241.150:7116",
"192.168.241.150:7117", "192.168.241.150:7118", "192.168.241.150:7119"
);
// 获取Redis集群配置信息
RedisClusterConfiguration rcf = new RedisClusterConfiguration(clusterNodes);
return new JedisConnectionFactory(rcf);
}
/**
* 创建redis模板
*
* @param factory
* @return
* @throws UnknownHostException
*/
@Primary
@Bean("redisTemplate")
public RedisTemplate redisTemplate(RedisConnectionFactory factory)
throws UnknownHostException {
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 可根据需要设置
// // redis value使用的序列化器
// template.setValueSerializer(new KryoRedisSerializer<>());
// template.setHashKeySerializer(new KryoRedisSerializer<>());
// // redis key使用的序列化器
// template.setKeySerializer(new KryoRedisSerializer<>());
// template.setHashValueSerializer(new KryoRedisSerializer<>());
template.afterPropertiesSet();
return template;
}
/**
* 返回redis客户端
*
* @param redisTemplate
* @return
*/
@Bean
public RedisClient redisClient(RedisTemplate redisTemplate) {
RedisClientImpl redisClient = new RedisClientImpl();
redisClient.setRedisTemplate(redisTemplate);
KryoRedisSerializer serializer = new KryoRedisSerializer<>();
redisClient.setKeySerializer(serializer);
redisClient.setValSerializer(serializer);
return redisClient;
}
/**
* redis缓存的切面
* @param redisClient
* @return
*/
@Bean
public RedisCacheAspect redisCacheAspect(RedisClient redisClient) {
RedisCacheAspect aspect = new RedisCacheAspect();
aspect.setRedisClient(redisClient);
return aspect;
}
}
5、应用的启动
package com.example.spring.boot.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Author: 王俊超
* Date: 2017-05-07 10:04
* All Rights Reserved !!!
*/
@SpringBootApplication
@EnableTransactionManagement
@EnableCaching
@EnableAutoConfiguration
//@EnableAspectJAutoProxy
public class AppRunner {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(AppRunner.class, args);
}
}
6、配置文件
server.context-path=/redis/cache
# 开启AOP
spring.aop.auto=true
1、测试数据
package com.example.spring.boot.redis.entity;
/**
* 县
* Author: 王俊超
* Date: 2017-06-12 20:06
* All Rights Reserved !!!
*/
public class City {
private long id;
private String name;
private Province province;
public City() {
}
public City(long id, String name, Province province) {
this.id = id;
this.name = name;
this.province = province;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Province getProvince() {
return province;
}
public void setProvince(Province province) {
this.province = province;
}
}
package com.example.spring.boot.redis.entity;
/**
* 国家
* Author: 王俊超
* Date: 2017-06-12 20:06
* All Rights Reserved !!!
*/
public class Country {
private long id;
private String name;
public Country() {
}
public Country(long id, String name) {
this.id = id;
this.name = name;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.example.spring.boot.redis.entity;
/**
* 省
* Author: 王俊超
* Date: 2017-06-12 20:06
* All Rights Reserved !!!
*/
public class Province {
private long id;
private String name;
private Country country;
public Province() {
}
public Province(long id, String name, Country country) {
this.id = id;
this.name = name;
this.country = country;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Country getCountry() {
return country;
}
public void setCountry(Country country) {
this.country = country;
}
}
package com.example.spring.boot.redis.entity;
/**
* 镇
* Author: 王俊超
* Date: 2017-06-12 20:06
* All Rights Reserved !!!
*/
public class Town {
private long id;
private String name;
private City city;
public Town() {
}
public Town(long id, String name, City city) {
this.id = id;
this.name = name;
this.city = city;
}
public long getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
}
package com.example.spring.boot.redis;
import com.example.spring.boot.redis.annotation.RedisCacheEvict;
import com.example.spring.boot.redis.annotation.RedisCacheGet;
import com.example.spring.boot.redis.annotation.RedisCachePut;
import com.example.spring.boot.redis.entity.City;
import com.example.spring.boot.redis.entity.Country;
import com.example.spring.boot.redis.entity.Province;
import com.example.spring.boot.redis.entity.Town;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* Author: 王俊超
* Date: 2017-06-10 06:26
* All Rights Reserved !!!
*/
@Component
public class TestData {
public final static Country COUNTRY = new Country(111111, "CHINA");
public final static Province PROVINCE = new Province(222222, "GuangZhou", COUNTRY);
public final static City CITY = new City(333333, "ShenZhen", PROVINCE);
public final static Town TOWN = new Town(444444, "Where", CITY);
public final static String LOCATION = "location";
@RedisCachePut(cacheName = LOCATION, key = "#town.city.province.country.id",
expire = 60, timeUnit = TimeUnit.SECONDS)
public Country createCountry(Town town) {
return town.getCity().getProvince().getCountry();
}
@RedisCacheGet(cacheName = LOCATION, key = "#id", expire = 60, timeUnit = TimeUnit.SECONDS)
public Country getCountry(long id) {
return COUNTRY;
}
@RedisCacheEvict(cacheName = LOCATION, key = "#id")
public void deleteCountry(long id) {
// 清除缓存
}
}
2、测试用例
import com.example.spring.boot.redis.AppRunner;
import com.example.spring.boot.redis.TestData;
import com.example.spring.boot.redis.common.KryoRedisSerializer;
import com.example.spring.boot.redis.common.RedisClient;
import com.example.spring.boot.redis.entity.Country;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* Author: 王俊超
* Date: 2017-06-12 21:10
* All Rights Reserved !!!
*/
@RunWith(SpringRunner.class)
@SpringBootTest( classes = AppRunner.class)
public class TestRunner {
@Autowired
RedisClient redisClient;
@Autowired
TestData testData;
/**
* 测试序列化,反序列化
*/
@Test
public void testSerialize() {
Country c1 = testData.createCountry(TestData.TOWN);
KryoRedisSerializer serializer = new KryoRedisSerializer<>();
byte[] b1 = serializer.serialize(c1);
Country c2 = (Country) serializer.deserialize(b1);
Assert.assertNotNull(c2);
Assert.assertEquals(c1.getId(), c2.getId());
Assert.assertEquals(c1.getName(), c2.getName());
}
@Test
public void testCachePut() {
// 先清理缓存
redisClient.del(TestData.LOCATION, TestData.COUNTRY.getId());
Country c = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId());
Assert.assertNull(c);
// 创建一个国家
Country c1 = testData.createCountry(TestData.TOWN);
// 直接从缓存中取数据,说明数据已经入缓存
Country c2 = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId());
Assert.assertNotNull(c2);
Assert.assertEquals(c1.getId(), c2.getId());
Assert.assertEquals(c1.getName(), c2.getName());
}
@Test
public void testCacheGet() {
// 先清理缓存
redisClient.del(TestData.LOCATION, TestData.COUNTRY.getId());
Country c = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId());
Assert.assertNull(c);
// 取数据
Country c1 = testData.getCountry(TestData.COUNTRY.getId());
// 从缓存中取
Country c2 = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId());
Assert.assertNotNull(c2);
Assert.assertEquals(c1.getId(), c2.getId());
Assert.assertEquals(c1.getName(), c2.getName());
}
@Test
public void testCacheEvict() {
// 创建一个国家
Country c = testData.createCountry(TestData.TOWN);
redisClient.del(TestData.LOCATION, TestData.COUNTRY.getId());
c = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId());
Assert.assertNull(c);
}
}