自定义缓存组件 代替 Spring@Cache缓存注解
在实现上述功能之前先来点基础的,redis在SpringBoot项目中常规的用法,好对缓存和redis客户端的使用有一定了解。
1.添加依赖 redis客户端依赖(连接redis服务端必备 )
- <!-- 客户端依赖二选一 -->
- <dependency>
- <groupId>redis.clients</groupId>
- <artifactId>jedis</artifactId>
- </dependency>
- <dependency>
- <groupId>io.lettuce</groupId>
- <artifactId>lettuce-core</artifactId>
- </dependency>
- <!-- redis所需依赖 -->
- <dependency>
- <groupId>org.springframework.data</groupId>
- <artifactId>spring-data-redis</artifactId>
- </dependency>
- <!-- mysql依赖-->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <!-- mybaties 依赖-->
- <dependency>
- <groupId>org.mybatis.spring.boot</groupId>
- <artifactId>mybatis-spring-boot-starter</artifactId>
- </dependency>
2. yaml配置项
- server:
- port: 8702
- spring:
- application:
- name: eurekaClient8702 #此处切记不能用a_b命名
- datasource:
- driver-class-name: com.mysql.jdbc.Driver
- url: jdbc:mysql://127.0.0.1:3306/test
- username: root
- password: admin
- redis:
- host: 192.168.32.130
- port: 6379
- timeout: 20000
3. 配置类
- /**
- * @author Heian
- * @time 19/07/07 16:59
- * @description:配置类
- */
- @Configuration
- @EnableCaching//开启缓存注解
- //@Profile ("single")
- public class webconfig {
-
- // 配置Spring Cache注解功能:指定缓存类型redis
- @Bean
- public CacheManager redisCacheManage(RedisConnectionFactory redisConnectionFactory) {
- System.out.println ("-----------------Spring 定义CacheManager的缓存类型-----------------");
- RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
- RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
- RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
- return cacheManager;
- }
-
- }
下面进行一些简单的案例测试:
测试一:往redis存值(当然,redis服务必须是开启的)
- @RunWith(SpringRunner.class)
- @SpringBootTest
- public class CustomersApplicationTests {
-
- // 直接注入StringRedisTemplate,则代表每一个操作参数都是字符串
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
-
-
- //测试一:存值
- @Test
- public void setByCache() {
- stringRedisTemplate.opsForValue().set("k1", "我是k1");//如果是中文stringRedisTemplate会默认采用String类的序列化机制
- }
-
- }
测试二:对象缓存功能(先查看缓存中有无该对象,有则直接读取;无则加载到mysql数据库并添加到到redis缓存中)
- @RunWith(SpringRunner.class)
- @SpringBootTest
- public class CustomersApplicationTests {
- // 参数可以是任何对象,默认由JDK序列化
- @Resource
- private RedisTemplate<Integer,User> redisTemplate;
- @Autowired
- private UserService userService;
- @Test//对象缓存功能
- public void findUser() {
- User user = null;
- int id = 1;
- // 1、 判定缓存中是否存在
- user = (User) redisTemplate.opsForValue().get(id);
- if (user != null) {
- System.out.println("从缓存中读取到值:" + user.toString ());
- }else {
- // 2、不存在则读取数据库
- user = userService.getUserFromDB (id);
- // 3、 同步存储value到缓存。
- redisTemplate.opsForValue().set(id, user);
- System.out.println (user.toString ());
- }
- }
- }
- /**
- * @author Heian
- * @time 19/07/07 17:24
- * @description:
- */
- @Service
- public class UserService {
-
- @Autowired
- private UserDao userDao;
-
- //从数据库获取代当前User对象
- public User getUserFromDB(int id) {
- User user = userDao.getUserById (id);
- return user;
- }
-
- /* @Cacheable 支持如下几个参数:
- * value:缓存位置名称,不能为空,如果使用EHCache,就是ehcache.xml中声明的cache的name
- * key:缓存的key,默认为空,既表示使用方法的参数类型及参数值作为key,支持Sp EL表达式
- * condition:触发条件,只有满足条件的情况才会加入缓存,默认为空,既表示全部都加入缓存,支持Sp EL表达式
- */
- // value~单独的缓存前缀
- // key缓存key 可以用springEL表达式 cache-1:123
- @Cacheable(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
- public User findUserById(int id) {
- // 读取数据库
- User user = new User(id, "Heian",26);
- System.out.println("从数据库中读取到数据:" + user);
- return user;
- }
-
- @CacheEvict(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
- public void deleteUserById(int id) {
- // 先数据库删除,成功后,删除Cache
- // 先判断Cache里面是不是有?有则删除
- System.out.println("用户从数据库删除成功,请检查缓存是否清除~~" + id);
- }
-
- // 如果数据库更新成功,更新redis缓存
- @CachePut(cacheManager = "redisCacheManage", value = "cache-1", key = "#user.id", condition = "#result ne null")
- public User updateUser(User user){
- // 先更新数据库,更成功
- // 更新缓存
- // 读取数据库
- System.out.println("数据库进行了更新,检查缓存是否一致");
- return user; // 返回最新内容,代表更新成功
- }
-
-
-
- }
备注:此时数据库是有id=1的数据的,但缓存中也没有,ok,执行,第一次执行发现缓存中没有此对象,然后查询mysql数据库将返回的对象添加在缓存中。第二次执行会直接从数据库中查的,如下图。
测试三:利用spring的注解实现缓存功能
- @RunWith(SpringRunner.class)
- @SpringBootTest
- public class CustomersApplicationTests {
-
- @Autowired
- private UserService userService;
-
- @Test
- public void testSpringCache(){
- User user = userService.findUserById (1);
- System.out.println("测试类:" +user.toString ());
- }
-
-
- }
- /**
- * @author Heian
- * @time 19/07/07 17:24
- * @description:
- */
- @Service
- public class UserService {
-
- @Autowired
- private UserDao userDao;
-
- /* @Cacheable 支持如下几个参数:
- * value:缓存位置名称,不能为空,如果使用EHCache,就是ehcache.xml中声明的cache的name
- * key:缓存的key,默认为空,既表示使用方法的参数类型及参数值作为key,支持Sp EL表达式
- * condition:触发条件,只有满足条件的情况才会加入缓存,默认为空,既表示全部都加入缓存,支持Sp EL表达式
- */
- // value~单独的缓存前缀
- // key缓存key 可以用springEL表达式 cache-1:123
- @Cacheable(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
- public User findUserById(int id) {
- // 读取数据库
- User user = userDao.getUserById (id);
- System.out.println("从数据库中读取到数据:" + user);
- return user;
- }
-
- @CacheEvict(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
- public void deleteUserById(int id) {
- // 先数据库删除,成功后,删除Cache
- // 先判断Cache里面是不是有?有则删除
- System.out.println("用户从数据库删除成功,请检查缓存是否清除~~" + id);
- }
-
- // 如果数据库更新成功,更新redis缓存
- @CachePut(cacheManager = "redisCacheManage", value = "cache-1", key = "#user.id", condition = "#result ne null")
- public User updateUser(User user){
- // 先更新数据库,更成功
- // 更新缓存
- // 读取数据库
- System.out.println("数据库进行了更新,检查缓存是否一致");
- return user; // 返回最新内容,代表更新成功
- }
-
- }
备注:点击执行时,首先会执行service中的查询的代码,然后spring注解@Cacheable会默默的往redis存入缓存,当你执行第二遍时,不在执行service里的逻辑代码,而是直接从缓存中拿到值了。
那么正题来了,如果我们不使用Spring的注解,或者说想使用的更加灵活的使用,又该如何使用呢?肯定又要用到Aop了。现在就对Aop进行一个简单的了解,并且使用Aop自定义一个注解,完成和@Cacheable同样的功能。
Aop概念
AOP是Spring提供的两个核心功能之一:IOC(控制反转),AOP(Aspect Oriented Programming 面向切面编程);IOC有助于应用对象之间的解耦,AOP可以实现横切关注点和它所影响的对象之间的解耦,它通过对既有的程序定义一个横向切入点,然后在其前后切入不同的执行内容,来拓展应用程序的功能,常见的用法如:打开事务和关闭事物,记录日志,统计接口时间等。AOP不会破坏原有的程序逻辑,拓展出的功能和原有程序是完全解耦的,因此,它可以很好的对业务逻辑的各个部分进行隔离,从而使业务逻辑的各个部分之间的耦合度大大降低,提高了部分程序的复用性和灵活性。
实现aop切面,主要有以下几个关键点需要了解:
- @Aspect,此注解将一个类定义为一个切面类;
- @Pointcut,此注解可以定义一个切入点,可以是规则表达式,也可以是某个package下的所有函数,也可以是一个注解等,其实就是执行条件,满足此条件的就切入;
- 然后可以定义切入位置,我们可以选择在切入点的不同位置进行切入:
- @Before在切入点开始处切入内容;
- @After在切入点结尾处切入内容;
- @AfterReturning在切入点return内容之后切入内容(可以用来对返回值做一些处理);
- @Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容;
- @AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑;
自定义注解进行一个实现来替代Spring的CacheManage注解
第一:首先自定义注解类,运行于方法之上
- package com.example.customers.annotations;
-
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
-
- /**
- * @author Heian
- * @time 19/07/12 8:21
- * @description:自定义缓存组件
- */
- @Target (ElementType.METHOD)//作用于方法之上
- @Retention (RetentionPolicy.RUNTIME)//运行期间生效
- public @interface MyRedisCache {
-
- //key值:存在redis的key,可以使用springEL表达式,可以使用方法执行的一些参数
- String key();
-
- }
第二:设置切面类,这里可以使用环绕@Around(等价于@Before+@After),逻辑和上述类似,先定义切点,指出切入的对象,然后再你要切入的逻辑,我这里的逻辑和上述类似:
- 通过joinpoint拿到签名,然后取得对应注解上的方法和注解标记的参数
- 根据SpringEl提供的类来解析注入值,SpringEL好处可参考博文:https://blog.csdn.net/u011305680/article/details/80271423
- 先从缓存中拿值,拿不到从数据库拿,并存于redis,以便于下次取值可以直接去缓存中拿。
- package com.example.customers.aspect;
-
- import com.example.customers.annotations.MyRedisCache;
- import com.example.customers.entity.User;
- 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.beans.factory.annotation.Autowired;
- import org.springframework.core.DefaultParameterNameDiscoverer;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.expression.EvaluationContext;
- import org.springframework.expression.Expression;
- 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.io.Reader;
- import java.lang.reflect.Method;
- import java.util.Map;
-
- /**
- * @author Heian
- * @time 19/07/12 8:30
- * @description:自定义注解的切面
- */
- @Component
- @Aspect
- public class MyRedisCacheAspect {
-
- @Autowired
- private RedisTemplate redisTemplate;
-
-
- //返回类型任意,方法参数任意,方法名任意
- @Pointcut(value = "@annotation(com.example.customers.annotations.MyRedisCache)")
- public void myredisPointcut(){
-
- }
-
- @Around ("myredisPointcut()")
- public Object myredisAround(ProceedingJoinPoint joinPoint){
- Object obj = null;
- try {
- MethodSignature signature = (MethodSignature)joinPoint.getSignature ();
- //取得使用注解的方法 有了方法就能:方法的参数、返回值类型、注解、该方法的所在类等等
- Method method =signature.getMethod ();
- // 通过反射拿到该方法的注解类的比如PostMapping的方法 参数必须是注解
- MyRedisCache myRedisCache = method.getAnnotation (MyRedisCache.class);
- String key = myRedisCache.key ();//#{userid}
- EvaluationContext context = new StandardEvaluationContext ();
- // joinPoint 取该调用注解方法的传来的具体参数的值如:1
- Object[] args = joinPoint.getArgs();
- DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
- // 取该调用注解方法的参数如:id
- String[] parameterNames = discover.getParameterNames(method);
- for (int i = 0; i < parameterNames.length; i++) {
- context.setVariable(parameterNames[i], args[i].toString());// id :1
- }
- //拿到key值后 然后进行解析
- ExpressionParser parser = new SpelExpressionParser ();
- Expression expression = parser.parseExpression (key);//"#id" 取得自定义注解的key的内容
- String realKey = expression.getValue(context).toString();//映射#id" --> id
- // 1、 判定缓存中是否存在
- obj = redisTemplate.opsForValue ().get (realKey);
- if (obj != null) {
- System.out.println("从缓存中读取到值:" + obj);
- return obj;
- }
- // 2、不存在则执行方法,相当于我们Method.invoke(对象,"setName()");
- obj = joinPoint.proceed();
- //3、并且存于redis中
- redisTemplate.opsForValue ().set (realKey,obj);
- } catch (Throwable e) {
- e.printStackTrace ();
- }
- return obj;
-
- }
-
-
-
- }
第三:定义Controller层类和Service类进行测试
- package com.example.customers.controller;
- import com.example.customers.annotations.MyRedisCache;
- import com.example.customers.entity.User;
- import com.example.customers.service.AopService;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.*;
-
- import java.util.concurrent.TimeUnit;
-
- /**
- * @author Heian
- * @time 19/07/11 14:07
- * @description:学习Aop测试
- */
- @RestController
- @RequestMapping("/StudyAop")
- public class AopController {
-
- private Logger logger = LoggerFactory.getLogger(AopController.class);
- @Autowired
- private AopService aopService;
-
- @GetMapping("test1")
- public String test1(){
- logger.info ("进入test1()方法");
- return "欢迎进入Aop的学习1";
- }
-
- @GetMapping("test2")
- public String test2() throws InterruptedException{
- TimeUnit.SECONDS.sleep (5);
- return "欢迎进入Aop的学习2";
- }
-
- @PostMapping("test3")
- public User test3(@RequestParam int id){
- User user = aopService.implRedisCache (id);
- logger.info ("返回的对象" + user.toString ());
- return user;
- }
-
- }
- package com.example.customers.service;
-
- import com.example.customers.annotations.MyRedisCache;
- import com.example.customers.dao.UserDao;
- import com.example.customers.entity.User;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
-
- /**
- * @author Heian
- * @time 19/07/12 11:29
- * @description:
- */
-
- @Service
- public class AopService {
-
- @Autowired
- private UserDao userDao;
-
- @MyRedisCache(key = "#id")
- public User implRedisCache(int id){
- User user = userDao.getUserById (id);
- System.out.println ("从数据库中读的对象为"+user.toString ());
- return user;
- }
-
-
- }
备注:这里使用Postman测试(我已在本地redis服务清空了数据flushdb,所以第一次访问是没有缓存的)
第一次访问:
第二次访问:(接口耗时也减少了)
ok,至此完成,这就利用AOP实现了两个功能:1.统计接口耗时和访问次数 2.自定义缓存组件。
最近单徐循环的歌曲:水木年华的一首《中学时代》
原文链接:https://blog.csdn.net/qq_40826106/article/details/95240629 标签:缓存,自定义,Spring,user,key,org,import,id From: https://www.cnblogs.com/sunny3158/p/18394408