1. 程式人生 > >Java AOP & Spring AOP 原理和實現

Java AOP & Spring AOP 原理和實現

1 定義介面,並建立介面實現類

public interface HelloWorld
{
    void printHelloWorld();
    void doPrint();
}
public class HelloWorldImpl1 implements HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Enter HelloWorldImpl1.printHelloWorld()");
    }
    
    public void doPrint()
    {
        System.out.println("Enter HelloWorldImpl1.doPrint()");
        return ;
    }
}
public class HelloWorldImpl2 implements HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Enter HelloWorldImpl2.printHelloWorld()");
    }
    
    public void doPrint()
    {
        System.out.println("Enter HelloWorldImpl2.doPrint()");
        return ;
    }
}


2 編輯AOP中需要使用到的通知類(橫切關注點,這裡是列印時間)

public class TimeHandler
{
    public void printTime()
    {
        System.out.println("CurrentTime = " + System.currentTimeMillis());
    }
}

在此可以定義前置和後置的列印。

配置容器初始化時需要的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:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
        
        <bean id="helloWorldImpl1" class="com.xrq.aop.HelloWorldImpl1" />
        <bean id="helloWorldImpl2" class="com.xrq.aop.HelloWorldImpl2" />
        <bean id="timeHandler" class="com.xrq.aop.TimeHandler" />
        
        <aop:config>
            <aop:aspect id="time" ref="timeHandler">
                <aop:pointcut id="addAllMethod" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
                <aop:before method="printTime" pointcut-ref="addAllMethod" />
                <aop:after method="printTime" pointcut-ref="addAllMethod" />
            </aop:aspect>
        </aop:config>
</beans>


4 測試程式碼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("aop.xml");
        
        HelloWorld hw1 = (HelloWorld)ctx.getBean("helloWorldImpl1");
        HelloWorld hw2 = (HelloWorld)ctx.getBean("helloWorldImpl2");
        hw1.printHelloWorld();
        System.out.println();
        hw1.doPrint();
    
        System.out.println();
        hw2.printHelloWorld();
        System.out.println();
        hw2.doPrint();
    }
}


執行結果為:

CurrentTime = 1446129611993

Enter HelloWorldImpl1.printHelloWorld()

CurrentTime = 1446129611993

CurrentTime = 1446129611994

Enter HelloWorldImpl1.doPrint()

CurrentTime = 1446129611994

CurrentTime = 1446129611994

Enter HelloWorldImpl2.printHelloWorld()

CurrentTime = 1446129611994

CurrentTime = 1446129611994

Enter HelloWorldImpl2.doPrint()

CurrentTime = 1446129611994

對於需要使用多個aspect,可以使用如下xml定義:

        <aop:config>
            <aop:aspect id="time" ref="timeHandler" order="1">
                <aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
                <aop:before method="printTime" pointcut-ref="addTime" />
                <aop:after method="printTime" pointcut-ref="addTime" />
            </aop:aspect>
            <aop:aspect id="log" ref="logHandler" order="2">
                <aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.*(..))" />
                <aop:before method="LogBefore" pointcut-ref="printLog" />
                <aop:after method="LogAfter" pointcut-ref="printLog" />
            </aop:aspect>
        </aop:config>
 

要想讓logHandler在timeHandler前使用有兩個辦法:

1)aspect裡面有一個order屬性,order屬性的數字就是橫切關注點的順序

2)把logHandler定義在timeHandler前面,Spring預設以aspect的定義順序作為織入順序pointcut的expression用於匹配方法,增減其粒度可以達到不同的過濾效果。

 <aop:config>
            <aop:aspect id="time" ref="timeHandler" order="1">
                <aop:pointcut id="addTime" expression="execution(* com.xrq.aop.HelloWorld.print*(..))" />
                <aop:before method="printTime" pointcut-ref="addTime" />
                <aop:after method="printTime" pointcut-ref="addTime" />
            </aop:aspect>
            <aop:aspect id="log" ref="logHandler" order="2">
                <aop:pointcut id="printLog" expression="execution(* com.xrq.aop.HelloWorld.do*(..))" />
                <aop:before method="LogBefore" pointcut-ref="printLog" />
                <aop:after method="LogAfter" pointcut-ref="printLog" />
            </aop:aspect>
        </aop:config>

表示timeHandler只會織入HelloWorld介面print開頭的方法,logHandler只會織入HelloWorld介面do開頭的方法。

使用CGLIB生成代理

CGLIB針對代理物件為類的情況使用。在spring配置檔案中加入<aop:aspectj-autoproxy proxy-target-class="true"/>可強制使用CGLIB生成代理。

1、如果目標物件實現了介面,預設情況下會採用JDK的動態代理實現AOP 

2、如果目標物件實現了介面,可以強制使用CGLIB實現AOP 

3、如果目標物件沒有實現類介面,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換

JDK動態代理和CGLIB位元組碼生成的區別? 

* JDK動態代理只能對實現了介面的類生成代理,而不能針對類 

* CGLIB是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法,因為是繼承,所以該類或方法最好不要宣告成final

CGLIB建立代理主要是建立Enhancer enhancer,並通過AdvisedSupport設定相應的屬性,比如目標類rootClass,如果由介面合併介面給代理類,最主要的是設定Callback集合和CallbackFilter,使用CallBackFilter可以根據方法的不同使用不同的Callback進行攔截和增強方法。其中最主要的使用於AOP的Callback是DynamicAdvisedInterceptor。

Spring配置檔案中配置的區別:

<bean id="#" class="org.springframework.ProxyFactoryBean">
<property name="proxyTargetClass">
<value>true</value>
</property>
</bean>
 
<bean id="#" class="org.springframework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>com.gc.impl.TimeBookInterface</value>
</property>
</bean>

4.2 使用註解配置AOP

使用註解方式開發AOP比較靈活方便,其實現需要如下5個步驟。

1. 配置檔案中加入AOP的名稱空間

 使用<aop:config/>標籤,需要給Spring配置檔案中引入基於xml schemaSpring AOP名稱空間。完成後的Spring配置檔案如下:

<?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"  
        xsi:schemaLocation="http://www.springframework.org/schema/beans  
              http://www.springframework.org/schema/beans/spring-beans-2.5.xsd   
              http://www.springframework.org/schema/aop   
              http://www.springframework.org/schema/aop/spring-aop-2.5.xsd >  
<!--Spring配置資訊-->
</beans>  

2. 啟用自動代理功能

<?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/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
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-3.0.xsd">
 
<!-- 啟用元件掃描功能,在包com.spring.aop.imp及其子包下面自動掃描通過註解配置的元件 -->
<context:component-scan base-package="com.spring.aop.service"/>
<!-- 啟用自動代理功能 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- 使用者服務物件 -->
<bean id="userService" class="cn.spring.aop.service.imp.PersonServiceBean" />
 
</beans>

3. 編寫實現類

package com.spring.aop.service;  
  
public interface PersonServer {  
  
    public void save(String name);  
    public void update(String name, Integer id);  
    public String getPersonName(Integer id);  
      
}  
package com.spring.aop.service.imp;  
import com.spring.aop.service.PersonServer;  
public class PersonServiceBean implements PersonServer{
    @Override  
    public void save(String name) {  
        System.out.println("我是save方法");  
    //  throw new RuntimeException();  
    }  
  
    @Override  
    public void update(String name, Integer id) {
        System.out.println("我是update()方法");  
    }  
  
    @Override  
    public String getPersonName(Integer id) {
        System.out.println("我是getPersonName()方法");  
        return "xxx";  
    }
}  

4. 編寫切面類,幷包含@Aspect註解

切面類用於實現切面功能,切面首先是一個IOC中的bean,即加入@Component註解,切面還需要加入@Aspect註解

package com.spring.aop.impl;
 
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
 
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
 
//指定切面的優先順序,當有多個切面時,數值越小優先順序越高
@Order(1)
//把這個類宣告為一個切面:需要把該類放入到IOC容器中。再宣告為一個切面.
@Aspect
@Component
public class LoggingAspect {
 
    /**
     * 宣告切入點表示式,一般在該方法中不再新增其他程式碼。
     * 使用@Pointcut來宣告切入點表示式。
     * 後面的通知直接使用方法名來引用當前的切入點表示式。
     */
    @Pointcut("execution(public int com.spring.aop.impl.PersonServiceBean.*(..))")//此處定義了其切入點
    public void declareJoinPointExpression() {}
 
    /**
    *前置通知,在目標方法開始之前執行。
    *@Before("execution(public int com.spring.aop.impl.PersonServiceBean.add(int, int))")這樣寫可以指定特定的方法。
     * @param joinpoint
     */
    @Before("declareJoinPointExpression()")
    //這裡使用切入點表示式即可。後面的可以都改成切入點表示式。如果這個切入點表示式在別的包中,在前面加上包名和類名即可。
    public void beforeMethod(JoinPoint joinpoint) {
        String methodName = joinpoint.getSignature().getName();
        List<Object>args = Arrays.asList(joinpoint.getArgs());
        System.out.println("前置通知:The method "+ methodName +" begins with " + args);
    }
 
    /**
    *後置通知,在目標方法執行之後開始執行,無論目標方法是否丟擲異常。
    *在後置通知中不能訪問目標方法執行的結果。
     * @param joinpoint
     */
    @After("execution(public int com.spring.aop.impl.PersonServiceBean.*(int, int))")
    public void afterMethod(JoinPoint joinpoint) {
        String methodName = joinpoint.getSignature().getName();
        //List<Object>args = Arrays.asList(joinpoint.getArgs());  後置通知方法中可以獲取到引數
        System.out.println("後置通知:The method "+ methodName +" ends ");
    }
     
    /**
    *返回通知,在方法正常結束之後執行。
    *可以訪問到方法的返回值。
     * @param joinpoint
     * @param result 目標方法的返回值
     */
    @AfterReturning(value="execution(public int com.spring.aop.impl.PersonServiceBean.*(..))", returning="result")
    public void afterReturnning(JoinPoint joinpoint, Object result) {
        String methodName = joinpoint.getSignature().getName();
        System.out.println("返回通知:The method "+ methodName +" ends with " + result);
    }
     
    /**
    *異常通知。目標方法出現異常的時候執行,可以訪問到異常物件,可以指定在出現特定異常時才執行。
    *假如把引數寫成NullPointerException則只在出現空指標異常的時候執行。
     * @param joinpoint
     * @param e
     */
    @AfterThrowing(value="execution(public int com.spring.aop.impl.PersonServiceBean.*(..))", throwing="e")
    public void afterThrowing(JoinPoint joinpoint, Exception e) {
        String methodName = joinpoint.getSignature().getName();
        System.out.println("異常通知:The method "+ methodName +" occurs exception " + e);
    }
     
    /**
     * 環繞通知類似於動態代理的全過程,ProceedingJoinPoint型別的引數可以決定是否執行目標方法。
     * @param point 環繞通知需要攜帶ProceedingJoinPoint型別的引數。
     * @return 目標方法的返回值。必須有返回值。
     */
     /*不常用
    @Around("execution(public int com.spring.aop.impl.PersonServiceBean.*(..))")
    public Object aroundMethod(ProceedingJoinPoint point) {
        Object result = null;
        String methodName = point.getSignature().getName();
        try {
            //前置通知
            System.out.println("The method "+ methodName +" begins with " + Arrays.asList(point.getArgs()));
            //執行目標方法
            result = point.proceed();
            //翻譯通知
            System.out.println("The method "+ methodName +" ends with " + result);
        } catch (Throwable e) {
            //異常通知
            System.out.println("The method "+ methodName +" occurs exception " + e);
            throw new RuntimeException(e);
        }
        //後置通知
        System.out.println("The method "+ methodName +" ends");
        return result;
    }
    */
}

5. 測試類

package com.spring.aop.aspect;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
import cn.spring.aop.service.imp.UserService;
import cn.spring.mvc.bean.User;
 
/**
 * Spring AOP測試
 * @author Shenghany
 * @date 2013-5-28
 */
public class Tester {
 
    private final static Log log = LogFactory.getLog(Tester.class);
    public static void main(String[] args) {
        //啟動Spring容器
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //獲取service元件
        UserService service = (UserService) context.getBean("userService");
        //以普通的方式呼叫UserService物件的三個方法
        User user = service.get(1L);
        service.save(user);
        try {
            service.delete(1L);
        } catch (Exception e) {
            if(log.isWarnEnabled()){
                log.warn("Delete user : " + e.getMessage());
            }
        }
    }
}

執行可知其達到了AOP的目的。

4.3 零配置實現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方便,但不便釋出後修改。

Advice類程式碼如下:

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("----------異常通知----------");
    }
}

測試程式碼如下:

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();
    }
 
}

測試結果能夠滿足要求。

5. Spring 中的advice和aspect的區別

5.1 含義區別

— /切 面(Aspect):一個關注點的模組化,這個關注點實現可能另外橫切多個物件。事務管理是J2EE應用中一個很好的橫切關注點例子。方面用springAdvisor或攔截器實現。
— 通知(Advice):在特定的連線點,AOP框架執行的動作。各種型別的通知包括“around”“before”“throws”通知。
— 切入點(Pointcut):指定一個通知將被引發的一系列連線點的集合。AOP框架必須允許開發者指定切入點,例如,使用正則表示式。

— 連線點/織入點(Joinpoint):程式執行過程中明確的點,如方法的呼叫或特定的異常被丟擲。

所以“<aop:aspect>”實際上是定義橫切邏輯,就是在連線點上做什麼,“<aop:advisor>”則定義了在哪些連線點應用什麼<aop:aspect>Spring這樣做的好處就是可以讓多個橫切邏輯(即<aop:aspect>定義的)多次使用,提供可重用性。 

1Advisor是一種特殊的AspectAdvisor代表spring中的Aspect 
2、區別:advisor只持有一個Pointcut和一個advice,而aspect可以多個pointcut和多個advice

5.2 advisor和aspect的使用區別

spring的配置中,會用到這兩個標籤.那麼他們的區別是什麼呢?

<bean id="testAdvice" class="com.myspring.app.aop.MyAdvice"/> //切面程式碼

分別使用aspect和advisor定義一個aspect如下:

<aop:config>
        <aop:aspect ref="testAdvice" id="testAspect">
            <aop:pointcut expression="(execution(* com.myspring.app.aop.TestPoint.*(..)))" id="testPointcut"/>
            <aop:before  method="doBefore" pointcut-ref="testPointcut"/>
        </aop:aspect>
    </aop:config>
 
    <aop:config>
        <aop:pointcut expression="(execution(* com.myspring.app.aop.TestPoint.*(..)))"  id="mypoint"/>
        <aop:advisor advice-ref="testAdvice" pointcut-ref="mypoint"/>
    </aop:config>

使用程式碼如下:

package com.myspring.app.aop;
 
import java.lang.reflect.Method;
 
import org.aspectj.lang.JoinPoint;
import org.springframework.aop.MethodBeforeAdvice;
 
/**
 * 方法前置通知
 * @author Michael
 *
 */
@Component("myAdvice")//如果是自動裝配,在定義切面的時候直接寫在ref屬性裡就可以了
public class MyAdvice implements MethodBeforeAdvice{
    //如果使用aop:advisor配置,那麼切面邏輯必須要實現advice接口才行!否則會失敗!
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("前置通知");
    }
 
    //如果是<aop:aspect>配置,編寫一般的方法就可以了,然後在切面配置中指定具體的方法名稱!
    public void doBefore(JoinPoint point) throws Throwable {
 
    }
}


兩者的區別在於:

1. 如果使用aop:advisor配置,那麼切面邏輯必須要實現advice接口才行!否則會失敗。
2. 如果是aop:aspect配置,編寫一般的方法就可以了,然後在切面配置中指定具體的方法名稱。