天天看點

Spring AOP 動态代理入門(一)

1.經典Spring AOP 類圖(主要針對com.springframework.aop包)

Spring AOP 動态代理入門(一)

AOP Alliance(http://aopalliance.sourceforge.net/) 是個聯合的開源協作組織,在多個項目間進行協作以期提供一套标準的AOP Java接口

Spring AOP就是基于AOP Alliance标準API實作的,如果你打算使用Spring的AOP或基于AOP的任何特性,隻需這個JAR檔案

前置通知【BeforeAdvice】、後置通知【AfterAdvice】、異常通知【ThrowAdvice】都繼承自标準接口 Advice

而環繞通知則直接使用AOP Alliance提供的标準接口【MethodInterceptor】

2.經典Spring AOP使用方式

本質上是基于JDK的動态代理實作的面向切面程式設計,按照JDK動态代理的要求:必須基于接口生成目标對象

(1)定義一個接口 ICalculate,包含幾個簡單方法 

public interface ICalculate {

    int add(int a, int b);

    int sub(int a, int b);

}
           

(2)定義一個實作類CalculateImpl

public class CalculateImpl implements ICalculate {

    /*
     * (non-Javadoc)
     * 
     * @see com.yli.sample.spring.exam.ICalculate#add(int, int)
     */
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.yli.sample.spring.exam.ICalculate#sub(int, int)
     */
    @Override
    public int sub(int a, int b) {
        if (0 == b) {
            throw new IllegalArgumentException("發生除0錯誤!");
        }
        return a / b;
    }
}
           

為什麼要稱為經典的Spring SOP使用方式呢,因為和自行實作JDK的動态代理過程是類似的,屬于基礎程式設計,不能算AOP架構的進階程式設計方式

======================================那麼先介紹自行實作JDK的動态代理==========================================

再定義一個類實作JDK提供的InvocationHandler接口,為委托類【即CalculateImpl】生成代理對象

public class CalculateInvocationHandler implements InvocationHandler {

    // 委托對象
    private Object target;

    public CalculateInvocationHandler(Object target) {
        this.target = target;
    }

    // 為委托對象生成代理對象
    public Object newProxyInstance() throws IllegalArgumentException {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass()
                .getInterfaces(), this);
    }

    // 執行委托方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("========開始執行方法========");
        System.out.println("方法名稱: " + method.getName());
        System.out.println("參數值: " + Arrays.toString(args));
        Object result = method.invoke(target, args);
        System.out.println("========結束方法執行========");
        return result;
    }
}
           

然後使用 main 方法測試

public static void main(String[] args) {
        ICalculate ical = new CalculateImpl();
        CalculateInvocationHandler handler = new CalculateInvocationHandler(ical);
        ICalculate icalProxy  = (ICalculate)handler.newProxyInstance();
        icalProxy.add(5, 3);
        
    }
           

列印執行結果

========開始執行方法========
方法名稱: add
參數值: [5, 3]
========結束方法執行========
           

======================================繼續介紹經典的 Spring AOP 實作方式==========================================

(3)前置通知、後置通知、異常通知、環繞通知

/**
 * Spring AOP架構-BeforeAdvice-前置通知
 */
public class CalculateLogBeforeAdvice implements MethodBeforeAdvice {

    private static Logger log = LoggerFactory.getLogger(CalculateLogBeforeAdvice.class);

    /**
     * 實作before方法
     * @param method 要執行的方法
     * @param args 方法參數
     * @param target 執行該方法的目标對象
     */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        // 列印方法名稱
        log.info("the method " + method.getName() + "() begin with");
        // 列印參數清單
        int index = 0;
        for (Object arg : args) {
            log.info("param(" + (index++) + "):" + arg.toString());
        }
    }
}
           
/**
 * 後置通知
 */
public class CalculateLogAfterAdvice implements AfterReturningAdvice {

    private static Logger log = LoggerFactory.getLogger(CalculateLogAfterAdvice.class);

    /**
     * 實作afterReturning方法
     * @param returnValue 方法執行傳回值
     * @param method 要執行的方法
     * @param args 方法參數
     * @param target 執行該方法的目标對象
     */
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target)
            throws Throwable {
        log.info("the method " + method.getName() + "() ends with " + returnValue);
    }
}
           

前置通知和後置通知,直接實作目标接口[MethodBeforeAdvice]和[AfterReturningAdvice]定義的方法

異常通知和環繞通知,其目标接口沒有定義方法(參考最上面的類圖也可以看出來),是以要自行實作方法

/**
 * 異常通知,目标接口最終是繼承自AfterAdvice
 * 而AfterAdvice是沒有定義方法的,是以目标類要實作自己的方法
 */
public class CalculateLogExceptionAdvice implements ThrowsAdvice {

    private static Logger log = LoggerFactory.getLogger(CalculateLogExceptionAdvice.class);

    /**
     * 方法名稱使用afterThrowing
     * 方法參數可以隻使用運作時異常
     */
    public void afterThrowing(IllegalArgumentException exp)
            throws Throwable {
        log.error(exp.getMessage());
    }
    /**
     * 方法簽名可以擴充,比如要列印其他資訊
     * method,args,target要麼全部加上,要麼一個都不加
     */
    // public void afterThrowing(Method method, Object[] args, Object target,
            // IllegalArgumentException exp) throws Throwable {
        // log.error(exp.getMessage());
    //}
}
           
/**
 * 環繞通知,表現更為直接
 * 目标接口直接來自于AOP Alliance包,并非來自Spring架構
 */
public class CalculateLogAroundAdvice implements MethodInterceptor {

    private static Logger log = LoggerFactory.getLogger(CalculateLogAroundAdvice.class);

    /**
     * 方法參數和[前置、後置、異常通知】不同
     * 因為目标方法來自于AOP标準架構
     */
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("Around:The method " + invocation.getMethod().getName() + "() begins with "
                + Arrays.toString(invocation.getArguments()));

        try {
            // 執行代理bean的業務方法
            // 相當于 method.invoke()
            // 必須要執行
            Object result = invocation.proceed();
            log.info("Around:The method " + invocation.getMethod().getName() + "() ends with " + result);
            
            // 可以傳回任何值,但建議傳回執行的實際結果
            return result;
        } catch (IllegalArgumentException exp) {
            log.error("Around:Illegal argument " + Arrays.toString(invocation.getArguments())
                    + " for method " + invocation.getMethod().getName() + "()");
            throw exp;
        }
    }
}
           

(4)配置bean

<?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:cxf="http://cxf.apache.org/core"
	xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
	xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
   	http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
   	http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
   	http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
   	http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
   	http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
   	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
   	http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
   	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
   	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
   	
   	<!-- 業務bean -->
   	<bean id="calculate" class="com.yli.sample.spring.aop.CalculateImpl"></bean>
   	
   	<!-- 前置通知:記錄業務日志 -->
   	<bean id="beforeLog" class="com.yli.sample.spring.aop.CalculateLogBeforeAdvice"></bean>
   	
   	<!-- 後置通知:記錄業務日志 -->
   	<bean id="afterLog" class="com.yli.sample.spring.aop.CalculateLogAfterAdvice"></bean>
   	
   	<!-- 異常通知:記錄業務日志 -->
   	<bean id="exceptionLog" class="com.yli.sample.spring.aop.CalculateLogExceptionAdvice"></bean>
   	
   	<!-- 環繞通知:記錄業務日志 -->
   	<bean id="aroundLog" class="com.yli.sample.spring.aop.CalculateLogAroundAdvice"></bean>
   	
   	<!-- 為業務Bean生成代理Bean -->
   	<bean id="calculateProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
   		<!-- JDK的動态代理 需要業務類必須實作一個接口,此處可以指定接口 -->
   		<property name="proxyInterfaces">
   			<list>
   				<value>com.yli.sample.spring.aop.ICalculate</value>
   			</list>
   		</property>
   		<!-- 如果不指定要代理的接口,spring會使用cgliab直接代理目标類 -->
   		<property name="target" ref="calculate"></property>
   		<!-- 定義代理Bean要使用的通知 -->
   		<property name="interceptorNames">
   			<list>
   				<value>beforeLog</value>
   				<value>afterLog</value>
   				<value>exceptionLog</value>
   				<value>aroundLog</value>
   			</list>
   		</property>
   	</bean>
   	
</beans>
           

(5)業務接口、實作類、各種通知和bean配置完成了,可以main方法測試

public static void main(String[] args) {
        String configLocation = "classpath:/conf/spring/aop-bean.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(configLocation);

        // 注意擷取的是代理Bean,不是原生的業務Bean
        // 使用經典的Spring AOP,隻有代理Bean才能觸發通知
        ICalculate calculateProxy = (ICalculate) context.getBean("calculateProxy");
        calculateProxy.sub(15, 5);
        calculateProxy.sub(15, 0);
    }
           

執行結果

11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - the method sub() begin with
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - param(0):15
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - param(1):5
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogAroundAdvice - Around:The method sub() begins with [15, 5]
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogAroundAdvice - Around:The method sub() ends with 3
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogAfterAdvice - the method sub() ends with 3
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - the method sub() begin with
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - param(0):15
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - param(1):0
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogAroundAdvice - Around:The method sub() begins with [15, 0]
Exception in thread "main" 11:24:17.126 [main] ERROR c.y.s.s.aop.CalculateLogAroundAdvice - Around:Illegal argument [15, 0] for method sub()
11:24:17.126 [main] DEBUG o.s.a.f.a.ThrowsAdviceInterceptor - Found handler for exception of type [java.lang.IllegalArgumentException]: public void com.yli.sample.spring.aop.CalculateLogExceptionAdvice.afterThrowing(java.lang.IllegalArgumentException) throws java.lang.Throwable
11:24:17.126 [main] ERROR c.y.s.s.a.CalculateLogExceptionAdvice - 發生除0錯誤!
           

(6)這種動态代理方式有兩個缺點

業務類的所有方法都被代理,不能根據規則比對方法;隻能使用代理Bean來執行方法才能觸發通知事件!

經典Spring AOP 提供多種方式滿足以上需求,可以【增強部分】,也可以【自動建立代理Bean】

<?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:cxf="http://cxf.apache.org/core"
	xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
	xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
   	http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
   	http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
   	http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
   	http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
   	http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
   	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
   	http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
   	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
   	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
   	   	
   	<!-- 業務Bean -->
   	<bean id="autoCalculate" class="com.yli.sample.spring.aop.CalculateImpl"></bean>
   	
   	<!-- 環繞通知 -->
   	<bean id="aroundLog" class="com.yli.sample.spring.aop.CalculateLogAroundAdvice"></bean>
   	
   	<!-- 增強器 支援【部分增強】  -->
   	<bean id="pointAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
   		<!-- 要增強的方法名稱 -->
   		<property name="mappedNames">
   			<list>
   				<value>add</value>
   				<value>sub</value>
   			</list>
   		</property>
   		<!-- 增強部分使用的【通知事件】 -->
   		<property name="advice" ref="aroundLog"></property>
   	</bean>
   	
   	<!-- BeanNameAutoProxyCreator 支援【自動代理】 -->
	<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
		<property name="beanNames">
			<list>
				<!-- 通配符比對哪些Bean需要自動建立代理 -->
				<value>*Calculate*</value>
			</list>
		</property>
		<property name="interceptorNames">
			<list>
				<!-- 指定【增強器】 -->
				<value>pointAdvisor</value>
				<!-- 其實此處可以直接指定【通知】 -->
				<!-- <value>aroundLog</value> -->
			</list>
		</property>
	</bean>
   	
</beans>
           

main方法測試

public static void main(String[] args) {
        
        String location = "classpath:conf/spring/aop2-bean.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(location);
        
        // 使用Spring提供的自動建立代理
        // 此處擷取的Bean就可以直接使用業務Bean了
        ICalculate calculate1 = (ICalculate) context.getBean("autoCalculate");
        calculate1.add(3, 5);
    }
           

除了上面那種方式,Spring還提供一個預設的自動代理,隻需要指定一個【增強器】即可

Spring會自動檢查IOC容器中每個Bean,自動比對增強器

<?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:cxf="http://cxf.apache.org/core"
	xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
	xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
   	http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
   	http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
   	http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
   	http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
   	http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
   	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
   	http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
   	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
   	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
   	   	
   	<!-- 業務Bean -->
   	<bean id="autoCalculate" class="com.yli.sample.spring.aop.CalculateImpl"></bean>
   	
   	<!-- 環繞通知 -->
   	<bean id="aroundLog" class="com.yli.sample.spring.aop.CalculateLogAroundAdvice"></bean>
	
	<!-- 例如指定一個增強器【正規表達式比對的增強器】
		前面已經設定過【根據名稱比對的增強器】
	 -->
	<bean id="regAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
   		<property name="patterns">
   			<list>
   				<value>.*sub.*</value>
   			</list>
   		</property>
   		<property name="advice" ref="aroundLog"></property>
   	</bean>
	
	<!-- DefaultAdvisorAutoProxyCreator 也支援【自動代理】
		Spring會檢查IOC中每個增強器與Bean,如果曾強器切入點與Bean相比對,則自動代理
	 -->
	<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
   	
</beans>