
我准备开一个系列,就是写一些在简要的学习项目中可能会用到的奇奇怪怪的功能,比如线程池或者统一异常处理类
取名为【项目demo】系列
然后将会同步到GitHub中:https://github.com/Livorth/FunctionalLearning
这里推荐看 统一缓存帝国,实战 Spring Cache!,不能说感受良多,只能说受益匪浅
但是讲Spring Cache不是我的本意,所以Spring Cache的部分就不过多介绍,只简单描述使用过程
SpringBoot系列之缓存使用教程介绍了其他相关的注解
自定义注解的实现,也就是使用AOP进行切片处理
这里我主要参考
上面这个相对简单,下面这个相当复杂,不过我还是一下面这个为蓝本来写的
自定义缓存注解RedisCache因为是自定义的,所以可以根据自己的需求来进行变动,我按照我自己的想法进行了一定的改动
package cn.livorth.functionallearning.common.cache;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@documented
public @interface RedisCache {
// key的前缀
String nameSpace() default "";
// key
String key() default "";
// 设置过期时间,默认1分钟
long expireTime() default 1 * 60 * 1000;
// 是否为查询操作,如果为写入数据库的操作,该值需置为 false
boolean read() default true;
}
具体AOP代理方法类RedisCacheAspect
和常规的AOP代理方法类相同,主要的是其中实现的具体逻辑
其中有两个地方需要注意
package cn.livorth.functionallearning.common.cache;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
@Slf4j
public class RedisCacheAspect {
@Resource
private RedisHandler handler;
@Pointcut(value = "@annotation(cn.livorth.functionallearning.common.cache.RedisCache)")
public void redisCache() {
}
// 在使用 redisCache 注解的地方织入此切点
@Around(value = "redisCache()")
private Object saveCache(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("<======拦截到redisCache方法:{}.{}======>" ,
proceedingJoinPoint.getTarget().getClass().getName(), proceedingJoinPoint.getSignature().getName());
// 获取切入的方法对象
// 这个m是代理对象的,没有包含注解
Method m = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
// this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
Method methodWithAnnotations = proceedingJoinPoint.getTarget().getClass().getDeclaredMethod(
proceedingJoinPoint.getSignature().getName(), m.getParameterTypes());
Object result;
// 根据目标方法对象获取注解对象
RedisCache annotation = methodWithAnnotations.getDeclaredAnnotation(RedisCache.class);
// 解析key
String key = parseKey(methodWithAnnotations, proceedingJoinPoint.getArgs(), annotation.key(), annotation.nameSpace());
// 到redis中获取缓存
log.info("<====== 通过key:{}从redis中查询 ======>", key);
String cache = handler.getCache(key);
if (cache == null) {
log.info("<====== Redis 中不存在该记录,从数据库查找 ======>");
// 若不存在,则到数据库中去获取
result = proceedingJoinPoint.proceed();
if (result != null) {
// 从数据库获取后存入redis, 若有指定过期时间,则设置
long expireTime = annotation.expireTime();
if (expireTime != -1) {
handler.saveCache(key, result, expireTime, TimeUnit.SECONDS);
} else {
handler.saveCache(key, result);
}
}
// else {
// // 这里可以做一个布隆过滤器的处理
// }
return result;
} else {
// 如果缓存中存在数据
return deSerialize(m, cache);
}
}
private Object deSerialize(Method m, String cache) {
// 原方法的返回数据类型类
Class returnTypeClass = m.getReturnType();
log.info("从缓存中获取数据:{},返回类型为:{}" , cache, returnTypeClass);
Object object = null;
// 原方法的返回数据类型类
Type returnType = m.getGenericReturnType();
// 判断是否是ParameterizedType的实例,即泛型
if(returnType instanceof ParameterizedType){
ParameterizedType type = (ParameterizedType) returnType;
Type[] typeArguments = type.getActualTypeArguments();
for(Type typeArgument : typeArguments){
// 如果是泛型则需要将其中每个单独转换
Class typeArgClass = (Class) typeArgument;
log.info("<======获取到泛型:{}" , typeArgClass.getName());
object = JSON.parseArray(cache, typeArgClass);
}
}else {
// 不是泛型则直接转换
object = JSON.parseObject(cache, returnTypeClass);
}
return object;
}
private String parseKey(Method method, Object[] argValues, String key, String nameSpace) {
// 创建解析器,但是Spring表达式语言我并不是很熟悉,所以这里暂时不考虑使用
// expressionParser parser = new SpelexpressionParser();
// expression expression = parser.parseexpression(key);
// evaluationContext context = new StandardevaluationContext();
//
// // 添加参数
// DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
// String[] parameterNames = discover.getParameterNames(method);
// for (int i = 0; i < parameterNames.length; i++) {
// context.setVariable(parameterNames[i], argValues[i]);
// }
// // 解析
// return nameSpace + expression.getValue(context).toString();
// 简单点也可以 命名空间+key+方法名+参数的MD5加密
StringBuilder prefix = new StringBuilder();
prefix.append(nameSpace).append(".").append(key);
prefix.append(".").append(method.getName());
StringBuilder sb = new StringBuilder();
for (Object obj : argValues) {
sb.append(obj.toString());
}
return prefix.append(DigestUtils.md5DigestAsHex(sb.toString().getBytes())).toString();
}
@Component
class RedisHandler {
@Resource
RedisTemplate cache;
void saveCache(String key, T t, long expireTime, TimeUnit unit) {
String value = JSON.toJSONString(t);
log.info("<====== 存入Redis 数据:{}", value);
cache.opsForValue().set(key, value, expireTime, unit);
}
void saveCache(String key, T t) {
String value = JSON.toJSONString(t, SerializerFeature.WRITE_MAP_NULL_FEATURES);
cache.opsForValue().set(key, value);
}
void removeCache(String key) {
cache.delete(key);
}
String getCache(String key) {
return cache.opsForValue().get(key);
}
}
}
注意这里面的RedisHandler其实可以使用之前自己写的redis封装类RedisUtils
然后就是布隆过滤器的使用,其实我只知道概念,具体使用暂时还不清楚
进行测试说是测试也只是加个注解的事情
UserController.java
@LogAnnotation(logModule = "user", logType = "select", logDescription = "通过分页获取所有用户信息")
@RedisCache(nameSpace = "user", key = "getAllUserByPage")
@GetMapping("page/{thePage}/{pageSize}")
public List getAllUserByPage(@PathVariable("thePage") int thePage, @PathVariable("pageSize") int pageSize){
Page page = new Page<>(thePage, pageSize);
return userService.getAllUserByPage(page);
}
然后进行多次访问并查看日志信息
http://localhost:8888/user/page/3/5
第一次是对数据库中信息进行访问,第二次测试从缓存中找
写在后面
注解自定义,代理方法类自己实现,很多东西的决定权在自己手上
要想简单写可以写的很简单,要写复杂那都不是几百行可以搞定的
只能说看需求来吧,我这也只能起到一个抛砖引玉的作用
我的项目demo系列都会同步到GitHub上,欢迎围观
https://github.com/Livorth/FunctionalLearning