前提
這篇博文是這套Spring學習筆記的第六篇——AOP篇(上),主要內容包含Spring AOP的基礎知識及應用,全篇以一個我遇到的真實編程問題的出現、思考及通過Spring AOP得以解決的過程,使大家對AOP的應用場景可以有一個深刻的認識。如果需要了解有關Spring的綜述信息或博文的索引信息,請移步:
《綜述篇》
什麼是AOP?
先從概念上來說,AOP即Aspect Oriented Programing——面向切面的編程,類似的一個概念即OOP(Object Oriented Programing)——面向對象的編程。AOP並不是用來取代OOP的,而是應OOP中某些特定的應用場景而誕生的。
具體的應用場景以下面的例子給出。
在《配置篇》的“controller包和UserController”這一小節,我們創建了一個UserController類,其中有個handleLoginRequest函數用來執行用戶登錄的業務邏輯,我們再給它添上一個Register註冊函數,代碼如下:
@Controller
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("Login")
private void handleLoginRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
try (PrintWriter out = response.getWriter()) {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
response.setCharacterEncoding("utf-8");
//此處登錄功能的業務邏輯省略
}
}
@RequestMapping("Register")
private void handleRegisterRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
try (PrintWriter out = response.getWriter()) {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
response.setCharacterEncoding("utf-8");
//此處註冊功能的業務邏輯省略
}
}
}
可以看到兩個函中有三行重複代碼,它們的作用是設置請求和響應中的字符編碼:
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
response.setCharacterEncoding("utf-8");
我們可以想象到的是,在工程的規模逐漸擴大後,這種請求處理函數會有很多,如果每個函數前面都有這麼三行重複代碼,很明顯違反了OOP的原則。那麼一般想法就是給這三行代碼提到一個函數中去,每個處理請求的函數再來調用這個函數。但是這樣做只是把重複的三行代碼變成了一行,並不算是“優雅”地解決了這個問題。
我們設想能有一種類似函數監聽器的東西,能在我們指定的函數執行之前先執行這三行代碼。這樣我們就不用在業務邏輯中見到它們了。
Spring AOP就是用來實現類似的功能的,除了上述場景,AOP主要適用於性能監控、日誌記錄等與業務邏輯關係不大的場景中。在傳統的OOP中,一般這類代碼必須加在主要業務邏輯的一端或兩端,通過AOP可以在保證功能的前提下對這些代碼和業務邏輯進行解耦。
AOP的基礎術語
①連接點(JoinPoint)
準確的說這個概念更貼近於“時間上的點”,而不是“代碼順序上的點”。比如函數執行前、執行後和拋出異常時等。
②切點(Pointcut)
切點是連接點的子概念,一個函數可以有多個連接點,但是我們關心的,要對其執行操作的連接點纔是切點。比如上述例子中,處理請求的各函數的“執行前”這個連接點纔是切點,其他的連接點我們不關心,也就不是切點。
③增強(Advice)
即我們在切點上要做的操作。比如上述例子中設置請求和響應中的字符編碼的那三行代碼。
④織入(Weaving)
即AOP將增強和原代碼中的業務邏輯結合起來的過程。
⑤切面(Aspect)
它是切點和增強概念的結合,Spring AOP把切面定義的增強編織到切面定義的切點中。通俗的來說,就是Spring AOP會根據我們的命令在函數運行的特定的點上執行特定的操作。
增強的類型
Spring AOP支持五種增強類型:前置增強、後置增強、環繞增強、異常拋出增強和引介增強。
①前置增強(Before):即在函數調用前施以增強;
②後置增強(AfterReturning):即在函數返回後施以增強;
③環繞增強(Around):即在函數執行前後都施以增強;
④異常拋出增強(AfterThrowing):即在函數拋出異常後施以增強;
⑤引介增強(Introduction):不同於其他增強,引介增強可以對指定的類在運行時動態的實現指定的接口。
基於AspectJ的AOP
AspectJ是一個AOP框架,Spring無縫集成了AspectJ。
注意:使用AspectJ前需要額外添加aspectjtools和cglib的Jar包到庫中。
aspectjtools:鏈接 密碼:du9t
cglib:鏈接 密碼:7tva
我們先給出上述問題的實現代碼,首先是切面類:
@Aspect //註釋①
public class BeforeHandleRequestAspect{
@Before("execution(* com.implementist.MyFirstWebApp.controller.*(..))") //註釋②
public void beforeHandleRequest(JoinPoint joinPoint){ //註釋③
Object[] args = joinPoint.getArgs();
HttpServletRequest request = (HttpServletRequest) args[0];
HttpServletResponse response = (HttpServletResponse) args[1];
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
response.setCharacterEncoding("utf-8");
}
}
註釋:
①@Aspect註解標示該類是一個切面類;
②@Before註解標示這是一個前置增強,括號裏的字符串是一個切點表達式,作用是定位com.implementist.MyFirstWebApp.controller包下的所有類中的所有函數。這一行的整體效果是對上述所有函數織入前置增強;
③增強的函數體,即前置增強期間要做的操作。通過定義JoinPoint參數可以訪問切點的信息,如參數列表等。
接着需要對spring-mvc.xml文件做兩部分修改:
①爲<beans>標籤增加aop命名空間的聲明:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
">
②因爲作爲被增強的對象,controller包下面的類在經歷掃描後已經生成了對應的bean,所以還需要添加前置增強切面類的bean和aop自動代理的代碼:
<bean class="com.implementist.MyFirstWebApp.BeforeHandleRequestAspect"/>
<aop:aspectj-autoproxy/>
這樣,Spring AOP就會自動在指定的函數切點上織入前置增強,每當函數執行前,都會先執行增強函數來設置request和response的字符編碼。
切點表達式函數
在上述代碼中,我們看到
@Before("execution(* com.implementist.MyFirstWebApp.controller.*(..))")
這個前置增強註解的參數好像有一個用字符串定義的函數
execution(* com.implementist.MyFirstWebApp.controller.*(..))
這個execution()就是Spring支持的9個切點函數之一,另外8個函數是:@annotation()、args()、@args()、within()、target()、@within()、@target()和this()。
通配符
上述函數中,有的可以使用通配符,AspectJ一共支持三種通配符:
①*匹配單個任意字符串;
②..匹配多個任意字符串,表示類時必須與*連用,即..*;
③+按類型匹配,如execute(* Animal+(..))
匹配Animal類及其子類的所有函數。
注意:
①execution()和within()支持全部通配符;
②args()、this()和target()僅支持+通配符;
③其餘的函數不支持任何通配符。
邏輯運算符
和Java中的邏輯運算符相似,上述9個切點表達式函數可以通過邏輯運算符進行邏輯運算,邏輯運算符即①&&表示與;②||表示或;③!表示非
如我需要對controller包下所有類的所有函數中,參數爲request和response的函數織入後置增強:
@AfterReturning("within(com.implementist.MyFirstWebApp.controller.*) and args(request,response)")
各增強對應的註解
前置增強——@Before
後置增強——@AfterReturning
環繞增強——@Around
異常拋出增強——@AfterThrowing
引介增強——@DeclareParents
另外,AspectJ還支持一個Final增強——@After,這個增強相當於異常捕獲模塊的final塊,無論程序正常還是異常退出,都會執行該增強。
各切點表達式函數的使用方法
①execution:最常用的函數,其語法爲:
execution(<修飾符>?<返回類型><函數名>(<參數>)<異常>?)
如 execution(public * com..*.*Function(String,int,..)) 表示權限爲public的com包下的所有類中,名稱以“Function”爲後綴的,前兩個參數分別是String和int型,後面可以有任意多個參數的函數。
注意:在
java.lang
包下的類直接寫非全限定名即可,如String
、int
、double
等;除此之外的類都要寫全限定名,如com.implementist.MyFirstApp.domain.User
。
②@annotation:表示標註了指定註解的全部函數,參數爲指定註解的全限定名,如
@annotation(com.implementist.MyFirstWebApp.ContainsBugs)
表示所有被冠以@ContainsBugs註解
的函數。
③args:表示參數是指定的類型的函數,如
args(String,int)
表示僅有兩個參數,且分別爲String和int類型的全部函數(順序必須一致);
args(String,int,*)
表示僅有三個參數,前兩個參數分別爲String和int類型,第三個參數爲任意類型的全部函數;
args(String,int,..)
表示前兩個參數分別爲String和int類型,後面可以有任意個任意類型參數的全部函數。
④@args:表示參數被冠以指定類型註解的函數,如
@args(com.implementist.MyFirstWebApp.ContainsBugs)
表示僅有一個參數,且該參數被冠以了@ContainsBugs
註解的全部函數。
注意:因爲註解是可以被繼承的,在
A◁——B◁——C
這樣一個類繼承樹上:
①如果A中有函數被冠以@ContainsBugs
註解,在B和C中用@args(com.implementist.MyFirstWebApp.ContainsBugs)
做切點匹配不到任何連接點,因爲註解點A高於判斷點B或C;
②反之,在C中有函數被冠以@ContainsBugs
註解,在A和B中用@args(com.implementist.MyFirstWebApp.ContainsBugs)
做切點都可以匹配到C,因爲註解點C低於判斷點A或B。
⑤within:表示類匹配模式,它可以匹配指定類及其子類中的全部函數,它是靜態植入的,即在程序運行前, 參數最低只能指定到類級別,如
within(com.implementist.MyFirstWebApp.controller.*)
表示com.implementist.MyFirstWebApp.controller
包下的所有類及其子類的所有函數。
⑥target:表示類匹配模式,它可以匹配指定類及其子類中的全部函數,它是動態植入的,即在程序運行期間, 參數最低只能指定到類級別,如
target(com.implementist.MyFirstWebApp.Utils)
表示com.implementist.MyFirstWebApp.Utils
及其子類的全部函數。
注意,如果此時通過引介增強等手段爲Utils類或者其子類實現了額外的接口比如
Collection
接口,那麼其中的size()等函數也會被匹配爲切點。這邊是target動態植入與within靜態植入的不同。
⑦@within:表示被冠以指定註解的類及其子類的全部函數,如
@within(com.implementist.MyFirstWebApp.ContainsBugs)
表示被冠以@ContainsBugs
註解的類及其子類的全部函數。
⑧@target:表示匹配被冠以指定註解的類的所有函數,如
@within(com.implementist.MyFirstWebApp.ContainsBugs)
表示被冠以@ContainsBugs
註解的類的全部函數。
⑨this:表示代理類匹配模式,它可以匹配指定類及其子類中的全部函數,它是動態植入的,即在程序運行期間,一般情況下它的效果和target()
是等效的
注意,它與
target()
的不同點在於,當通過引介增強爲目標類動態實現一個接口時,該接口內的函數也會被全部匹配。
後記
這一篇就先到這了,因爲AOP這裏的知識確實多,所以我決定分爲上下兩篇,一可以讓讀者休息一下,不至於一口氣看的太累。二來我也可以先整體瀏覽一下這前半部分,然後休息一下接着寫。
另外,有一些在AOP發展過程中很重要,曾經需要我們手動添加但現在已經被封裝的一些東西我就省略掉了,比如最後只提到了一下的代理這個東西。雖然在AOP中可以自動代理,但是代理這個概念還是很重要的,比如設計模式中就有一個代理模式,有興趣的話可以移步《Java設計模式之代理模式(Proxy)》。