Spring AOP 之 Aspect

之前我用了很多篇幅去介紹AOP的,現在我們使用一個最爲常用的AOP使用方式,使用基於AspectJ的表達式進行定義切面,我們採用兩種方式一種是通過annotation的另外一種就是通過XML進行配置的方式,在AspectJ中是使用annotation的方式進行使用的,所以我們首先會介紹一下如何去使用annotation去完成我們的代理功能。


在我們使用@AspectJ切面的前提是,我們在配置文件中啓動aspectJ的自動代理:

<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/beans/spring-aop.xsd">


    <!-- 業務service -->
    <bean id="userService" class="com.maxfunner.service.impl.UserService" />
    <bean id="adminService" class="com.maxfunner.service.impl.AdminService" />
   <context:component-scan base-package="com.maxfunner"  />
<!-- 基於@AspectJ切面的驅動器 --> <aop:aspectj-autoproxy proxy-target-class="true" /></beans>

其中asp:aspectj-autoproxy標籤實際上使用 org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator 進行代理自動創建的,所以如果不想引入aop命名空間 直接 使用 <bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" /> 也是等價的。當然我們這裏也可以使用 proxy-target-class="true" 這個也不用多說了,就是使用CGlib直接對模板對象生成虛擬子類進行代理。

然後我們創建一個類並使用@Aspect進行定義切點和增強(注意我們在@Before中的參數 execution 是比較常用的切點描述方法,後面會有更加詳細的講解):

@Component
@Aspect
public class AspectExample {

    @Before("execution(* login(..))")
    public void begin(){
        System.out.println(">>>>>>>>>>>>begin login");
    }

}

然後我們使用正常獲取bean的方式進行調用,發現AspectExample.begin方法已經織入到userService中的login方法當中

public static void main(String[] args){
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = (UserService) context.getBean("userService");
    userService.login("TONY","PWD");
}

剛剛我們可以發現我們在@Before裏面使用了execution,這個是屬於切點表達式函數的一種,而且我們在execution裏面使用瞭如.. *等這些通配符,當然aspectJ的切點表達式函數遠遠不止這些,還有以下這些都屬於aspectJ的切點表達式函數,在本文後面會逐一舉例說明(包括有:execution,@annotation,args,@args,within,target,@within,@target,this),目前先介紹表達式的通配符。

@AspectJ支持3種通配符:

* 匹配任意字符,但是隻能匹配一個元素,例如一個類、一個參數、一個方法。

.. 匹配任意字符,可以匹配任意多個元素。表示類的時候,必須與*聯合使用,但是在表示方法入參的時候可以表示多個參數而不需要聯合*一起使用。

+ 表示類以下的所有子類,所以這個必須聯合類來使用,而且必須定義在類名的後面,例如:UserService+


剛剛我們使用了@Before 增強,然而Aspect提供了一下的的增強:


@Before : 前置增強,相當於BeforeAdvice,其annotation成員有:

value - > 定義切點

argNames - > 由於無法通過Java反射獲得方法的入參名稱,所以如果在Java編譯未啓動調試信息或者需要在運行解析切點,就必須通過這個成員指定所標註的增強方法的參數名。


@AfterReturning : 後置增強,相當於AfterReturningAdvice,其annotation成員如下:

pointcut - >表示切點信息,如果定義了pointcut會覆蓋value的屬性,和value功能一樣

value - > 定義切點

argNames - > 由於無法通過Java反射獲得方法的入參名稱,所以如果在Java編譯未啓動調試信息或者需要在運行解析切點,就必須通過這個成員指定所標註的增強方法的參數名。


@Around 環繞增強,相當於MethodInterceptor,Around註解類擁有兩個成員

value - > 定義切點

argNames - > 由於無法通過Java反射獲得方法的入參名稱,所以如果在Java編譯未啓動調試信息或者需要在運行解析切點,就必須通過這個成員指定所標註的增強方法的參數名。


@After Final增強,這個增強比較提別,沒有一個相對應之前用過的增強類,@After 可以看做 ThrowsAdvice和 AfterReturningAdvice的混合,就是說無論正常推出還是拋出異常,這個增強方法都會被執行,一般用於釋放資源,相當於tyr{}finally{}的控制流。@Afer註解用於成員如下:

value - > 定義切點

argNames - > 由於無法通過Java反射獲得方法的入參名稱,所以如果在Java編譯未啓動調試信息或者需要在運行解析切點,就必須通過這個成員指定所標註的增強方法的參數名。


@DecareParents 引介增強,相當於IntroductionInterceptor, @DecareParents 註解類擁有兩個成員:

value - > 定義切點

defaultImpl -> 默認的接口實現


由於DecareParents跟其他的增強使用方式不一樣,所以以下會有相應的解釋

@DecareParents 引介增強的使用:

同樣我們使用UserService類,同時我們新建一個AccountSecurity的接口,然後提供一個resetPassword()的方法,同時我們新建AccountSecurityImpl類,同時通過@DecareParents 讓UserService的proxy類implement於AccountSecurity接口,同時使用AccountSecurityImpl作爲resetPassword等方法的實現:


以下是AccountSecurity 和 AccountSecurityImpl

public interface AccountSecurity {

    void resetPassword(String account,String newPassword);
}

public class AccountSecurityImpl implements AccountSecurity {

    public void resetPassword(String account, String newPassword) {
        if(newPassword == null || newPassword.length() < 6){
            System.out.println("accout : " + account + " , reset password fail.");
            return;
        }
        System.out.println("account : " + account + " , reset password success.");
    }

}

創建一個Aspect:

public class AccountSecurityAspect {
    
    @DeclareParents(value = "com.maxfunner.service.impl.UserService",defaultImpl = AccountSecurityImpl.class)
    public AccountSecurity accountSecurity;  //這個是目標接口,defaultImpl是目標實現

}

@Aspect
@Component
public class AccountSecurityAspect {

    @DeclareParents(value = "com.maxfunner.service.impl.UserService",defaultImpl = AccountSecurityImpl.class)
    public AccountSecurity accountSecurity;  //這個是目標接口,defaultImpl是目標實現

}

配置文件和上文貼出的代碼一直,然後我們嘗試調用UserService的代理:

public static void main(String[] args){

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = (UserService) context.getBean("userService");
    userService.login("TONY","PWD");
    AccountSecurity accountSecurity = (AccountSecurity) userService;
    accountSecurity.resetPassword("TONY","NEW_PWD");

}

輸出如下:

Login : TONY, pwd : PWD
account : TONY , reset password success.



以上通配符和Aspect的增強annotation描述 摘錄於《Spring 3.x 企業應用開發實戰》。


首先我們先了解如何使用aspectJ的切點表達式函數,下面將會一一介紹:


@annotation() 切面表達式函數

函數中的參數爲一個annotation類,定義了該annotation的類方法都會進行織入。下面我們將會自定義一個annotation類名爲MyProxy,然後分別在AdminService 和 UserService兩個類中標記這個@MyProxy,最後測試效果:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyProxy {
}

把這個@MyProxy 標記的AdminService.login方法 和 UserService.register方法中:

public class AdminService {

    @MyProxy
    public void login(String username,String password){
        System.out.println("Admin Login : " + username + ", pwd : " + password);
    }

    public void register(String username,String password){
        System.out.println("Admin register : " + username + " , pwd " + password);
    }

}

public class UserService {

    public void login(String username,String password){
        System.out.println("Login : " + username + ", pwd : " + password);
    }

    @MyProxy
    public void register(String username,String password){
        System.out.println("register : " + username + " , pwd " + password);
    }

}

我也做個一個測試,嘗試將@MyProxy直接標記在類中,但是並沒有進行織入,說明這個@annotation的表達式函數只對方法有效,有興趣的也可以測試一下,當然在定義 MyProxy annotation類的時候記得要在@target添加ElementType.Type,這個是annotation的知識我就不多說了,有興趣可以百度一下annotation的相關知識。

然後我們定義Aspect類(其實我也不知道應該如何稱呼這種類,先稱它爲aspect類吧)

@Component
@Aspect
public class AnnotationAspect {

    @AfterReturning("@annotation(com.maxfunner.annotation.MyProxy)")
    public void afterReturning(){
        System.out.println("afterReturning annocation test !");
    }

}
注意我們使用的增強是@AfterReturning 所以是在方法執行之後調用。

現在我們在main方法測試並輸出結果:

public static void main(String[] args){

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = (UserService) context.getBean("userService");
    AdminService adminService = (AdminService) context.getBean("adminService");

    userService.login("TONY","PWD");
    userService.register("TONY","PWD");
    adminService.login("TONY","PWD");
    adminService.register("TONY","PWD");

}
輸出結果中可以看到,只有標記了@MyProxy的類方法纔會被織入:

Login : TONY, pwd : PWD
register : TONY , pwd PWD
afterReturning annocation test !
Admin Login : TONY, pwd : PWD
afterReturning annocation test !
Admin register : TONY , pwd PWD


最後我貼出一下本文所有實驗所用的不變配置,之後的本文的實驗將不會再貼出applicationContext.xml的配置文件:

<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: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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">


    <!-- 業務service -->
    <bean id="userService" class="com.maxfunner.service.impl.UserService" />
    <bean id="adminService" class="com.maxfunner.service.impl.AdminService" />


    <context:component-scan base-package="com.maxfunner" />

    <!-- 基於@AspectJ切面的驅動器 -->
    <aop:aspectj-autoproxy proxy-target-class="true" />


</beans>


execution() 切面表達式函數

execution 切面表達式函數是最爲常用的表達式函數,語法如下:

execution(<修飾符模式[可選]>  <返回類型>  <方法名>(<參數>)),以下將列出幾個範例:

1、execution(public * *(..)) :定義一個方法修飾符爲public的  不限制返回類型 不限制方法名 不限制參數的 方法。

2、execution(* *Service(..) ):定義不限制修飾符(因爲例子中忽略沒有寫) 不限方法的返回類型 方法名以Service結尾 不限參數的 所有方法。

3、execution(* com.tony.service.UserService.*(..)) : 定義com.tony.service.UserService類下的所有方法

4、execution(* com.tony.service.UserService+.*(..)) : 定義com.tony.service.UserService類以及其子類下的所有方法

5、execution(* con.tony.service..*(..)): 定義com.tony.service包下以及其子孫包的所有類的所有方法被織入

6、execution(* com.tony.service.*(..)): 定義com.tony.service包下的所有類的所有方法被織入,但不包括其子孫包的類。

7、execution(* login(String,String)): 定義方法名爲login 其參數爲兩個String類型的方法被織入。

8、execution(* getUserId(String,*)):定義方法名爲getUserId 其方法參數有兩個一個是String 另外一個爲任意類型的 方法被織入。

9、execution(* resetPassword(String,..)):定義方法名爲resetPassword 其方法參數有多個第一個參數爲String 其餘的參數任何類型 的方法被織入。

10、execution(* saveUserInDB(User+)):定義方法名爲saveUserInDB 其方法參數是一個User對象獲取是User派生的對象。


arg() 切面表達式函數

args接受一個類名,表示目標方法的入參類型爲目標類型或者目標類型的派生類型:

@Component
@Aspect
public class ArgsAspect {

    @After("args(com.maxfunner.entity.User)")
    public void after(){
        System.out.println(">>>>>>Args Aspect test!!!");
    }

}
以上代碼 匹配所有參數只有一個User的方法,如果想匹配方法中第一個參數爲User 其他參數不限制可以同樣式樣aspectJ提供的通配符:

@Component
@Aspect
public class ArgsAspect {

    @After("args(com.maxfunner.entity.User,..)")
    public void after(){
        System.out.println(">>>>>>Args Aspect test!!!");
    }

}

@arg() 切面表達式函數

@arg 接受annotation類,匹配方法中的標記了目標annotation的類,下面是一個範例:

@Component
@Aspect
public class ArgsAspect {

    @After("@args(com.maxfunner.annotation.MyProxy,..)")
    public void after(){
        System.out.println(">>>>>>@Args Aspect test!!!");
    }

}

使用MyProxy標記User類

@MyProxy
public class User implements BeanFactoryAware,BeanNameAware,InitializingBean,DisposableBean {

以下方法會匹配:

public void testA(User user){
    System.out.println("testA");
}

public void testB(User user,String username){
    System.out.println("testB");
}


within() 切面表達式函數

within和execution差不多,但是within最小範圍匹配到類,而execution是匹配到方法,所以說execution的功能基本涵蓋within。以下是一些例子:

1、within(com.tony.service.*)匹配com.tony.service包一下的所有類

2、within(com.tony.service.UserService) 精確匹配UserService類

基本上execution能用的通配符within都能用。


@within()和@target 切面表達式函數

@within和@target都是傳入一個annotation的類,@within是標記的目標annotation的類的派生類也會匹配,但是@target只匹配標記了目標annotation的類,這裏就不再描述了。


target()和this() 切面表達式函數

target和this都接受一個類爲參數,但是target指定目標類的所有方法織入,但是this會連目標類的代理類方法也會織入,等於說如果我們使用this的話目標類如果有一個引介增強的話,會連同引介增強的方法也會被織入。


@AspectJ支持邏輯運算符

&&  用於和另外一個切點表達式函數 形成交集組合切面,後面會有相應的例子。

|| 用於和另外一個切點表達式函數 形成並集組合切面。

! 代表除了不匹配當前切點表達式函數 匹配所有。

使用邏輯於運算符 實現 切點複合運算

@Component
@Aspect
public class ArgsAspect {

    @After("@args(com.maxfunner.annotation.MyProxy,..) && execution(* com.maxfunner.service.*(..))")
    public void after(){
        System.out.println(">>>>>>@Args Aspect test!!!");
    }

}

其他運算符同理,但是後面會我們說到使用xml配置的方式去定義切點,所以&& || 這些符號 spring提供了其他的表達,分別是 &&的替代and  ||的替代or  !的替代not 


命名切點

我們可以將切點分開命名,這個比較難說明,我們用一個例子舉例就明白了,使用@Pointcut

@Aspect
public class MyPointcut {

    @Pointcut("execution(* com.maxfunner.service..*(..))")
    public void allServicemMethod(){}

    @Pointcut("args(com.maxfunner.entity.User+,..)")
    public void userArgPointcut(){}

}

然後我們在Aspect類中使用pointcut

@Component
@Aspect
public class AspectExample {

    @Before("com.maxfunner.pointcut.MyPointcut.allServicemMethod() " +
            "&& com.maxfunner.pointcut.MyPointcut.userArgPointcut()")
    public void begin(){
        System.out.println(">>>>>>>>>>>>begin");
    }

}

最後被注入的方法是以下的調用方法:

public static void main(String[] args){

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = (UserService) context.getBean("userService");
    userService.testA(new User());

}

JointPoint連接點信息:

有兩個主要的類一個是JoinPoint 另外一個是 ProceedingJoinPoint, 其中ProceedingJoinPoint 是應用於@Around增強的,因爲他有兩個執行目標方法的接口,proceed() 和可以改變其參數的proceed(Object[] args)

JointPoint的主要方法有:

Object[] getArgs() : 獲得目標方法的參數

Signature getSignature() : 獲得連接點的方法簽名對象

getTarget() : 獲得連接點所在的目標對象

getThis() : 獲得連接點代理對象本身

注意:JointPoint可以應用到所有的增強方法,在要添加其參數在增強方法即可。


ProceedingJoinPoint 繼承了JoinPoint子接口,上面也有說過比JointPoint多了兩個執行方法分別是:

Object proceed() throws Throwable

Object proceed(Object[] args) throws Throwable


以下使用環繞增強進行演示:

@Component
@Aspect
public class AroundAspect {


    @Around("execution(* com.maxfunner.service..*(..))")
    public void LogProxy(ProceedingJoinPoint joinPoint) {
        System.out.println("class : " + joinPoint.getSignature().getDeclaringTypeName());
        System.out.println("method : " + joinPoint.getSignature().getName());
        System.out.println("args : " + getArgsStr(joinPoint.getArgs()));
        Object result = null;
        try {
            result = joinPoint.proceed();
            System.out.println("result : " + result);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    public String getArgsStr(Object[] args) {
        StringBuffer stringBuffer = new StringBuffer();
        for (Object item : args) {
            stringBuffer.append(item.toString() + " ");
        }
        return stringBuffer.toString();
    }

}

main方法調用已經輸出結果:

public static void main(String[] args){

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = (UserService) context.getBean("userService");
    userService.login("TONY","PWD");

}
class : com.maxfunner.service.impl.UserService
method : login
args : TONY PWD 
Login : TONY, pwd : PWD
result : null


綁定連接點方法入參:

除了使用JointPoint我們還可以使用綁定連點方法入參數的方式獲得參數,下面使用一個例子進行演示:

@Component
@Aspect
public class AspectExample {

    //args的順序和目標方法一致
    @Before(value = "execution(* *(String,String)) && args(name,pwd)")
    //增強方法的入參的方法可以和目標的方法不一致,但是名字必須要和args定義的一致
    public void begin(String pwd,String name){
        System.out.println(">>>>>>>>>>>>proxy name : " + name + " , pwd : " + pwd);
    }

}

main方法代碼已經輸出結果:

public static void main(String[] args){

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = (UserService) context.getBean("userService");
    userService.login("TONY","PWD");

}
>>>>>>>>>>>>proxy name : TONY , pwd : PWD
Login : TONY, pwd : PWD


通過this()或target綁定目標代理對象

使用方法和上面的arg()綁定參數一致

@Component
@Aspect
public class AspectExample {

    //args的順序和目標方法一致
    @Before(value = "execution(* *(String,String)) && this(proxyObj)")
    //增強方法的入參的方法可以和目標的方法不一致,但是名字必須要和args定義的一致
    public void begin(Object proxyObj){
        System.out.println(">>>>>>>>>>>>proxy name : " + proxyObj.getClass());
    }

}

註解對象綁定

@Component
@Aspect
public class AspectExample {

    //args的順序和目標方法一致
    @Before(value = "@within(proxy)")
    //增強方法的入參的方法可以和目標的方法不一致,但是名字必須要和args定義的一致
    public void begin(MyProxy proxy){
        System.out.println(">>>>>>>>>>>>proxy name : " + proxy.getClass());
    }

}
使用MyProxy標記UserService類並運行可見已經成功綁定

@MyProxy
public class UserService {
輸出結果:

>>>>>>>>>>>>proxy name : class com.sun.proxy.$Proxy4
Login : TONY, pwd : PWD


返回值綁定

@Component
@Aspect
public class AnnotationAspect {

    @AfterReturning(value = "@within(com.maxfunner.annotation.MyProxy)",returning = "result")
    public void afterReturning(Object result){
        System.out.println("afterReturning result : " + result);
    }

}

然後我們嘗試調用UserService 中的getUserLevel的方法

@MyProxy
public class UserService {

    public String getUserLevel(String userId){
        return "LEVEL 10";
    }

}
public static void main(String[] args){

    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = (UserService) context.getBean("userService");
    userService.getUserLevel("ABC");

}

輸出結果:

afterReturning result : LEVEL 10


拋出的異常綁定

@Component
@Aspect
public class ThrowingAspect {

    @AfterThrowing(value = "execution(* com.maxfunner..*(..))",throwing = "e")
    public void throwingTest(Exception e){
        e.printStackTrace();
    }

}





















發佈了53 篇原創文章 · 獲贊 65 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章