一、AOP:
是對OOP編程方式的一種補充。翻譯過來為“面向切面編程”。
可以理解為一個攔截器框架,但是這個攔截器會非常武斷,如果它攔截一個類,那么它就會攔截這個類中的所有方法。如對一個目標列的代理,增強了目標類的所有方法。
兩個解決辦法:
1.不優雅的做法:
在添加增強時,根據方法名去判斷,是否添加增強,但是這樣就得一直去維護這個增強類。
2.面向切面:
將增強類和攔截條件組合在一起,然后將這個切面配置到 ProxyFactory 中,從而生成代理。
二、AOP 和 切面的關系
1.類比於 OOP 和 對象,AOP 和 切面就是這樣的一種關系。
2.也可以將 切面 看成是 AOP 的一個工具。
三、幾個概念
切面(Advisor):是AOP中的一個術語,表示從業務邏輯中分離出來的橫切邏輯,比如性能監控,日志記錄,權限控制等。
這些功能都可以從核心的業務邏輯中抽離出去。可以解決代碼耦合問題,職責更加單一。封裝了增強和切點。
增強(Advice):增強代碼的功能的類,橫切到代碼中。
目標:目標方法(JDK代理)或目標類(CGLIB代理)
代理:JDK代理,CGLIB代理。或是通過 ProxyFactory 類生產。
切點:通過一個條件來匹配要攔截的類,這個條件稱為切點。如攔截所有帶 Controller 注解的類。增強的條件。
連接點:作為增強方法的入參,可以獲取到目標方法的信息。
四、概括為一張圖
五、增強
1.Weaving(織入):對方法進行增強
(1)前置增強(BeforeAdvice):在目標方法前調用。
(2)后置增強(AfterAdvice):在目標方法后調用。
(3)環繞增強(AroundAdvice):將 Before 和 After ,甚至拋出增強和返回增強合到一起。
(4)返回增強(AfterReturningAdvice):在方法返回結果后執行,該增強可以接收到目標方法返回結果。
(5)拋出增強(AfterThrowingAdvice):在目標方法拋出對應的類型后執行,可以接收到對應的異常信息。
2.Introduction(引入):對類進行增強
(1)引入增強(DeclareParentsAdvice):想讓程序在運行的時候動態去實現某個接口,需要引入增強。
六、SpringAOP
1.編程式
(1)前置增強,需要實現:MethodBeforeAdvice 接口
增強類:
/** * @author solverpeng * @create 2016-07-27-11:07 */ public class CarBeforeAdvice implements MethodBeforeAdvice{ @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("before"); } }
測試方法:
@Test public void test03() { ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setTarget(new Car()); proxyFactory.addAdvice(new CarBeforeAdvice()); Wheel carProxy = (Wheel)proxyFactory.getProxy(); carProxy.run(); }
(2)后置增強:實現 AfterReturningAdvice 接口
(3)環繞增強:實現 org.aopalliance.intercept.MethodInterceptor 接口,使用 Object result = methodInvocation.proceed(); 調用目標方法。在目標方法前后添加增強。
2.聲明式:Spring + AspectJ
開發步驟:
(1)定義切面類,將該切面類放入到 IOC 容器中
/** * @author solverpeng * @create 2016-07-27-13:10 */ @Component public class XMLLoggingAspect { public void beforeAdvice(JoinPoint point) { System.out.println("xml aspects logging before"); } public void afterAdvice(JoinPoint point) { System.out.println("xml aspects logging after"); } public void afterReturningAdvice(JoinPoint point, Object result) { System.out.println("xml aspects logging afterReturningAdvice"); } public void afterThrowingAdvice(JoinPoint point, Exception e) { System.out.println("xml aspects logging afterThrowingAdvice"); } public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable { Object result = point.proceed(); System.out.println("xml aspects logging aroundAdvice"); return result; } }
(2)Spring AOP 配置都必須定義在 <aop:config>元素內部。
(3)在 <aop:config> 中,每個切面都需要創建一個 <aop:aspect> 元素
(4)為具體的切面實現引用后端的 bean 實例。
下面展示 前置增強、后置增強、環繞增強、返回增強、拋出增強 的一個例子:
<aop:config> <aop:aspect ref="XMLLoggingAspect"> <aop:pointcut id="carPointcut" expression="execution(void run())"/> <aop:before method="beforeAdvice" pointcut-ref="carPointcut"/> <aop:after method="afterAdvice" pointcut-ref="carPointcut"/> <aop:after-returning method="afterReturningAdvice" pointcut-ref="carPointcut" returning="result"/> <aop:after-throwing method="afterThrowingAdvice" pointcut-ref="carPointcut" throwing="e"/> <aop:around method="aroundAdvice" pointcut-ref="carPointcut"/> </aop:aspect> </aop:config>
控制台輸出:
xml aspects logging before
i am a car, i can run.
xml aspects logging aroundAdvice
xml aspects logging afterReturningAdvice
xml aspects logging after
基於聲明式的 Spring AspectJ 織入增強配置說明:
支持配置兩個級別的公共切點表達式,一個是針對某個切面的所有方法(定義在 <aop:aspect> 節點內),另一個是針對所有切面(定義在<aop:config>節點內)。使用 pointcut-ref 來引入切點。
下面展示 引入增強 的一個例子:
對於引入增強,只需要在配置在 <aop:aspect> 節點下就可以,不需要去切面類中添加任何屬性。
<aop:config> <aop:aspect ref="XMLLoggingAspect"> <aop:declare-parents types-matching="com.nucsoft.spring.target.impl.Student" implement-interface="com.nucsoft.spring.target.Fly" default-impl="com.nucsoft.spring.target.impl.SuperMan"/> </aop:aspect> </aop:config>
測試:
@Test public void test04() { Person student = context.getBean(Student.class); System.out.println(student.say("james")); Fly fly = (Fly) student; fly.fly(); }
控制台輸出:
hello,james
i am super man, i can fly.
基於聲明式的 Spring AspectJ 引入增強配置說明:
(1)引入增強是類級別的,所以不存在切點表達式。
(1)利用 <aop:declare-parents> 節點在 <aop:aspect> 內部聲明
(2)types-matching 屬性,要增強的目標類,這里需要全類名。
(3)implement-interface 屬性:動態的增強類接口。
(4)default-impl 屬性:動態增強類接口的實現類。
3.注解:Spring + AspectJ
對切面類添加 @Aspect 注解,將切面類和目標類放入到 IOC 容器中,可以通過 <context:component-scan base-package=""/> 進行掃描。
添加增強方法(包括增強類型和切點表達式,以及連接點)。
在 Spring Config 文件中添加 <aop:aspectj-autoproxy proxy-target-class="true"/>, proxy-target-class屬性,false 只能代理接口(JDK動態代理),true 代理類(CGLIB動態代理)
3.1 通過切點表達式(AspectJ execution)進行攔截
spring-config.xml
<context:component-scan base-package="com.nucsoft.spring"/> <aop:aspectj-autoproxy proxy-target-class="true"/>
Person 接口:
/** * Created by solverpeng on 2016/7/26. */ public interface Person { String say(String name); }
Person 實現類 Student:
/** * @author solverpeng * @create 2016-07-26-17:55 */ @Component public class Student implements Person{ @Override public String say(String name) { return "hello," + name; } }
測試:
/** * @author solverpeng * @create 2016-07-26-18:15 */ public class SpringTest { @Test public void test01() { ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); Student student = context.getBean(Student.class); String james = student.say("james"); System.out.println(james); } }
(1)前置增強:關鍵字:@Before,JoinPoint,execution 切點表達式,表達式內容支持通配符
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @Before("execution(String say(String))") public void before(JoinPoint point) { System.out.println("before"); } }
控制台輸出:
before
hello,james
(2)后置增強:關鍵字:@After
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @After("execution(String say(String))") public void afterAdvice(JoinPoint point) { System.out.println("after..."); } }
控制台輸出:
after...
hello,james
(3)環繞增強:關鍵字:@Around,ProceedingJoinPoint
將 Student 類還原
切面類:
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @Around("execution(String say(String))") public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable { before(); Object result = point.proceed(); after(); return result; } private void before(){ System.out.println("before"); } private void after(){ System.out.println("after"); } }
控制台輸出:
before
after
hello,james
注意:
<1>.增強方法的返回值為 Object 類型的,該返回值與目標方法返回值一致。
<2>.Object result = point.proceed(); 該 result 即為目標方法執行后的返回值。
<3>.在環繞通知中需要明確調用 ProceedingJoinPoint 的 proceed() 方法來執行被代理的方法. 如果忘記這樣做就會導致通知被執行了, 但目標方法沒有被執行
<4>.環繞通知的方法需要返回目標方法執行之后的結果, 即調用 joinPoint.proceed(); 的返回值, 否則會出現空指針異常
(4)返回增強:關鍵字:@AfterReturning,returning,JoinPoint
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @AfterReturning(value = "execution(String say(String))", returning = "str") public void aferRetruningAdvice(JoinPoint point, String str) { System.out.println("str:" + str); System.out.println("aferRetruningAdvice"); } }
控制台輸出:
str:hello,james
aferRetruningAdvice
hello,james
(5)拋出增強:關鍵字:@AfterThrowing,throwing。注意:拋出的異常類型必須和切面拋出增強接收的異常類型相同或是其子類。
更改 Student 類,手動拋出一個異常:
/** * @author solverpeng * @create 2016-07-26-17:55 */ @Component public class Student implements Person{ @Override public String say(String name) { throw new RuntimeException("exception"); } }
切面類:
/** * @author solverpeng * @create 2016-07-26-17:42 */ @Aspect @Component public class LoggingAspect { @AfterThrowing(value = "execution(String say(String))", throwing = "e") public void AfterThrowingAdvice(JoinPoint point, Exception e) { String message = e.getMessage(); System.out.println(message); System.out.println("AfterThrowingAdvice"); } }
控制台輸出:
exception
AfterThrowingAdvice
3.2 通過切點注解表達式(AspectJ @annotation)進行攔截
開發步驟:
(1)定義注解類
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthorityTag {}
(2)為切面類中增強指定注解表達式
/** * Created by solverpeng on 2016/7/27. */ @Aspect @Component public class AuthorityAspect { @Before("@annotation(com.nucsoft.spring.annotation.AuthorityTag)") public void before(JoinPoint point) { System.out.println("authority before"); } }
(3)在目標類目標方法上標注注解
/** * @author xzsw * @create 2016-07-27-9:59 */ @Component public class Car implements Wheel{ @AuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); } }
各種增強的使用:
(1)前置增強
上面介紹步驟的例子就是一個前置增強。
控制台輸出:
authority before
i am a car, i can run.
(2)后置增強
可以為多個增強使用同一個注解,如:
/** * Created by solverpeng on 2016/7/27. */ @Aspect @Component public class AuthorityAspect { @Before("@annotation(com.nucsoft.spring.annotation.AuthorityTag)") public void before(JoinPoint point) { System.out.println("authority before"); } @After("@annotation(com.nucsoft.spring.annotation.AuthorityTag)") public void afterAdvice(JoinPoint point) { System.out.println("authority after"); } }
控制台輸出:
authority before
i am a car, i can run.
authority after
也可以為每個增強使用不同的注解,如:
注解:
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface BeforeAuthorityTag {}
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AfterAuthorityTag {}
切面:
/** * Created by solverpeng on 2016/7/27. */ @Aspect @Component public class AuthorityAspect { @Before("@annotation(com.nucsoft.spring.annotation.BeforeAuthorityTag)") public void before(JoinPoint point) { System.out.println("authority before"); } @After("@annotation(com.nucsoft.spring.annotation.AfterAuthorityTag)") public void afterAdvice(JoinPoint point) { System.out.println("authority after"); } }
使用:
/** * @author solverpeng * @create 2016-07-27-9:59 */ @Component public class Car implements Wheel{ @BeforeAuthorityTag @AfterAuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); } }
控制台輸出:
authority before
i am a car, i can run.
authority after
(3)環繞增強
注解類:
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AroundAuthorityTag {}
切面增強:
@Around(value = "@annotation(com.nucsoft.spring.annotation.AroundAuthorityTag)") public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable { Object result = point.proceed(); after(); System.out.println("authority aroundAdvice"); return result; } private void after() { System.out.println("after"); }
目標類:
/** * @author solverpeng * @create 2016-07-27-9:59 */ @Component public class Car implements Wheel{ @AroundAuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); } }
控制台輸出:
i am a car, i can run.
after
authority aroundAdvice
(4)返回增強
注解類:
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AfterReturningAuthorityTag {}
切面增強:
@AfterReturning(value = "@annotation(com.nucsoft.spring.annotation.AfterReturningAuthorityTag)", returning = "result") public void afterReturningAdvice(JoinPoint point, Object result) { System.out.println("authority afterReturning"); }
目標類:
/** * @author solverpeng * @create 2016-07-27-9:59 */ @Component public class Car implements Wheel{ @AfterReturningAuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); } }
控制台輸出:
i am a car, i can run.
authority afterReturning
(5)拋出增強
注解類:
/** * Created by solverpeng on 2016/7/27. */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AfterThrowingAuthorityTag {}
切面增強:
@AfterThrowing(value = "@annotation(com.nucsoft.spring.annotation.AfterThrowingAuthorityTag)", throwing = "e") public void afterThrowAdvice(JoinPoint point, Exception e) { System.out.println(e.getMessage()); System.out.println("authority afterThrowing"); }
目標類:
@Component public class Car implements Wheel{ @AfterThrowingAuthorityTag @Override public void run() { System.out.println("i am a car, i can run."); throw new RuntimeException("throw a new runtimeException"); } }
控制台輸出:
i am a car, i can run.
throw a new runtimeException
authority afterThrowing
java.lang.RuntimeException: throw a new runtimeException
(6)引入增強:關鍵字:@DeclareParents
將要引入的接口:
/**
* Created by solverpeng on 2016/7/26.
*/
public interface Fly {
void fly(); }
將要引入的接口的實現:
/**
* @author solverpeng
* @create 2016-07-26-20:55
*/
public class SuperMan implements Fly{
@Override
public void fly() { System.out.println("i am super man, i can fly."); } }
切面類:
/**
* @author solverpeng
* @create 2016-07-26-17:42
*/
@Aspect
@Component
public class LoggingAspect { @DeclareParents(value = "com.nucsoft.spring.target.impl.Student", defaultImpl = SuperMan.class) private Fly fly; }
測試類:
/**
* @author solverpeng
* @create 2016-07-26-18:15
*/
public class SpringTest {
@Test
public void test01() { ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); Person student = context.getBean(Student.class); String james = student.say("james"); System.out.println(james); Fly fly = (Fly) student; fly.fly(); } }
控制台輸出:
hello,james
i am super man, i can fly.
說明:
<1>.在 Aspect 類中定義一個需要引入增強的接口,它也就是運行時需要動態實現的接口。
<2>.@DeclareParents 注解:
value 屬性:目標類,可以是一個 AspectJ 類型的表達式,可以引入到多個類中。
defaultImpl 屬性:引入接口的默認實現類。
<3>.雖然切面類中標注有@DeclareParents 注解 的屬性可以是任意的,但是一般還是將其設置為 引入增強類型。
<4>.從 ApplicationContext 中獲取到的 student 對象其實是一個代理對象,可以轉型為自己靜態實現的接口 Person,也可以轉型為動態實現的接口 Fly,切換起來非常方便。
3.3 對比基於注解的切點表達式和注解表達式
注解表達式:更加靈活,但是相應的在開發的過程中,需要程序員手動的去為每個需要添加增強的方法添加對應注解。更加容易擴展。
切點表達式:可以寫出通用的增強,也不需要程序員手動的去為每個方法添加增強,但是需要切點表達式適配。
七、小的知識點
1.利用方法簽名編寫 AspectJ 切點表達式
execution * com.nucsoft.spring.Calculator.*(..): 匹配 Calculator 中聲明的所有方法,
第一個 * 代表任意修飾符及任意返回值. 第二個 * 代表任意方法. .. 匹配任意數量的參數. 若目標類與接口與該切面在同一個包中, 可以省略包名.
execution public * Calculator.*(..): 匹配 ArithmeticCalculator 接口的所有公有方法.
execution public double Calculator.*(..): 匹配 Calculator 中返回 double 類型數值的方法
execution public double Calculator.*(double, ..): 匹配第一個參數為 double 類型的方法, .. 匹配任意數量任意類型的參數
execution public double Calculator.*(double, double): 匹配參數類型為 double, double 類型的方法.
2.可以合並切點表達式 使用 &&, ||, ! 來合並。如:
execution(void run()) || execution(void say())
3.切面優先級:
可以通過實現 Ordered 接口或利用 @Order 注解指定。
(1)實現 Ordered 接口,getOrder() 方法返回的值越小,優先級越高。
(2)使用 @Order 注解,需要出現在注解中,同樣是值越小優先級越高。
4.重用切點定義
在 AspectJ 切面中, 可以通過 @Pointcut 注解將一個切入點聲明成簡單的方法. 切入點的方法體通常是空的, 因為將切入點定義與應用程序邏輯混在一起是不合理的。
切入點方法的訪問控制符同時也控制着這個切入點的可見性。如果切入點要在多個切面中共用, 最好將它們集中在一個公共的類中。
在這種情況下, 它們必須被聲明為 public。在引入這個切入點時, 必須將類名也包括在內。如果類沒有與這個切面放在同一個包中, 還必須包含包名。
其他通知可以通過方法名稱引入該切入點。
如:
@Pointcut("execution(void run())") public void LoggingPointcut(){}; @Before("LoggingPointcut()") public void before() { System.out.println("before"); }
八、總結
1.在學習 Spring AOP 之前,最好先理解了 靜態代理,JDK動態代理,CGLIB動態代理。
2.明白切面和增強以及切點之間的關系
3.幾個增強之間的區別
除環繞增強外,所有的連接點使用的都是 JoinPoint 類型的入參,而環繞增強使用的是 ProceedingJoinPoint。
返回增強能接受到返回值
拋出增強能接收到拋出的異常
環繞增強的返回值類型為目標方法返回值類型
4.Spring AOP 支持聲明式和基於注解的方式,注解優先。
5.步驟:
1.定義切面,以及增強,編寫切點表達式
2.如果切點表達式是基於注解的,還需要對目標方法添加對應的注解。