自定義緩存組件 代替 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.自定義緩存組件。

最近單徐循環的歌曲:水木年華的一首《中學時代》

                          

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章