AOP (aspect oriented programming 面向切面編程)
什麼是AOP?
與OOP相比,AOP是處理一些橫向性問題,這些橫向性問題不會影響到主邏輯的實現,但是會散落到代碼各部分,難以維護。AOP就是把這些問題和主業務分開,達到與主業務邏輯解耦的目的。
應用場景
- 日誌記錄
- 權限驗證
- 效率檢查
- 事務管理
spring Aop
AOP可以說是一種面向切面編程的思想,spring Aop和AspectJ是兩種實現方式,而spring Aop借用AspectJ中的註解語法。SpringAop又有JDK動態代理和CGLIB代理兩種實現方式。
spring Aop中的專業術語
aspect(切面)
交給spring去管理,裏面定義了 (pointcut)切點、(advice)通知方法
Joinpoint(連接點)
目標對象,表示要關注的點和增強的方法,也就是我們要作用的點;
point(切點)
切點表示 (Joinpoint)連接點 的集合,主要告訴通知連接點在哪裏,切點表達式決定要通知哪些連接點。
advice(通知)
要給目標方法增強什麼功能,實現橫向業務的邏輯代碼;
target(目標對象、原始對象)
aop Proxy(代理對象)
包含了原始對象(target)的代碼和增加後的代碼的那個對象;
spring AOP提供兩種編程風格:@AspectJ support利用aspectJ註解和Schema-based AOP support 利用
xml aop:config
命令空間。
使用步驟
- 添加依賴
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.22.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
</dependencies>
- 編寫切面
@Aspect
public class 切面類{
@Around("切點表達式") //切點表達式用來匹配目標和通知
public Object 通知方法(ProceedingJoinpoint pjp){
//重複邏輯 計時,事務控制,權限控制
//調用目標 result是目標方法返回的結果
Object result=pjp.proceed();
retrun result;
}
}
//表示此類是一個切面類
@Aspect
@Component
public class TimeAspect {
//表示匹配service包下的UserserviceTarget類中的所有方法
// @Around("within(service.UserServiceTarget)")
//表示匹配service包下的UserServiceTarget中的修飾符爲public 無返回值的a方法,參數任意個數、任意值
@Around("execution(public void service.UserServiceTarget.a(..))")
//組成部分1:通知方法
//ProceedingJoinPoint 用來調用目標方法
public Object time(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
//調用目標方法
Object proceed = pjp.proceed();
long end = System.nanoTime();
System.out.println("方法運行時間:"+(end-start));
return proceed;
}
}
- 把切面類和目標類都交給Spring容器管理(使用基於xml的配置方式)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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"
>
<!--啓用切面編程相關注解 如:@Aspect,@Around 還提供自動產生代理類的功能-->
<aop:aspectj-autoproxy/>
<context:component-scan base-package="service,aspect"/>
</beans>
- 使用切面
從容器getBean根據接口獲取,容器返回的是代理對象,代理對象中進行切點的匹配,匹配到了進行通知,通知內部調用目標;
public class TestProxy {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
//實際上調用的是代理類
UserService bean = context.getBean(UserService.class);
System.out.println(bean.getClass());//class com.sun.proxy.$Proxy8
bean.a(10,20);
}
}
切點表達式
1. within 匹配類中的所有方法(粒度粗,最小粒度爲類)
語法:
within(包名.類名)
example:
@Pointcut(“within(com.chenss.dao.)")//匹配com.chenss.dao包中的任意方法
@Pointcut("within(com.chenss.dao…)”)//匹配com.chenss.dao包及其子包中的任意方法
2. execution 表達式 可以精確到類中的每個方法(粒度細),推薦使用
語法:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern
(param-pattern) throws-pattern?)
- modifires-pattern:方法的可見性即修飾符,
?
表示可有可無; - ret-type-pattern:返回值類型;
- declaring-type-pattern:所在類的全路徑名,如com.spring.Aspect
- name-pattern:方法名類型;
- param-pattern:方法參數類型,如java.lang.String
- throws-pattern:方法拋出異常的配型
example:
@Pointcut(“execution(* com.chenss.dao..(…))”)//匹配com.chenss.dao包下的任意接口和類的任意方法
@Pointcut(“execution(public * com.chenss.dao..(…))”)//匹配com.chenss.dao包下的任意接口和類的public方法
@Pointcut(“execution(public * com.chenss.dao..())”)//匹配com.chenss.dao包下的任意接口和類的public 無方法參數的方法
@Pointcut(“execution(* com.chenss.dao..(java.lang.String, …))”)//匹配com.chenss.dao包下的任意接口和類的第一個參數爲String類型的方法
@Pointcut(“execution(* com.chenss.dao..(java.lang.String))”)//匹配com.chenss.dao包下的任意接口和類的只有一個參數,且參數爲String類型的方法
@Pointcut(“execution(* com.chenss.dao..(java.lang.String))”)//匹配com.chenss.dao包下的任意接口和類的只有一個參數,且參數爲String類型的方法
@Pointcut(“execution(public * (…))")//匹配任意的public方法
@Pointcut("execution( te*(…))”)//匹配任意的以te開頭的方法
@Pointcut(“execution(* com.chenss.dao.IndexDao.(…))")//匹配com.chenss.dao.IndexDao接口中任意的方法
@Pointcut("execution( com.chenss.dao….(…))”)//匹配com.chenss.dao包及其子包中任意的方法
- 返回值的位置寫
*
表示返回值類型是任意的; - 類名寫
*
表示任意的類; *
寫在參數位置只能表示一個參數;..
寫在參數位置,匹配任意個數和任意類型的參數
3. args
args表達式的作用是匹配指定參數類型和參數數量的方法,與包名和類型無關;
example:
@Pointcut(“args(java.io.Serializable)”)//匹配運行時傳遞的參數類型爲指定類型的、且參數個數和順序匹配
@Pointcut("@args(com.chenss.anno.Chenss)")//接受一個參數,並且傳遞的參數的運行時類型具有@Classified
4. @annotation註解方式
自定義一個註解,方法或類上有這個註解,就通知
@annotation(包名.註解名)
//表示方法上有@Time註解的就通知
@Around("@annotation(aspect.Time)")
5. this
指定一個類,如果某個類或接口是這個類的子類或者就是這個類本類,或者實現了這個接口,就通知;
6.target
指定一個類,如果某個類是這個類的目標類,就通知;
JDK代理和CGLIB代理
JDK代理的實現方式是基於接口實現,代理類繼承Proxy,實現指定接口;而CGLIB繼承被代理的類來實現。所以獲取一個代理類,如果是JDK代理則獲取到的是一個新類和目標類沒有關係;如果是CGLIB則獲取到的是目標類的子類或者目標接口的實現。
通知類型
- @Around 環繞通知 (功能最強大)
- @Before 前置通知:在目標方法調用之前通知
- @After 後置通知
- @AfterReturning 正常返回通知
- @AfterThrowing 異常返回通知
所有的修飾都是針對目標方法;
面向切面的應用
-
spring中的事務管理
代理對象中與@Transactional進行匹配,如果方法上有這個註解,就會調用事務通知,繼續調用目標方法TransactionIntercepter跟事務相關的通知代碼; -
統一的權限控制
-
緩存控制
-
統一的日誌記錄
spring中如果目標類沒有實現接口
如果目標對象實現了接口:底層spring會調用jdk的動態代理技術來生成代理;要求代理對象和目標對象實現共同的接口;
如果不使用接口,spring通過CGLIB代理技術來生成代理對象,並不要求目標對象和代理對象實現共同接口,底層代理類繼承自目標類;
基於XML的配置
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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd>
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--開啓註解掃描-->
<context:component-scan base-package="com.luban"/>
<!-- bean definitions here -->
</beans>
** 聲明切面**
<bean id="aspectJ" class="com.luban.app.LubanAspectJ"></bean>
<aop:config>
<aop:aspect id="aspect" ref="aspectJ"></aop:aspect>
</aop:config>
聲明切點
允許在多個切面和通知之間共享切點定義。
<aop:config>
<!--定義切面-->
<aop:aspect id="aspect" ref="aspectJ">
<!--定義一個切點-->
<aop:pointcut id="pointCut" expression="execution(* com.luban.dao.*.*(..))"/>
</aop:aspect>
</aop:config>
聲明通知
<aop:config>
<!--定義切面-->
<aop:aspect id="aspect" ref="aspectJ">
<!--定義一個切點-->
<aop:pointcut id="pointCut" expression="execution(* com.luban.dao.*.*(..))"/>
<!--定義一個通知-->
<aop:before method="before" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
具體通知方法在<bean id="aspectJ" class="com.luban.app.LubanAspectJ"></bean>
引用的類中實現;
public class LubanAspectJ {
public void pointCut(){
}
public void pointCut1(){
}
public void pointCut2(){
}
public void aroundM(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before");
joinPoint.proceed();
System.out.println("目標類"+joinPoint.getTarget());
System.out.println("代理類"+joinPoint.getThis());
System.out.println("around after");
}
public void before(){
System.out.println("before advance");
}
public void before1(){
System.out.println("args match");
}
}
擴展知識點
Introduction
代表類型聲明其他方法或字段。Spring AOP允許您向任何被建議的對象引入新的接口(以及相應的實現)。例如,可以使用Introduction使bean實現IsModified接口,以簡化緩存。(在AspectJ社區中,介紹稱爲類型間聲明。)
使用類型間聲明使Aspect能夠聲明被通知的對象實現了給定的接口,併爲這些對象提供了該接口的實現。
com.luban.dao.Dao
public interface Dao {
void test();
}
com.luban.dao.IndexDao
@Repository
public class IndexDao implements Dao{
public void test() {
System.out.println("hello world");
}
}
在com.luban.dao下定義一個空類,並交由spring去管理
@Repository("introDao")
public class IntroDao {
}
在Aspect切面類中使用Introduction
@DeclareParents(value = "com.luban.dao.*",defaultImpl = IndexDao.class)
public static Dao dao;
意思是找到com.luban.dao
下面的所有類,讓這些類去引入Dao
接口的IndexDao
類的實現。
測試:
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext a =
new AnnotationConfigApplicationContext(Appconfig.class);
Dao introDao = (Dao) a.getBean("introDao");
introDao.test();
}
}
運行結果:
\
環繞通知Around案例
環繞通知意思就是在目標方法前執行一次,在目標方法後執行一次。
@Pointcut("execution(* com.luban.dao.*.*(..))")
public void pointCut2(){
}
@Around("pointCut2()")
public void aroundM(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around before");
joinPoint.proceed();
System.out.println("around after");
}
其中proceed表示繼續執行下一個通知或目標方法的調用,可以理解爲調用執行底層方法可以理解爲執行了目標類裏的方法。
可以通過ProceedingJoinPoint
訪問到代理類、目標類等信息,其父類JoinPoint
也可以訪問到,但是父類沒有提供proceed。提供對連接點可用狀態和關於連接點的靜態信息的反射訪問。這些信息可以從使用特殊形式This joinpoint的advance正文中獲得。這種反射信息的主要用途是跟蹤和記錄應用程序。
sping AOP和Full AspectJ使用分析
使用最簡單的方法。Spring AOP比使用完整的AspectJ更簡單,因爲不需要在開發和構建過程中引入AspectJ編譯器/編織器。如果您只需要建議在Spring bean上執行操作,那麼Spring AOP是正確的選擇。如果需要通知Spring容器不管理的對象(通常是域對象),則需要使用AspectJ。如果希望建議連接點而不是簡單的方法執行(例如,字段get或set連接點等等),還需要使用AspectJ。
@AspectJ註解和XML的使用分析
現有Spring用戶可能最熟悉XML樣式,它由真正的pojo支持。當使用AOP作爲配置企業服務的工具時,XML可能是一個很好的選擇(一個好的測試是,您是否將切入點表達式視爲您可能希望獨立更改的配置的一部分)。使用XML樣式,從配置中可以更清楚地看出系統中存在哪些方面。
XML樣式有兩個缺點。首先,它沒有將它所處理的需求的實現完全封裝在一個地方。DRY原則認爲,系統中的任何知識都應該有一個單一的、明確的、權威的表示。當使用XML樣式時,關於如何實現需求的知識會在支持bean類的聲明和配置文件中的XML之間進行分割。當您使用@AspectJ樣式時,這些信息被封裝在一個模塊中:方面。其次,XML樣式稍微有些限制。
@AspectJ樣式支持額外的實例化模型和更豐富的切入點組合。它的優點是保持方面作爲模塊單元。它還有一個優點,即@AspectJ方面可以被Spring AOP和AspectJ理解(從而使用)。因此,如果您稍後決定需要AspectJ的功能來實現額外的需求,您可以輕鬆地遷移到一個經典的AspectJ設置。總的來說,除了簡單的企業服務配置之外,Spring團隊更喜歡使用@AspectJ樣式來定製方面。也可以混合兩種方式使用。