自定义缓存组件 代替 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.自定义缓存组件。
最近单徐循环的歌曲:水木年华的一首《中学时代》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
2022-09-03 Insomnia的下载
2022-09-03 Spring Boot 全局异常处理@ControllerAdvice
2022-09-03 com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server 报错问题
2022-09-03 SpringBoot启动时报错:The bean 'dataSource', defined in BeanDefinition defined in class path resource
2022-09-03 @ControllerAdvice全局异常处理
2022-09-03 spring cloud 多模块打包部署解决坑
2022-09-03 引入版本问题 spring-cloud版本:Finchley.RC1 和 spring-boot版本:2.0.4.RELEASE Non-resolvable import POM: Failure to find org.springframework.cloud:spring-cloud-dependencies