使用AOP技术实现自定义缓存注解
使用java做业务开发,缓存是必不可少的性能提升技术。如果把缓存的逻辑加入到业务代码中, 有几个弊端: 1)代码冗余。 2)代码逻辑侵入性高,容易引入bug, 3)不同的人代码风格不同,导致后期维护困难。 有一种办法,在入口上增加缓存注解,接口在调用之前,自动获取缓存,没有缓存在走接口逻辑, 这样就清晰多了,代码也干净多了, 要实现上面的思路,需要有三部需要做
第一步:写切面逻辑,创建一个切面文件,如下:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.local.datashadow.annotion.LocalRedisCache;
import com.local.datashadow.utils.DateUtils;
import com.local.datashadow.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Type;
import java.util.concurrent.TimeUnit;
/**
* @Author : test
* @create 2022/12/07 14:42
* description:
*/
@Slf4j
@Aspect
@Component
public class RedisCacheAspect {
private static ExpressionParser parser = new SpelExpressionParser();
@Pointcut("@annotation(com.local.datashadow.annotion.LocalRedisCache)")//注解在方法上
public void pointcut() {
System.out.println( "进入切面执行" );
}
/**
* 环绕通知
* 根据redisKey先查缓存、缓存没有就查数据库,然后将结果缓存
*/
@Around(value = "pointcut() && @annotation(localRedisCache)", argNames = "pjp,localRedisCache")
public Object AroundMethod(ProceedingJoinPoint pjp, LocalRedisCache localRedisCache) throws Throwable {
log.info("redis缓存切面开始");
String redisKey = localRedisCache.redisKey();
// 1.获取参数对象数组
Object[] args = pjp.getArgs();
// 2.获取参数名数组
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
String[] argNames = methodSignature.getParameterNames();
// 3.将参数名和参数值数组放入context
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < args.length; i++) {
context.setVariable(argNames[i], args[i]);
}
//spring EL表达式,此处进行了key的组装,此处需要注意,如果key不需要带参数,需要把key放到注解的单引号里引起来,不然报错
redisKey = buildKey(redisKey,context);
String redisEventValue = RedisUtil.get( redisKey );
long expirationTime = localRedisCache.expirationTime();
if (expirationTime == 0 ){
log.info("获取的缓存时长=",expirationTime);
expirationTime = DateUtils.getExpirationTime();
}
Type genericReturnType = methodSignature.getMethod().getGenericReturnType();
if(redisEventValue == null){
log.info("redis key="+redisKey+"缓存不存在...");
Object result = pjp.proceed();
if (result != null){
JSONObject jsonObject = (JSONObject)JSON.toJSON(result);
if (("00000".equals(jsonObject.getString("rspCode")) ||
"00000".equals(jsonObject.getString("code")))
&& (jsonObject.getInteger("resultDataStatus") == 1)){
log.info("将 key="+redisKey+" 缓存到redis里面...");
RedisUtil.set(redisKey, JSON.toJSONString(result), expirationTime, TimeUnit.MILLISECONDS);
}
} else {
log.info("查询结果不存在不缓存到redis里面...");
}
return result;
}else {
log.info(" key="+redisKey+"的结果是从缓存里面获取的");
Object returnObj = JSONObject.parseObject(redisEventValue, genericReturnType);
return returnObj;
}
}
protected static String buildKey(String key, EvaluationContext context) {
String combined = "";
if (key != null && key.length()>0) {
combined = parser.parseExpression(key).getValue(context).toString();
}
return combined;
}
}
第二部:编写自定义注解接口
package com.local.datashadow.annotion;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author : test
* @create 2022/12/07 14:28
* description:
* Redis 缓存设置
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LocalRedisCache {
String redisKey() default "";
/**
* 缓存默认时长毫秒
* @return
*/
long expirationTime() default 0;
}
接口中的属性,依照自己的需要添加,这个属性会通过切面被拦截到,然后在切面里拿到,做相应的处理。
第三步:使用方法一:
上面的工作做好了以后,就开始使用了,在service的实现类里,方法上面加上注解
比如下面:
@LocalRedisCache(redisKey = "'dataShadow:kpi_month_user_active_summary'")
@Override
public BaseResponse getMonthUserActiveSummary() {
log.info("月活逻辑处理开始");
SummarySearchDTO searchDto = new SummarySearchDTO();
searchDto.setDate6Month();
String endMonth = searchDto.getEndDate().substring(0, 6);
String starMonth = searchDto.getStarDate().substring(0, 6);
searchDto.setEndDate(endMonth);
searchDto.setStarDate(starMonth);
注意上面使用的注解,就是自定义注解,注解括号里的参数就是自定义注解接口中定义的参数
第三步:使用方法二:
有些rediskey需要和参数关联上,比如给用户推荐的内容,因为不同的用户id或者设备id他的推荐不同,这个时候,rediskey就需要和用户的属性关联上,也就是rediskey需要带上参数
比如:
@Override
@LocalRedisCache(
redisKey = "'dataShadow:event_property_' + #carEventDimPo.eventCode",expirationTime = 86400000)
public BaseResponse<List> getCarEventProperties(CarEventDimPo carEventDimPo) {
try {
Integer eventNum = carEventDimPo.getEventNum();
List<String> eventCodeList = new ArrayList<>();
//查询公共属性
eventCodeList.add("all");
使用#号取参数,直接把请求参数中的这个值获取到,这个方式是使用了EL表达式,EL表达式自行学习。
值得注意的是,key里的字符串需要使用单引号引起来,用来处理成string字符串。
上面三个步骤是redis的使用,当然在使用前需要做准备工作,比如配置redis环境,如下配置文件
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
redis的properties参数配置在了nacos统一文件里。
spring:
main:
allow-bean-definition-overriding: true
redis:
# Redis服务器地址 ***.**.**.***
host: ***************.com
# Redis服务器连接端口
port: 6379
# Redis数据库索引(默认为0)
database: 2
# Redis服务器连接密码(默认为空)
password: ********
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
#timeout: 30000
除了配置文件还有redis的存、取等公共方法类。