【JavaEE】面向切面編程

面向切面編程

一切從Spring AOP的底層技術——動態代理開始

一個簡單的約定遊戲

約定規則

首先提供一個Interceptor接口,定義如下:

public interface Interceptor {

	public void before(Object obj);

	public void after(Object obj);

	public void afterReturning(Object obj);

	public void afterThrowing(Object obj);
}

這是一個攔截接口,可以對它創建實現類。代碼如下:

public class ProxyBeanFactory {
   public static <T> T getBean(T  obj, Interceptor interceptor) {
        return (T) ProxyBeanUtil.getBean(obj, interceptor);
    }
}

具體類ProxyBeanUtil的getBean方法的邏輯不需要去理會,當使用了這個方法後,存在如下約定,即當一個對象通過ProxyBeanFactory的getBean方法定義後,擁有這樣的約定:

  • Bean必須是一個實現了某一個接口的對象
  • 最先會執行攔截器的before方法
  • 其次執行Bean的方法(通過反射的形式)
  • 執行Bean方法時,無論是否產生異常,都會執行after方法
  • 執行Bean方法時,如果不產生異常,則執行afterRunning方法;如果產生異常,則執行afterThrowing方法。

自己的代碼

比如打印一個角色信息,由於約定服務對象必須實現接口,於是自定義一個RoleService接口,代碼如下:

import com.ssm.chapter11.game.pojo.Role;

public interface RoleService {
   
   public void printRole(Role role);
}

然後編寫它的實現類,代碼如下:

import com.ssm.chapter11.game.pojo.Role;
import com.ssm.chapter11.game.service.RoleService;

public class RoleServiceImpl implements RoleService {
   @Override
   public void printRole(Role role) {
      System.out.println(
            "{id =" + role.getId() + ", roleName=" + role.getRoleName() + ", note=" + role.getNote() + "}");
   }
}

這裏還欠缺一個攔截器,代碼如下:

import com.ssm.chapter11.game.Interceptor;

public class RoleInterceptor implements Interceptor {
   @Override
    public void before(Object obj) {
        System.out.println(
           "準備打印角色信息");
    }

    @Override
    public void after(Object obj) {
        System.out.println(
           "已經完成角色信息的打印處理");
    }

    @Override
    public void afterReturning(Object obj) {
         System.out.println(
             "剛剛完成打印功能,一切正常。");
    }

    @Override
    public void afterThrowing(Object obj) {
        System.out.println(
            "打印功能執行異常了,查看一下角色對象爲空了嗎?");
    }
}

測試代碼如下:

import com.ssm.chapter11.game.interceptor.RoleInterceptor;
import com.ssm.chapter11.game.pojo.Role;
import com.ssm.chapter11.game.service.RoleService;
import com.ssm.chapter11.game.service.impl.RoleServiceImpl;

public class GameMain {
   public static void main(String[] args) {
      RoleService roleService = new RoleServiceImpl();
      Interceptor interceptor = new RoleInterceptor();
      RoleService proxy = ProxyBeanFactory.getBean(roleService, interceptor);
      Role role = new Role(1L, "role_name_1", "role_note_1");
      proxy.printRole(role);
      System.out.println("##############測試afterthrowing方法###############");
      role = null;
      proxy.printRole(role);
   }
}

使用動態代理實現流程

通過JDK動態代理實現上述流程,代碼如下:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
class ProxyBeanUtil implements InvocationHandler {
    //被代理對象
    private Object obj;
    //攔截器
    private Interceptor interceptor = null;

    /**
     * 獲取動態代理對象.
     * @param obj 被代理對象
     * @param interceptor 攔截器
     * @param aroundFlag 是否啓用around方法
     * @return  動態代理對象
     */
    public static Object getBean(Object obj, Interceptor interceptor) {
        //使用當前類,作爲代理方法,此時被代理對象執行方法的時候,會進入當前類的invoke方法裏
        ProxyBeanUtil _this = new ProxyBeanUtil();
        //保存被代理對象
        _this.obj = obj;
        //保存攔截器
        _this.interceptor = interceptor;
        //生成代理對象,並綁定代理方法
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),
                obj.getClass().getInterfaces(), _this);
    }

    /**
     *  代理方法.
     * @param proxy 代理對象
     * @param method 當前調度方法
     * @param args 參數
     * @return 方法返回
     * @throws Throwable 異常 
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object retObj = null;
        //是否產生異常
        boolean exceptionFlag = false;
        //before方法
        interceptor.before(obj);
        try {
                //反射原有方法
                retObj = method.invoke(obj, args);
        } catch (Exception ex) {
           exceptionFlag = true;
        } finally {
            //after方法
            interceptor.after(obj);
        }
        if (exceptionFlag) {
           //afterThrowing方法
            interceptor.afterThrowing(obj);
        } else {
           //afterReturning方法
            interceptor.afterReturning(obj);
        }
        return retObj;
    }

}

首先, 通過getBean方法保存了被代理對象、攔截器(interceptor)和參數(args),爲之後的調用奠定了基礎。然後,生成了JDK動態代理對象(proxy),同時綁定了ProxyBeanUtil返回的對象作爲其代理類,這樣當代理對象調用方法的時候,就會進入到ProxyBeanUtil的invoke方法中,於是焦點又到了invoke方法上。
在invoke方法中,將攔截器的方法實現了一遍,其中設置了異常標誌(exceptionFlag),通過這個標誌就能判斷反射原有對象方法的時候是否發生了異常。

Spring AOP的基本概念

AOP的概念和使用原因

AOP編程有者重要的意義,首先它可以攔截一些方法,然後把各個對象組織成一個整體。
首先了解正常執行SQL的邏輯步驟,一個正常的SQL是:

  • 打開通過數據庫連接池獲得數據庫連接資源,並做一定的設置工作
  • 執行對應的SQL語句,對數據進行操作
  • 如果SQL執行過程中發生異常,回滾事務
  • 如果SQL執行過程中沒有發生異常,最後提交事務
  • 到最後的階段,需要關閉一些連接資源。
    如果通過AOP框架實現,則過程如下:
  • 當方法標註爲@Transactional時,則方法啓用數據庫事務功能
  • 在默認的情況下(注意是默認的情況下,可以通過配置改變),如果原有方法出現異常,則回滾事務;如果沒有發生異常,那麼就提交事務,這樣整個事務管理AOP就完成了整個流程,無須開發者編寫任何代碼去實現
  • 最後關閉數據庫資源。
    這是使用最廣的執行流程,符合約定優於配置的開發原則。在大部分的情況下,只需要使用默認的約定即可,或者進行一些特定的配置,來完成所需要的功能,這樣對於開發者而言就更爲關注業務開發,而不是資源控制、事務異常處理,這些AOP框架都可以完成。

AOP是通過動態代理模式,帶來管控各個對象操作的切面環境,管理包括日誌、數據庫事務等操作,可以在反射原有對象方法之前正常返回、異常返回事後插入自己的邏輯代碼,有時候甚至取代原始方法。在一些常用的流程中,比如數據庫事務,AOP會提供默認的實現邏輯,也會提供一些簡單的配置,程序員就能比較方便地修改默認的實現,達到符合真實應用的效果,這樣就可以大大降低開發的工作量,提高代碼的可讀性和可維護性,將開發集中在業務邏輯上。

面向切面編程的術語

  1. 切面(Aspect)
    切面就是在一個怎麼樣的環境中工作,它可以定義後面需要介紹的各類通知、切點和引入等內容,然後Spring AOP會將其定義的內容織入到約定的流程中,在動態代理中可以把它理解成一個攔截器。
  2. 通知(Advice)
    通知是切面開啓後,切面的方法。它根據在代理對象真實方法調用前、後的順序和邏輯區分,它和約定遊戲的例子裏的攔截器的方法十分接近。
  • 前置通知(before):在動態代理反射原有對象方法或者執行環繞通知前執行的通知功能。
  • 後置通知(after):在動態代理反射原有對象方法或者執行環繞通知後執行的通知功能。無論是否拋出異常,它都會被執行。
  • 返回通知(afterReturning):在動態代理反射原有對象方法或者執行環繞通知後正常返回(無異常)執行的通知功能。
  • 異常通知(afterThrowing):在動態代理反射原有對象方法或者執行環繞通知產生異常後執行的通知功能。
  • 環繞通知(around):在動態代理中,它可以取代當前被攔截對象的方法,提供回調原有被攔截對象的方法。
  1. 引入(Introduction)
    引入允許在現有的類裏添加自定義的類和方法。
  2. 切點(Pointcut)
    這是一個告訴Spring AOP在什麼時候啓動攔截並織入對應的流程中,因爲並不是所有的開發都需要啓動AOP的,它往往通過正則表達式進行限定。
  3. 連接點(join point)
    連接點對應的是具體需要攔截的東西。
  4. 織入(Weaving)
    織入是一個生成代理對象並將切面內容放入到流程中的過程,實際的代理可以分爲靜態代理和動態代理。靜態代理是在編譯class文件時生成的代碼邏輯,但是在Spring中並不使用這樣的方法。一種是通過ClassLoader也就是在類加載的時候生成的代碼邏輯,但是它在應用程序代碼運行前就生成對應的邏輯。還有一種是運行期,動態生成代碼的方式,這是Spring AOP所採用的方式,Spring是以JDK和CGLIB動態代理來生成代理對象的。

Spring對AOP的支持

AOP並不是Spring框架特有的,Spring只是支持AOP編程的框架之一。Spring AOP是一種基於方法攔截的AOP,換句話說Spring只能支持方法攔截的AOP。在Spring中有4種方式去實現AOP的攔截功能。

  • 使用ProxyFactoryBean和對應的接口實現AOP
  • 使用XML配置AOP
  • 使用@AspectJ註解驅動切面
  • 使用AspectJ注入切面。
    在Spring AOP的攔截方法中,真正常用的是用@AspectJ註解的方式實現的切面,有時候XML配置也有一定的輔助作用。

使用@AspectJ註解開發Spring AOP

選擇連接點

Spring是方法級別的AOP框架,而我們主要也是以某個類的某個方法作爲連接點,用動態代理的理論來說,就是要攔截哪個方法織入對應AOP通知。首先,創建一個接口:

import com.ssm.chapter11.game.pojo.Role;

public interface RoleService {
   
   public void printRole(Role role);

}

接下里,提供一個實現類:

import org.springframework.stereotype.Component;

import com.ssm.chapter11.aop.service.RoleService;
import com.ssm.chapter11.game.pojo.Role;

@Component
public class RoleServiceImpl implements RoleService {
   
   @Override
   public void printRole(Role role) {
      System.out.println("{id: " + role.getId() + ", " 
           + "role_name : " + role.getRoleName() + ", "
           + "note : " + role.getNote() + "}");
   }
}

如果這個時候把printRole作爲AOP的連接點,那麼用動態代理的語言就是要爲類RoleServiceImpl生成代理對象,然後攔截printRole方法,於是可以產生各種AOP通知方法。

創建切面

選擇好了連接點就可以創建切面了,對於動態代理的概念而言,它就如同一個攔截器,在Spring中只要使用@AspectJ註解一個類,那麼Spring IoC容器就會認爲這是一個切面了。代碼如下:

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.DeclareParents;
import org.aspectj.lang.annotation.Pointcut;

import com.ssm.chapter11.aop.verifier.RoleVerifier;
import com.ssm.chapter11.aop.verifier.impl.RoleVerifierImpl;
import com.ssm.chapter11.game.pojo.Role;

@Aspect
public class RoleAspect {
   @Before("execution(*
   com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void before() {
      System.out.println("before ....");
   }

   @After("execution(*
   com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void after() {
      System.out.println("after ....");
   }

   @AfterReturning("execution(*
   com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void afterReturning() {
      System.out.println("afterReturning ....");
   }

   @AfterThrowing("execution(*
   com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void afterThrowing() {
      System.out.println("afterThrowing ....");
   }
}

這段代碼中的註解使用了對應的正則式,這些正則式是切點的問題,也就是要告訴Spring AOP,需要攔截什麼對象的什麼方法。

定義切點

Spring是通過這個正則表達式判斷是否需要攔截你的方法,這個表達式是:

execution(*
com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))

依次對這個表達式做出分析:

  • execution:代表執行方法的時候會觸發
  • *:代表任意返回類型的方法
  • com.ssm.chapter11.aop.service.impl.RoleServiceImpl:代表類的全限定名
  • printRole:被攔截方法名稱
  • (…):任意的參數
AspectJ指示器 描述
arg() 限制連接點匹配參數爲指定類型的方法
@args() 限制而連接點匹配參數爲指定註解標註的執行方法
execution 用於匹配連接點的執行方法,這是最常用的匹配,可以通過類似上面的正則式進行匹配
this() 限制連接點匹配AOP代理的Bean,引用爲指定類型的類
target 限制連接點匹配被代理對象爲指定的類型
@target() 限制連接點匹配特定的執行對象,這些對象要符合指定的註解類型
within() 限制連接點匹配指定的包
@within() 限制連接點匹配指定的類型
@annotation 限定匹配帶有指定註解的連接點

注意,Spring只能支持上表列出的AspectJ的指示器,如果使用了非表格中所列舉的指示器,那麼它將會拋出IllegalArgumentException異常。
此外,Spring還根據自己的需求擴展了一個Bean()的指示器,使得我們可以根據bean id或者名稱去定義對應的Bean。
例如,只需要對com.ssm.chapter11.aop.impl包及其下面的包的類進行匹配,因此要修改前置通知,如下所示:

@Before("execution(* com.ssm.chapter11.*.*.*.*.printRole(..)) 
	&& within(com.ssm.chapter11.aop.service.impl.*)"
public void before() {
   System.out.println("before ....");
}

使用within去限定了execution定義的正則表達式下的包的匹配,從而達到了限制效果。&&表示並且的含義,如果使用XML方式引入,&在XML中具有特殊含義,因此可以用and代替他。運算符||可以用or代替,非運算符!可以用not代替。
正則表達式需要重複書寫多次,比較麻煩,引入另一個註解@Pointcut定義一個切點就可以避免這個麻煩,代碼如下:

import com.ssm.chapter11.aop.verifier.RoleVerifier;
import com.ssm.chapter11.aop.verifier.impl.RoleVerifierImpl;
import com.ssm.chapter11.game.pojo.Role;

@Aspect
public class RoleAspect {
   
   @Pointcut("execution(* com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void print() {
   }

   @Before("print()")
   // @Before("execution(*
   // com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void before() {
      System.out.println("before ....");
   }

   @After("print()")
   // @After("execution(*
   // com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void after() {
      System.out.println("after ....");
   }

   @AfterReturning("print()")
   // @AfterReturning("execution(*
   // com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void afterReturning() {
      System.out.println("afterReturning ....");
   }

   @AfterThrowing("print()")
   // @AfterThrowing("execution(*
   // com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..))")
   public void afterThrowing() {
      System.out.println("afterThrowing ....");
   }
}

測試AOP

首先對Spring的Bean進行配置,採用註解Java配置,代碼如下:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import com.ssm.chapter11.aop.aspect.RoleAspect;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.ssm.chapter11.aop")
public class AopConfig {
   
   @Bean
    public RoleAspect getRoleAspect() {
        return new RoleAspect();
    }
}

其中的@EnableAspectJAutoProxy代表着啓用AspectJ框架的自動代理,這個時候Spring纔會生成動態代理對象,進而可以使用AOP,而getRoleAspect方法,則生成一個切面實例。
Spring還提供了XML的方式,這裏就需要使用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-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd        
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
   <aop:aspectj-autoproxy />
   <bean id="roleAspect" class="com.ssm.chapter11.aop.aspect.RoleAspect" />
   <bean id="roleService" class="com.ssm.chapter11.aop.service.impl.RoleServiceImpl" />
</beans>

無論用XML還是用Java的配置,都能使Spring產生動態代理對象,從而組織切面,把各類通知織入到流程當中。測試主函數如下:

private static void testAnnotation() {
   ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
   RoleService roleService = ctx.getBean(RoleService.class);
   Role role = new Role();
   role.setId(1L);
   role.setRoleName("role_name_1");
   role.setNote("note_1");
   roleService.printRole(role);
   System.out.println("#################");
   // 測試異常通知
   role = null;
   roleService.printRole(role);
}

環繞通知

環繞通知是Spring AOP中最強大的通知,它可以同時實現前置通知和後置通知,它保留了調度被代理對象原有方法的功能,所以它及強大,又靈活,但是可控制性不那麼強,如果不需要大量改變業務邏輯,一般而言並不需要使用它。代碼如下:

@Around("print()")
public void around(ProceedingJoinPoint jp) {
   System.out.println("around before ....");
   try {
      jp.proceed();
   } catch (Throwable e) {
      e.printStackTrace();
   }
   System.out.println("around after ....");
}

這樣在一個切面裏通過@Around註解加入了切面的環繞通知,這個通知裏有一個ProceedingJoinPoint參數,這個參數是Spring提供的,使用它可以反射連接點的方法。

織入

織入是生成代理對象並將切面內容放入約定流程的過程。在上述代碼中,連接點所在的類都是擁有接口的類,而事實上即使沒有接口,Spring也能提供AOP的功能,所以是否擁有接口不是使用Spring AOP的一個強制要求。於是Spring提供了一個規則:當類的實現存在接口的時候,Spring將提供JDK動態代理,從而織入各個通知;而當類不存在接口的時候沒有辦法使用JDK動態代理,Spring會採用CGLIB來生成代理對象。
動態代理對象是由Spring IoC容器根據描述生成的,一般不需要修改它。

給通知傳遞參數

在Spring AOP各類通知中,除了環繞通知外,並沒有討論參數的傳遞,有時候還是希望能夠傳遞參數的。示例代碼如下,修改連接點爲一個多參數的方法:

public void printRole(Role role, int sort) {
   System.out.println("{id: " + role.getId() + ", " 
        + "role_name : " + role.getRoleName() + ", "
        + "note : " + role.getNote() + "}");
   System.out.println(sort);
}

這裏存在兩個參數,一個是角色,一個是整型排序參數,那麼要把這個方法作爲連接點,也就是使用切面攔截這個方法,定義切點如下,以前置通知爲例:

@Before("execution(* com.ssm.chapter11.aop.service.impl.RoleServiceImpl.printRole(..)) " + "&& args(role, sort)")
public void before(Role role, int sort) {
   System.out.println("before ....");
}

引入

先定義一個RoleVerifier接口:

public interface RoleVerifier {
   public boolean verify(Role role);
}

創建一個實現類RoleVerifierImpl:

public class RoleVerifierImpl implements RoleVerifier {
   @Override
   public boolean verify(Role role) {
      System.out.println("引入,檢測一下角色是否爲空");
      return role != null;
   }
}

在其RoleAspect類中加入一個新的屬性,代碼如下:

@DeclareParents(value= "com.ssm.chapter11.aop.service.impl.RoleServiceImpl+", defaultImpl=RoleVerifierImpl.class)
public RoleVerifier roleVerifier;

註解@DeclareParents的配置如下:

  • value=“com.ssm.chapter11.aop.service.impl.RoleServiceImpl+”:表示對RoleServiceImpl類進行增強,也就是在RoleServiceImpl中引入一個新的接口
  • defaultImpl:代表其默認的實現類,這裏是RoleVerifierImpl。
    然後對這個方法進行測試,代碼如下:
ApplicationContext ctx = new AnnotationConfigApplicationContext (AopConfig.class);
RoleService roleService = ctx.getBean(RoleService.class);
RoleVerifier roleVerifier = (RoleVerifier) roleService;
Role role = new Role();
role.setId(1L);
role.setRoleName("role_name_1");
role.setNote("note_1");
if (roleVerifier.verify(role)) {
    roleService.printRole(role);
}

使用強制轉換之後就可以把roleService轉化爲RoleVerifier接口對象,然後就可以使用verify方法了。而RoleVerifier調用的方法verify,顯然它就是通過RoleVerifierImpl來實現的。
分析它的原理,我們知道Spring AOP依賴於動態代理來實現,生成動態代理對象是通過類似於下面這行代碼來實現的:

// 生成代理對象,並綁定代理方法
return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),_this);

obj.getClass().getInterfaces()意味着代理對象掛在多個接口之下,換句話說,只要Spring AOP讓代理對象掛在RoleService和RoleVerifier兩個接口下,那麼就可以把對應的Bean通過強制轉換,讓其在RoleService和RoleVerifier之間互相轉換了。
同樣的如果RoleServiceImpl沒有接口,那麼它也會使用CGLIB動態代理,使用增強者(Enhancer)也會由一個interfaces的屬性,允許代理對象掛到對應的多個接口下,於是也可以按照JDK動態代理那樣使得對象可以在多個接口之間相互轉換。

使用XML配置開發Spring AOP

使用XML方式開發AOP,其實它們的原理是相同的。這裏需要在XML中引入AOP的命名空間,下表是XML配置AOP的元素:

AOP配置元素 用途 備註
aop:advisor 定義AOP的通知器 一種較老的方式,目前很少使用
aop:aspect 定義一個切面
aop:before 定義前置通知
aop:after 定義後置通知
aop:around 定義環繞方式
aop:after-returning 定義返回通知
aop:after-throwing 定義異常通知
aop:config 頂層的AOP配置元素 AOP的配置是以它爲開始的
aop:declare-parents 給通知引入新的額外接口,增強功能
aop:pointcut 定義切點

先定義要攔截的類和方法,儘管Spring並不強迫定義接口使用AOP,但是建議使用接口,有利於實現和定義相分離,使得系統更爲靈活。
首先定義一個新的接口,代碼如下:

import com.ssm.chapter11.game.pojo.Role;

public interface RoleService {

   public void printRole(Role role);
}

然後給出實現類,代碼如下:

import com.ssm.chapter11.game.pojo.Role;
import com.ssm.chapter11.xml.service.RoleService;

public class RoleServiceImpl implements RoleService {

   @Override
   public void printRole(Role role) {
      System.out.print("id = " + role.getId()+",");
      System.out.print("role_name = " + role.getRoleName()+",");
      System.out.println("note = " + role.getNote());
   }

}

通過AOP來增強它的功能,爲此需要一個切面類,代碼如下:

import org.aspectj.lang.ProceedingJoinPoint;

public class XmlAspect {

   public void before() {
      System.out.println("before ......");
   }
   
   public void after() {
      System.out.println("after ......");
   }
   
   public void afterThrowing() {
      System.out.println("after-throwing ......");
   }
   
   public void afterReturning() {
      System.out.println("after-returning ......");
   }
}

同樣的也沒有任何的註解,這就意味着需要我們使用XML去向Spring IoC容器描述它們。

前置通知、後置通知、返回通知和異常通知

配置代碼如下:

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

   <bean id="xmlAspect" class="com.ssm.chapter11.xml.aspect.XmlAspect" />
   <bean id="roleService" class="com.ssm.chapter11.xml.service.impl.RoleServiceImpl" />

   <aop:config>     
   <!--引用xmlAspect作爲切面-->
      <aop:aspect ref="xmlAspect">
         <aop:before method="before"
            pointcut="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
         <aop:after method="after"
            pointcut="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
         <aop:after-throwing method="afterThrowing"
            pointcut="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
         <aop:after-returning method="afterReturning"
            pointcut="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
         </aop:aspect>
   </aop:config>
 </beans>

這裏首先通過引入的XML定義了AOP的命名空間,然後定義了一個roleService類和切面xmlAspect類,最後通過aop:config\取定義AOP的內容信息。
和使用註解一樣,也可以通過定義切點,然後引用到別的通知上。代碼如下:

<aop:config>
   <aop:aspect ref="xmlAspect">
       <!-- 自定義切點 -->
      <aop:pointcut id="printRole"
         expression="execution(* com.ssm.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))" />
      <!-- 定義通知 -->
      <aop:before method="before" pointcut-ref="printRole" />
      <aop:after method="after" pointcut-ref="printRole" />
      <aop:after-throwing method="afterThrowing"
         pointcut-ref="printRole" />
      <aop:after-returning method="afterReturning"
         pointcut-ref="printRole" />
      <aop:around method="around" pointcut-ref="printRole" />
     </aop:aspect>
</aop:config>

通過這段代碼可以定義切點並進行引入,這樣就可以避免多次書寫同一正則式的麻煩。

環繞通知

和其他通知一樣,環繞通知也可以織入到約定的流程當中,代碼如下:

public void around(ProceedingJoinPoint jp){
	System.out.println("around before......");
	try{
		jp.proceed();
	}catch (Throwable e){
		new RuntimeException("回調原有流程,產生異常...");
	}
	System.out.println("around after......");
}

通過調度ProceedingJointPoint的proceed方法就能夠調用原有的方法了。加入環繞通知的配置如下:

<aop:around method="around" pointcut-ref="printRole" />

測試代碼如下:

public static void main(String []args) {
   ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg4.xml");
   RoleService roleService = ctx.getBean(RoleService.class);
   Role role = new Role();
   role.setId(1L);
   role.setRoleName("role_name_1");
   role.setNote("note_1");
   roleService.printRole(role);
}

這裏讀入了XML文件,然後通過容器獲取了Bean,創建了角色了,然後打印角色,就能得到日誌。顯然所有的通知都被織入了AOP所約定了流程,但是請注意,環繞通知的before是在前置通知之後打印出來的。

給通知傳遞參數

通過XML的配置,也可以引入參數到通知當中。首先,改寫代碼如下:

public void before(Role role){
	System.out.println("role_id="+role.getId()+"before ......")}

此時帶上了參數role,修改前置通知的配置代碼如下:

<aop:before method="before" pointcut="execution(*
com.ssh.chapter11.xml.service.impl.RoleServiceImpl.printRole(..))  and
args(role)" />

引入

無論是使用JDK動態代理,還是使用CGLIB動態代理都可以將代理對象下掛到多個接口之下,這樣就能夠引入新的方法了。
在代碼中加入一個新的屬性RoleVerifier類對象:

public RoleVerifier roleVerifier = null;

此時可以使用XML配置它,配置的內容和註解引入的方法相當,它是使用<aop:declare-parents>去引入的,代碼如下:

<aop:declare-parents
   types-matching="com.ssm.chapter11.xml.service.impl.RoleServiceImpl+"
   implement-interface="com.ssm.chapter11.aop.verifier.RoleVerifier"
   default-impl="com.ssm.chapter11.aop.verifier.impl.RoleVerifierImpl" />

經典Spring AOP應用程序

先定義一個類來實現前置通知,它要求類實現MethodBeforeAdvice接口的before方法,代碼如下:

import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;

public class ProxyFactoryBeanAspect implements MethodBeforeAdvice {

   @Override
   /***
    * 前置通知
    * @param method 被攔截方法(切點)
    * @param params 參數 數組[role]
    * @param roleService 被攔截對象
    */
   public void before(Method method, Object[] params, Object roleService) throws Throwable {
      System.out.println("前置通知!!");
   }
}

有了它還需要對Spring IoC容器描述對應的信息,這時候需要一個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"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
   http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

   <bean id="proxyFactoryBeanAspect" class="com.ssm.chapter11.aspect.ProxyFactoryBeanAspect" />

   <!--設定代理類 -->
   <bean id="roleService" class="org.springframework.aop.framework.ProxyFactoryBean">
      <!--這裏代理的是接口 -->
      <property name="proxyInterfaces">
         <value>com.ssm.chapter11.game.service.RoleService</value>
      </property>

      <!--是ProxyFactoryBean要代理的目標類 -->
      <property name="target">
         <bean class="com.ssm.chapter11.game.service.impl.RoleServiceImpl" />
      </property>

      <!--定義通知 -->
      <property name="interceptorNames">
         <list>
            <!-- 引入定義好的spring bean -->
            <value>proxyFactoryBeanAspect</value>
         </list>
      </property>
   </bean>
</beans>

測試代碼如下:

public static void main(String[] args) {
   ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
   Role role = new Role();
   role.setId(1L);
   role.setRoleName("role_name");
   role.setNote("note");
   RoleService roleService = (RoleService) ctx.getBean("roleService");
   roleService.printRole(role);
}

這樣Spring AOP就被用起來了,它雖然很經典,但是已經不是主流方式。

多個切面

Spring也能支持多個切面。當有多個切面時,在測試過程中發現它不會存在任何順序,這些順序代碼會隨機生成,但是有時候希望它按照指定的順序運行。在此之前要先定義一個連接點,爲此新建一個接口——MultiBean,它十分簡單,代碼如下:

public interface MultiBean {
   public void testMulti();
}

實現接口:

import org.springframework.stereotype.Component;

import com.ssm.chapter11.multi.bean.MultiBean;

@Component
public class MultiBeanImpl implements  MultiBean {

   @Override
   public void testMulti() {
      System.out.println("test multi aspects!!");
   }

}

定義好連接點,然後需要切面:Aspect1、Aspect2和Aspect3進行AOP編程,這3個切面的定義如下:

@Aspect
public class Aspect1 {
   @Pointcut("execution(* com.ssm.chapter11.multi.bean.impl.MultiBeanImpl.testMulti(..))")
   public void print() {
   }
   
   @Before("print()")
   public void before() {
      System.out.println("before 1 ......");
   }
   
   @After("print()")
   public void after() {
      System.out.println("after 1 ......");
   }
   
   @AfterThrowing("print()")
   public void afterThrowing() {
      System.out.println("afterThrowing 1 ......");
   }
   
   @AfterReturning("print()") 
   public void afterReturning() {
      System.out.println("afterReturning 1 ......");
   }
}
@Aspect
public class Aspect2 {
   
   @Pointcut("execution(* com.ssm.chapter11.multi.bean.impl.MultiBeanImpl.testMulti(..))")
   public void print() {
   }
   
   @Before("print()")
   public void before() {
      System.out.println("before 2 ......");
   }
   
   @After("print()")
   public void after() {
      System.out.println("after 2 ......");
   }
   
   @AfterThrowing("print()")
   public void afterThrowing() {
      System.out.println("afterThrowing 2 ......");
   }
   
   @AfterReturning("print()") 
   public void afterReturning() {
      System.out.println("afterReturning 2 ......");
   }
}
@Aspect
public class Aspect3 {

   @Pointcut("execution(* com.ssm.chapter11.multi.bean.impl.MultiBeanImpl.testMulti(..))")
   public void print() {
   }

   @Before("print()")
   public void before() {
      System.out.println("before 3 ......");
   }

   @After("print()")
   public void after() {
      System.out.println("after 3 ......");
   }

   @AfterThrowing("print()")
   public void afterThrowing() {
      System.out.println("afterThrowing 3 ......");
   }

   @AfterReturning("print()")
   public void afterReturning() {
      System.out.println("afterReturning 3 ......");
   }
}

Java環境配置代碼如下:

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.ssm.chapter11.multi")
public class MultiConfig {

   @Bean
    public Aspect1 getAspect1() {
        return new Aspect1();
    }
   
   @Bean
    public Aspect2 getAspect2() {
        return new Aspect2();
    }  
   @Bean
    public Aspect3 getAspect3() {
        return new Aspect3();
    }
}

通過AnnotationConfigApplicationContext加載配置文件,顯然多個切面是無序的。如何讓它有序執行,在Spring中有多種方法,如果使用註解的切面,那麼可以給切面加入註解@Ordered。例如:

@Aspect
@Order(1)
public class Aspect1 {
......

以此類推。

在Spring AOP的實現方式是動態代理,換句話說,Spring底層也是通過責任鏈模式來處理多個切面,事實上,還有其他的方法,比如也可以讓切面實現Ordered(org.springframework.core.Ordered)接口,它定義了一個getOrder方法,如果需要取代Aspect1中的@Order(1)的功能,那麼代碼修改爲:

/******* imports *******/
@Aspect
public class Aspect1 implements Ordered{
	@Override
	public int getOrder(){
		return 1;
	}
}
......

顯然,沒有使用@Order註解方便,另外也可以在XML文件中配置,示例如下:

<aop:aspect ref="aspect1" order="1">
......
</aop:aspect>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章