慢慢來比較快,虛心學技術
前言:DI (依賴注入)有助於應用對象之間的解耦,而 AOP(面向切面編程) 可以實現橫切關注點與它們所影響的對象之間的解耦
一、什麼是面向切面編程
Ⅰ、橫切關注點:在軟件開發中,散佈於應用中多處的功能被稱爲橫切關注點( cross-cutting concern )【比如說日誌,安全和事務管理等】。通常來講,這些橫切關注點從概念上是與應用的業務邏輯相分離的(但是往往會直接嵌入到應用的業務邏輯之中)。把這些橫切關注點與業務邏輯相分離正是面向切面編程( AOP )所要解決的問題。
Ⅱ、切面:橫切關注點可以被模塊化爲特殊的類,這些類被稱爲切面( aspect )。
Ⅲ、AOP術語
①通知( Advice ):切面的工作被稱爲通知。通知描述切面的工作,同時決定切面何時工作【定義了切面工作做什麼,什麼時候做】
- 前置通知( Before ):在目標方法被調用之前調用通知功能;
- 後置通知( After ):在目標方法完成之後調用通知,此時不會關心方法的輸出是什麼;
- 返回通知( After-returning ):在目標方法成功執行之後調用通知;
- 異常通知( After-throwing ):在目標方法拋出異常後調用通知;
- 環繞通知( Around ):通知包裹了被通知的方法,在被通知的方法調用之前和調用之後執行自定義的行爲
②連接點( Join point ):觸發切面工作的點,比如方法執行,異常拋出等行爲
③切點( Poincut ):決定切面工作的地方,比如某個方法等【定義了切面工作在哪裏做】
④切面( Aspect ):通知和切點的結合【面】
⑤引入( Introduction ):允許我們向現有的類添加新方法或屬性
⑥織入( Weaving ):把切面應用到目標對象並創建新的代理對象的過程【切面在指定的連接點被織入到目標對象中】
通知包含了需要用於多個應用對象的橫切行爲;連接點是程序執行過程中能夠應用通知的所有點;切點定義了通知被應用的具體位置(在哪些連接點)。其中關鍵的概念是切點定義了哪些連接點會得到通知
Ⅳ、Spring對AOP的支持【 Spring AOP 構建在動態代理基礎之上,因此, Spring 對 AOP 的支持侷限於方法攔截。】
- 基於代理的經典 Spring AOP ;
- 純 POJO 切面;
- @AspectJ 註解驅動的切面;
- 注入式 AspectJ 切面(適用於 Spring 各版本)。
二、面向切面編程實現
1、定義切點:
Spring支持通過AspectJ的切點表達式語言來定義 Spring 切面,同時增加通過bean的id指定bean的寫法。
如:execution( com.my.spring.bean..(..))* 指定com.my.spring.bean包下所有類的所有方法作爲切點
其結構解析如下:
AspectJ切點表達式的指示器不只有execution:
- arg() :限制連接點匹配參數爲指定類型的執行方法
- execution() :用於匹配是連接點的執行方法
- this() :限制連接點匹配 AOP 代理的 bean 引用爲指定類型的類
- target :限制連接點匹配目標對象爲指定類型的類
- within() :限制連接點匹配指定的類型
各指示器之間可以通過&&(與),||(或),!(非)連接符進行連接實現多條件查詢定義節點
如:execution(* com.my.spring.bean.* . *(..))&&arg(java.lang.Integer)
2.示例Demo
Spring AOP的實現依賴於spring-aop包和aspectjweaver包,需在pom文件引入:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${org.springframework.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
通過實例demo引入概念:
①定義一個基礎接口類BaseInterface和實現類BaseBean
public interface BaseInterface {
/**
* 新增歌曲
*
* @param author 作者
* @param songTitle 歌曲名
*
* @return java.lang.Integer 返回當前歌曲總數
*
* @author xxx 2019/3/4
* @version 1.0
**/
Integer addSong(String author,String songTitle);
/**
* 刪除歌曲
*
* @param author 作者
* @param songTitle 歌曲名
*
* @return java.lang.Integer 返回當前歌曲總數
*
* @author xxx 2019/3/4
* @version 1.0
**/
Integer delSong(String author,String songTitle);
}
@Component
public class BaseBean implements BaseInterface{
private String author;
private String songTitle;
private Integer count=0;
@Override
public Integer addSong(String author,String songTitle){
this.author = author;
this.songTitle = songTitle;
System.out.println("新增了一首歌:"+author+"-"+songTitle);
count++;
return count;
}
@Override
public Integer delSong(String author,String songTitle){
this.author = author;
this.songTitle = songTitle;
System.out.println("刪除了一首歌:"+author+"-"+songTitle);
count--;
return count;
}
}
②創建一個切面類
@Aspect
@Component
public class BaseBeanAspect {
private Logger logger = LoggerFactory.getLogger(BaseBeanAspect.class);
/**
* 方法執行前的通知
*/
@Before("execution(* com.my.spring.bean.*.*(..))")
public void beforeInvoke(){
logger.debug("方法執行前");
}
/**
* 方法執行後的通知
*/
@After("execution(* com.my.spring.bean.*.*(..))")
public void afterInvoke(){
logger.debug("方法執行後");
}
/**
* 方法執行返回後的通知
*/
@AfterReturning("execution(* com.my.spring.bean.*.*(..))")
public void afterReturning(){
logger.debug("==================方法執行完成");
}
/**
* 方法拋出異常的通知
*/
@AfterThrowing("execution(* com.my.spring.bean.*.*(..))")
public void afterThrowing(){
logger.debug("==================方法執行報錯");
}
}
③創建自動化裝配的配置類
@Configuration
@EnableAspectJAutoProxy//開啓自動代理開關,啓用切面
@ComponentScan(basePackages = {"com.my.spring"})
public class ComponentConfig {
}
④測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {ComponentConfig.class})
public class AppTest {
@Autowired
private BaseInterface baseInterface;
@Test
public void testBean(){
baseInterface.addSong("myBean","mySong");
baseInterface.delSong("myBean","mySong");
}
}
⑤測試結果
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行前
新增了一首歌:myBean-mySong
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行後
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法執行完成
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行前
刪除了一首歌:myBean-mySong
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行後
2019-03-04 14:32:55.019 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法執行完成
3、註解分析
前代碼是使用註解實現SpringAOP的簡單示例,我們瞭解以下其中用到的註解和實現:
Ⅰ、定義切面:@Aspect------------標識當前類是一個切面類(僅僅只是標識,我們可以看到該註解源碼並沒有使用@Component註解,所以使用該註解的bean依舊只是一個普通的POJO,使用時依舊需要顯式或自動裝配)
Ⅱ、定義切點:@PointCut
我們看到上述代碼中在多個地方使用切點使用的是重複性的表達式,其實通過@PointCut註解定義切點,同時通過指定空方法名引入切點到各個通知即可
@Pointcut("execution(* com.my.spring.bean.*.*(..))")
public void pointCut(){//被用於標識的空方法
}
@Before("pointCut()")//以切點方法名引入
public void beforeInvoke(){
logger.debug("方法執行前");
}
Ⅱ、定義通知:
@Beafore(切點)-----------------切點方法執行前的通知
@After(切點)-------------------------切點方法執行後的通知
@AfterReturning(切點)-----------切點方法執行返回後的通知
@AfterThrowing(切點)------------切點方法拋異常後的通知
Ⅲ、開啓自動代理:@EnableAspectJAutoProxy--------在配置類中使用,如果不啓用的話,編寫的切面將不生效
Ⅳ、定義環繞通知:@Around("pointCut()")
環繞通知是從方法執行前一直包裹直到方法執行完成後的一個通知,用的比較多,其中被定義的方法需要引入參數ProceedingJoinPoint,ProceedingJoinPoint對象封裝了當前運行對象的具體信息,簡單實現如下:
@Around("pointCut()")
public void aroundInvoke(ProceedingJoinPoint jp){
try {
logger.debug("=====================環繞執行方法開始");
Signature signature = jp.getSignature();
String methodName = signature.getName();
MethodSignature methodSignature = (MethodSignature) signature;
logger.debug("方法名:{}",methodName);
List<Object> args = Arrays.asList(jp.getArgs());
logger.debug("參數列表:{}",args);
Class<?> returnType = methodSignature.getMethod().getReturnType();
logger.debug("方法返回類型:{}",returnType);
Object proceed = jp.proceed();
logger.debug("======================環繞執行方法結束,方法執行結果:{}",proceed);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
其中,ProceedingJoinPoint對象源碼分析如下:
JoinPoint
java.lang.Object[] getArgs()//獲取連接點方法運行時的入參列表;
Signature getSignature() //獲取連接點的方法簽名對象;
java.lang.Object getTarget() //獲取連接點所在的目標對象;
java.lang.Object getThis() //獲取代理對象本身;
ProceedingJoinPoint繼承JoinPoint子接口,它新增了兩個用於執行連接點方法的方法
java.lang.Object proceed() throws java.lang.Throwable:通過反射執行目標對象的連接點處的方法;
java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通過反射執行目標對象連接點處的方法,不過使用新的入參替換原來的入參。
4、XML實現切面編程
我們知道使用註解實現切面編程是很方便直接的,但是有時候我們並不一定擁有通知類的源碼,也就無法給對應的方法添加註解,這時候就需要使用XML配置實現了。XML配置實現與註解實現十分類似,我們可以看一下基本的實現節點:
①定義切面:<aop:aspect ref="切面類在xml文件中對應bean的id">
②定義切點:<aop:pointcut id="切點id" expression="切點表達式"/>
③定義通知:
<aop:beafore method="通知方法名" pointcut-ref="切點id"/>-------------定義方法執行前的通知
<aop:after method="通知方法名" pointcut-ref="切點id"/>-------------定義方法執行後的通知
<aop:afterReturning method="通知方法名" pointcut-ref="切點id"/>-------------定義方法執行返回後的通知
<aop:afterThrowing method="通知方法名" pointcut-ref="切點id"/>-------------定義方法執行拋出異常後的通知
<aop:around method="通知方法名" pointcut-ref="切點id"/>-------------定義方法環繞通知
④開啓自動代理:<aop:aspectj-autoproxy/>
⑤表示aop配置:<aop:config></aop:config>
除了開啓自動代理,aop的所有節點都需要包含在<aop:config></aop:config>節點中,如下demo演示:
applicaiton.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: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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--開啓自動代理-->
<aop:aspectj-autoproxy/>
<!--裝配基本類-->
<bean class="com.my.spring.bean.BaseBean" id="baseBean" name="baseBean"/>
<!--裝配切面類-->
<bean class="com.my.spring.aspect.BaseBeanAspect" id="baseBeanAspect"/>
<!--aop配置-->
<aop:config>
<!--配置切面-->
<aop:aspect ref="baseBeanAspect">
<!--定義切點-->
<aop:pointcut id="pointCut" expression="execution(* com.my.spring.bean.*.*(..))"/>
<!--定義前置通知-->
<aop:before method="beforeInvoke" pointcut-ref="pointCut"/>
<!--定義後置通知-->
<aop:after method="afterInvoke" pointcut-ref="pointCut"/>
<!--定義方法執行返回後通知-->
<aop:after-returning method="afterReturning" pointcut-ref="pointCut"/>
<!--定義方法異常後通知-->
<aop:after-throwing method="afterThrowing" pointcut-ref="pointCut"/>
<!--定義方法環繞通知通知-->
<aop:around method="aroundInvoke" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
</beans>
將BaseBean和BaseBeanAspect類中的註解去除,編寫測試類:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:application.xml"})//將配置文件作爲裝配環境
public class AppXMLTest {
@Autowired
private BaseInterface baseInterface;
@Test
public void testBean(){
baseInterface.addSong("Mr D","The World!!");
baseInterface.delSong("Mr D","The World!!");
}
}
測試結果:
2019-03-04 22:07:37.901 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行前
2019-03-04 22:07:37.903 DEBUG com.my.spring.aspect.BaseBeanAspect - =====================環繞執行方法開始
2019-03-04 22:07:37.907 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法名:addSong
2019-03-04 22:07:37.910 DEBUG com.my.spring.aspect.BaseBeanAspect - 參數列表:[Mr D, The World!!]
2019-03-04 22:07:37.910 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法返回類型:class java.lang.Integer
2019-03-04 22:07:37.910 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行前
新增了一首歌:Mr D-The World!!
2019-03-04 22:07:37.911 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法執行完成
2019-03-04 22:07:37.911 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行後
2019-03-04 22:07:37.911 DEBUG com.my.spring.aspect.BaseBeanAspect - ======================環繞執行方法結束,方法執行結果:1
2019-03-04 22:07:37.912 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法執行完成
2019-03-04 22:07:37.912 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行後
2019-03-04 22:07:37.915 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行前
2019-03-04 22:07:37.915 DEBUG com.my.spring.aspect.BaseBeanAspect - =====================環繞執行方法開始
2019-03-04 22:07:37.916 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法名:delSong
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 參數列表:[Mr D, The World!!]
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法返回類型:class java.lang.Integer
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行前
刪除了一首歌:Mr D-The World!!
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法執行完成
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行後
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - ======================環繞執行方法結束,方法執行結果:0
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - ==================方法執行完成
2019-03-04 22:07:37.917 DEBUG com.my.spring.aspect.BaseBeanAspect - 方法執行後