天天看點

Spring實作AOP之四種方式詳解

AOP:Aspect Oriented Programming 面向切面程式設計

  AOP,可以通過預編譯方式和運作其動态代理實作在不修改源代碼的情況下給程式動态統一添加某種特定功能的一種技術。也是Spring的兩大特色IOC(Inversion of Controler控制反轉)和AOP之一.

Spring中提供了四種實作AOP的方式:實作Spring提供的AOP接口

  1.JDK提供的代理方式(又分為靜态和動态)

  2.原生Spring API接口

  3.Spring純配置方式

  4.Spring注解

一.使用JDK提供的代理方式(此種方式不依賴于Spring架構)

  1.1靜态代理

  我們通過一個小女孩去看電影的生活案例來說明,上源碼~

  interface GoOut,先把案例抽象成接口:

package com.ndkj.StaticProxy;

/**女孩看電影的接口類*/
public interface GoOut {
9           
    public void lookMovie();
    
}

           

  class AGril,在抽象接口的基礎上具體出小女孩的類:

package com.ndkj.StaticProxy;

/**小女孩實作類*/
public class AGirl implements GoOut{

    @Override
    public void lookMovie() {
        System.out.println("我要看電影~!!");
    }
    
}

           

  到這裡就是我們完全不使用AOP思想進行類實作的流程,接下來就要開始進行AOP實作了,我們想在不改變AGril類的基礎上,在女孩看電影前為女孩加上更加詳細的步驟,例如先準備号money,先買一點零食(假設女孩去的電影院可以自己帶零食~),先坐公共汽車…

  Class Proxy,建立一個代理類,此類也需要實作小女孩的接口,它将幫助小女孩完成看電影中所有的工作:

package com.ndkj.StaticProxy;

/**代理類*/
public class Proxy implements GoOut{
	
	//通過引入女孩類的方式實作類的關聯
    private AGirl AGirl;

    @Override
    public void lookMovie() {
        takeMoney();
        takeBus();
        AGirl.lookMovie();
        goHome();
    }

    //帶上錢~
    public void takeMoney(){
        System.out.println("看電影前帶上Money~!");
    }

    //坐大巴~
    public void takeBus(){
        System.out.println("坐公共汽車去看電影~");
    }

    //買一點吃的!
    public void buySemeFood(){
        System.out.println("看電影前買一些爆米花!");
    }

    //高高興興回家~~
    public void goHome(){
        System.out.println("今天的電影真好看~回家!!");
    }

    public Proxy() {
    }

    public Proxy(AGirl AGirl) {
        this.AGirl = AGirl;
    }
}

           

  至此我們的準備工作已經做完了~可以放心讓小女孩去看電影了!

  Class LetsGo測試類:

package com.ndkj.StaticProxy;

/**測試*/
public class LetsGo {
    public static void main(String[] args) {
    
        AGirl AGirl = new AGirl();
        //将AGril類導入到代理類中,全程讓代理類幫我們實作
        Proxy proxy = new Proxy(AGirl);
        proxy.lookMovie();
        
    }
}

           

No Bug,No Error:

Spring實作AOP之四種方式詳解

  2.2動态代理

  在上面的靜态代理中,每有一個新的類産生,我們都需要寫一個代理類來代理,針對靜态代理的弊端,我們使用動态代理.動态代理的實際實作類并不需要我們書寫,我們通過寫代理類的處理類來自動生成實際代理類,它差別于靜态代理于此.

  我們把之前的接口和類搬下來:

package com.ndkj;

/**極度抽象的接口*/
public interface GoOut{
    
    public void lookMovie();
    
}

           

  Class AGril:

package com.ndkj;

/**小女孩類*/
public class AGril implements GoOut {
    
    @Override
    public void lookMovie() {
        System.out.println("我出去看電影~");
    }
    
}

           

  在使用動态代理時,我們的代理類需要實作InvocationHandler類,通過這個類來動态生成實際的代理類.上代碼:

package com.ndkj;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

//我們會使用這個類自動生成代理類,代替AGril類來完成小女孩看電影的全部操作
public class ProxyInvocationHandler implements InvocationHandler {

    //被代理的接口
    private AGril aGril;

    public void setRent(AGril aGril) {
        this.aGril = aGril;
    }

    //生成得到代理類
    public Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), aGril.getClass().getInterfaces(),this );
    }

    /**此方法為目标類的代理類,處理代理執行個體,并傳回結果*/
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //動态代理的本質,就是使用反射機制實作
        takeMoney();
        buyFood();
        Object result = method.invoke(aGril, args);
        backHome();
        return null;
    }

    //帶上錢~
    public void takeMoney(){
        System.out.println("先帶上錢~");
    }

    //買爆米花
    public void buyFood(){
        System.out.println("買了一些爆米花!");
    }

    //回家
    public void backHome(){
        System.out.println("回家!!");
    }

}

           

  測試again:

package com.ndkj;

public class Client {
    public static void main(String[] args) {
        //真實角色
        AGril aGril = new AGril();

        //代理角色
        ProxyInvocationHandler pih = new ProxyInvocationHandler();
        pih.setRent(aGril);
        GoOut proxy = (GoOut) pih.getProxy();  //這裡的proxy就是動态生成的,我們并沒有寫他

        proxy.lookMovie();
    }
}

           

  no Bug~

Spring實作AOP之四種方式詳解

二.通過實作Spring提供的AOP接口

  Spring架構為我們提供了多種實作AOP的方式,這是Spring提供的方式之一.

放上源碼:

  interface UserService:

package com.ndkj.service;

public interface UserService {
    public void add();
    public void delete();
    public void update();
    public void query();
}

           

class UserServiceImpl接口實作類:

package com.ndkj.service;

public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("增加了一個使用者!");
    }

    @Override
    public void delete() {
        System.out.println("删除了一個使用者!");
    }

    @Override
    public void update() {
        System.out.println("修改了一個使用者!");
    }

    @Override
    public void query() {
        System.out.println("查詢了一個使用者!");
    }
}

           

  要使用Spring提供的接口來實作AOP,我們需要繼承相應的功能增強的Spring接口,以下以前置通知MethodBeforeAdvice接口,後置通知AfterReturningAdvice為例,來實作為業務方法提供日志功能:

  Class BeforeLog前置通知類,實作MethodBeforeAdvice接口:

package com.ndkj.log;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class BeforeLog implements MethodBeforeAdvice {
    //method:要執行的目标對象的方法
    //args:參數
    //target:目标對象
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("[前置Advice]: "+target.getClass().getName()+"的"+method.getName()+"方法被執行了");
    }
}

           

  Class AfterLog後置通知類,實作AfterReturningAdvice接口:

package com.ndkj.log;

import org.springframework.aop.AfterAdvice;
import org.springframework.aop.AfterReturningAdvice;

import java.lang.reflect.Method;

public class AfterLog implements AfterReturningAdvice {

    //returnValue:切入目标方法的傳回值,切入的後置方法可以對傳回值進行操作
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("[後置Advice]: 執行了"+method.getName()+"方法,傳回值為"+returnValue);
    }

}

           

 &emsp後置通知在;重寫afterReturning時會傳入Object returnValue參數,它可以提供目标業務方法的傳回值.

  至此我們需要準備的類和接口都已經準備完畢,接下來還需要配置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:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="userServiceImpl" class="com.ndkj.service.UserServiceImpl"/>
    <bean id="beforeLog" class="com.ndkj.log.BeforeLog"/>
    <bean id="afterLog" class="com.ndkj.log.AfterLog"/>

    <!--AOP實作方式之一:使用原生Spring API接口-->
    <!--配置AOP,在xml檔案中引用AOP功能,必須先導入AOP的頭檔案依賴-->
    <!--xmlns:aop="http://www.springframework.org/schema/aop"-->
    <aop:config>
        <!--切入點:id切入點的id,expression:确定要切入的地方和其詳細資訊
        expression()-->
        <aop:pointcut id="pointcut" expression="execution(* com.ndkj.service.UserServiceImpl.*(..))"/>

        <!--執行環繞增強-->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>

    </aop:config>

</beans>
           

  至此使用Spring原生API接口實作AOP已經全部配置完畢,我們繼續寫一個測試類:

package com.ndkj.service;

import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyTest {
    @Test
    public void AOPTest(){
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) context.getBean("userServiceImpl");
        userService.add();
    }
}

           

  注意:在getBean得到執行個體化對象的必須傳回和強轉該對象的接口類型,不能使用實作類的類型.假設傳回實作類類型UserServiceImpl,那麼這個傳回的實作類對象userService就會出現兩種不同的表現形式(一種是沒有功能增強的,一種是具備功能增強的),對于一個固定的類來說,這顯然是自相沖突的.

Spring實作AOP之四種方式詳解

三.使用Spring純配置實作

  使用純配置實作比使用Spring原生API接口要簡單,它不需要繼承接口,僅僅是一個簡單的類而已~

  我們繼續把接口和實作類拷下來:

  interface UserService:

package com.ndkj.service;

public interface UserService {
    public void add();
    public void delete();
    public void update();
    public void query();
}

           

  Class UserServiceImpl:

package com.ndkj.service;

public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("增加了一個使用者!");
    }

    @Override
    public void delete() {
        System.out.println("删除了一個使用者!");
    }

    @Override
    public void update() {
        System.out.println("修改了一個使用者!");
    }

    @Override
    public void query() {
        System.out.println("查詢了一個使用者!");
    }
}

           

接下來我們建立一個切面類,注意以下切面類沒有使用任何AOP注解和實作接口,這僅僅是一個普通類。

  Class DiyAspect:

package com.ndkj.diy;

public class DiyAspect {

    public void before(){
        System.out.println("DIY-AOP前置方法執行");
    }

    public void after(){
        System.out.println("DIY-AOP後置方法執行");
    }

}

           

  我們需要使用Spring純配置方式來實作AOP,接下來是使用純配置實作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:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean id="diyAspect" class="com.ndkj.diy.DiyAspect"/>
    <bean id="userServiceImpl" class="com.ndkj.service.UserServiceImpl"/>

    <aop:config>
        <aop:aspect ref="diyAspect">
            <aop:pointcut id="point" expression="execution(* com.ndkj.service.UserServiceImpl.*(..))"/>
            <aop:before method="before" pointcut-ref="point"/>
            <aop:after method="after" pointcut-ref="point"/>
        </aop:aspect>

    </aop:config>

</beans>
           

  至此我們完成了使用純配置方式實作了AOP:

package com.ndkj.service;

import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyTEst {
    @Test
    public void AOPTest(){
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = (UserService) context.getBean("userServiceImpl");
        userService.add();
    }
}

           

  一切正常~

Spring實作AOP之四種方式詳解

四.使用注解實作AOP

  注解方式分為純注解(完全不依賴配置檔案bean的AOP)和依賴(或部分依賴)配置檔案bean的注解方式AOP,需要注意兩者都需要在配置檔案中做設定,例如:

<?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4     xmlns:p="http://www.springframework.org/schema/p"
 5     xmlns:context="http://www.springframework.org/schema/context"
 6     xmlns:aop="http://www.springframework.org/schema/aop"
 7     xsi:schemaLocation="
 8         http://www.springframework.org/schema/beans
 9         http://www.springframework.org/schema/beans/spring-beans.xsd
10         http://www.springframework.org/schema/context
11         http://www.springframework.org/schema/context/spring-context.xsd
12         http://www.springframework.org/schema/aop
13         http://www.springframework.org/schema/aop/spring-aop.xsd">
14 
15       <!-- 開啟注解掃描,此方式可以在類上使用@ComponentScan("com...")替代 -->
16       <context:component-scan base-package="com.bie.aop"></context:component-scan>   
17         
18       <!-- 開啟aop注解方式,預設為false -->    
19       <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
20       
21 </beans> 
           

  使用注解方式AOP,我們需要在進行橫切的類和方法上寫明注釋,切面類上使用@Aspect,切面類的橫切方法上使用@After(),@Before(),@AfterThrowing(),@AfterReturning(),@Around()注解進行标注該切面方法的切入時機。

  通知類型介紹

    (1)Before:在目标方法被調用之前做增強處理,@Before隻需要指定切入點表達式即可

    (2)AfterReturning:在目标方法正常完成後做增強,@AfterReturning除了指定切入點表達式後,還可以指定一個傳回值形參名returning,代表目标方法的傳回值

    

    (3)AfterThrowing:主要用來處理程式中未處理的異常,@AfterThrowing除了指定切入點表達式後,還可以指定一個throwing的傳回值形參名,可以通過該形參名

來通路目标方法中所抛出的異常對象

    (4)After:在目标方法完成之後做增強,無論目标方法時候成功完成。@After可以指定一個切入點表達式

    (5)Around:環繞通知,在目标方法完成前後做增強處理,環繞通知是最重要的通知類型,像事務,日志等都是環繞通知,注意程式設計中核心是一個ProceedingJoinPoint

  例如:

package com.ndkj.diy;

/**@Aspect:放在類的上面,标注這個類是一個切面類,也就是給業務方法附加的類,
 * 相當于使用純配置方式中的<aop:aspect ref="diyAspect">*/
@Aspect
public class Log {

    /**使用@Before來标記切入點,放在方法的上面*/
    @Before("execution(* com.ndkj.service.UserServiceImpl.*(..))")
    public void addBeforeLog(){
        System.out.println("[@Before]By Annotation to AOP:" + new Date());
    }

    @After("execution(* com.ndkj.service.UserServiceImpl.*(..))")
    public void addAfterLog(){
        System.out.println("[@After]By Annotation to AOP:" + new Date());
    }

    /**在使用環繞注解時,可以傳入ProceedingJoinPoint的對象來擷取業務方法的傳回值*/
    @Around("execution(* com.ndkj.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint pj) throws Throwable {
        System.out.println("[@Around]By Annotation to AOP:"+"@Around前");
        UserService userService =(UserService) pj.proceed();
        System.out.println("[@Around]By Annotation to AOP:"+"@Around後userServiceImpl傳回值"+userService);
    }

}

}

           

  測試結果:

Spring實作AOP之四種方式詳解

  我們可以在需要切入的業務方法上加上@PointCut定義一個切入點,這樣在切入方法中引入execution的包名中可以直接使用定義的pointcut點進行定位。

寫作時間緊迫,可能有疏漏或不嚴謹之處,歡迎讀者交流指正。