Spring AOP 面向切面編程
參考資料:https://lfvepclr.gitbooks.io/spring-framework-5-doc-cn/content/
1. AOP 原理
AOP Aspect Oriented Programing,面向切面編程。
AOP 採取橫向抽取機制,取代了傳統縱向繼承體系重複性代碼(性能監視、事務管理、安全檢查、緩存)
Spring AOP 使用純 Java 實現,不需要專門的編譯過程和類加載器,在運行期通過代理方式向目標類織入增強代碼
。
AOP底層原理:就是代理機制
動態代理:
-
特點:字節碼隨用隨創建,隨用隨加載
-
作用:不修改源碼的基礎上對方法增強
分類:
- 基於
接口
的動態代理 - JDK 動態代理 - 基於
繼承
的動態代理 - CGLib 動態代理
1.1 JDK 動態代理
- 接口:UserDao
public interface UserDao {
public void add();
public void update();
}
- 實現類:UserDaoImpl
public class UserDaoImpl implements UserDao {
public void add() {
System.out.println("add...");
}
public void update() {
System.out.println("update...");
}
}
- JDK 動態代理機制(實現 InvocationHandler 接口,重寫 invoke() 方法)
public class JDKProxy implements InvocationHandler{
private UserDao userDao;
public JDKProxy(UserDao userDao) {
this.userDao = userDao;
}
public UserDao createProxy() {
UserDao proxy = (UserDao) Proxy.newProxyInstance(
userDao.getClass().getClassLoader(),
userDao.getClass().getInterfaces(),
this); // this : 類的實例對象,即 new 出來的對象
return proxy;
}
// 調用目標對象的任何一個方法 都相當於調用invoke();
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if("update".equals(method.getName())){
// 記錄日誌:
System.out.println("日誌記錄...");
Object result = method.invoke(userDao, args);
return result;
}
return method.invoke(userDao, args);
}
}
- JDK 動態代理機制(匿名內部類直接調用返回代理對象,與實現 InvocationHandler 接口等價)
public class TestProxy {
public static void main(String[] args) {
UserDao userDao = new UserDaoImpl();
//動態代理設計模式:匿名內部類對象
UserDao p = (UserDao) Proxy.newProxyInstance(
userDao.getClass().getClassLoader(),
userDao.getClass().getInterfaces(),
//Lambda表達式,等價於 new InvocationHandler(){ invoke(){...} }
(proxy, method, argss) -> {
String methodName = method.getName();
Object result;
//方法名比較:只增強指定的需要增強的方法
if ("add".equals(methodName)) {
System.out.println("日誌記錄...");
result = method.invoke(userDao, argss);
} else {
result = method.invoke(userDao, argss);
}
return result;
});
p.add();
p.update();
}
- 測試
import org.junit.Test;
public class SpringDemo1 {
@Test
public void test1(){
UserDao userDao = new UserDaoImpl();
userDao.add();
userDao.update();
}
@Test
public void test2(){
// 被代理對象
UserDao userDao = new UserDaoImpl();
// 創建代理對象的時候傳入被代理對象.
UserDao proxy = new JDKProxy(userDao).createProxy();
proxy.add();
proxy.update();
}
}
test2 結果中打印了增強方法的信息:日誌記錄… add… update…
1.2 CGLib 動態代理
- 普通類:ProductDaoImpl
public class ProductDaoImpl {
public void add(){
System.out.println("add...");
}
public void update(){
System.out.println("update...");
}
}
- CGLib 動態代理機制(實現 MethodInterceptor 接口,重寫 intercept() 方法)
/**
* 使用CGLib生成代理對象
*/
public class CGLibProxy implements MethodInterceptor{
private ProductDaoImpl productDao;
public CGLibProxy(ProductDaoImpl productDao) {
this.productDao = productDao;
}
public ProductDaoImpl createProxy(){
// 使用CGLIB生成代理:
// 1.創建核心類
Enhancer enhancer = new Enhancer();
// 2.爲其設置父類
enhancer.setSuperclass(productDao.getClass());
// 3.設置回調
enhancer.setCallback(this);
// 4.創建代理
return (ProductDaoImpl) enhancer.create();
}
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
if("add".equals(method.getName())){
System.out.println("日誌記錄...");
Object obj = methodProxy.invokeSuper(proxy, args);
return obj;
}
return methodProxy.invokeSuper(proxy, args);
}
}
- 測試
import org.junit.Test;
public class SpringDemo2 {
@Test
public void test1(){
ProductDaoImpl productDao = new ProductDaoImpl();
productDao.add();
productDao.update();
}
@Test
public void test2(){
ProductDaoImpl productDao = new ProductDaoImpl();
ProductDaoImpl proxy = new CGLibProxy(productDao).createProxy();
proxy.add();
proxy.update();
}
}
test2 結果中打印了增強方法的信息:日誌記錄… add… update…
1.3 Spring AOP 代理方式
JDK 動態代理
:被代理對象必須要實現接口,才能產生代理對象,如果沒有接口將不能使用動態代理技術。CGLib 代理機制
:第三方代理技術,CGLib 代理,可以對任何類生成代理。代理的原理是對目標對象進行繼承代理
。 如果目標對象被 final 修飾,那麼該類無法被 CGLib 代理。
【結論】
Spring 框架中:
- 如果類實現了接口,就使用 JDK 的動態代理生成代理對象;
- 如果這個類沒有實現任何接口,使用 CGLib 生成代理對象。
2. AOP 術語
- Joinpoint(
連接點
) : 所謂連接點是指那些被攔截到的點。在 spring 中,這些點指的是方法,因爲 spring 只支持方法類型的連接點。 - Pointcut(
切入點
) : 所謂切入點是指我們要對哪些 Joinpoint 進行攔截的定義。 - Advice(
通知/增強
) : 所謂通知是指攔截到 Joinpoint 之後所要做的事情就是通知,通知分爲前置通知、後置通知、異常通知、最終通知、環繞通知(切面要完成的功能)。 - Introduction(引介) : 引介是一種特殊的通知在不修改類代碼的前提下, Introduction 可以在運行期爲類動態地添加一些方法或 Field。——類級別的增強。
- Target(
目標對象
) : 代理的目標對象 - Weaving(
織入
) : 是指把增強應用到目標對象來創建新的代理對象的過程,spring 採用動態代理織入,而AspectJ 採用編譯期織入和類裝載期織入。 - Proxy(
代理
) : 一個類被 AOP 織入增強後,就產生一個結果代理類 - Aspect(
切面
) : 是切入點和通知(引介)的結合
3. AOP 實現
- 依賴配置
在 pom.xml 添加 aop 依賴:aspectjweaver
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
- 創建通知類
- 前置通知(before):目標方法運行
之前
調用 - 最終通知(after):在目標方法運行
之後
調用 (無論是否出現異常) - 後置通知(after-returning):在目標方法運行
之後
調用 (未出現異常就會調用
) - 異常攔截通知(after-throwing):在目標方法運行
之後
調用(如果出現異常就調用
) - 環繞通知(around):在目標方法
之前和之後
都調用 (ProceedingJoinPoint對象 -->> 調用proceed方法)
public class MyAdvice {
public void before() {
System.out.println("前置通知(執行目標方法之前執行)");
}
public void after() {
System.out.println("後置通知 目標方法之後執行(無論是否發生異常都會執行)");
}
public void after_returning() {
System.out.println("後置通知 目標方法之後執行(未發生異常才執行)");
}
public void after_throwing() {
System.out.println("異常通知(發生異常才執行)");
}
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("環繞通知(執行目標方法之前)");
Object proceed = joinPoint.proceed();
System.out.println("環繞通知(執行目標方法之後)");
return proceed;
}
}
3.1 XML 配置 AOP
創建 applicationContext.xml,添加 aop 約束
<aop:config>
<!-- 配置切入點 切入點表達式的寫法:execution(表達式)
public void com.abyg.service.UserServiceImpl.save() 精確查找 save 方法
void com.qf.service.UserServiceImpl.save() 其他修飾符無返回值的save空參方法
* com.qf.service.UserServiceImpl.save() 有或者無返回值的save空參方法
* com.qf.service.UserServiceImpl.*() 有或者無返回值的所有空參方法
* com.qf.service.*ServiceImpl.*(..) 有或者無返回值的所有有參或者空參方法
* com.qf.service..*ServiceImpl.*(..) 一般不用,service包下子和孫包以ServiceImpl結尾的類中的方法
-->
<aop:pointcut id="pc" expression="execution(* com.qf.service.*ServiceImpl.*(..))" />
<aop:aspect ref="myAdvice" >
<!-- 指定名爲before方法作爲前置通知 -->
<aop:before method="before" pointcut-ref="pc" />
<!-- 後置 -->
<aop:after method="after" pointcut-ref="pc"/>
<!-- 後置 -->
<aop:after-returning method="after_returning" pointcut-ref="pc" />
<!-- 異常攔截通知 -->
<aop:after-throwing method="after_throwing" pointcut-ref="pc"/>
<!-- 環繞通知 -->
<aop:around method="around" pointcut-ref="pc" />
</aop:aspect>
</aop:config>
3.2 註解 配置 AOP
Spring中的註解配置AOP:
//通知類: 表示該類是一個通知類
@Aspect
public class MyAdvice {
//自己設置一個切點,管理重複代碼
@Pointcut("execution(* com.qf.service.*ServiceImpl.*(..))")
public void pc(){}
//前置通知 : 指定該方法是前置通知,並制定切入點
@Before("MyAdvice.pc()")
public void before() {
System.out.println("前置通知(執行目標方法之前執行)");
}
//後置通知
@After("execution(* com.qf.service.*ServiceImpl.*(..))")
public void after() {
System.out.println("後置通知 目標方法之後執行(無論是否發生異常都會執行)");
}
//後置通知
@AfterReturning("execution(* com.qf.service.*ServiceImpl.*(..))")
public void after_returning() {
System.out.println("後置通知 目標方法之後執行(未發生異常才執行)");
}
//異常通知
@AfterThrowing("execution(* com.qf.service.*ServiceImpl.*(..))")
public void after_throwing() {
System.out.println("異常通知(發生異常才執行)");
}
//環繞通知
@Around("execution(* com.qf.service.*ServiceImpl.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("環繞通知(執行目標方法之前)");
Object proceed = joinPoint.proceed();
System.out.println("環繞通知(執行目標方法之後)");
return proceed;
}
}
<!-- applicationContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
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.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd ">
<!-- 準備工作: 導入aop(約束)命名空間 -->
<!-- 1.配置目標對象 -->
<bean name="userService" class="com.qf.service.UserServiceImpl" />
<!-- 2.配置通知對象 -->
<bean name="myAdvice" class="com.qf.annotation_aop.MyAdvice" />
<!-- 3.開啓使用註解完成織入 -->
<aop:aspectj-autoproxy />
</beans>