Spring AOP——從Logger攔截說起

先用500字以內的大白話說明什麼是AOP

引用zhihu上的一個回答:
作者:欲眼熊貓
鏈接:https://www.zhihu.com/question/24863332/answer/48376158
來源:知乎
著作權歸作者所有,轉載請聯繫作者獲得授權。

面向切面編程(Aspect Oriented Program的首字母縮寫) 。面向對象的特點是繼承、多態和封裝。而封裝就要求將功能分散到不同的對象中去。實際上也就是說,讓不同的類設計不同的方法。這樣代碼就分散到一個個的類中去了。這樣做的好處是降低了代碼的複雜程度,使類可重用。

但是在分散代碼的同時,也增加了代碼的重複性。什麼意思呢?比如說,在兩個類中,需要在每個方法中各自寫日誌。按面向對象的設計方法,我們就必須在兩個類的方法中都加入完全相同的寫日誌的代碼,因爲面向對象的封裝讓類與類之間無法共享代碼,而不能將這些重複的代碼統一起來。

也許有人會說,那好辦啊,我們可以將這段代碼寫在第三個類的獨立的方法裏,然後再在這兩個類中調用。但是,這樣一來,這兩個類跟我們上面提到的獨立的類就有耦合了,它的改變會影響這兩個類。那麼,有沒有什麼辦法,能讓我們在需要的時候,隨意地加入代碼呢?這種在運行時,動態地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程。

一般而言,我們管切入到指定類指定方法的代碼片段稱爲切面。有了AOP,我們就可以把幾個類共有的代碼,抽取到一個切片中,等到需要時再切入對象中去,從而改變其原有的行爲。

這樣看來,AOP其實只是OOP的補充而已。OOP從橫向上區分出一個個的類來,而AOP則從縱向上向對象中加入特定的代碼。有了AOP,OOP變得立體了。如果加上時間維度,AOP使OOP由原來的二維變爲三維了,由平面變成立體了。從技術上來說,AOP基本上是通過代理機制實現的。

AOP在編程歷史上可以說是里程碑式的,對OOP編程是一種十分有益的補充。

個人理解,Spring中的事務管理是一個很好的AOP的例子,各個DAO(Data Access Object)類只管增刪改查,事務統一用Spring的事務管理。

此外,遍佈代碼各個角落的大量log4j日誌也可以通過AOP方式集中在一個AOP類中,統一管理。

再用一個Spring Logger攔截的例子說明AOP

這裏的全部用法參考spring官網4.2.9文檔的第10章《Aspect Oriented Programming with Spring》,不保證和最新版本一致

需要用到的jar包

Spring 4.x一共有20個jar包,推薦使用Maven或Gradle配置方式下載。如果公司局域網不能使用Maven只能去官網下載jar包:
官方地址1
官方地址2
官方地址3

此外,依賴AOP標準相關的包必須額外下載,推薦使用Maven網頁版下載,搜索框輸入aspectj,結果點進去就能下載jar包
下載網頁

(千萬別漏了3個aop相關的jar,需要額外下載!)

aspectj-1.8.9.jar
aopalliance-1.0.jar
aspectjweaver-1.6.9.jar

spring-aop-4.2.3.RELEASE.jar
spring-aspects-4.2.3.RELEASE.jar
spring-beans-4.2.3.RELEASE.jar
spring-context-4.2.3.RELEASE.jar
spring-context-support-4.2.3.RELEASE.jar
spring-core-4.2.3.RELEASE.jar
spring-expression-4.2.3.RELEASE.jar
spring-jdbc-4.2.3.RELEASE.jar
spring-jms-4.2.3.RELEASE.jar
spring-messaging-4.2.3.RELEASE.jar
spring-orm-4.2.3.RELEASE.jar
spring-test-4.2.3.RELEASE.jar
spring-tx-4.2.3.RELEASE.jar
spring-web-4.2.3.RELEASE.jar
spring-webmvc-4.2.3.RELEASE.jar

spring配置文件

Spring提供了4種實現AOP的方式:

  1. 經典的基於代理的AOP
  2. @Aspect註解驅動的切面
  3. 純POJO切面
  4. 注入式AspectJ切面

這裏我們使用的是第二種,因爲這種方式和第一種相比較簡單。而且spring 4對註解的支持已經非常完善了。

Web App項目的spring配置文件路徑是項目/WebContent/x-servlet.xml(文件名中的”x“是同目錄下web.xml中配置的服務名 <servlet-name>x</servlet-name>),配置完spring AOP後,x-servlet.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:task="http://www.springframework.org/schema/task"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context.xsd 
       http://www.springframework.org/schema/mvc 
       http://www.springframework.org/schema/mvc/spring-mvc.xsd 
       http://www.springframework.org/schema/websocket 
       http://www.springframework.org/schema/websocket/spring-websocket.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
       http://www.springframework.org/schema/task 
       http://www.springframework.org/schema/task/spring-task-3.0.xsd">

    <context:component-scan base-package="com.my.demo" >
    </context:component-scan>

    <!--有了這個Spring就能夠自動掃描被@Aspect標註的切面了-->
    <aop:aspectj-autoproxy />
</beans>
import org.apache.log4j.Logger;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import cn.paic.rep.pare.annotation.SysLogger;
import cn.paic.rep.pare.modal.EbdUserAccountEntity;
import cn.paic.rep.pare.sessionManager.SessionManager;


/**
 * 利用Spring AOP攔截日誌
 */
@Aspect //說明這個類是AOP概念中的切面
@Component //說明這個類是使用Spring的註解方式生成(單例)實例
public class SysLoggerAspect {
    private static Logger logger = Logger.getLogger(SysLoggerAspect.class); // 使用log4j類記錄日誌

    /*
    (1)括號裏是國際標準AspectJ 5規定的語法,具體請看下面介紹
    (2)方法返回值必須void
    */
    @Before("execution(* *(..))")
    public void checkBefore(String sessionKey,SysLogger sysLogger) {
        System.out.println("寫日誌");  
    }
}

@Before註解的括號中各個元素分別表示

  • (可選)修飾符匹配 modifier-pattern
  • 返回值匹配 ret-type-pattern
  • (可選)類路徑匹配 declaring-type-pattern
  • 方法名和參數匹配 name-pattern(param-pattern)
  • (可選)異常類型匹配 throws-pattern

在各個pattern中可以使用“*”來表示匹配所有。在參數匹配 param-pattern 中,遵循由ApsectJ 5定義的一系列規則1

  • 可以指定具體的參數類型,如(String)表示匹配一個String參數的方法
  • 多個參數間用“,”隔開
  • 可以用“*”來表示匹配任意類型的參數;(*,String)表示匹配有兩個參數的方法,第一個參數可以是任意類型,而第二個參數是String類型;
  • 可以用(..)表示零個或多個任意參數。
  • 在類路徑匹配中,”.*”表示包下所有類,”..*”表示包以及子包下的所有類。如com.savage.service.* 表示 com.savage.service包下的所有類。

現在來看看幾個例子:

  1. execution(* *(..))
    表示匹配所有方法
  2. execution(public * com. savage.service.UserService.*(..))
    表示匹配com.savage.server.UserService中所有的公有方法
  3. execution(* com.savage.server...(..))
    表示匹配com.savage.server包及其子包下的所有方法

一個Pointcut定義由 AspectJ 表示式和 函數簽名組成,例如:

//Pointcut表示式
@Pointcut("execution(* com.savage.aop.MessageSender.*(..))")
//Pointcut簽名
private void log(){}    

其中,具體有三種Pointcut
@Before(一般用來記錄入參),
@After(一般用來記錄返回值),
@Around(包含了@Before和@After的功能,此外,一般用來計算方法執行的時間)

@Before,@After,@Around可以指定已經用@Pointcut修飾好的函數簽名,如

//因爲已經在log()方法上聲明瞭Pointcut表示式,
//所以這裏等價於@Before("execution(* com.savage.aop.MessageSender.*(..))")
@Before("log()")

除了execution表示式外,還有within、this、target、args等 AspectJ 表示式。多種表示式可以通過邏輯運算符號 &&、||、! 鏈接起來

@Pointcut("execution(* com.savage.aop.MessageSender.*(..))")
private void logSender(){}
@Pointcut("execution(* com.savage.aop.MessageReceiver.*(..))")
private void logReceiver(){}
@Pointcut("logSender() || logReceiver()")
private void logMessage(){}

在AOP中獲得調用方信息的辦法

獲得當前調用方方法

獲取到接口的方法

@Around("execution(* *(..))")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable{
    Signature s = pjp.getSignature();
    MethodSignature ms = (MethodSignature)s;
    Method m = ms.getMethod();
}

獲取到實現類的方法

//ProceedingJoinPoint 作爲參數時,只能用@Around註解,不然報錯
@Around("execution(* *(..))")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable{
    Signature sig = pjp.getSignature();
    MethodSignature msig = null;
    if (!(sig instanceof MethodSignature)) {
        throw new IllegalArgumentException("該註解只能用於方法");
    }
    msig = (MethodSignature) sig;
    Object target = pjp.getTarget();
    Method currentMethod = target.getClass()
        .getMethod(msig.getName(), msig.getParameterTypes());
    //獲得方法入參
    Object[] args = joinPonit.getArgs();
        String[] strs = new String[args.length];
        for (int i=0; i<args.length; i++) {
            if (args[i] == null) {
                continue;
            }
            strs[i] = args[i].toString();//當url不帶某參數時,spring會令它爲null,這裏就會NPE
        }
}

附錄 術語中英文對照

Aspect - 切面
Join point - 切入點
Advice - 增強2(個人認爲翻譯爲”通知“更好)
Pointcut - 切點。AOP通過“切點”定位特定的連接點。
Introduction - 聲明額外的methods or fields on behalf of a type.
Target object - 需要織入通知的目標類。
AOP proxy - 爲bean代理
Weaving - 織入。將Advice(通知)添加到目標類具體Joinpoint(連接點)上的過程。
Joinpoint - 連接點。Spring支持方法調用前、方法調用後、方法拋出異常時以及Around(方法調用前後)
AspectJ - 一種描述AOP的國際標準,Spring支持部分的ApsectJ


  1. 陳雄華, 林開雄. Spring 3.x 企業應用開發實戰[M]. 北京:電子工業出版社, 2012: 233~234
  2. 陳雄華, 林開雄. Spring 3.x 企業應用開發實戰[M]. 北京:電子工業出版社, 2012: 187~188
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章