Spring實現AOP的多種方式
AOP(Aspect Oriented Programming)面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的橫向多模塊統一控制的一種技術。AOP是OOP的補充,是Spring框架中的一個重要內容。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。AOP可以分為靜態織入與動態織入,靜態織入即在編譯前將需織入內容寫入目標模塊中,這樣成本非常高。動態織入則不需要改變目標模塊。Spring框架實現了AOP,使用註解配置完成AOP比使用XML配置要更加方便與直觀。上一篇隨筆中已經詳細講了代理模式。
一、基於XML配置的Spring AOP
在講註解實現AOP功能前先用前面學習過的使用xml配置Spring AOP功能,這樣是為了對比以便更好的理解。
1.1、新建一個Maven項目,添加引用,項目的pom.xml文件如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zhangguo</groupId>
<artifactId>Spring052</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Spring052</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>4.3.0.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<version>4.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.4</version>
</dependency>
</dependencies>
</project>
1.2、創建要被代理的Math類,代碼如下:
package com.zhangguo.Spring052.aop01;
/**
* 被代理的目標類
*/
public class Math{
//加
public int add(int n1,int n2){
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
return result;
}
//減
public int sub(int n1,int n2){
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
return result;
}
//乘
public int mut(int n1,int n2){
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
return result;
}
//除
public int div(int n1,int n2){
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
return result;
}
}
1.3、編輯AOP中需要使用到的通知類Advices.java代碼如下:
package com.zhangguo.Spring052.aop01;
import org.aspectj.lang.JoinPoint;
/**
* 通知類,橫切邏輯
*
*/
public class Advices {
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
}
1.4、配置容器初始化時需要的XML文件,aop01.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:p="http://www.springframework.org/schema/p"
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/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 被代理對象 -->
<bean id="math" class="com.zhangguo.Spring052.aop01.Math"></bean>
<!-- 通知 -->
<bean id="advices" class="com.zhangguo.Spring052.aop01.Advices"></bean>
<!-- aop配置 -->
<aop:config proxy-target-class="true">
<!--切面 -->
<aop:aspect ref="advices">
<!-- 切點 -->
<aop:pointcut expression="execution(* com.zhangguo.Spring052.aop01.Math.*(..))" id="pointcut1"/>
<!--連接通知方法與切點 -->
<aop:before method="before" pointcut-ref="pointcut1"/>
<aop:after method="after" pointcut-ref="pointcut1"/>
</aop:aspect>
</aop:config>
</beans>
1.5、測試代碼Test.java如下:
package com.zhangguo.Spring052.aop01;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("aop01.xml");
Math math = ctx.getBean("math", Math.class);
int n1 = 100, n2 = 5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
}
運行結果:
二、使用註解配置AOP
2.1、在上一個示例中修改被代理的類Math,為了實現IOC掃描在Math類上註解了@Service並命名bean為math。相當於上一個示例中在xml配置文件中增加了一個bean,<!-- 被代理對象 --><bean id="math" class="com.zhangguo.Spring052.aop01.Math"></bean>,Math類的代碼如下:
package com.zhangguo.Spring052.aop02;
import org.springframework.stereotype.Service;
/**
* 被代理的目標類
*/
@Service("math")
public class Math{
//加
public int add(int n1,int n2){
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
return result;
}
//減
public int sub(int n1,int n2){
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
return result;
}
//乘
public int mut(int n1,int n2){
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
return result;
}
//除
public int div(int n1,int n2){
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
return result;
}
}
2.2、修改通知類Advices,代碼中有3個註解,@Component表示該類的實例會被Spring IOC容器管理;@Aspect表示聲明一個切面;@Before表示before為前置通知,通過參數execution聲明一個切點,Advices.java代碼如下所示:
package com.zhangguo.Spring052.aop02;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 通知類,橫切邏輯
*
*/
@Component
@Aspect
public class Advices {
@Before("execution(* com.zhangguo.Spring052.aop02.Math.*(..))")
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
@After("execution(* com.zhangguo.Spring052.aop02.Math.*(..))")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
}
上面的代碼與下面的配置基本等同
<!-- 通知 -->
<bean id="advices" class="com.zhangguo.Spring052.aop01.Advices"></bean>
<!-- aop配置 -->
<aop:config proxy-target-class="true">
<!--切面 -->
<aop:aspect ref="advices">
<!-- 切點 -->
<aop:pointcut expression="execution(* com.zhangguo.Spring052.aop01.Math.*(..))" id="pointcut1"/>
<!--連接通知方法與切點 -->
<aop:before method="before" pointcut-ref="pointcut1"/>
<aop:after method="after" pointcut-ref="pointcut1"/>
</aop:aspect>
</aop:config>
2.3、新增配置文件aop02.xml,在配置IOC的基礎上增加了aop:aspectj-autoproxy節點,Spring框架會自動為與AspectJ切面配置的Bean創建代理,proxy-target-class="true"屬性表示被代理的目標對象是一個類,而非實現了接口的類,主要是為了選擇不同的代理方式。
<?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:p="http://www.springframework.org/schema/p"
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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<context:component-scan base-package="com.zhangguo.Spring052.aop02">
</context:component-scan>
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
</beans>
2.4、測試運行代碼Test.java如下:
package com.zhangguo.Spring052.aop02;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("aop02.xml");
Math math = ctx.getBean("math", Math.class);
int n1 = 100, n2 = 5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
}
運行結果:
三、AspectJ切點函數
切點函數可以定位到準確的橫切邏輯位置,在前面的示例中我們只使用過execution(* com.zhangguo.Spring052.aop02.Math.*(..)),execution就是一個切點函數,但該函數只什麽方法一級,如果我們要織入的範圍是類或某個註解則execution就不那麽好用了,其實一共有9個切點函數,有不同的針對性。
@AspectJ使用AspectJ專門的切點表達式描述切面,Spring所支持的AspectJ表達式可分為四類:
方法切點函數:通過描述目標類方法信息定義連接點。
方法參數切點函數:通過描述目標類方法入參信息定義連接點。
目標類切點函數:通過描述目標類類型信息定義連接點。
代理類切點函數:通過描述代理類信息定義連接點。
常見的AspectJ表達式函數:
execution():滿足匹配模式字符串的所有目標類方法的連接點
@annotation():任何標註了指定註解的目標方法鏈接點
args():目標類方法運行時參數的類型指定連接點
@args():目標類方法參數中是否有指定特定註解的連接點
within():匹配指定的包的所有連接點
target():匹配指定目標類的所有方法
@within():匹配目標對象擁有指定註解的類的所有方法
@target():匹配當前目標對象類型的執行方法,其中目標對象持有指定的註解
this():匹配當前AOP代理對象類型的所有執行方法
最常用的是:execution(<修飾符模式>?<返回類型模式><方法名模式>(<參數模式>)<異常模式>?)切點函數,可以滿足多數需求。
為了展示各切點函數的功能現在新增一個類StrUtil,類如下:
package com.zhangguo.Spring052.aop03;
import org.springframework.stereotype.Component;
@Component("strUtil")
public class StrUtil {
public void show(){
System.out.println("Hello StrUtil!");
}
}
測試代碼如下:
package com.zhangguo.Spring052.aop03;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("aop03.xml");
IMath math = ctx.getBean("math", Math.class);
int n1 = 100, n2 = 5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
StrUtil strUtil=ctx.getBean("strUtil",StrUtil.class);
strUtil.show();
}
}
3.1、切點函數execution,通知與切面的定義如下:
package com.zhangguo.Spring052.aop03;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 通知類,橫切邏輯
*
*/
@Component
@Aspect
public class Advices {
@Before("execution(* com.zhangguo.Spring052.aop03.Math.*(..))")
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
//execution切點函數
//com.zhangguo.Spring052.aop03包下所有類的所有方法被切入
@After("execution(* com.zhangguo.Spring052.aop03.*.*(..))")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
}
運行結果如下:
execution(<修飾符模式>?<返回類型模式><方法名模式>(<參數模式>)<異常模式>?)
3.2、切點函數within
//within切點函數
//com.zhangguo.Spring052.aop03包下所有類的所有方法被切入
@After("within(com.zhangguo.Spring052.aop03.*)")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
3.3、this切點函數
//this切點函數
//實現了IMath接口的代理對象的任意連接點
@After("this(com.zhangguo.Spring052.aop03.IMath)")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
3.4、args切點函數
//args切點函數
//要求方法有兩個int類型的參考才會被織入橫切邏輯
@After("args(int,int)")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
如果參數類型不是基本數據類型則需要包名。
3.5、@annotation切點函數
先自定義一個可以註解在方法上的註解
package com.zhangguo.Spring052.aop03;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnno {
}
//@annotation切點函數
//要求方法必須被註解com.zhangguo.Spring052.aop03.MyAnno才會被織入橫切邏輯
@After("@annotation(com.zhangguo.Spring052.aop03.MyAnno)")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
package com.zhangguo.Spring052.aop03;
import org.springframework.stereotype.Component;
@Component("strUtil")
public class StrUtil {
@MyAnno
public void show(){
System.out.println("Hello StrUtil!");
}
}
運行結果:
其它帶@的切點函數都是針對註解的
四、AspectJ通知註解
AspectJ通知註解共有6個,常用5個,引介少用一些。
先解決定義切點復用的問題,如下代碼所示,切點函數的內容完全一樣:
package com.zhangguo.Spring052.aop04;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 通知類,橫切邏輯
*
*/
@Component
@Aspect
public class Advices {
@Before("execution(* com.zhangguo.Spring052.aop04.Math.*(..))")
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
@After("execution(* com.zhangguo.Spring052.aop04.Math.*(..))")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
}
可以先定義一個切點然後復用,如下所示:
package com.zhangguo.Spring052.aop04;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 通知類,橫切邏輯
*/
@Component
@Aspect
public class Advices {
//切點
@Pointcut("execution(* com.zhangguo.Spring052.aop04.Math.*(..))")
public void pointcut(){
}
@Before("pointcut()")
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
@After("pointcut()")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
}
修改Advices.java文件,增加各種通知類型如下:
package com.zhangguo.Spring052.aop04;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 通知類,橫切邏輯
*/
@Component
@Aspect
public class Advices {
//切點
@Pointcut("execution(* com.zhangguo.Spring052.aop04.Math.a*(..))")
public void pointcut(){
}
//前置通知
@Before("pointcut()")
public void before(JoinPoint jp){
System.out.println(jp.getSignature().getName());
System.out.println("----------前置通知----------");
}
//最終通知
@After("pointcut()")
public void after(JoinPoint jp){
System.out.println("----------最終通知----------");
}
//環繞通知
@Around("execution(* com.zhangguo.Spring052.aop04.Math.s*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println(pjp.getSignature().getName());
System.out.println("----------環繞前置----------");
Object result=pjp.proceed();
System.out.println("----------環繞後置----------");
return result;
}
//返回結果通知
@AfterReturning(pointcut="execution(* com.zhangguo.Spring052.aop04.Math.m*(..))",returning="result")
public void afterReturning(JoinPoint jp,Object result){
System.out.println(jp.getSignature().getName());
System.out.println("結果是:"+result);
System.out.println("----------返回結果----------");
}
//異常後通知
@AfterThrowing(pointcut="execution(* com.zhangguo.Spring052.aop04.Math.d*(..))",throwing="exp")
public void afterThrowing(JoinPoint jp,Exception exp){
System.out.println(jp.getSignature().getName());
System.out.println("異常消息:"+exp.getMessage());
System.out.println("----------異常通知----------");
}
}
運行結果:
五、零配置實現Spring IoC與AOP
為了實現零配置在原有示例的基礎上我們新增一個類User,如下所示:
package com.zhangguo.Spring052.aop05;
public class User {
public void show(){
System.out.println("一個用戶對象");
}
}
該類並未註解,容器不會自動管理。因為沒有xml配置文件,則使用一個作為配置信息,ApplicationCfg.java文件如下:
package com.zhangguo.Spring052.aop05;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration //用於表示當前類為容器的配置類,類似<beans/>
@ComponentScan(basePackages="com.zhangguo.Spring052.aop05") //掃描的範圍,相當於xml配置的結點<context:component-scan/>
@EnableAspectJAutoProxy(proxyTargetClass=true) //自動代理,相當於<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
public class ApplicationCfg {
//在配置中聲明一個bean,相當於<bean id=getUser class="com.zhangguo.Spring052.aop05.User"/>
@Bean
public User getUser(){
return new User();
}
}
該類的每一部分內容基本都與xml 配置有一對一的關系,請看註釋,這樣做要比寫xml方便,但不便發布後修改。測試代碼如下:
package com.zhangguo.Spring052.aop05;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
// 通過類初始化容器
ApplicationContext ctx = new AnnotationConfigApplicationContext(ApplicationCfg.class);
Math math = ctx.getBean("math", Math.class);
int n1 = 100, n2 = 0;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
try {
math.div(n1, n2);
} catch (Exception e) {
}
User user=ctx.getBean("getUser",User.class);
user.show();
}
}
advices.java 同上,沒有任何變化,運行結果如下:
Spring實現AOP的多種方式