1. 程式人生 > >spring框架(三)——Spring的核心之AOP

spring框架(三)——Spring的核心之AOP

上一節我們瞭解到spring的一個核心之IOC控制反轉(依賴注入),那麼 我們接著聊一聊spring的另外一個核心aop(面向切面程式設計)

1、AOP概念及原理

1.1、什麼是AOP

OOP:Object Oriented Programming面向物件程式設計
AOP:Aspect Oriented Programming面向切面程式設計

我想直接說可能不懂。我們就從圖片中讀取資訊吧。啥也不說,上圖:


AOP:面向切面程式設計.AOP的出現不是替換OOP.解決OOP開發中一些問題.是OOP一個延伸一個擴充套件.
AOP採取橫向抽取機制,取代了傳統縱向繼承體系重複性程式碼(效能監視、事務管理、安全檢查、快取)

AOP的底層原理  底層原理實質就是代理機制.

1.2、代理

充分理解:間接
主要作用:攔截被代理物件執行的方法,同時對方法進行增強。

1.2.1、靜態代理

特點:代理類是一個真實存在的類。裝飾者模式就是靜態代理的一種體現形式。

1.2.2、動態代理

特點:位元組碼是隨用隨建立,隨用隨載入。是在執行期間生成的一個類。
a、基於介面的動態代理
提供者:JDK官方的Proxy類。
要求:被代理類必須實現一個或多個介面。
b、基於子類的動態代理
提供者:第三方的CGLib,如果報asmxxxx異常,需要匯入asm.jar。
要求:被代理類必須是一個子類(不能用final修飾的(最終類),其餘類都沒問題,因為都是Object的子類)。
注意:在spring中,框架會根據目標類是否實現了介面來決定採用哪種動態代理的方式。

Spring的AOP的底層採用兩種代理機制:

  • JDK的動態代理:   只能對實現了介面的類生成代理.
  • CGLIB的動態代理:   可以對沒有實現介面的類生成代理.(採用的是比較底層的位元組碼技術,對類生成一個子類物件.)

1.3、代理的總結:

Spring在執行期,生成動態代理物件,不需要特殊的編譯器
Spring AOP的底層就是通過JDK動態代理或CGLib動態代理技術 為目標Bean執行橫向織入
1.若目標物件實現了若干介面,spring使用JDK的java.lang.reflect.Proxy類代理。
2.若目標物件沒有實現任何介面,spring使用CGLIB庫生成目標物件的子類。
程式中應優先對介面建立代理,便於程式解耦維護
標記為final的方法,不能被代理,因為無法進行覆蓋
JDK動態代理,是針對介面生成子類,介面中方法不能使用final修飾
CGLib 是針對目標類生產子類,因此類或方法 不能使final的
Spring只支援方法連線點,不提供屬性連線

2、Spring中的AOP

2.1、基本概念(AOP的術語)

  1. Joinpoint(連線點):所謂連線點是指那些被攔截到的點。在spring中,這些點指的是方法,因為spring只支援方法型別的連線點.
  2. Pointcut(切入點):所謂切入點是指我們要對哪些Joinpoint進行攔截的定義.
  3. Advice(通知/增強):所謂通知是指攔截到Joinpoint之後所要做的事情就是通知.通知分為前置通知,後置通知,異常通知,最終通知,環繞通知(切面要完成的功能)
  4. Introduction(引介):引介是一種特殊的通知在不修改類程式碼的前提下, Introduction可以在執行期為類動態地新增一些方法或Field.
  5. Target(目標物件):代理的目標物件
  6. Weaving(織入):是指把增強應用到目標物件來建立新的代理物件的過程. spring採用動態代理織入,而AspectJ採用編譯期織入和類裝在期織入
  7. Proxy(代理):一個類被AOP織入增強後,就產生一個結果代理類
  8. Aspect(切面): 是切入點和通知(引介)的結合

2.2、具體配置

2.2.1、Spring中的AOP

a、開發階段(我們做的)
  1. 編寫核心業務程式碼(開發主線):大部分程式設計師來做,要求熟悉業務需求。
  2. 把公用程式碼抽取出來,製作成通知。(開發階段最後再做):AOP程式設計人員來做。
  3. 在配置檔案中,宣告切入點與通知間的關係,即切面。:AOP程式設計人員來做。
b、執行階段(Spring框架完成的)
Spring框架監控切入點方法的執行。一旦監控到切入點方法被執行,使用代理機制,動態建立目標物件的代理物件,根據通知類別,在代理物件的對應位置,將通知對應的功能織入,完成完整的程式碼邏輯執行。

3 基於XML的配置(spring的xml開發)

3.1 AOP的基本配置

a、匯入AOP相關的jar包(Spring的AOP是基於IoC的,所以IoC的jar也必須存在)AOP有關的jar包:4個


b、引入aop名稱空間


c 編寫核心業務程式碼必須交給spring容器


d 編寫一個切面類:(某一個通知型別)也必須交給spring管理


上圖的logger類是後期做的。現在是用下面的,bean的配置一樣不變

package springAop.aop;
public class Logger {
	//列印日誌的公共方法
	//計劃讓其在業務核心程式碼(切入點方法)之前執行。 前置通知
	public void printLog(){
		System.out.println("Logger中的printLog方法開始輸出日誌了。。。。。");
	}
}

注意:通知型別稍後做詳解

e 配置切面


注意:切入點表示式以及切入點的配置方式稍後做詳解

f 測試:編寫測試程式碼並測試

package springAop.test;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import springAop.user.UserService;
public class Client {

	public static void main(String[] args) {
		//Spring容器的初始化
		ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("springAop/bean.xml");
		//獲取資源
//		UserServiceImpl userService = (UserServiceImpl) ac.getBean("userServiceImpl");
		UserService userService = (UserService)ac.getBean("userServiceImpl");
		userService.save();
	}
}
測試的時候我獲取ac.getBean的物件轉換為它的介面型別,使用它實際型別拋錯,不知道為什麼?

結果:

Logger中的printLog方法開始輸出日誌了。。。。。
UserServiceImpl的save方法執行了。。。
注意:到這裡一個簡單的基於xml的spring的AOP就配置完畢!其它的東西全部都是完善!

4 詳解切入點表示式(優化上面的e步驟,配置切面)

execution:匹配方法的執行(常用) execution(public * *(..))
基本語法:execution([修飾符] 返回值型別 包名.類名.方法名(引數))
  • within:匹配包或子包中的方法(瞭解) within(springAop.aop..*)
  • this:匹配實現介面的代理物件中的方法(瞭解) this(springAop.aop.user.UserDAO)
  • target:匹配實現介面的目標物件中的方法(瞭解) target(springAop.aop.user.UserDAO)
  • args:匹配引數格式符合標準的方法(瞭解) args(int,int)
Spring支援使用如下三個邏輯運算子來組合切入點表示式:
  • &&:要求連線點同時匹配兩個切點表示式
  • ||:要求連線點匹配至少一個切入點表示式
  • !:要求連線點不匹配指定的切入點表示式
例子:

方式一:

<?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-3.2.xsd
       			http://www.springframework.org/schema/aop
   				http://www.springframework.org/schema/aop/spring-aop-4.2.xsd ">
   				
   	<!-- 1 核心業務程式碼:service層等一系列 -->
   	<bean id="userServiceImpl" class="springAop.user.impl.UserServiceImpl"></bean>
   	<!-- 2 切面類:某一個通知型別的類 -->
   	<bean id="loggerAdvice" class="springAop.aop.Logger"></bean>
   	
   	<aop:config>
   		<aop:aspect ref="loggerAdvice" >
   		
   		<!--表示式全匹配 <aop:before method="printLog" pointcut="execution(public void springAop.user.impl.UserServiceImpl.save())"/> -->
   		<!--訪問修飾符是可以省略的<aop:before method="printLog" pointcut="execution(void springAop.user.impl.UserServiceImpl.save()"/> -->
   		<!--使用*來替換返回值型別,表明可以是任意返回值<aop:before method="printLog" pointcut="execution(* springAop.user.impl.UserServiceImpl.save())"/> -->
   		<!--使用*來通配方法名稱。可以直接使用*,也可以使用*+部分方法名稱<aop:before method="printLog" pointcut="execution(* springAop.user.impl.UserServiceImpl.*User())"/> -->
   		<!--使用*來替換包名,表明包的名稱可以是任意名稱<aop:before method="printLog" pointcut="execution(* springAop.*.*.UserServiceImpl.*())"/> -->
   		<!--使用..來替換包名,..表明的是在當前包及其子包中 <aop:before method="printLog" pointcut="execution(* springAop.user..UserServiceImpl.*())"/> -->
   		<!--使用*通配類的名稱。 <aop:before method="printLog" pointcut="execution(* springAop.user..*.*())"/>  -->
   		<!--使用明確資料型別的引數。基本資料型別直接寫:int,float等。引用型別:String可以直接寫,也可以使用java.lang.String <aop:before method="printLog" pointcut="execution(* springAop.user..*.*(int))"/>  -->
   		<!--使用*通配任意型別的引數。要求是必須有一個引數 <aop:before method="printLog" pointcut="execution(* springAop.user..*.*(*))"/>  -->
   		<!--使用..表明有無引數都可以 <aop:before method="printLog" pointcut="execution(* springAop.user..*.*(..))"/>  -->
   
   			<aop:before method="printLog" pointcut="execution(* springAop.*.*.UserServiceImpl.*())"/>
   		</aop:aspect>
   	</aop:config>

</beans>
注意:方式一的所有配置中都有一個共同缺點,不能複用
方式二:
<?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-3.2.xsd
       			http://www.springframework.org/schema/aop
   				http://www.springframework.org/schema/aop/spring-aop-4.2.xsd ">
   				
   	<!-- 1 核心業務程式碼:service層等一系列 -->
   	<bean id="userServiceImpl" class="springAop.user.impl.UserServiceImpl"></bean>
   	<!-- 2 切面類:某一個通知型別的類 -->
   	<bean id="loggerAdvice" class="springAop.aop.Logger"></bean>
   	
   	<aop:config>
   		<aop:aspect ref="loggerAdvice" >
   			<!-- 使用 配置切入點的方式,實現複用
   				aop:pointcut 用於配置切入點,expression是切入點表示式,原來在pointcut屬性中怎麼寫在這還怎麼寫
   				使用aop:before(或者其它通知)引入切點id
   			-->
     		<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"/>
   			<aop:before method="printLog" pointcut-ref="pt1" />
   		</aop:aspect>
   	</aop:config>
</beans>
方式二隻是將切入點單獨提取出來,然後在通知時引入配置切入點的id。但只是提高了一定通用性,只能當前的切面使用。
方式三:
<?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-3.2.xsd
       			http://www.springframework.org/schema/aop
   				http://www.springframework.org/schema/aop/spring-aop-4.2.xsd ">
   				
   	<!-- 1 核心業務程式碼:service層等一系列 -->
   	<bean id="userServiceImpl" class="springAop.user.impl.UserServiceImpl"></bean>
   	<!-- 2 切面類:某一個通知型別的類 -->
   	<bean id="loggerAdvice" class="springAop.aop.Logger"></bean>
   	
   	<aop:config>
 		<!-- 把切入點宣告在外面,使切入點成為全域性的,這樣其它的切面也可以使用 -->
   		<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"/>
   		<aop:aspect ref="loggerAdvice" >
   			<aop:before method="printLog" pointcut-ref="pt1" />
   		</aop:aspect>
   	</aop:config>
</beans>

方式三隻是將切入點表示式提到全域性位置中。這樣大家都可以引用到了。

OK、切入點表示式、和如何引用。講完了。

5 通知型別

通知型別有5個如下:

<?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-3.2.xsd
       			http://www.springframework.org/schema/aop
   				http://www.springframework.org/schema/aop/spring-aop-4.2.xsd ">
   				
   	<!-- 1 核心業務程式碼:service層等一系列 -->
   	<bean id="userServiceImpl" class="springAop.user.impl.UserServiceImpl"></bean>
   	<!-- 2 切面類:某一個通知型別的類 -->
   	<bean id="loggerAdvice" class="springAop.aop.Logger"></bean>
   	
   	<aop:config>
 		<!-- 把切入點宣告在外面,使切入點成為全域性的,這樣其它的切面也可以使用 -->
   		<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"/>
   		<aop:aspect ref="loggerAdvice" >
   			<!-- 配置前置通知 :before是前置通知。永遠在切入點方法(核心業務方法)執行之前執行。-->
   			<!-- <aop:before method="printLog" pointcut-ref="pt1" /> -->
   			<!-- 配置後置通知:after-retruning。當正常執行完成切入點方法之後執行。 如果切入點方法出現了異常則不會執行 -->
   			<!-- <aop:after-returning method="printLog" pointcut-ref="pt1" /> -->
     		<!-- 配置異常通知:after-throwing。當切入點方法出現異常時執行。如果切入點方法正常執行沒有異常的話,則不會執行。-->
     		<!--  <aop:after-throwing method="printLog" pointcut-ref="pt1" /> -->
     		<!-- 配置最終通知 :after。無論切入點方法執行成功與否,最終通知都會執行。-->
     		<!-- <aop:after mmethod="printLog" pointcut-ref="pt1" /> -->
     		<!-- 配置環繞通知:配置環繞通知時,並沒有呼叫核心業務方法(切入點方法)。-->
     		<aop:around method="printLog" pointcut-ref="pt1"/>
   		</aop:aspect>
   	</aop:config>
</beans>

6 基於註解的配置

1 使用Spring註解進行AOP配置的前提
a、資源交給Spring管理(核心業務物件,通知物件均在類中添加註解)在此之前要開啟掃描包


注意:程式碼中請使用@Compoment註解。

b、AOP有關的註解配置
開啟Spring對@AspectJ註解的支援

<?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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       			http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
       			http://www.springframework.org/schema/aop
   				http://www.springframework.org/schema/aop/spring-aop-4.2.xsd 
   				http://www.springframework.org/schema/context 
   				http://www.springframework.org/schema/context/spring-context-3.2.xsd">
   				
   <!-- 指定Spring要掃描的包(同ioc註解一樣)
      		注意:它會掃描當前包和當前包的子包下的所有類(base-package:以點分割包名)
       -->
		<context:component-scan base-package="springAop"></context:component-scan>
	
	<!-- 開啟spring對 @Aspectj註解的支援-->
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
c、常用的AOP註解
@Aspect 配置切面

@Before:前置通知
@AfterReturning:後置通知
@AfterThrowing:異常通知
@After:最終通知
@Around:環繞通知

d、配置一個前置通知的例子

第一步:編寫通知類:並交給spring管理,並配置切面,並指定通知型別

package springAop.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

/**
 * 註解spring開發
 * @author mch
 *
 */
@Component  //交給spring管理--掃描包的時候就會掃描
@Aspect     //配置切面
public class Logger {

	//列印日誌的公共方法
	//計劃讓其在業務核心程式碼(切入點方法)之前執行。 前置通知
	@Before("execution(* springAop.user.impl.UserServiceImpl.save())")
	public void printLog(){
		System.out.println("Logger中的printLog方法開始輸出日誌了。。。。。");
	}
}
第二步:編寫核心業務類,並交給spring管理
package springAop.user.impl;

import org.springframework.stereotype.Component;

/**
 * 註解spring的核心業務類
 * @author mch
 *
 */
@Component  //交給spring來管理
public class UserServiceImpl {	
	public void save(){
		System.out.println("UserServiceImpl的save方法執行了。。。");
	}
	public void update() {
		System.out.println("UserServiceImpl的update方法執行了。。。");
	}
}
第三步:前提準備的a b 和c

<?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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       			http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
       			http://www.springframework.org/schema/aop
   				http://www.springframework.org/schema/aop/spring-aop-4.2.xsd 
   				http://www.springframework.org/schema/context 
   				http://www.springframework.org/schema/context/spring-context-3.2.xsd">
   				
   <!-- 指定Spring要掃描的包(同ioc註解一樣)
      		注意:它會掃描當前包和當前包的子包下的所有類(base-package:以點分割包名)
       -->
		<context:component-scan base-package="springAop"></context:component-scan>
	
	<!-- 開啟spring對 @Aspectj註解的支援-->
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
第4步:編寫測試類、執行檢視結果
package springAop.test;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import springAop.user.UserService;
import springAop.user.impl.UserServiceImpl;
public class Client {

	public static void main(String[] args) {
		//Spring容器的初始化
		ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("springAop/bean.xml");
		//獲取資源
		UserServiceImpl userService = (UserServiceImpl) ac.getBean("userServiceImpl");
		userService.save();
	}
}
執行結果:
Logger中的printLog方法開始輸出日誌了。。。。。
UserServiceImpl的save方法執行了。。。
OK、到這裡基於註解的spring開發就整理完了。其它的詳細內容,比如前置通知獲取引數、後置通知獲取返回值,異常通知獲取異常資訊等。等到實際專案開發用到的時候,在另行補充。