使用AOP技术通过自定义注解实现统一缓存

使用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的存、取等公共方法类。

猜你喜欢

转载自blog.csdn.net/weixin_48363639/article/details/128399141