(002)Spring 之 AOP

林灣村龍貓發表於2017-12-14

概述

Spring的最終目的是簡化應用開發。通俗的講減少重複程式碼,少寫程式碼達到相同的目的。面向切面程式設計(AOP, Aspect Oriented Programming)就是一種減重複程式碼方式。我們都知道JAVA是一門物件導向程式設計(OOP, Object Oriented Programming)語言,在java中將一個個功能模組抽象成一個個物件。這些物件通過一定的聯絡完成我們所看到的一個個應用,一個個服務。它的核心就是物件(Object)。

要理解切面程式設計,就需要先理解什麼是切面。用刀把一個西瓜分成兩瓣,切開的切口就是切面;炒菜,鍋與爐子共同來完成炒菜,鍋與爐子就是切面。web層級設計中,web層->閘道器層->服務層->資料層,每一層之間也是一個切面。程式設計中,物件與物件之間,方法與方法之間,模組與模組之間都是一個個切面

類應該是純淨的,不應含有與本身無關的邏輯。單一職責原則。

切面程式設計,可以帶來程式碼的解耦;同時,切面程式設計也是需要執行程式碼的,增加了一些額外的程式碼執行量。因地制宜,使用AOP。

他山之石

具象化理解

我們一般做活動的時候,一般對每一個介面都會做活動的有效性校驗(是否開始、是否結束等等)、以及這個介面是不是需要使用者登入。

按照正常的邏輯,我們可以這麼做。

第1版

這有個問題就是,有多少介面,就要多少次程式碼copy。對於一個“懶人”,這是不可容忍的。好,提出一個公共方法,每個介面都來呼叫這個介面。這裡有點切面的味道了。

第2版

同樣有個問題,我雖然不用每次都copy程式碼了,但是,每個介面總得要呼叫這個方法吧。於是就有了切面的概念,我將方法注入到介面呼叫的某個地方(切點)。

第3版

這樣介面只需要關心具體的業務,而不需要關注其他非該介面關注的邏輯或處理。

紅框處,就是面向切面程式設計。

理論

使用AOP之前,我們需要理解幾個概念。

各個概念關係

連線點(Join Point)

所有可能的需要注入切面的地方。如方法前後、類初始化、屬性初始化前後等等。

切點(Poincut)

需要做某些處理(如列印日誌、處理快取等等)的連線點。如何來指明一個切點?spring使用了AspectJ的切點表示式語言來定義Spring切面。

切點定義方式

多個表示式之間,可用“and” 、“or”、“not”做邏輯連線。

其中較為複雜的是execution。

execution使用方式

目前,Spring只支援方法的切點定義

通知(Advice)

定義在什麼時候做什麼事情。spring支援5種方法上的通知型別

支援的通知型別

切面(Aspect)

通知+切點的集合,定義在什麼地方什麼時間做什麼事情。

引入(Introduction)

允許我們向現有的類新增新方法屬性。這不就是把切面(也就是新方法屬性:通知定義的)用到目標類中嗎

目標(Target)

引入中提到的目標類,也就是要被通知的物件。也就是真正的業務邏輯,他可以在毫不知情的情況下,被我們們織入切面。而自己專注於業務本身的邏輯。

織入(Weaving)

把切面應用到目標物件來建立新的代理物件的過程。

spring中AOP實現原理

AOP的實現實際上是用的是代理模式。

代理

代理概念:簡單的理解就是通過為某一個物件建立一個代理物件,我們不直接引用原本的物件,而是由建立的代理物件來控制對原物件的引用。

按照代理的建立時期,代理類可以分為兩種。

  • 靜態代理:由程式設計師建立或特定工具自動生成原始碼,再對其編譯。在程式執行前,代理類的.class檔案就已經存在了。
  • 動態代理:在程式執行時,運用反射機制動態建立而成,無需手動編寫程式碼。動態代理不僅簡化了程式設計工作,而且提高了軟體系統的可擴充套件性,因為Java反射機制可以生成任意型別的動態代理類。

代理原理:代理物件內部含有對真實物件的引用,從而可以操作真實物件,同時代理物件提供與真實物件相同的介面以便在任何時刻都能代替真實物件。同時,代理物件可以在執行真實物件操作時,附加其他的操作,相當於對真實物件進行封裝。

spring使用的是動態代理。

動態代理之JDK

java jdk 本身支援動態建立代理物件。

jdk動態代理

java的動態代理,有個缺點,它只支援實現了介面的類。因此可以引入cglib(Code Generation Library)三方庫支援類的代理。 對JDK動態代理有興趣的可以參考一下:blog.csdn.net/u012410733/…

動態代理之cglib

cglib動態代理

對cglib庫有興趣可以參考一下:blog.csdn.net/danchu/arti…

樣例

有這麼一個需求,對某些介面做快取或列印日誌。有不想每個介面中都呼叫快取方法或列印日誌方法。可以這麼來做。 程式碼:

  • 定義動物抽象介面:Animal
  • 定義具體動物貓:Cat
  • 定義bean掃描配置類:AnimalConfig
  • 定義快取註解:Cache
  • 定義快取切面:CacheAspect
  • 定義日誌切面:LogAspect
  • 定義測試入口類:App

1. Animal介面

package bean;

public interface Animal {

    public String sayName(String name, Integer num);
}
複製程式碼

2. Cat類

package bean;

import org.springframework.stereotype.Component;

@Component
public class Cat implements Animal {

    @Cache(60)
    public String sayName(String name, Integer num) {
        return "this is cat " + name + "," + num;
    }
}
複製程式碼

3. AnimalConfig spirng配置掃描類

package bean;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AnimalConfig {
}
複製程式碼

4.Cache註解(與CacheAspect配合使用)

package bean;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cache {
    int value() default 0;
}
複製程式碼

5.CacheAspect 快取切面

package bean;

import com.alibaba.fastjson.JSONObject;
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.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;

@Component
@Aspect
@Order(1000)
public class CacheAspect {

    // 定義切入點:帶有Cache註解的方法
    @Pointcut("@annotation(Cache)")
    private void cache(){}

    // 臨時儲存區
    private static Map<String, Object> cacheList = new LinkedHashMap<String, Object>();

    // 定義環繞通知,處理介面/方法新增快取
    @Around("cache()")
    private Object cacheAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        Object object = proceedingJoinPoint.getTarget();
        Object[] args = proceedingJoinPoint.getArgs();
        String className = object.getClass().getName();
        MethodSignature signature = (MethodSignature)proceedingJoinPoint.getSignature();
        Method method = signature.getMethod();
        
        // 組裝cache key
        String cacheKey = className + "_" + method.getName() + "_" + JSONObject.toJSONString(args);
        if (cacheList.containsKey(cacheKey)){
            System.out.println("data get  cache");
            return cacheList.get(cacheKey);
        }else {
            System.out.println("data get  db");
            Object result = proceedingJoinPoint.proceed();
            cacheList.put(cacheKey, result);
            return result;
        }
    }
}
複製程式碼

6.LogAspect 日誌切面

package bean;

import com.alibaba.fastjson.JSONObject;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Order(100)
public class LogAspect {
    //定義切點:包含任意引數的、任意返回值、的公共方法sayName
    @Pointcut("execution(public * *sayName(..))")
    private void log(){}
    
    //定義環繞通知:處理日誌注入
    @Around("log()")
    private Object logAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object[] args = proceedingJoinPoint.getArgs();

        System.out.println("before, params:" + JSONObject.toJSONString(args));
        Object result = proceedingJoinPoint.proceed();
        System.out.println("after, result:" + JSONObject.toJSONString(result));
        return result;
    }
}
複製程式碼

7. APP 測試入口類

import bean.Animal;
import bean.AnimalConfig;
import bean.Cat;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class App {
    public static void main(String[] args){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AnimalConfig.class);

        Animal animal = applicationContext.getBean("cat", Cat.class);
        String result = animal.sayName("rudytan", 12);
        String result1 = animal.sayName("rudytan", 12);
        String result2 = animal.sayName("rudytan", 12);
        String result3 = animal.sayName("rudytan", 13);
        String result4 = animal.sayName("rudytan", 13);
        System.out.println(result);
    }
}
複製程式碼

說明:

  • @Cache為自定義註解(標記該方法需要快取)
  • @EnableAspectJAutoProxy 為是否開啟cglib動態代理
  • @Aspect為定義該類為切面類
  • @Order為當有多個切面類的時候,定義執行順序,數值越大,約先執行。
  • @Pointcut定義當前函式為切點。
  • @Around定義環繞通知

相關文章