前言
上一篇我們介紹了Spring的核心概念DI,DI有助與應(yīng)用對(duì)象之間的解耦录别。今天我們就來介紹下另一個(gè)非常核心的概念阀捅,面向切面編程AOP沮脖。
正文
在軟件開發(fā)中凯正,散布于應(yīng)用中多處的功能被稱為橫切關(guān)注點(diǎn)(cross-cutting concern)院仿。通常來講秸抚,這些橫切關(guān)注點(diǎn)從概念上是與應(yīng)用的業(yè)務(wù)邏輯相分離的。比如:日志歹垫、聲明式事物剥汤、安全和緩存。這些東西都不是我們平時(shí)寫代碼的核心功能排惨,但許多地方都要用到吭敢。
把這些橫切關(guān)注點(diǎn)與業(yè)務(wù)相分離正是面向切面編程(AOP)索要解決的問題。
簡(jiǎn)單的說就是把這些許多地方都要用到暮芭,但又不是核心業(yè)務(wù)的功能鹿驼,單獨(dú)剝離出來封裝欲低,通過配置指定要切入到指定的方法中去。
什么是面向切面編程
如上圖所示畜晰,這就是橫切關(guān)注點(diǎn)的概念砾莱,水平的是核心業(yè)務(wù),這些切入的箭頭就是我們的橫切關(guān)注點(diǎn)凄鼻。
橫切關(guān)注點(diǎn)可以被模塊化為特殊的類腊瑟,這些類被稱為切面(aspect)。這樣做有兩個(gè)好處:
- 首先块蚌,現(xiàn)在每個(gè)關(guān)注點(diǎn)都集中于一個(gè)地方闰非,而不是分割到多處代碼中
- 其次,服務(wù)模塊更簡(jiǎn)潔峭范,因?yàn)樗鼈冎话饕P(guān)注點(diǎn)(或核心功能)的代碼河胎,而次要關(guān)注點(diǎn)的代碼被轉(zhuǎn)移到切面中了。
定義AOP術(shù)語
為了理解AOP虎敦,我們必須先了解AOP的相關(guān)術(shù)語游岳,很簡(jiǎn)單不難:
通知(Advice):
在AOP中,切面的工作被稱為通知其徙。通知定義了切面“是什么”以及“何時(shí)”使用胚迫。除了描述切面要完成的工作,通知還解決了何時(shí)執(zhí)行這個(gè)工作的問題唾那。
Spring切面可以應(yīng)用5種類型的通知:
- 前置通知(Before):在目標(biāo)方法被調(diào)用之前調(diào)用通知功能
- 后置通知(After):在目標(biāo)方法完成之后調(diào)用通知访锻,此時(shí)不會(huì)關(guān)心方法的輸出是什么
- 返回通知(After-returning):在目標(biāo)方法成功執(zhí)行之后調(diào)用通知
- 異常通知(After-throwing):在目標(biāo)方法拋出異常后調(diào)用通知
- 環(huán)繞通知(Around):通知包裹了被通知的方法,在被通知的方法調(diào)用之前和調(diào)用之后執(zhí)行自定義的行為
連接點(diǎn)(Join point):
連接點(diǎn)是在應(yīng)用執(zhí)行過程中能夠插入切面的一個(gè)點(diǎn)闹获。這個(gè)點(diǎn)可以是調(diào)用方法時(shí)期犬、拋出異常時(shí)、甚至修改一個(gè)字段時(shí)避诽。切面代碼可以利用這些點(diǎn)插入到應(yīng)用的正常流程之中龟虎,并添加行為。
切點(diǎn)(Pointcut):
如果說通知定義了切面“是什么”和“何時(shí)”的話沙庐,那么切點(diǎn)就定義了“何處”鲤妥。比如我想把日志引入到某個(gè)具體的方法中,這個(gè)方法就是所謂的切點(diǎn)拱雏。
切面(Aspect):
切面是通知和切點(diǎn)的結(jié)合棉安。通知和切點(diǎn)共同定義了切面的全部?jī)?nèi)容———他是什么,在何時(shí)和何處完成其功能铸抑。
引入(Introduction):
引入允許我們向現(xiàn)有的類添加新的方法和屬性(Spring提供了一個(gè)方法注入的功能)贡耽。
織入(Weaving):
把切面應(yīng)用到目標(biāo)對(duì)象來創(chuàng)建新的代理對(duì)象的過程,織入一般發(fā)生在如下幾個(gè)時(shí)機(jī):
- 編譯時(shí):當(dāng)一個(gè)類文件被編譯時(shí)進(jìn)行織入,這需要特殊的編譯器才可以做的到蒲赂,例如AspectJ的織入編譯器
- 類加載時(shí):使用特殊的ClassLoader在目標(biāo)類被加載到程序之前增強(qiáng)類的字節(jié)代碼
- 運(yùn)行時(shí):切面在運(yùn)行的某個(gè)時(shí)刻被織入,SpringAOP就是以這種方式織入切面的阱冶,原理應(yīng)該是使用了JDK的動(dòng)態(tài)代理技術(shù)
Spring對(duì)AOP的支持
創(chuàng)建切入點(diǎn)來定義切面所織入的連接點(diǎn)是AOP框架的基本功能。
Spring提供了4種類型的AOP支持:
- 基于代理的經(jīng)典Spring AOP
- 純POJO切面
- @AspectJ注解驅(qū)動(dòng)的切面
- 注入式AspectJ切面(使用與Spring各版本)
前三種都是Spring AOP實(shí)現(xiàn)的變體凳宙,Spring AOP構(gòu)建在動(dòng)態(tài)代理基礎(chǔ)之上,因此职祷,Spring對(duì)AOP的支持局限于方法攔截氏涩。
這里我不準(zhǔn)備介紹經(jīng)典Spring AOP,因?yàn)橐肓撕?jiǎn)單的聲明式AOP和基于直接的AOP后有梆,Spring經(jīng)典的AOP看起來就顯得非常笨重和過于復(fù)雜是尖。
對(duì)于新手入門來說,我們不需要知道這么多泥耀,在這里我也只介紹2,3兩種方式饺汹,簡(jiǎn)單的說就是一個(gè)基于xml配置,一個(gè)基于注解痰催。
下面就直接開始舉兩個(gè)例子分別來介紹下這兩種AOP方式兜辞,我們就拿簡(jiǎn)單的日志來說明。
基于注解的方式
首先基于注解的方式需要引入這些包夸溶,對(duì)用的pom.xml如下:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.8</version>
</dependency>
我們還是舉前面用到的UserController來說明逸吵,下面方法很簡(jiǎn)單,執(zhí)行進(jìn)入這個(gè)方法的時(shí)候會(huì)打印“進(jìn)來了”信息缝裁,現(xiàn)在我打算給這個(gè)方法加日志扫皱,在執(zhí)行該方法前打印“進(jìn)來前”,在執(zhí)行完方法后執(zhí)行“進(jìn)來后”捷绑。
package com.tengj.demo.controller;
@Controller
@RequestMapping(value="/test")
public class UserController {
@Autowired
UserService userService;
@RequestMapping(value="/view",method = RequestMethod.GET)
public String index(){
userService.sayHello("tengj");
return "index";
}
}
servie層代碼:
package com.tengj.demo.service
public interface UserService {
public void sayHello(String name);
}
servie實(shí)現(xiàn)類代碼:
package com.tengj.demo.service.impl;
@Service("userService")
public class UserServiceImpl implements UserService{
@Override
public void sayHello(String name) {
System.out.println("hello韩脑,"+name);
}
}
上面方法index()其實(shí)就是我們之前定義的切點(diǎn),表示在哪里切入AOP粹污。
如圖所示段多,我們使用execution()指示器選擇UserServiceImpl的sayHello方法。方法表達(dá)式以“*”號(hào)開始壮吩,表明了我們不關(guān)心方法返回值的類型衩匣。然后,我們指定了全限定類名和方法名粥航。對(duì)于方法參數(shù)列表琅捏,我們使用兩個(gè)點(diǎn)號(hào)(..)表明切點(diǎn)要選擇任意的sayHello()方法,無論該方法的入?yún)⑹鞘裁础?/p>
接下來我們要定義個(gè)切面递雀,也就是所謂的日志功能的類柄延。
package com.tengj.demo.aspect;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component //注入依賴
@Aspect //該注解標(biāo)示該類為切面類
public class LogAspect {
@Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
public void logAop(){}
@Before("logAop() && args(name)")
public void logBefore(String name){
System.out.println(name+"前置通知Before");
}
@AfterReturning("logAop()")
public void logAfterReturning(){
System.out.println("返回通知AfterReturning");
}
@After("logAop() && args(name)")
public void logAfter(String name){
System.out.println(name+"后置通知After");
}
@AfterThrowing("logAop()")
public void logAfterThrow(){
System.out.println("異常通知AfterThrowing");
}
}
上面就是切面類的代碼,很簡(jiǎn)單,這里用到了前面提的通知的幾種類型搜吧。
這樣就能實(shí)現(xiàn)切入功能了
@Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
public void logAop(){}
這里的@Pointcut注解是為了定義切面內(nèi)重用的切點(diǎn)市俊,也就是說把公共的東西抽出來,定義了任意的方法名稱logAop滤奈,這樣下面用到的各種類型通知就只要寫成
@Before("logAop() && args(name)")
@AfterReturning("logAop()")
@AfterThrowing("logAop()")
這樣既可摆昧,否則就要寫成
@Before("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterReturning("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterThrowing("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
大家是否注意到了@Before("logAop() && args(name)")
這里多出來個(gè)&& args(name)
,這個(gè)是用來傳遞參數(shù)的,定義只要跟sayHello參數(shù)名稱一樣就可以蜒程。
如果就此止步的話绅你,LogAspect只會(huì)是Spring容器中的一個(gè)Bean,即便使用了AspectJ注解,但它并不會(huì)被視為切面昭躺,這些注解不會(huì)解析忌锯,也不會(huì)創(chuàng)建將其轉(zhuǎn)換為切面的代理氢伟。
所以需要在XML里面配置一下烈和,需要使用Spring aop命名空間中的<aop:aspectj-autoproxy/>
元素,簡(jiǎn)單如下:
<?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: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-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"
default-lazy-init="true">
<context:component-scan base-package="com.tengj.demo"/>
<mvc:resources location="/WEB-INF/pages/" mapping="/pages/**"/>
<!-- 默認(rèn)的注解映射的支持 -->
<mvc:annotation-driven/>
<!--啟用AspectJ自動(dòng)代理-->
<aop:aspectj-autoproxy/>
<!-- 視圖解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
接著就可以啟動(dòng)工程叫惊,訪問index這個(gè)方法帝洪,http://localhost:8080/SpringMVCMybatis/test/view
執(zhí)行結(jié)果:
tengj前置通知Before
hello似舵,tengj
tengj后置通知After
返回通知AfterReturning
根據(jù)前面學(xué)的我們知道,除了上面提到的通知外葱峡,還有一個(gè)更強(qiáng)大通知類型啄枕,就是環(huán)繞通知∽逦郑可以自定義我們需要切入的位置频祝,可以替代上面提到的所有通知〈嘌停看例子:
@Around("logAop()")
public void logAround(ProceedingJoinPoint jp){
try {
System.out.println("自定義前置通知Before");
jp.proceed();//將控制權(quán)交給被通知的方法常空,也就是執(zhí)行sayHello方法
System.out.println("自定義后置通知After");
} catch (Throwable throwable) {
System.out.println("異常處理~");
throwable.printStackTrace();
}
}
執(zhí)行結(jié)果:
自定義前置通知Before
hello,tengj
自定義后置通知After
這里主要是通過ProceedingJoinPoint這個(gè)參數(shù)盖溺。其中里面的proceed()方法就是將控制權(quán)交給被通知的方法漓糙。如果你忘記調(diào)用這個(gè)方法,那么你的通知實(shí)際上會(huì)阻塞對(duì)被通知方法的調(diào)用烘嘱。
有意思的是昆禽,你可以不調(diào)用proceed()方法,從而阻塞堆被通知方法的訪問蝇庭,與之類似醉鳖,你也可以在通知中對(duì)它進(jìn)行多次調(diào)用。要這樣做的一個(gè)場(chǎng)景就是實(shí)現(xiàn)重試邏輯哮内,也就是在被通知方法失敗后盗棵,進(jìn)行重復(fù)嘗試。
基于XML配置的方式
這里介紹使用XML配置的方式來實(shí)現(xiàn),在Spring的aop命名空間中纹因,提供了多個(gè)元素用來在XML中聲明切面喷屋。
AOP配置元素 | 用 途 |
---|---|
<aop:advisor> |
定義AOP通知器 |
<aop:after> |
定義AOP后置通知(不管被通知的方法是否執(zhí)行成功) |
<aop:after-returning> |
定義AOP返回通知 |
<aop:after-throwing> |
定義AOP異常通知 |
<aop:around> |
定義AOP環(huán)繞通知 |
<aop:aspect> |
定義一個(gè)切面 |
<aop:aspectj-autoproxy> |
啟用@AspectJ注解驅(qū)動(dòng)的切面 |
<aop:before> |
定義一個(gè)AOP前置通知 |
<aop:config> |
頂層的AOP配置元素,大多數(shù)的<aop:*> 元素必須包含在<aop:config> 元素內(nèi) |
<aop:declare-parents> |
以透明的方式為被通知的對(duì)象引入額外的接口 |
<aop:pointcut> |
定義一個(gè)切點(diǎn) |
我們已經(jīng)看過了<aop:aspectj-autoproxy/>
元素瞭恰,它能夠自動(dòng)代理AspectJ注解的通知類屯曹。aop命名空間的其他元素能夠讓我們直接在Spring配置中聲明切面,而不需要使用注解惊畏。
所以恶耽,我們重新來看看一下這個(gè)LogAspect類,這次我們將它所有的AspectJ注解全部移除掉:
package com.tengj.demo.aspect;
public class LogAspect {
public void logBefore(String name){
System.out.println(name+"前置通知Before");
}
public void logAfterReturning(String name){
System.out.println("返回通知AfterReturning");
}
public void logAfter(String name){
System.out.println(name+"后置通知After");
}
public void logAfterThrow(String name){
System.out.println("異常通知AfterThrowing");
}
}
然后在xml配置文件中使用Spring aop命名空間中的一些元素陕截,詳細(xì)基本配置參考上面注解方式中的xml配置驳棱,這里是貼出來關(guān)鍵的代碼:
<bean id="logAspect" class="com.tengj.demo.aspect.LogAspect" />
<aop:config>
<aop:aspect id="log" ref="logAspect">
<aop:pointcut id="logAop" expression="execution(* com.tengj.demo.service.impl.UserServiceImpl.sayHello(..)) and args(name)"/>
<aop:before method="logBefore" pointcut-ref="logAop"/>
<aop:after method="logAfter" pointcut-ref="logAop"/>
<aop:after-returning method="logAfterReturning" pointcut-ref="logAop"/>
<aop:after-throwing method="logAfterThrow" pointcut-ref="logAop"/>
<!--<aop:around method="logAfterThrow" pointcut-ref="logAop"/>-->
</aop:aspect>
</aop:config>
配置也 很好理解
- xml里面配置aop批什,都是放在
<aop:config>
里面 - 然后使用
<aop:aspect>
一個(gè)切面农曲,指向具體的bean類。 - 使用
<aop:pointcut>
定義切點(diǎn)驻债,基本跟注解的很像乳规,其中要注意的是xml配置里面如果要帶參數(shù)的,用的不再是&&合呐,要使用and關(guān)鍵字才行(因?yàn)樵赬ML中暮的,“&”符號(hào)會(huì)被解析為實(shí)體的開始) - 然后就是使用各種通知標(biāo)簽了,簡(jiǎn)單淌实。
執(zhí)行效果如下:
tengj前置通知Before
hello冻辩,tengj
tengj后置通知After
返回通知AfterReturning
環(huán)繞通知也很簡(jiǎn)單,直接貼代碼:
xml配置:
<aop:around method="logAround" pointcut-ref="logAop"/>
切面方法:
public void logAround(ProceedingJoinPoint jp,String name){
try {
System.out.println(name+"自定義前置通知Before");
jp.proceed();
System.out.println(name+"自定義后置通知After");
} catch (Throwable throwable) {
System.out.println("異常處理~");
throwable.printStackTrace();
}
}
執(zhí)行結(jié)果:
tengj自定義前置通知Before
hello拆祈,tengj
tengj自定義后置通知After
總結(jié)
Spring AOP是Spring學(xué)習(xí)中最關(guān)鍵的恨闪,我總結(jié)的這2種寫法也是開發(fā)中最常用的。也不知道大家能不能理解~看得時(shí)候如果有不懂的地方可以提出來放坏,我好修改一下咙咽,讓更多的人理解并掌握AOP,希望對(duì)你有所幫助淤年。
一直覺得自己寫的不是技術(shù)钧敞,而是情懷,一篇篇文章是自己這一路走來的痕跡麸粮「瓤粒靠專業(yè)技能的成功是最具可復(fù)制性的,希望我的這條路能讓你少走彎路弄诲,希望我能幫你抹去知識(shí)的蒙塵炊昆,希望我能幫你理清知識(shí)的脈絡(luò),希望未來技術(shù)之巔上有你也有我。