項(xiàng)目-無(wú)侵入代碼方式使用Redis實(shí)現(xiàn)緩存功能

redis

前言

近期有同學(xué)問(wèn)我嗡午,怎么在不使用spring自帶的@Cacheable來(lái) 使用aop方式用redis為項(xiàng)目接口調(diào)用添加緩存囤躁,在這里總結(jié)整理一下,博文難免會(huì)有紕漏荔睹,如有問(wèn)題請(qǐng)?jiān)u論不吝告知狸演。

在本文章,你會(huì)了解到如何使用redis应媚,以及如何通過(guò)jedis操作redis通過(guò)AOP的方式實(shí)現(xiàn)緩存严沥。在文章后面還介紹了AOP的相關(guān)知識(shí)點(diǎn)猜极,希望對(duì)大家有些許幫助~

如果轉(zhuǎn)載此博文中姜,請(qǐng)附上本文鏈接,謝謝合作~
如果感覺(jué)這篇文章對(duì)您有所幫助跟伏,請(qǐng)“點(diǎn)贊”或者“關(guān)注”博主丢胚,您的喜歡和關(guān)注將是我前進(jìn)的最大動(dòng)力~

一:環(huán)境準(zhǔn)備

1:準(zhǔn)備Redis環(huán)境

使用redis做緩存的話,需要有redis服務(wù)受扳,可以將服務(wù)部署在遠(yuǎn)程服務(wù)器上携龟,也可以部署到本機(jī)上。

1.1. 部署在linux服務(wù)器

1.1.1安裝Redis

#安裝redis勘高,當(dāng)前最新的版本是redis-5.0.0.tar.gz峡蟋,可以通過(guò)http://download.redis.io/releases地址查看最新版本
$ wget http://download.redis.io/releases/redis-5.0.0.tar.gz
$ tar xzf redis-5.0.0.tar.gz
$ cd redis-5.0.0
$ make

1.1.2啟動(dòng)Redis服務(wù)并使用

 #啟動(dòng)redis服務(wù)
$ cd src
$ ./redis-server
#使用redis客戶端測(cè)試redis
$ cd src
$ ./redis-cli
redis> set testkey  testvalue
OK
redis> get testkey
"testvalue"

如果上述過(guò)程沒(méi)有報(bào)錯(cuò)的話坟桅,那么恭喜你啟動(dòng)redis服務(wù)成功,下面我們將會(huì)使用jedis操作redis來(lái)實(shí)現(xiàn)緩存

1.2. 部署在windows服務(wù)器

2.1下載redis壓縮包

下載zip壓縮包(Redis-x64-*.zip):https://github.com/MSOpenTech/redis/releases
將其解壓到某一文件夾中蕊蝗,重命名為Redis

2.2啟動(dòng)redis服務(wù)并使用

打開(kāi)cmd仅乓,切換到解壓的Redis文件夾中,運(yùn)行如下命令,
會(huì)發(fā)現(xiàn)出現(xiàn)”The server is now ready to accept connections on port 6379“字樣表示啟動(dòng)成功

redis-server.exe redis.windows.conf

再打開(kāi)一個(gè)cmd蓬戚,原來(lái)的cmd不要關(guān)閉夸楣,保持打開(kāi)狀態(tài),輸入以下命令:
其中:127.0.0.1:為你的redis服務(wù)ip地址子漩,如果是本機(jī)安裝的就是127.0.0.1豫喧,端口6379是redis默認(rèn)監(jiān)聽(tīng)的端口

redis-cli.exe -h 127.0.0.1 -p 6379
#如果redis設(shè)置了密碼,可以添加參數(shù)-a指定密碼幢泼,例如:
redis-cli.exe -h 127.0.0.1 -p 6379 -a 12345

可以使用redis命令測(cè)試是否可以正常使用紧显,至此redis服務(wù)便準(zhǔn)備完畢了~

2:準(zhǔn)備項(xiàng)目環(huán)境

  1. 首先spring boot項(xiàng)目,當(dāng)然不是boot項(xiàng)目也可以缕棵,我是以boot項(xiàng)目舉例的
  2. pom文件添加依賴鸟妙,只列出了此功能設(shè)計(jì)特殊所需的
    ps: 以下版本為截止2019/10/10最新版本
        <!--jedis依賴-->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!--用于序列化 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency> 
  1. application.yml添加配置,如果你是xml格式的文件挥吵,yml格式和xml格式類似重父,只是yml格式更加明了一些,google一下轉(zhuǎn)換一下格式就行
spring: 
  jedis:
    max:
      total: 100     #jedis總量
      idle: 50        #空閑jedis實(shí)例最大值
      waitmillis: 500   #當(dāng)池內(nèi)沒(méi)有返回jedis對(duì)象時(shí)忽匈,最大等待時(shí)間
      timout: 0      #當(dāng)客戶端閑置多長(zhǎng)時(shí)間后關(guān)閉連接房午,如果指定為 0,表示關(guān)閉該功能丹允,連接不會(huì)斷
    host: 127.0.0.1  # redis服務(wù)ip地址
    port: 6379        # 端口
    password: test  # redis密碼

ps : redis的常用配置參數(shù)

此處的參數(shù)需要根據(jù)想要緩存接口的調(diào)用情況進(jìn)行動(dòng)態(tài)配置郭厌。
至此,環(huán)境配置完成了雕蔽,現(xiàn)在只需要操作redis實(shí)現(xiàn)緩存了~~

二:緩存功能實(shí)現(xiàn)

1:過(guò)程簡(jiǎn)介

  1. 對(duì)于不加緩存的項(xiàng)目折柠,我們每一次的請(qǐng)求都會(huì)去數(shù)據(jù)庫(kù)中查詢,即使兩次請(qǐng)求一樣并且獲取的數(shù)據(jù)一樣批狐,也是會(huì)去查詢數(shù)據(jù)庫(kù)扇售,這就造成了數(shù)據(jù)庫(kù)資源的浪費(fèi),并且如果并發(fā)量特別高的話嚣艇,數(shù)據(jù)庫(kù)的壓力太大承冰,容易造成查詢緩慢、數(shù)據(jù)庫(kù)宕機(jī)食零、查詢失敗等問(wèn)題困乒。
  2. 項(xiàng)目添加緩存之后,請(qǐng)求查詢數(shù)據(jù)的時(shí)候會(huì)先查詢緩存贰谣,緩存(這里指只有一級(jí)緩存)中沒(méi)有才會(huì)到達(dá)數(shù)據(jù)庫(kù)娜搂。相同的請(qǐng)求在緩存還沒(méi)有過(guò)期 的情況下迁霎,會(huì)得到緩存中的數(shù)據(jù)并返回,不會(huì)到達(dá)數(shù)據(jù)庫(kù)百宇,這樣做即減少了數(shù)據(jù)庫(kù)的壓力提高了并發(fā)量又提升了查詢速度欧引。
  3. 簡(jiǎn)易流程圖:
graph LR
A[請(qǐng)求]-->B[查詢緩存] 
B -- 數(shù)據(jù)不在緩存中 --> C[查詢數(shù)據(jù)庫(kù)]
E--> D[返回?cái)?shù)據(jù)]
B -- 數(shù)據(jù)在緩存中 --> E[獲得緩存中數(shù)據(jù)]
C --> F[將獲得數(shù)據(jù)緩存到緩存中]
F -->D

2:緩存AOP實(shí)現(xiàn)

在使用aop之前,先大致的了解一下 aop:

AOP(Aspect Oriented Programing):面向切面編程恳谎,將通用的邏輯從業(yè)務(wù)邏輯中分離出來(lái)芝此。

AOP把軟件系統(tǒng)分為兩個(gè)部分:核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn):

  • 主要的業(yè)務(wù)處理部分。業(yè)務(wù)處理的主要流程是核心關(guān)注點(diǎn)因痛,與之關(guān)系不大的部分是橫切關(guān)注點(diǎn)婚苹。
  • 橫切關(guān)注點(diǎn)的一個(gè)特點(diǎn)是,他們經(jīng)常發(fā)生在核心關(guān)注點(diǎn)的多處鸵膏,而各處都基本相似膊升。比如權(quán)限認(rèn)證、日志谭企、事務(wù)處理廓译。Aop 的作用在于分離系統(tǒng)中的各種關(guān)注點(diǎn),將核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)分離開(kāi)來(lái)债查。

正如Avanade公司的高級(jí)方案構(gòu)架師Adam Magee所說(shuō)非区,AOP的核心思想就是“將應(yīng)用程序中的商業(yè)邏輯同對(duì)其提供支持的通用服務(wù)進(jìn)行分離”。

AOP的底層實(shí)現(xiàn)主要是依賴動(dòng)態(tài)代理來(lái)實(shí)現(xiàn)的:

  • 比如Spring aop底層使用JDK proxy(實(shí)現(xiàn)接口)和CGLib(繼承目標(biāo)類盹廷、使用ASM庫(kù))來(lái)生成代理類來(lái)代替目標(biāo)類執(zhí)行征绸,默認(rèn)使用JDK proxy ,當(dāng)無(wú)接口時(shí)使用CGLib。底層采用動(dòng)態(tài)代理技術(shù)(動(dòng)態(tài)代理技術(shù)底層依賴的反射技術(shù))在運(yùn)行期動(dòng)態(tài)生成代理類(不同于aspectJ編譯期織入代碼到目標(biāo)類)俄占。
  • 再比如AspectJ做為java實(shí)現(xiàn)的統(tǒng)一AOP解決方案管怠,使用ASM字節(jié)碼操作庫(kù),需要使用特定的acj編譯器(不同于spring使用動(dòng)態(tài)代理)在編譯期將代碼直接織入到目標(biāo)類缸榄。

下面會(huì)詳細(xì)介紹aop相關(guān)

2.1.執(zhí)行過(guò)程

  1. 請(qǐng)求到達(dá)Controller中的接口時(shí)渤弛,因?yàn)槲覀冊(cè)?code>CacheAspect類中配置的切入點(diǎn)包含這個(gè)接口,所以進(jìn)入CacheAspect類的doAround方法中執(zhí)行緩存操作
  2. doAround中甚带,首先獲取key她肯,判斷redis中是否包含key,包含就返回緩存中的數(shù)據(jù)欲低,完成請(qǐng)求
  3. 不包含就執(zhí)行調(diào)用的接口通過(guò)查詢數(shù)據(jù)庫(kù)獲取數(shù)據(jù)辕宏,并將其緩存到redis中畜晰,完成一次請(qǐng)求不包含就執(zhí)行調(diào)用的接口通過(guò)查詢數(shù)據(jù)庫(kù)獲取數(shù)據(jù)砾莱,并將其緩存到redis中,完成請(qǐng)求

2.2. 組成部分與實(shí)現(xiàn)

  • 自定義注解:NeedCacheAop

用在方法上面標(biāo)識(shí)調(diào)用該方法的請(qǐng)求需要被緩存
其中的nxxx凄鼻、expx腊瑟、time等參數(shù)是為了可以更靈活的空值緩存的方式與過(guò)期時(shí)間聚假,具體含義請(qǐng)看下面”其他“中的set方法參數(shù)解析

/**
 * 自定義注解,用于標(biāo)識(shí)方法是否需要使用緩存
 */
@Target({ElementType.PARAMETER, ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedCacheAop {
    //代表緩存策咯闰非,nx:代表key不存在再進(jìn)行緩存kv膘格,xx:代表key存在再進(jìn)行緩存kv  默認(rèn)為"不存在key緩存key"
    String nxxx() default "nx";
    //代表過(guò)期時(shí)間單位,ex:秒 px:毫秒    默認(rèn)為"秒"
    String expx() default "ex";
    //過(guò)期時(shí)間
    long time() default 30*60;
}
  • 序列化工具類:SerializeUtil

使用FastJso對(duì)要緩存的數(shù)據(jù)進(jìn)行序列化后存儲(chǔ)與獲取緩存中的反序列化
使用fastjson對(duì)數(shù)據(jù)進(jìn)行序列化與反序列化财松,非常簡(jiǎn)單

public class SerializeUtil {
    private  static Logger logger = LoggerFactory.getLogger("SerializeUtil");
    public static String serializeObject(Object obj){
        logger.info("serialize object :"+obj);
        String jsonObj = JSON.toJSONString(obj);
        return jsonObj;
    }
    public static JSONObject unserializeObject(String serobj){
        logger.info("unserialize object :"+serobj);
        JSONObject jsonObj = JSON.parseObject(serobj);
        return jsonObj;
    }
}
  • 操作緩存service類:CacheService接口 與其實(shí)現(xiàn)類 CacheServiceImpl

方法內(nèi)部封裝了關(guān)于緩存的get set containKey getKeyAop等方法

public interface CacheService {
    /**獲取jedis實(shí)例*/
    Jedis getResource() throws Exception;
    /**設(shè)置key與value*/
    void set(String key, String value, String nxxx, String expx, long time);
    /**根據(jù)key獲取value*/
    String get(String key);
    /**判斷是否存在key*/
    boolean containKey(String key);
    /**釋放jedis實(shí)例資源*/
    void returnResource(Jedis jedis);
    /**獲取key*/
    String getKeyForAop(JoinPoint joinPoint, HttpServletRequest request);
}
@Service
public class CacheServiceImpl implements CacheService {
    private static Logger logger = LoggerFactory.getLogger(CacheServiceImpl.class);

    @Autowired
    private JedisPool jedisPool;

    /**獲取jedis實(shí)例*/
    public Jedis getResource() throws Exception{
        return jedisPool.getResource();
    }

    /**設(shè)置key與value*/
    public void set(String key, String value,String nxxx,String expx,long time) {
        Jedis jedis=null;
        try{
            jedis = getResource();
            jedis.set(key,value,nxxx,expx,time);
        } catch (Exception e) {
            logger.error("Redis set error: "+ e.getMessage() +" - " + key + ", value:" + value);
        }finally{
            returnResource(jedis);
        }
    }

    /**根據(jù)key獲取value*/
    public String get(String key) {
        String result = null;
        Jedis jedis=null;
        try{
            jedis = getResource();
            result = jedis.get(key);
        } catch (Exception e) {
            logger.error("Redis set error: "+ e.getMessage() +" - " + key + ", value:" + result);
        }finally{
            returnResource(jedis);
        }
        return result;
    }

    /**判斷是否存在key*/
    public boolean containKey(String key){
        boolean b;
        Jedis jedis = null;
        try{
            jedis = getResource();
            b = jedis.exists(key);
            return b;
        }catch (Exception e){
            logger.error("Redis server error::"+e.getMessage());
            return false;
        }finally {
            returnResource(jedis);
        }
    }

    /**釋放jedis實(shí)例資源*/
    public void returnResource(Jedis jedis) {
        if(jedis != null){
            jedis.close();
        }
    }

    /**獲取key*/
    public String getKeyForAop(JoinPoint joinPoint, HttpServletRequest request){
        //獲取參數(shù)的序列化
        Object[] objects = joinPoint.getArgs();
        String args = SerializeUtil.serializeObject(objects[0]);
        //獲取請(qǐng)求url
        String url = request.getRequestURI();
        //獲取請(qǐng)求的方法
        String method = request.getMethod();
        //獲取當(dāng)前日期,規(guī)避默認(rèn)init情況
        String date = LocalDate.now().toString();
        //key值獲取
        return args + url + method + date;
    }
}
  • 切面類:CacheAspect

用于對(duì)相應(yīng)的請(qǐng)求接口切入緩存存取的相關(guān)邏輯瘪贱,使用AOP可以對(duì)代碼0侵入性,是一個(gè)很好的方法

@Component
@Aspect
public class CacheAspect {

    @Autowired
    CacheService cacheService;

    /**設(shè)置切入點(diǎn)*/
    //方法上面有@NeedCacheAop的方法,增加靈活性
    @Pointcut("@annotation(com.xcar.data.web.backend.util.annotation.NeedCacheAop)")
    public void annotationAspect(){}
    //相應(yīng)包下所有以XcarIndex開(kāi)頭的類中的所有方法辆毡,減少代碼侵入性
    @Pointcut("execution(public * com.xcar.data.web.backend.controller.XcarIndex*.*(..))")
    public void controllerAspect(){}

    /**環(huán)繞通知*/
    @Around(value = "controllerAspect()||annotationAspect()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        //獲取請(qǐng)求
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getRequest();
        //存儲(chǔ)接口返回值
        Object object = new Object();

        //獲取注解對(duì)應(yīng)配置過(guò)期時(shí)間
        NeedCacheAop cacheAop = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(NeedCacheAop.class);  //獲取注解自身
        String nxxx;String expx;long time;
        if (cacheAop == null){//規(guī)避使用第二種切點(diǎn)進(jìn)行緩存操作的情況
            nxxx = "nx";
            expx = "ex";
            time = 30*60;  //默認(rèn)過(guò)期時(shí)間為30分鐘
        }else{
            nxxx = cacheAop.nxxx();
            expx = cacheAop.expx();
            time = cacheAop.time();
        }
        //獲取key
        String key = cacheService.getKeyForAop(joinPoint,request);
        if (cacheService.containKey(key)){
            String obj = cacheService.get(key);
            if ("fail".endsWith(obj)){  //規(guī)避redis服務(wù)不可用
                try {
                    //執(zhí)行接口調(diào)用的方法
                    joinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            }else{
                JSONObject klass =  SerializeUtil.unserializeObject(obj);
                return new ResponseEntity<>(klass.get("body"), HttpStatus.OK) ;
            }
        }else{
            try {
                ////執(zhí)行接口調(diào)用的方法并獲取返回值
                object = joinPoint.proceed();
                String serobj = SerializeUtil.serializeObject(object);
                cacheService.set(key,serobj,nxxx,expx,time);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
        return object;
    }
}
  • jedis配置類:JedisConfiguration

用于配置JedisPool的相關(guān)參數(shù)菜秦,與創(chuàng)建JedisPool對(duì)象,便于后面注入使用

@Configuration
public class JedisConfiguration extends CachingConfigurerSupport {
    private Logger logger = LoggerFactory.getLogger(JedisConfiguration.class);
    @Value("${spring.jedis.port}")
    private Integer port;
    @Value("${spring.jedis.host}")
    private String host;
    @Value("${spring.jedis.max.total}")
    private Integer maxTotal;
    @Value("${spring.jedis.max.idle}")
    private Integer maxIdle;
    @Value("${spring.jedis.max.waitmillis}")
    private Long maxWaitMillis;
    @Value("${spring.jedis.password}")
    private String password;

    public JedisConfiguration() {}
    /**設(shè)置*/
    @Bean
    public JedisPool redisPoolFactory(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        jedisPoolConfig.setMaxTotal(maxTotal);
        JedisPool jedisPool = new JedisPool(jedisPoolConfig,host,port,1000,password);
        logger.info("JedisPool build success!");
        logger.info("Redis host:" + host + ":" + port);
        return  jedisPool;
    }
    //下面屬性是get set方法省略
 }
  • 響應(yīng)數(shù)據(jù)對(duì)象:XcarIndexCarAttentionIndexResponse

響應(yīng)的數(shù)據(jù)對(duì)象舶掖,緩存就是對(duì)其進(jìn)行序列化后緩存
該對(duì)象類一定繼承Serializable接口球昨,使其可被序列化,例如:

public class XcarIndexCarAttentionIndexResponse implements Serializable{
    priate List<BaseChartsResponse.Line> lines = new ArrayList<>();
    private Series_DateBubble series_datebubble = new Series_DateBubble();
    private String flag = "1";
    public Series_DateBubble getSeries_datebubble() {
        if (series_datebubble == null) {
            series_datebubble = new Series_DateBubble();
        }
        return series_datebubble;
    }
    public String getFlag() {
        return flag;
    }
    public void setFlag(String flag) {
        this.flag = flag;
    }
    public void setSeries_datebubble(Series_DateBubble series_datebubble) {
        this.series_datebubble = series_datebubble;
    }
    public List<BaseChartsResponse.Line> getLines() {
        return lines;
    }
    public void setLines(List<BaseChartsResponse.Line> lines) {
        this.lines = lines;
    }
    public class Series_DateBubble {
        private List<BaseChartsResponse.Series_DateBubble> datas = new ArrayList<>();
        private String[] dataRange = {};
        public List<BaseChartsResponse.Series_DateBubble> getDatas() {
            return datas;
        }
        public void setDatas(List<BaseChartsResponse.Series_DateBubble> datas) {
            this.datas = datas;
        }
        public String[] getDataRange() {
            return dataRange;
        }
        public void setDataRange(String[] dataRange) {
            this.dataRange = dataRange;
        }
    }
}
  • 被切入的方法:getTrendPage

我們要添加緩存的Controller接口的實(shí)現(xiàn)眨攘,例如:我要切入的接口

package com.xcar.data.web.backend.controller;
.....
    @RequestMapping(value = "/page/trend", method = RequestMethod.POST)
    public ResponseEntity<XcarIndexCarIntentionIndexResponse> getTrendPage(@RequestBody XcarIndexCarIntentionIndexRequest ro, HttpServletRequest request) throws Exception {
        XcarIndexCarIntentionIndexResponse res = new XcarIndexCarIntentionIndexResponse();
        try {
            res = delegate.getTrendPage(ro);
        } catch (Exception e) {
            throw e;
        }
        return new ResponseEntity(res, HttpStatus.OK);
    }

2.3.非AOP實(shí)現(xiàn)

在一些情況下主慰,我們需要對(duì)方法內(nèi)部一些中間查詢結(jié)果進(jìn)行緩存,這樣就只能將緩存數(shù)據(jù)的代碼直接寫在方法體內(nèi)鲫售,實(shí)現(xiàn)也相對(duì)AOP實(shí)現(xiàn)方式來(lái)說(shuō)更加簡(jiǎn)單共螺,調(diào)用相關(guān)的jedis方法即可,可參考上述代碼實(shí)現(xiàn)情竹。

三:知識(shí)點(diǎn)

1:jedis中set方法參數(shù):

  • key :緩存的key值
  • value :緩存的value值
  • nxxx: NX|XX兩種選擇璃谨, NX -- 緩存不存在時(shí)才進(jìn)行緩存. XX -- 緩存存在時(shí)再進(jìn)行緩存
  • expx :EX|PX兩種選擇, 過(guò)期時(shí)間的代為鲤妥,EX 代表秒; PX 代表毫秒
  • time :過(guò)期時(shí)間的數(shù)值

2:AOP面向切面編程

如上述所說(shuō):

AOP(Aspect Oriented Programing):面向切面編程佳吞,將通用的邏輯從業(yè)務(wù)邏輯中分離出來(lái)。AOP把軟件系統(tǒng)分為兩個(gè)部分:核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)棉安。業(yè)務(wù)處理的主要流程是核心關(guān)注點(diǎn)底扳,與之關(guān)系不大的部分是橫切關(guān)注點(diǎn)。橫切關(guān)注點(diǎn)的一個(gè)特點(diǎn)是贡耽,他們經(jīng)常發(fā)生在核心關(guān)注點(diǎn)的多處衷模,而各處都基本相似。比如權(quán)限認(rèn)證蒲赂、日志阱冶、事務(wù)處理。Aop 的作用在于分離系統(tǒng)中的各種關(guān)注點(diǎn)滥嘴,將核心關(guān)注點(diǎn)和橫切關(guān)注點(diǎn)分離開(kāi)來(lái)木蹬。

正如Avanade公司的高級(jí)方案構(gòu)架師Adam Magee所說(shuō),AOP的核心思想就是“將應(yīng)用程序中的商業(yè)邏輯同對(duì)其提供支持的通用服務(wù)進(jìn)行分離”若皱。

AOP的底層實(shí)現(xiàn)主要是依賴動(dòng)態(tài)代理來(lái)實(shí)現(xiàn)的:

  • 比如Spring aop底層使用JDK proxy(實(shí)現(xiàn)接口)和CGLib(繼承目標(biāo)類镊叁、使用ASM庫(kù))來(lái)生成代理類來(lái)代替目標(biāo)類執(zhí)行尘颓,默認(rèn)使用JDK proxy ,當(dāng)無(wú)接口時(shí)使用CGLib。底層采用動(dòng)態(tài)代理技術(shù)(動(dòng)態(tài)代理技術(shù)底層依賴的反射技術(shù))在運(yùn)行期動(dòng)態(tài)生成代理類(不同于aspectJ編譯期織入代碼到目標(biāo)類)晦譬。
  • 再比如AspectJ做為java實(shí)現(xiàn)的統(tǒng)一AOP解決方案疤苹,使用ASM字節(jié)碼操作庫(kù),需要使用特定的acj編譯器(不同于spring使用動(dòng)態(tài)代理)在編譯期將代碼直接織入到目標(biāo)類敛腌。

AOP相關(guān)概念:

  • 連接點(diǎn)(Joinpoint): 表示需要在程序中插入橫切關(guān)注點(diǎn)的擴(kuò)展點(diǎn)卧土,連接點(diǎn)可能是類初始化、方法執(zhí)行像樊、方法調(diào)用夸溶、字段調(diào)用或處理異常等等,Spring只支持方法執(zhí)行連接點(diǎn)凶硅;在AOP中表示為“在哪里干”缝裁;
  • 切入點(diǎn)(Pointcut): 選擇一組相關(guān)連接點(diǎn)的模式,即可以認(rèn)為連接點(diǎn)的集合足绅,Spring支持perl5正則表達(dá)式和AspectJ切入點(diǎn)模式捷绑,Spring默認(rèn)使用AspectJ語(yǔ)法;在AOP中表示為“在哪里干的集合”氢妈;
  • 通知(Advice): 在連接點(diǎn)上執(zhí)行的行為粹污,通知提供了在AOP中需要在切入點(diǎn)所選擇的連接點(diǎn)處進(jìn)行擴(kuò)展現(xiàn)有行為的手段;包括前置通知(before advice)首量、后置通知(after advice)壮吩、環(huán)繞通知(around advice),在Spring中通過(guò)代理模式實(shí)現(xiàn)AOP加缘,并通過(guò)攔截器模式以環(huán)繞連接點(diǎn)的攔截器鏈織入通知鸭叙;在AOP中表示為“干什么”;
  • 切面(Aspect):橫切關(guān)注點(diǎn)的模塊化拣宏,比如日志組件沈贝。可以認(rèn)為是通知勋乾、引入和切入點(diǎn)的組合宋下;在Spring中可以使用Schema和@AspectJ方式進(jìn)行組織實(shí)現(xiàn);在AOP中表示為“在哪干和干什么集合”辑莫;
  • 引入(Introduction): 也稱為內(nèi)部類型聲明学歧,為已有的類添加額外新的字段或方法,Spring允許引入新的接口(必須對(duì)應(yīng)一個(gè)實(shí)現(xiàn))到所有被代理對(duì)象(目標(biāo)對(duì)象)各吨;在AOP中表示為“干什么(引入什么)”枝笨;
  • 目標(biāo)對(duì)象(Target Object):需要被織入橫切關(guān)注點(diǎn)的對(duì)象,即該對(duì)象是切入點(diǎn)選擇的對(duì)象,需要被通知的對(duì)象伺帘,從而也可稱為“被通知對(duì)象”昭躺;由于Spring AOP 通過(guò)代理模式實(shí)現(xiàn)忌锯,從而這個(gè)對(duì)象永遠(yuǎn)是被代理對(duì)象伪嫁;在AOP中表示為“對(duì)誰(shuí)干”;
  • AOP代理(AOP Proxy): AOP框架使用代理模式創(chuàng)建的對(duì)象偶垮,從而實(shí)現(xiàn)在連接點(diǎn)處插入通知(即應(yīng)用切面)张咳,就是通過(guò)代理來(lái)對(duì)目標(biāo)對(duì)象應(yīng)用切面。在Spring中似舵,AOP代理可以用JDK動(dòng)態(tài)代理或CGLIB代理實(shí)現(xiàn)脚猾,而通過(guò)攔截器模型應(yīng)用切面。
  • 織入(Weaving): 織入是一個(gè)過(guò)程砚哗,是將切面應(yīng)用到目標(biāo)對(duì)象從而創(chuàng)建出AOP代理對(duì)象的過(guò)程龙助,織入可以在編譯期、類裝載期蛛芥、運(yùn)行期進(jìn)行提鸟。組裝方面來(lái)創(chuàng)建一個(gè)被通知對(duì)象。這可以在編譯時(shí)完成(例如使用AspectJ編譯器)仅淑,也可以在運(yùn)行時(shí)完成(jdk自帶的動(dòng)態(tài)代理)称勋。Spring和其他純Java AOP框架一樣,在運(yùn)行時(shí)完成織入涯竟。

3:AOP中切點(diǎn)表達(dá)式

這部分內(nèi)容來(lái)自該 博客

切點(diǎn)指示符
切點(diǎn)指示符是切點(diǎn)定義的關(guān)鍵字赡鲜,切點(diǎn)表達(dá)式以切點(diǎn)指示符開(kāi)始。開(kāi)發(fā)人員使切點(diǎn)指示符來(lái)告訴切點(diǎn)將要匹配什么庐船,有以下9種切點(diǎn)指示符:execution银酬、within、this筐钟、target捡硅、args、@target盗棵、@args壮韭、@within、@annotation纹因,下面一一介結(jié)這9種切點(diǎn)指示符喷屋。

execution
execution是一種使用頻率比較高比較主要的一種切點(diǎn)指示符,用來(lái)匹配方法簽名瞭恰,方法簽名使用全限定名屯曹,包括訪問(wèn)修飾符(public/private/protected)、返回類型,包名恶耽、類名密任、方法名、參數(shù)偷俭,其中返回類型浪讳,包名,類名涌萤,方法淹遵,參數(shù)是必須的,如下面代碼片段所示:

@Pointcut("execution(public String org.baeldung.dao.FooDao.findById(Long))")
上面的代碼片段里的表達(dá)式精確地匹配到FooDao類里的findById(Long)方法负溪,但是這看起來(lái)不是很靈活透揣。假設(shè)我們要匹配FooDao類的所有方法,這些方法可能會(huì)有不同的方法名川抡,不同的返回值辐真,不同的參數(shù)列表,為了達(dá)到這種效果崖堤,我們可以使用通配符侍咱。如下代碼片段所示:

@Pointcut("execution(* org.baeldung.dao.FooDao.*(..))")

第一個(gè)通配符匹配所有返回值類型,第二個(gè)匹配這個(gè)類里的所有方法倘感,()括號(hào)表示參數(shù)列表放坏,括號(hào)里的用兩個(gè)點(diǎn)號(hào)表示匹配任意個(gè)參數(shù),包括0個(gè)

within
使用within切點(diǎn)批示符可以達(dá)到上面例子一樣的效果老玛,within用來(lái)限定連接點(diǎn)屬于某個(gè)確定類型的類淤年。如下面代碼的效果與上面的例子是一樣的:

@Pointcut("within(org.baeldung.dao.FooDao)")
我們也可以使用within指示符來(lái)匹配某個(gè)包下面所有類的方法(包括子包下面的所有類方法),如下代碼所示:
@Pointcut("within(org.baeldung..*)")

this 和 target
this用來(lái)匹配的連接點(diǎn)所屬的對(duì)象引用是某個(gè)特定類型的實(shí)例蜡豹,target用來(lái)匹配的連接點(diǎn)所屬目標(biāo)對(duì)象必須是指定類型的實(shí)例麸粮;那么這兩個(gè)有什么區(qū)別呢?原來(lái)AspectJ在實(shí)現(xiàn)代理時(shí)有兩種方式:

1镜廉、如果當(dāng)前對(duì)象引用的類型沒(méi)有實(shí)現(xiàn)自接口時(shí)弄诲,spring aop使用生成一個(gè)基于CGLIB的代理類實(shí)現(xiàn)切面編程
2、如果當(dāng)前對(duì)象引用實(shí)現(xiàn)了某個(gè)接口時(shí)娇唯,Spring aop使用JDK的動(dòng)態(tài)代理機(jī)制來(lái)實(shí)現(xiàn)切面編程
this指示符就是用來(lái)匹配基于CGLIB的代理類齐遵,通俗的來(lái)講就是,如果當(dāng)前要代理的類對(duì)象沒(méi)有實(shí)現(xiàn)某個(gè)接口的話塔插,則使用this梗摇;target指示符用于基于JDK動(dòng)態(tài)代理的代理類,通俗的來(lái)講就是如果當(dāng)前要代理的目標(biāo)對(duì)象有實(shí)現(xiàn)了某個(gè)接口的話想许,則使用target.:
public class FooDao implements BarDao {
...

}
比如在上面這段代碼示例中伶授,spring aop將使用jdk的動(dòng)態(tài)代理來(lái)實(shí)現(xiàn)切面編程断序,在編寫匹配這類型的目標(biāo)對(duì)象的連接點(diǎn)表達(dá)式時(shí)要使用target指示符, 如下所示:

@Pointcut("target(org.baeldung.dao.BarDao)")
如果FooDao類沒(méi)有實(shí)現(xiàn)任何接口糜烹,或者在spring aop配置屬性:proxyTargetClass設(shè)為true時(shí)违诗,Spring Aop會(huì)使用基于CGLIB的動(dòng)態(tài)字節(jié)碼技為目標(biāo)對(duì)象生成一個(gè)子類將為代理類,這時(shí)應(yīng)該使用this指示器:

@Pointcut("this(org.baeldung.dao.FooDao)")

參數(shù)
參數(shù)指示符是一對(duì)括號(hào)所括的內(nèi)容疮蹦,用來(lái)匹配指定方法參數(shù):

@Pointcut("execution(* *..find(Long))")
這個(gè)切點(diǎn)匹配所有以find開(kāi)頭的方法诸迟,并且只一個(gè)Long類的參數(shù)。如果我們想要匹配一個(gè)有任意個(gè)參數(shù)挚币,但是第一個(gè)參數(shù)必須是Long類的亮蒋,我們這可使用下面這個(gè)切點(diǎn)表達(dá)式:
@Pointcut("execution(
*..find*(Long,..))")

@Target
這個(gè)指示器匹配指定連接點(diǎn)扣典,這個(gè)連接點(diǎn)所屬的目標(biāo)對(duì)象的類有一個(gè)指定的注解:

@Pointcut("@target(org.springframework.stereotype.Repository)")

@args
這個(gè)指示符是用來(lái)匹配連接點(diǎn)的參數(shù)的妆毕,@args指出連接點(diǎn)在運(yùn)行時(shí)傳過(guò)來(lái)的參數(shù)的類必須要有指定的注解,假設(shè)我們希望切入所有在運(yùn)行時(shí)接受實(shí)@Entity注解的bean對(duì)象的方法:

@Pointcut("@args(org.baeldung.aop.annotations.Entity)")
public void methodsAcceptingEntities() {}

為了在切面里接收并使用這個(gè)被@Entity的對(duì)象贮尖,我們需要提供一個(gè)參數(shù)給切面通知:JointPoint:

@Before("methodsAcceptingEntities()")
public void logMethodAcceptionEntityAnnotatedBean(JoinPoint jp) {
logger.info("Accepting beans with @Entity annotation: " + jp.getArgs()[0]);
}

@within
這個(gè)指示器笛粘,指定匹配必須包括某個(gè)注解的的類里的所有連接點(diǎn):

@Pointcut("@within(org.springframework.stereotype.Repository)")
上面的切點(diǎn)跟以下這個(gè)切點(diǎn)是等效的:

@Pointcut("within(@org.springframework.stereotype.Repository *)")

@annotation
這個(gè)指示器匹配那些有指定注解的連接點(diǎn),比如湿硝,我們可以新建一個(gè)這樣的注解@Loggable:

@Pointcut("@annotation(org.baeldung.aop.annotations.Loggable)")
public void loggableMethods() {}
我們可以使用@Loggable注解標(biāo)記哪些方法執(zhí)行需要輸出日志:
@Before("loggableMethods()")
public void logMethod(JoinPoint jp) {
String methodName = jp.getSignature().getName();
logger.info("Executing method: " + methodName);
}

切點(diǎn)表達(dá)式組合
可以使用&&薪前、||住拭、!馏锡、三種運(yùn)算符來(lái)組合切點(diǎn)表達(dá)式,表示與或非的關(guān)系为严。

@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {}

@Pointcut("execution(* *..create*(Long,..))")
public void firstLongParamMethods() {}

@Pointcut("repositoryMethods() && firstLongParamMethods()")
public void entityCreationMethods() {}

總結(jié)

上述描述的緩存實(shí)現(xiàn)通過(guò)AOP方式實(shí)現(xiàn)了對(duì)代碼的低侵入性痢畜,使用常用的nosql數(shù)據(jù)庫(kù)redis做緩存數(shù)據(jù)庫(kù)垛膝,使用jedis調(diào)用redis API進(jìn)行數(shù)據(jù)操作。

好了丁稀,通過(guò)本博文你應(yīng)該是了解了如何通過(guò)AOP方式使用redis進(jìn)行緩存操作了吼拥,如果本博文對(duì)您有些許幫助,還請(qǐng)您“點(diǎn)贊”和“評(píng)論”支持一下线衫,謝謝~

推薦閱讀:
Git-【技術(shù)干貨】工作中Git的使用實(shí)踐
shell-【技術(shù)干貨】編寫shell腳本所需的語(yǔ)法和示例
Git - 使用git不知道內(nèi)部實(shí)現(xiàn)機(jī)制怎么行

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凿可,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子授账,更是在濱河造成了極大的恐慌枯跑,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件白热,死亡現(xiàn)場(chǎng)離奇詭異敛助,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)棘捣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門辜腺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)休建,“玉大人,你說(shuō)我怎么就攤上這事评疗〔馍埃” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵百匆,是天一觀的道長(zhǎng)砌些。 經(jīng)常有香客問(wèn)我,道長(zhǎng)加匈,這世上最難降的妖魔是什么存璃? 我笑而不...
    開(kāi)封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮雕拼,結(jié)果婚禮上纵东,老公的妹妹穿的比我還像新娘。我一直安慰自己啥寇,他們只是感情好偎球,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著辑甜,像睡著了一般衰絮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上磷醋,一...
    開(kāi)封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天猫牡,我揣著相機(jī)與錄音,去河邊找鬼邓线。 笑死淌友,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的褂痰。 我是一名探鬼主播亩进,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼缩歪!你這毒婦竟也來(lái)了归薛?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤匪蝙,失蹤者是張志新(化名)和其女友劉穎主籍,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體逛球,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡千元,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了颤绕。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幸海。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡祟身,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出物独,到底是詐尸還是另有隱情袜硫,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布挡篓,位于F島的核電站婉陷,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏官研。R本人自食惡果不足惜秽澳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望戏羽。 院中可真熱鬧担神,春花似錦、人聲如沸蛛壳。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)衙荐。三九已至,卻和暖如春浮创,著一層夾襖步出監(jiān)牢的瞬間忧吟,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工斩披, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留溜族,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓垦沉,卻偏偏與公主長(zhǎng)得像煌抒,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子厕倍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容