現有代碼缺陷
針對帶日志功能的實現類,我們發現有如下缺陷:
- 對核心業務功能有干擾,導致程序員在開發核心業務功能時分散了精力
- 附加功能分散在各個業務功能方法中,不利于統一維護
解決思路
解決核心:解耦。把附加功能從業務功能代碼中抽取出來。
困難
解決問題的困難:要抽取的代碼在方法內部,靠以前把子類中的重復代碼抽取到父類的方式沒法解決。所以需要引
入新的技術。
代理模式
概念
介紹
二十三種設計模式中的一種,屬于結構型模式。它的作用就是通過提供一個代理類,讓我們在調用目標方法的時
候,不再是直接對目標方法進行調用,而是通過代理類間接調用。讓不屬于目標方法核心邏輯的代碼從目標方法中
剝離出來一一解耦。調用目標方法時先調用代理對象的方法,減少對目標方法的調用和打擾,同時讓附加功能能夠
集中在一起也有利于統一維護。
相關術語
,代理:將非核心邏輯剝離出來以后,封裝這些非核心邏輯的類、對象、方法。
·目標:被代理“套用”了非核心邏輯代碼的類、對象、方法。
場景模擬
-
聲明計算器接口Calculator,包含加減乘除的抽象方法
package org.example; public interface Calculator { public int add(int i, int j); public int sub(int i, int j); public int mul(int i, int j); public int div(int i, int j); }
-
寫一個實現Calculator業務的實現類
package org.example; public class CalculatorImpl implements Calculator {@Overridepublic int add(int i, int j) {int result = i + j;System.out.println("result=" + result);return result;}@Overridepublic int sub(int i, int j) {int result = i - j;System.out.println("result=" + result);return result;}@Overridepublic int mul(int i, int j) {int result = i * j;System.out.println("result=" + result);return result;}@Overridepublic int div(int i, int j) {int result = i / j;System.out.println("result=" + result);return result;} }
-
寫一個實現Calculator業務的帶有日志功能的實現類
package org.example; public class CalculatorLogImpl implements Calculator {@Overridepublic int add(int i, int j) {System.out.println("計算開始,i=" + i + "j=" + j);int result = i + j;System.out.println("計算結束,i=" + i + "j=" + j + "result=" + result);System.out.println("result=" + result);return result;}@Overridepublic int sub(int i, int j) {System.out.println("計算開始,i=" + i + "j=" + j);int result = i - j;System.out.println("計算結束,i=" + i + "j=" + j + "result=" + result);System.out.println("result=" + result);return result;}@Overridepublic int mul(int i, int j) {System.out.println("計算開始,i=" + i + "j=" + j);int result = i * j;System.out.println("計算結束,i=" + i + "j=" + j + "result=" + result);System.out.println("result=" + result);return result;}@Overridepublic int div(int i, int j) {System.out.println("計算開始,i=" + i + "j=" + j);int result = i / j;System.out.println("計算結束,i=" + i + "j=" + j + "result=" + result);System.out.println("result=" + result);return result;} }
靜態代理
靜態代理確實實現了解耦,但是由于代碼都寫死了,完全不具備任何的靈活性。就拿日志功能來說,將來其
他地方也需要附加日志,那還得再聲明更多個靜態代理類,那就產生了大量重復的代碼,日志功能還是分散
的,沒有統一管理。
提出進一步的需求:將日志功能集中到一個代理類中,將來有任何日志需求,都通過這一個代理類來實現。
這就需要使用動態代理技術了。
動態代理
使用java.lang.reflect.Proxy類實現動態代理
官方示例代碼
InvocationHandler handler = new MyInvocationHandler(...);
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new class<?>[]{Foo.class}, handler);
創建一個代理工廠類
package org.example; import lombok.val; import javax.print.attribute.standard.JobKOctets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy; public class ProxyFactory { Object target; public ProxyFactory(Object target) { this.target = target; } public Object getProxy() {
/* 有三個參數 第一個參數:CLassLoader:加載動態生成代理類的來加載器 第二個參數:CLass[]interfaces:目錄對象實現的所有接口cLass類型數組 第三個參數:InvocationHandler:設置代理對象實現目標對象方法的過程*/ ClassLoader cLassLoader = target.getClass().getClassLoader(); Class[] classes = target.getClass().getInterfaces(); InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //調用方法前日志 System.out.println("[動態代理][調用前日志]" + method.getName() + "參數:" + args); //調用目標方法 Object result = method.invoke(target, args); //調用方法后日志 System.out.println("[動態代理][調用后日志]" + method.getName() + "參數:" + args); return result; } }; return Proxy.newProxyInstance(cLassLoader, classes, invocationHandler); }
}
編寫測試類
@Test
public void calculatorTest(){ ProxyFactory proxyFactory=new ProxyFactory(new CalculatorImpl()); Calculator proxy=(Calculator) proxyFactory.getProxy(); proxy.add(1,1);
}
輸出結果
[動態代理][調用前日志]add參數:[Ljava.lang.Object;@7d0587f1
result=2
[動態代理][調用后日志]add參數:[Ljava.lang.Object;@7d0587f1
基于注解的AOP
動態代理分類:JDK動態代理和cglib動態代理
JDK動態代理生成接口實現類代理對象
cglib動態代理繼承被代理的目標類,生成子類代理對象,不需要目標類實現接口
- 有接口可以使用JDK動態代理和cblib動態代理
- 沒有接口只能使用cblib動態代理
Aspect:是AOP思想的一種實現。本質上是靜態代理,將代理邏輯“織入"被代理的目標類編譯得到的字節碼
文件,所以最終效果是動態的。weaver就是織入器。Spring只是借用了Aspect)中的注解。
使用AOP步驟
-
引入aop相關依賴
<!--spring aop依賴--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>6.0.2</version> </dependency> <!--spring aspects依賴--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>6.0.2</version> </dependency>
-
創建目標資源
-
接口
package com.example.annoAOP; public interface Calculator { public int add(int i, int j); public int sub(int i, int j); public int mul(int i, int j); public int div(int i, int j); }
-
實現類
package com.example.annoAOP; import org.springframework.stereotype.Component; @Component public class CalculatorImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; System.out.println("result=" + result); return result; } @Override public int sub(int i, int j) { int result = i - j; System.out.println("result=" + result); return result; } @Override public int mul(int i, int j) { int result = i * j; System.out.println("result=" + result); return result; } @Override public int div(int i, int j) { int result = i / j; System.out.println("result=" + result); return result; } }
-
第三步創建切面類
-
創建
bean.xml
,使用AOP約束,開啟AOP功能和掃描功能<?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: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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 開啟組件掃描 --> <context:component-scan base-package="com.example"/> <!--開啟aspectj自動代理,為目標對象生成代理--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
-
創建
LogAscept
類,增加一個方法的前置切入點package com.example.annoAOP; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect//表明這是一個AOP文件 @Component//讓IoC進行管理 public class LogAspect { //設置切入點和通知類型 //通知類型: // 前置 @Before(value="切入點表達式") // 返回 @AfterReturning // 異常 @AfterThrowing // 后置 @After() // 環繞 @Around() //切入點表達式寫法:execution(權限修飾 方法返回值 方法所在全類名.方法名 (參數列表)) //execution:固定語法 //權限修飾:這里寫*表示權限修飾符和返回值任意 //方法所在全類名:寫*表示任意包名;寫*...表示包名任意同時包層次深度任意 //類名用*號代替表示類名任意,部分用*代替,如*Service,表示匹配以Service結尾的列或接口 //方法名:用*號代替表示方法名任意;部分用*代替,如get*,表示匹配以get開頭的方法 //參數列表可以使用(...)形式表示參數列表任意 @Before(value = "execution(public int com.example.annoAOP.CalculatorImpl.add (int,int))") public void beforeAdd() { System.out.println("[前置通知][add()]計算開始"); } }
方法表達式寫法:
-
創建測試方法
@Test public void testAOPAdd(){ApplicationContext applicationContext=new ClassPathXmlApplicationContext("bean.xml");Calculator calculator=applicationContext.getBean(Calculator.class);calculator.add(1,1); }
-
輸出結果
[前置通知][add()]計算開始 result=2
通知類型
- 前置通知:在被代理的目標方法前執行
- 返回通知:在被代理的目標方法成功結束后執行(壽終正寢)
- 異常通知:在被代理的目標方法異常結束后執行(死于非命)
- 后置通知:在被代理的目標方法最終結束后執行(蓋棺定論)
- 環繞通知:使用try.catch.finally結構圍繞整個被代理的目標方法,包括上面四種通知對應的所有位置
修改LogAspect
類,添加五種通知方法
package com.example.annoAOP; import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component; @Aspect//表明這是一個AOP文件
@Component//讓IoC進行管理
public class LogAspect { //前置通知 @Before(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))") public void beforeMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("[前置通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("Args[]=" + args); } //后置通知 @After(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))") public void afterMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("[后置通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("Args[]=" + args); } //返回通知 @AfterReturning(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))", returning = "result") public void afterReturnMethod(JoinPoint joinPoint, Object result) { String MethodName = joinPoint.getSignature().getName(); System.out.println("[返回通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("[返回通知]result=" + result); } //異常通知 @AfterThrowing(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))", throwing = "exp") public void afterThrowing(JoinPoint joinPoint, Throwable exp) { String MethodName = joinPoint.getSignature().getName(); System.out.println("[異常通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println(exp); } //環繞通知 @Around("execution(* com.example.annoAOP.CalculatorImpl.* (..))") //ProceedingJoinPoint繼承JoinPoint,比JoinPoint功能更強大,可以更好的調用目標方法 public Object around(ProceedingJoinPoint joinPoint) { Object result = null; try { System.out.println("環繞通知-目標方法執行前"); result = joinPoint.proceed(); System.out.println("環繞通知-目標方法執行后"); } catch (Throwable throwable) { System.out.println("環繞通知-目標方法執行異常"); } finally { System.out.println("環繞通知-目標方法執行完成"); } return result; }
}
輸出結果
環繞通知-目標方法執行前
[前置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@62727399
result=2
[返回通知][CalculatorImpl.MethodName=add()
[返回通知]result=2
[后置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@62727399
環繞通知-目標方法執行后
環繞通知-目標方法執行完成
編寫測試方法,使測試方法引發異常
@Test
public void testAOPexp(){ ApplicationContext applicationContext=new ClassPathXmlApplicationContext("bean.xml"); Calculator calculator=applicationContext.getBean(Calculator.class); calculator.div(1,0);
}
運行結果
環繞通知-目標方法執行前
[前置通知][CalculatorImpl.MethodName=div()
Args[]=[Ljava.lang.Object;@4d9ac0b4
[異常通知][CalculatorImpl.MethodName=div()
java.lang.ArithmeticException: / by zero
[后置通知][CalculatorImpl.MethodName=div()
Args[]=[Ljava.lang.Object;@4d9ac0b4
環繞通知-目標方法執行異常
環繞通知-目標方法執行完成[之后是異常報錯信息]
重用切入點
-
定義一個切入點
package com.example.annoAOP; @Pointcut(value = "execution(* com.example.annoAOP.CalculatorImpl.* (..))") public void pointCut() {}
-
使用切入點
-
內部使用切入點
@After(value = "pointCut") public void afterMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("[后置通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("Args[]=" + args); }
-
外部使用切入點
@After(value = "com.example.annoAOP.pointCut") public void afterMethod(JoinPoint joinPoint) { String MethodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("[后置通知][CalculatorImpl.MethodName=" + MethodName + "()"); System.out.println("Args[]=" + args); }
-
切面的優先級
相同目標方法上同時存在多個切面時,切面的優先級控制切面的內外嵌套順序。
- 優先級高的切面:外面
- 優先級低的切面:里面
使用@Order注解可以控制切面的優先級: - @Order(較小的數):優先級高
- @Order(較大的數):優先級低
XML形式配置AOP
-
創建新包
xmlaop
,復制上文接口、實現類、AOP配置類 -
刪除
LogAspect
類的@Aspect注解和AOP注解 -
新建
XmlAop.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: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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 開啟組件掃描 --> <context:component-scan base-package="com.example.xmlAOP"/> <!--配置AOP--> <aop:config> <!-- 配置切面類 --> <aop:aspect ref="logAspect"> <!-- 配置切入點 --> <aop:pointcut id="cutpoint" expression="execution(* com.example.xmlAOP.CalculatorImpl.* (..))"/> <!-- 配置方法執行前通知 --> <aop:before method="beforeMethod" pointcut-ref="cutpoint"/> <!-- 配置方法執行后通知 --> <aop:after method="afterMethod" pointcut-ref="cutpoint"/> <!-- 配置方法返回后通知 --> <aop:after-returning method="afterReturnMethod" pointcut-ref="cutpoint" returning="result"/> <!-- 配置環繞通知 --> <aop:around method="around" pointcut-ref="cutpoint"/> <!-- 配置異常通知 --> <aop:after-throwing method="afterThrowing" pointcut-ref="cutpoint" throwing="exp"/> </aop:aspect> </aop:config> </beans>
-
編寫測試方法
@Test public void testXML_AOP(){ApplicationContext applicationContext=new ClassPathXmlApplicationContext("XmlAop.xml");//本項目存在兩個Calculator,需要注意使用的是哪個Calculator類com.example.xmlAOP.Calculator calculator=applicationContext.getBean(com.example.xmlAOP.Calculator.class);calculator.add(1,1); }
-
輸出結果
[前置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@eda25e5
環繞通知-目標方法執行前
result=2
環繞通知-目標方法執行后
環繞通知-目標方法執行完成
[返回通知][CalculatorImpl.MethodName=add()
[返回通知]result=2
[后置通知][CalculatorImpl.MethodName=add()
Args[]=[Ljava.lang.Object;@eda25e5