自定义缓存组件 代替 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切面,主要有以下几个关键点需要了解:

  1. @Aspect,此注解将一个类定义为一个切面类;
  2. @Pointcut,此注解可以定义一个切入点,可以是规则表达式,也可以是某个package下的所有函数,也可以是一个注解等,其实就是执行条件,满足此条件的就切入;
  3. 然后可以定义切入位置,我们可以选择在切入点的不同位置进行切入:
  4. @Before在切入点开始处切入内容;
  5. @After在切入点结尾处切入内容;
  6. @AfterReturning在切入点return内容之后切入内容(可以用来对返回值做一些处理);
  7. @Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容;
  8. @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),逻辑和上述类似,先定义切点,指出切入的对象,然后再你要切入的逻辑,我这里的逻辑和上述类似:

  1.  通过joinpoint拿到签名,然后取得对应注解上的方法和注解标记的参数 
  2. 根据SpringEl提供的类来解析注入值,SpringEL好处可参考博文:https://blog.csdn.net/u011305680/article/details/80271423
  3. 先从缓存中拿值,拿不到从数据库拿,并存于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.自定义缓存组件。

最近单徐循环的歌曲:水木年华的一首《中学时代》

                          

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值