首页 > 技术文章 > 自定义注解,实现请求缓存【Spring Cache】

yhc-910 2021-06-15 11:07 原文

前言

偶尔看到了spring cache的文章,我去,实现原理基本相同,哈哈,大家可以结合着看看

简介

实际项目中,会遇到很多查询数据的场景,这些数据更新频率也不是很高,一般我们在业务处理时,会对这些数据进行缓存,防止多次与数据库交互。

这次我们讲的是,所有这些场景,通过一个注解即可实现。

实现过程

1、首先我们添加一个自定义注解

 

package com.bangdao.parking.applets.api.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

/**
 * 仅针对查询场景使用,其它需要更新数据的请勿使用,不然重复请求不会进行处理
 * 请求参数必须是json格式
 *
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheRequest {

    /**
     * 缓存时间,默认60秒,单位:秒
     *
     * @return
     */
    @AliasFor("value")
    int expire() default 60;

    @AliasFor("expire")
    int value() default 60;

    /**
     * 是否按用户维度区分 
     * 比如用户A和用户B先后访问同一个接口,如果该值设置未true,则根据用户区分返回,否则返回用户A的数据
     * 场景A,获取用户个人信息,则此值设为true 
     * 场景B,获取车场数据(与个人无关),则此值可设为false
     * 
     * @return
     */
    boolean byUser() default true;
    
    /**
     * 自定义key,后续便于其它接口清理缓存。若无更新操作,可忽略此配置
     * @return
     */
    String key() default "";

}

定义两个属性,

①expire,设置缓存内容的过期时间,过期后再次访问,则从数据库查询再次进行缓存,

②byUser,是否根据用户维度区分缓存,有些场景不同用户访问的是相同数据,所以这个是否设置为false,则只缓存一份,更节省缓存空间

③key,不根据参数生成缓存,自定义配置,便于后续有更新操作无法处理,具体可以看下面aop的clearCache方法

 

2、添加切面,进行数据缓存处理

@Aspect
@Configuration
public class CacheRequestAop {

    private static final Logger log = LoggerFactory.getLogger(CacheRequest.class);


    @Autowired
    private RedisService redisService;

    // 这里项目会有个拦截器校验用户登录态,然后会缓存用户信息,根据实际场景获取,如需要,可看我其它博客
    @Autowired
    private CacheService cacheService;

    // 此处注解路径,按实际项目开发进行配置
    @Pointcut("@annotation(xxx.CacheRequest)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object handler(ProceedingJoinPoint pjp) throws Throwable {

        log.info("# [BEGIN]请求缓存处理");

        // 获取注解对象
        CacheRequest annotation = getDeclaredAnnotation(pjp, CacheRequest.class);
        long expire = annotation.expire();
        boolean byUser = annotation.byUser();

        // 请求参数排序
        TreeMap<String, String> args = new TreeMap<String, String>();
        Object[] objs = pjp.getArgs();
        if (objs.length > 0) {
            // json序列化工具,大家可自行选择,建议使用springboot的jackson或者google的gson
            // 这里默认取第一个参数对象,因为我们默认为请求格式为Json
            args = JacksonUtil.jsonToObject(JacksonUtil.marshallToString(objs[0]), new TypeReference<TreeMap<String, String>>() {
            });
        }

        if (byUser) {
            args.put("userId", cacheService.getUserId());
        }
        
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        args.put("requestUrl", request.getRequestURI());

        String sign = DigestUtils.md5Hex(JacksonUtil.marshallToString(args));
        log.info("# sign:{}", sign);
        
        // 一般项目的返回都会有基类,这里的BaseResult就是
        Object result = redisService.get("request:cache:" + sign, BaseResult.class);
        // 如果有缓存,则不会进行处理,直接返回缓存结果
        if (result != null) {
            log.info("# [END]请求返回缓存数据");
            return result;
        }

        // 不存在缓存,就进行处理,处理完成在进行缓存
        result = pjp.proceed();
        redisService.set("request:cache:" + sign, result, expire);

        log.info("# [END]请求缓存处理");
        return result;
    }

    /**
     * 获取当前注解对象
     * 
     * @param <T>
     * @param joinPoint
     * @param clazz
     * @return
     * @throws NoSuchMethodException
     */
    public static <T extends Annotation> T getDeclaredAnnotation(ProceedingJoinPoint joinPoint, Class<T> clazz) throws NoSuchMethodException {
        // 获取方法名
        String methodName = joinPoint.getSignature().getName();
        // 反射获取目标类
        Class<?> targetClass = joinPoint.getTarget().getClass();
        // 拿到方法对应的参数类型
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
        // 根据类、方法、参数类型(重载)获取到方法的具体信息
        Method objMethod = targetClass.getMethod(methodName, parameterTypes);
        // 拿到方法定义的注解信息
        T annotation = objMethod.getDeclaredAnnotation(clazz);
        // 返回
        return annotation;
    }
    
    public boolean clearCache(String key) {
        return clearCache(key, true);
    }

    public boolean clearCache(String key, boolean byUser) {
        TreeMap<String, Object> args = new TreeMap<String, Object>();
        args.put("key", key);
        if (byUser) {
            args.put("openId", cacheService.getUserId());
        }
        String sign = DigestUtils.md5Hex(JacksonUtil.marshallToString(args));
        return redisService.delete(RedisKeyPrefixConts.CACHE_REQUEST + sign);
    }

}

添加切面处理,一般根据三个维度进行缓存(请求地址、用户、请求参数),第一次请求进行返回数据的缓存,后续请求则直接获取缓存数据,不进入接口进行逻辑处理。

 有些需要更新信息的场景,需要更新数据后返回最新数据,则可以自定义key,在更新操作时调用clearCache方法即可。

 

3、项目使用

    // 使用默认配置,过期60S,根据用户维度区分
    @CacheRequest
    @RequestMapping("/test1")
    public void test1() {}
    
    // 过期60S,根据用户维度区分
    @CacheRequest(60)
    @RequestMapping("/test2")
    public void test2() {}
    
    // 过期60S,不根据用户维度区分
    @CacheRequest(expire = 60,byUser = false)
    @RequestMapping("/test3")
    public void test3() {}
    
    // 自定义key,便于后续更新操作可清空缓存,定义key时,说明有更新操作,则只需在业务处理时,注入切面,调用clearCache方法即可
    @CacheRequest(expire = 60,key="test4")
    @RequestMapping("/test4")
    public void test4() {}    

实际开发,只需要在请求接口添加注解,根据实际场景配置属性即可

 

4、测试

 

 

 可看到第二次请求,直接走的缓存返回结果,未进入接口进行逻辑处理。

 

大家有疑问或更好的建议,可以提出来,楼主看到会第一时间反应,谢谢。

 

推荐阅读