上一篇博客说到虽然使用Spring注解来指定缓存非常方便,但是想要进行非常详细、系统的设计缓存数据结构,仅仅使用自带注解还是完全不够的。
https://blog.csdn.net/weixin_43041241/article/details/89338076
本博客就是之前的博客进行的拓展:使用自定义注解+AOP完成定制Redis缓存结构。
·---------------------------------------------------------------------------------------------
首先,介绍一个数据结构:
NewsDetail是一条新闻,每条新闻都有新闻评论NewsComment。下面正式开始:
一、自定义注解:
1、@RedisSelect用来标记需要缓存查询
package com.cod4man.fakenews.annotation.redis;
import java.lang.annotation.*;
/**
* Created with IntelliJ IDEA.
*
* @author 张鸿杰
* Date:2019-04-17
* Time:9:58
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisSelect {
String group() ;
String key() ;
String returnType() default "Object";
boolean isCollection() default false;
String comment() default "";
}
2、@RedisDelete 用来标注需要缓存删除
package com.cod4man.fakenews.annotation.redis;
import java.lang.annotation.*;
/**
* Created with IntelliJ IDEA.
*
* @author 张鸿杰
* Date:2019-04-17
* Time:10:03
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisDelete {
String group() ;
String key() ;
String returnType() default "Object";
boolean isCollection() default false;
String comment() default "";
}
二、使用AOP设计缓存:
jar依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
Redis提供的java操作工具Jedis
<bean id="jedis" class="redis.clients.jedis.Jedis">
<constructor-arg name="host" value="127.0.0.1"/>
<constructor-arg name="port" value="6379"/>
</bean>
package com.cod4man.fakenews.redisaop;
import com.alibaba.fastjson.JSON;
import com.cod4man.fakenews.annotation.redis.RedisDelete;
import com.cod4man.fakenews.annotation.redis.RedisSelect;
import com.cod4man.fakenews.pojo.NewsComment;
import com.cod4man.fakenews.pojo.NewsDetail;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Jedis;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* Created with IntelliJ IDEA.
*
* @author 张鸿杰
* Date:2019-04-16
* Time:20:39
*/
@Aspect
public class FindAllNewsDetailAop {
private static final Logger log = Logger.getLogger(FindAllNewsDetailAop.class);
//Redis提供的java操作工具
@Autowired
private Jedis jedis ;
//AOP拦截RedisSelect注解,查询缓存操作
@Around("@annotation(com.cod4man.fakenews.annotation.redis.RedisSelect)")
public Object aroundLogger_SetAll(ProceedingJoinPoint jp) throws Throwable {
log.info("调用" + jp.getTarget() + "的" + jp.getSignature().getName() + "方法!方法参数为" + Arrays.toString(jp.getArgs()));
try {
//拿到方法
Method method= getMethod(jp);
//拿到注解的group属性值
String redisKey = method.getAnnotation(RedisSelect.class).group();
//判断是新闻还是评论,这里是将新闻列表和评论列表集合到同一个AOP方法进行处理
if ("CommentsByNewsId".equals(redisKey)) {
//Redis的键拼上评论id
redisKey += jp.getArgs()[0].toString();
}
//AOP拦截到查询后先不查主数据库,而是先从Redis缓存中查找
Map<String, String> mapNews = jedis.hgetAll(redisKey);
if (mapNews != null && !"{}".equals(mapNews.toString())) {
List<Object> list = new ArrayList<>();
for (String value : mapNews.values()) {
list.add(JSON.parse(value));
}
//缓存中有数据,则进行数据处理后,直接return结束方法,那么就不会再查询主数据库,从而减轻压力
return list;
}
//如果查询不到,说明是第一次查询,就需要查询数据库
//使用反射拿到返回值类型
Type returnType = method.getGenericReturnType();
Object result = jp.proceed();
System.out.println("returnType" + returnType.toString());
//将查询到的数据按一定数据结构存入Redis缓存
if ("java.util.List<com.cod4man.fakenews.pojo.NewsDetail>".equals(returnType.toString())) {
List<NewsDetail> list = (List<NewsDetail>) result;
for (NewsDetail newsDetail : list) {
//存入数据格式:大Key==》NewsDetail;
// 小Key ==》新闻id(可以保证唯一)NewsDetail.getId();
// Value==》新闻详情NewsDetail
jedis.hset(redisKey, newsDetail.getId().toString(), JSON.toJSONString(newsDetail));
}
} else if ("java.util.List<com.cod4man.fakenews.pojo.NewsComment>".equals(returnType.toString())) {
List<NewsComment> list = (List<NewsComment>) result;
for (NewsComment newsComment : list) {
//存入数据格式:大Key==》NewsComment+(新闻编号NewsDetail.getId());
// 小Key ==》新闻评论id(可以保证唯一)NewsComment.getId();
// Value==》评论详情NewsComment
jedis.hset(redisKey, newsComment.getId().toString(), JSON.toJSONString(newsComment));
}
}
log.info("调用" + jp.getTarget() + "的" + jp.getSignature().getName()
+ "方法!方法参数为" + Arrays.toString(jp.getArgs()) + "。方法的返回值为:" + result);
return result;
} catch (Throwable throwable) {
log.error(jp.getSignature().getName() + "方法发生了异常," + throwable);
throwable.printStackTrace();
throw throwable;
} finally {
log.info(jp.getSignature().getName() + "方法执行结束!");
}
}
//拦截@RedisDelete,删除缓存操作
@Around("@annotation(com.cod4man.fakenews.annotation.redis.RedisDelete)")
public Object around_DeleteById(ProceedingJoinPoint jp) throws Throwable {
log.info("调用" + jp.getTarget() + "的" + jp.getSignature().getName() + "方法!方法参数为" + Arrays.toString(jp.getArgs()));
try {
Method method = getMethod(jp);
// 获取注解对象
RedisDelete redisDelete = method.getAnnotation(RedisDelete.class);
//获取注解的group属性
String redisKey = redisDelete.group();
boolean mark = true;
//该删除也是集成了评论删除和新闻删除,如果是评论删除,则拼接上新闻的id来组成大Key
if ("CommentsByNewsId".equals(redisKey)) {
redisKey += jp.getArgs()[0].toString();
}
//查询Redis中是否有缓存
Map<String, String> mapNews = jedis.hgetAll(redisKey);
//直接查缓存
if ( mapNews == null || "{}".equals(mapNews.toString())) {
if (!"CommentsByNewsId".equals(redisKey)) {
//判断缓存中是否有该键,没有说明数据也没有,就无需访问数据库了
mark = mapNews.containsKey(jp.getArgs()[0].toString());
}
if (mark) {
System.out.println("缓存中没有key");
//缓存中没有该Key,直接return,
return -1;
}
}
//否则,缓存中有该Key即数据库有数据,需要删除
Object result = jp.proceed();
if (!redisKey.contains("CommentsByNewsId")) {
//删除缓存
jedis.hdel(redisKey, jp.getArgs()[0].toString());
} else {
System.out.println("评论Id" + redisKey);
System.out.println("是否删除评论:" + jedis.del(redisKey));
}
log.info("调用" + jp.getTarget() + "的" + jp.getSignature().getName()
+ "方法!方法参数为" + Arrays.toString(jp.getArgs()) + "。方法的返回值为:" + result);
//删除数据库
return result;
} catch (Throwable throwable) {
log.error(jp.getSignature().getName() + "方法发生了异常," + throwable);
throwable.printStackTrace();
throw throwable;
} finally {
log.info(jp.getSignature().getName() + "方法执行结束!");
}
}
//同理可以写增加、修改数据的AOP
/**
* 获取被注释方法
* @Title: getMethod
* @Description: 获取被拦截方法对象
* @param joinPoint
* @return
*/
protected Method getMethod(JoinPoint joinPoint) throws Exception {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Method realMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), method.getParameterTypes());
return realMethod;
}
}
三、测试:
1、新闻详情业务处理NewsDetailServiceImpl.java
@Override
@RedisSelect(group = "NewsDetail", key = "", comment = "查询全部")
public List<NewsDetail> findAllNews(int startPage, int pageSize) {
System.out.println("查询全部");
return newsDetailMapper.findAllNews((startPage-1)*pageSize,pageSize);
}
@Override
@RedisDelete(group = "NewsDetail", key = "")
public int deleteNewsById(long newsId) {
if (newsDetailMapper.deleteNewsById(newsId) >0) {
return newsCommentService.deleteCommentByNewsId(newsId);
}
return -1;
}
2、新闻评论业务处理NewsCommentServiceImpl.java
@Override
@RedisDelete(group = "CommentsByNewsId", key = "")
public int deleteCommentByNewsId(long newsId) {
return newsCommentMapper.deleteCommentByNewsId(newsId);
}
@Override
@RedisSelect(group = "CommentsByNewsId", key = "")
public List<NewsComment> findCommentsByNewsId(long newsId) {
System.out.println("查看什么");
return newsCommentMapper.findCommentsByNewsId(newsId);
}
3、运行结果:新闻以及评论按AOP中所设计的数据结构进行存储,这样在更新操作的时候,就可以指定更新数据内部的一行小数据,虽然更新操作也是需要更新主数据库,但是查询操作就不必再查询数据库了,只需要查询Redis缓存。
有什么不妥的欢迎大家指正!需要源码的可以留言联系我。