自定义注解,实现请求缓存【Spring Cache】
阅读原文时间:2023年09月06日阅读:6

前言

偶尔看到了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、测试

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

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