第六章 AOP編程

target

掌握AOP概念和相關(guān)術(shù)語
會(huì)編寫基于XML的AOP編程和基于注解的AOP編程
理解MethodBeforeAdvice接口和execution表達(dá)式的使用
理解AOP底層原理
了解AOP中的一個(gè)"坑"

1. AOP概述

AOP (Aspect Oriented Programing) ?向切?編程 ,aop編程本質(zhì)上就是Spring動(dòng)態(tài)代理開發(fā)

以切?為基本單位的程序開發(fā),通過切?間的彼此協(xié)同,相互調(diào)?,完成程序的構(gòu)建

切? = 切?點(diǎn) + 額外功能

OOP (Object Oritened Programing) ?向?qū)ο缶幊?,比如:Java

以對(duì)象為基本單位的程序開發(fā),通過對(duì)象間的彼此協(xié)同,相互調(diào)?,完成程序的構(gòu)建

POP (Producer Oriented Programing) ?向過程(?法、函數(shù))編程 ,比如:C語言

以過程為基本單位的程序開發(fā),通過過程間的彼此協(xié)同,相互調(diào)?,完成程序的構(gòu)建

舉個(gè)例子來說明一下:

如果有一個(gè)UserService類,里面有這樣三個(gè)方法:addUser()、updateUser()、deleteUser()?,F(xiàn)在希望給這三個(gè)方法都加上事務(wù),但是不能修改這個(gè)類的源碼。所以我們想到寫一個(gè)子類去繼承他,然后修改子類就好了。

SonUserService類繼承UserService類,子類里可以直接調(diào)用父類的方法。比如:addUser()方法我就可以先調(diào)用父類的addUser()方法:super.addUser()。然后在前面開啟事務(wù),在后面提交事務(wù)。

我們發(fā)現(xiàn),后面兩個(gè)方法依次都要這么做,而且開啟事務(wù)、提交事務(wù)不斷重復(fù)出現(xiàn)。

這是我們以前寫的東西,我們稱之為縱向繼承。這就是傳統(tǒng)的oop思想。


縱向繼承

現(xiàn)在有一個(gè)新的解決方案:

既然開啟事務(wù)和提交事務(wù)是重復(fù)代碼,可以把它抽取出來放在一個(gè)類里面。

寫一個(gè)A類,before()方法里面寫開啟事務(wù),after()方法里面寫提交事務(wù)。我希望A類的方法都要作用到UserService類的相應(yīng)方法的前后去,此時(shí)就可以用到代理。寫一個(gè)代理類,把這兩個(gè)無關(guān)的類建立起關(guān)系,就像房產(chǎn)中介就是代理類,他把買房子的和買房子的建立起關(guān)系。代理類把左邊的代碼拿過來,把右邊的代碼也拿過來,然后組合在一起。所以代理類這樣寫:

A.before();
UserService.addUser();
A.after();

有人可能說,你這么寫完和剛才有啥區(qū)別?不是一樣了嗎?

不一樣的。Spring提供了動(dòng)態(tài)代理,代理類其實(shí)是Spring去做的,我們只需要將A類寫好,UserService類寫好,然后都交給Spring去做了,這樣我們做的事就僅僅是業(yè)務(wù)邏輯了,該有的功能都有,但是代碼量卻少了。這就說我們即將學(xué)習(xí)的AOP。

AOP有如下特點(diǎn)和應(yīng)用:

  • 經(jīng)典應(yīng)用:性能檢測(cè)、事務(wù)管理、安全檢查、緩存。

  • Spring AOP使用純Java實(shí)現(xiàn),不需要專門的編譯過程和類加載器,在運(yùn)行期通過代理方式向目標(biāo)類織入增強(qiáng)代碼。

1.1 AOP定義

AOP的概念: 本質(zhì)就是Spring的動(dòng)態(tài)代理開發(fā),通過代理類為原始類增加額外功能。 好處: 利于原始類的維護(hù),減少代碼量,使開發(fā)人員專注于業(yè)務(wù)邏輯的開發(fā)。 注意: AOP編程不可能取代OOP,OOP編程有益補(bǔ)充。

1.2 AOP術(shù)語

AOP術(shù)語
  • target:目標(biāo)類,需要被代理的類。也就是上圖的UserService類。

  • JointPoint:連接點(diǎn),指可能被攔截到的方法。例如:所有的方法。

  • PointCut:切入點(diǎn),已經(jīng)被增強(qiáng)的方法。例如:addUser()

    怎么記連接點(diǎn)和切入點(diǎn):

    比如:洗手間所有的馬桶就是連接點(diǎn),正在被使用的馬桶就是切入點(diǎn)

  • advice:通知/增強(qiáng),增強(qiáng)的代碼。例如:before()、after()

  • Weaving:織入,把增強(qiáng)advice應(yīng)用到目標(biāo)對(duì)象target來創(chuàng)建新的代理對(duì)象Proxy的過程。

  • Proxy:代理類

  • aspect:切面,切入點(diǎn)和通知的結(jié)合。

1.3 4種advice

如果要對(duì)一個(gè)切入點(diǎn)進(jìn)行增強(qiáng),首先考慮的應(yīng)該是在切入點(diǎn)的什么位置進(jìn)行增強(qiáng),是在切入點(diǎn)前面增強(qiáng),還是后面,還是前后一起?

通知類型 需要實(shí)現(xiàn)的接口 接口中的方法 執(zhí)行時(shí)機(jī)
前置通知 org.springframework.aop.MethodBeforeAdvice before() 目標(biāo)方法之前
后置通知 org.springframework.aop.AfterReturningAdvice afterReturning() 目標(biāo)方法執(zhí)行后
異常通知 org.springframework.aop.ThrowsAdvice 目標(biāo)方法發(fā)生異常時(shí)
環(huán)繞通知 org.aopalliance.intercept.MethodInterceptor invoke() 調(diào)用目標(biāo)方法的整個(gè)過程

2. 基于xml的AOP編程

新建Java項(xiàng)目:Spring-06

2.1 前置通知

前置通知需要實(shí)現(xiàn)MethodBeforeAdvice接口。

① 代碼實(shí)現(xiàn)

第一步:導(dǎo)入jar

  • aopalliance.jar

  • aspectjweaver.jar

  • commons-logging-1.2.jar

  • spring-aop-4.3.9.RELEASE.jar

  • spring-beans-4.3.9.RELEASE.jar

  • spring-context-4.3.9.RELEASE.jar

  • spring-core-4.3.9.RELEASE.jar

  • spring-expression-4.3.9.RELEASE.jar

注意:必須有 aopalliance 和 aspectjweaver 的支持,否則 aop功能無法實(shí)現(xiàn)。

第二步:編寫目標(biāo)類

  • UserService.java
package com.lee.spring.service;

public interface UserService {
    void addUser();
    void updateUser();
    boolean deleteUser(int no);
}
  • UserServiceImpl.java
package com.lee.spring.service.impl;

public class UserServiceImpl implements UserService {

    @Override
    public void addUser() {
        System.out.println("addUser...");
    }

    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...");
    return true;
    }

}

第三步:編寫通知類BeforeAdvice.java

package com.lee.spring.aop;

public class BeforeAdvice implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("前置通知執(zhí)行了。。。。");
    }
    
}

第四步:編寫配置文件

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

    <!-- 將目標(biāo)類納入到IOC容器 -->
    <bean id="userService" class="com.lee.spring.service.impl.UserServiceImpl"></bean>

    <!-- 將通知類納入到IOC容器 -->
    <bean id="beforeAdvice" class="com.lee.spring.aop.BeforeAdvice"></bean>
    
    <!-- 配置aop -->
    <aop:config>
        <!-- 配置切入點(diǎn) -->
        <aop:pointcut expression="execution(void com.lee.spring.service.impl.UserServiceImpl.addUser())"  id="pointcut"/>
        <!-- 將切入點(diǎn)和通知連接起來 -->
        <!-- advisor就是顧問的意思,顧問就是給兩個(gè)點(diǎn)起一個(gè)橋梁作用 -->
        <aop:advisor advice-ref="beforeAdvice" pointcut-ref="pointcut"/>
    </aop:config>
</beans>

第五步:測(cè)試

@Test
public void test01() {
  //1.加載配置文件
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  //2. 獲取bean
  UserService userService  = (UserService)context.getBean("userService");
  userService.addUser();
  userService.deleteUser(1);
  userService.updateUser();
}

輸出:

前置通知執(zhí)行了。。。。
addUser...
deleteUser...
updateUser...

可以通過 debug的方式來查看 addUser()方法、deleteUser()、updateUser()方法:


Debug As 進(jìn)行啟動(dòng):


可以發(fā)現(xiàn),userService對(duì)象使用的就是代理對(duì)象。

擴(kuò)展:

如果我們想給delete方法也配置一個(gè)前置通知,直接修改配置文件就可以:

<!-- 配置aop -->
<aop:config>
  <!-- 配置切入點(diǎn) -->
  <aop:pointcut expression="execution(void com.lee.spring.service.UserServiceImpl.impl.addUser()) or execution( boolean com.lee.spring.service.UserServiceImpl.impl.deleteUser(int))"  id="pointcut"/>
  <!-- 將切入點(diǎn)和通知連接起來 -->
  <!-- advisor就是顧問的意思,顧問就是給兩個(gè)點(diǎn)起一個(gè)橋梁作用 -->
  <aop:advisor advice-ref="beforeAdvice" pointcut-ref="pointcut"/>
</aop:config>

② MethodBeforeAdvice詳解

MethodBeforeAdvice接?作?:額外功能運(yùn)?在原始?法執(zhí)?之前,進(jìn)?額外功能操作。

關(guān)于前置通知類BeforeAdvice中before()方法中的參數(shù)解釋

public class BeforeAdvice implements MethodBeforeAdvice {

  /**
        Method: 額外功能所增加給的那個(gè)原始?法
                比如:login?法、register?法、showOrder?法
        Object[]: 額外功能所增加給的那個(gè)原始?法的參數(shù)。
                比如:String name,String password、User
        Object: 額外功能所增加給的那個(gè)原始對(duì)象 
                比如:UserServiceImpl、OrderServiceImpl
  */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        
        System.out.println("method名字:" + method.getName());
        System.out.println("method參數(shù)個(gè)數(shù):" + method.getParameterCount());
        
        System.out.println( "args:" + Arrays.toString(args));
        
        System.out.println("target:" + target);
        
        System.out.println("前置通知執(zhí)行了。。。。");
    }
    
}

輸出:

method名字:deleteUser
method參數(shù)個(gè)數(shù):1
method的返回值類型:boolean
args:[1]
target:com.lee.service.UserServiceImpl@6ab7a896
前置通知執(zhí)行了。。。。

Method: 切入點(diǎn)(目標(biāo)方法)

Object[] args:原始方法的參數(shù)

Object target:原始對(duì)象或目標(biāo)對(duì)象(UserServiceImpl對(duì)象)

③ execution詳解

例: execution (* com.sample.service..*.*(..))

整個(gè)表達(dá)式可以分為五個(gè)部分:

1、execution():表達(dá)式主體。

2、第一個(gè)*號(hào):表示返回類型, *號(hào)表示所有的類型。

3、包名:表示需要攔截的包名,后面的兩個(gè)句點(diǎn)表示當(dāng)前包和當(dāng)前包的所有子包,com.sample.service包、子孫包下所有類的方法。

4、第二個(gè)*號(hào):表示類名,*號(hào)表示所有的類。

5、*(..):最后這個(gè)星號(hào)表示方法名,*號(hào)表示所有的方法,后面括弧里面表示方法的參數(shù),兩個(gè)句點(diǎn)表示任何參數(shù)

??:

  • boolean addStudent(com.lee.entity.Student)

    返回值類型為boolean,參數(shù)類型為com.lee.entity.Student的所有叫addStudent()方法。

  • boolean com.lee.service.StudentService.addStudent(com.lee.entity.Student)

    返回值為boolean,參數(shù)類型為com.lee.entity.Student,在com.lee.service.StudentService類下的addStudent方法。

  • * addStudent(com.lee.entity.Student)

    返回值任意,參數(shù)類型為com.lee.entity.Student的所有叫addStudent()方法。

  • void *(com.lee.entity.Student)

    返回值為void,參數(shù)類型為com.lee.entity.Student的任意方法。

  • * com.lee.service.*.*(..)

    返回值任意,com.lee.service包下的所有類下的所有方法,參數(shù)類型任意。(不包含子包)

  • * com.lee.service..*.*(..)

    返回值任意,com.lee.service包下的所有類下的所有方法,參數(shù)類型任意。(包含子包)

2.2 后置通知

后置通知需要實(shí)現(xiàn)AfterReturningAdvice接口。

第一步:導(dǎo)入jar

  • aopalliance.jar

  • aspectjweaver.jar

第二步:編寫目標(biāo)類

  • UserService.java
public interface UserService {
    void addUser();
    void updateUser();
    boolean deleteUser(int no);
}
  • UserServiceImpl.java
public class UserServiceImpl implements UserService {

    @Override
    public void addUser() {
        System.out.println("addUser...");
    }

    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...");
        return true;
    }

}

第三步:編寫通知類AfterAdvice.java

public class AfterAdvice implements AfterReturningAdvice {

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("返回值:" + returnValue);
        
        System.out.println("method方法名:" + method.getName());
        System.out.println("method參數(shù)個(gè)數(shù):" + method.getParameterCount());
        System.out.println("method返回值類型:" + method.getReturnType());
        
        System.out.println( "目標(biāo)對(duì)象:" +target);
        
        System.out.println("后置通知執(zhí)行。。。");
    }

}

第四步:編寫配置文件

<?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: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/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- 將目標(biāo)類納入到IOC容器 -->
    <bean id="userService" class="com.lee.service.UserServiceImpl"></bean>

    <!-- 將通知類納入到IOC容器 -->
    <bean id="afterAdvice" class="com.lee.aop.AfterAdvice"></bean>
    
    <!-- 配置aop -->
    <aop:config>
        <!-- 配置切入點(diǎn)(目標(biāo)方法) -->
        <aop:pointcut expression="execution(* com.lee.spring.service.UserServiceImpl.impl.updateUser()) or execution(* com.lee.spring.service.UserServiceImpl.impl.deleteUser(int))" id="pointcut"/>
        <!-- 配置顧問(將目標(biāo)方法和通知連接) -->
        <aop:advisor advice-ref="afterAdvice" pointcut-ref="pointcut"/>
    </aop:config>
    
</beans>

第五步:測(cè)試

@Test
    public void test02() {
        //1.加載配置文件
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2. 獲取bean
        UserService userService  = (UserService)context.getBean("userService");
        userService.addUser();
        System.out.println("-----------");
        userService.deleteUser(1);
        System.out.println("-------------");
        userService.updateUser();
    }

輸出:

addUser...
-----------
deleteUser...
返回值:true
method方法名:deleteUser
method參數(shù)個(gè)數(shù):1
method返回值類型:boolean
目標(biāo)對(duì)象:com.lee.service.UserServiceImpl@fdefd3f
后置通知執(zhí)行。。。
-------------
updateUser...
返回值:null
method方法名:updateUser
method參數(shù)個(gè)數(shù):0
method返回值類型:void
目標(biāo)對(duì)象:com.lee.service.UserServiceImpl@fdefd3f
后置通知執(zhí)行。。。

2.3 異常通知

異常通知需要實(shí)現(xiàn)ThrowsAdvice接口。

第一步:導(dǎo)入jar

  • aopalliance.jar

  • aspectjweaver.jar

第二步:編寫目標(biāo)類

  • UserService.java
public interface UserService {
    void addUser();
    void updateUser();
    boolean deleteUser(int no);
}
  • UserServiceImpl.java
public class UserServiceImpl implements UserService {

    @Override
    public void addUser() {
        System.out.println("addUser...");
    }

    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...");
        return true;
    }

}

第三步:編寫通知類

public class ExceptionAdvice implements ThrowsAdvice {

    public void afterThrowing(Method method, Object[] args, Object target, Exception ex){
        
        System.out.println("method方法名:" + method.getName());
        System.out.println("method方法參數(shù)個(gè)數(shù):" + method.getParameterCount());
        System.out.println("method方法默認(rèn)值:" + method.getDefaultValue());
        
        System.out.println("args" +Arrays.toString( args));
        
        System.out.println("目標(biāo)對(duì)象:" + target);
        
        System.out.println("異常信息:" + ex.getMessage());
        
    }

}

我們發(fā)現(xiàn)ThrowsAdvice接口中沒有聲明方法,但是文檔中有提示,告訴我們必須寫一個(gè)這樣的方法:


第四步:編寫配置文件

<?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: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/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- 將目標(biāo)類納入到IOC容器 -->
    <bean id="userService" class="com.lee.service.UserServiceImpl"></bean>

    <!-- 將通知類納入到IOC容器 -->
    <bean id="excetionAdvice" class="com.lee.aop.ExceptionAdvice"></bean>
    
    <!-- 配置aop -->
    <aop:config>
        <!-- 配置切入點(diǎn)(目標(biāo)方法) -->
        <aop:pointcut expression="execution(* com.lee.service.UserServiceImpl.updateUser()) or execution(* com.lee.service.UserServiceImpl.deleteUser(int))" id="pointcut"/>
        <!-- 配置顧問(將目標(biāo)方法和通知連接) -->
        <aop:advisor advice-ref="excetionAdvice" pointcut-ref="pointcut"/>
    </aop:config>
    
</beans>

我們將updateUser和deleteUser方法配置了增強(qiáng)。

第五步:測(cè)試

@Test
public void testBeforeAdvice() {
  //1.加載配置文件
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  //2. 獲取bean
  UserService userService  = (UserService)context.getBean("userService");
  userService.addUser();
  System.out.println("-----------");
  userService.deleteUser(1);
  System.out.println("-------------");
  userService.updateUser();
}

輸出:

addUser...
-----------
deleteUser...
-------------
updateUser...

發(fā)現(xiàn),增強(qiáng)沒有起作用。這是由于切入點(diǎn)(目標(biāo)方法)沒有異常,所以異常通知不會(huì)觸發(fā)。

修改目標(biāo)方法:給UserServiceImpl類中deleteUser()方法制造一個(gè)異常

@Override
public boolean deleteUser(int no) {
  int i = 1/0;
  System.out.println("deleteUser...");
  return true;
}

輸出:

addUser...
-----------
method方法名:deleteUser
method方法參數(shù)個(gè)數(shù):1
method方法默認(rèn)值:null
args[1]
目標(biāo)對(duì)象:com.lee.service.UserServiceImpl@11dc3715
異常信息:/ by zero

發(fā)現(xiàn),異常通知觸發(fā),而且程序也拋出了異常,并且終止在發(fā)生異常的代碼處,后面的代碼不會(huì)繼續(xù)執(zhí)行。

2.4 環(huán)繞通知

看名字就知道環(huán)繞通知是環(huán)繞著目標(biāo)方法的,既然是環(huán)繞,所有他能實(shí)現(xiàn)前置通知、后置通知和異常通知。

環(huán)繞通知需要實(shí)現(xiàn)MethodInterceptor接口。

環(huán)繞通知的本質(zhì)是攔截器。

下面看一下案例實(shí)現(xiàn):

第一步:導(dǎo)入jar

  • aopalliance.jar

  • aspectjweaver.jar

第二步:編寫目標(biāo)類

  • UserService.java
public interface UserService {
    void addUser();
    void updateUser();
    boolean deleteUser(int no);
}
  • UserServiceImpl.java
public class UserServiceImpl implements UserService {

    @Override
    public void addUser() {
        System.out.println("addUser...");
    }

    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...");
        return true;
    }

}

第三步:編寫通知類RoundAdvice.java

public class RoundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        /*
         * invocation里包含著目標(biāo)方法的全部信息
         * 包括:方法名、參數(shù)個(gè)數(shù)、參數(shù)值、返回值。。。。
         * 甚至可以控制目標(biāo)方法是否執(zhí)行
         */
        Object result = null;
        try {
            //1.invocation.proceed()之前的代碼是前置通知
            System.out.println("環(huán)繞通知實(shí)現(xiàn)的前置通知。。。");
            System.out.println("目標(biāo)方法名:" + invocation.getMethod().getName());
            result = invocation.proceed();//控制目標(biāo)方法的執(zhí)行
            //2.invocation.proceed()之后的代碼是后置通知
            System.out.println("環(huán)繞通知實(shí)現(xiàn)的后置通知。。。");
            System.out.println("目標(biāo)對(duì)象:" + invocation.getThis());
            System.out.println("返回值:" + result);
        }catch (Exception e) {
            //3.異常通知
            System.out.println("環(huán)繞通知實(shí)現(xiàn)的異常通知。。。");
        }
        /*
         * 返回值是目標(biāo)方法的返回值
         * 由于環(huán)繞通知擁有目標(biāo)方法的絕對(duì)控制權(quán),他甚至可以偷梁換柱:將目標(biāo)方法的返回值進(jìn)行更改
         */
        return result;
    }

}

第四步:編寫配置文件

<?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: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/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- 將目標(biāo)類納入到IOC容器 -->
    <bean id="userService" class="com.lee.service.UserServiceImpl"></bean>

    <!-- 將通知類納入到IOC容器 -->
    <bean id="roundAdvice" class="com.lee.aop.RoundAdvice"></bean>
    
    <!-- 配置aop -->
    <aop:config>
        <!-- 配置切入點(diǎn)(目標(biāo)方法) -->
        <aop:pointcut expression="execution(* com.lee.service.UserServiceImpl.deleteUser(int))" id="pointcut"/>
        <!-- 配置顧問(將目標(biāo)方法和通知連接) -->
        <aop:advisor advice-ref="roundAdvice" pointcut-ref="pointcut"/>
    </aop:config>
    
</beans>

第五步:測(cè)試

@Test
public void test04() {
  //1.加載配置文件
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  //2. 獲取bean
  UserService userService  = (UserService)context.getBean("userService");
  userService.addUser();
  System.out.println("-----------");
  userService.deleteUser(1);
  System.out.println("-------------");
  userService.updateUser();
}

輸出:

addUser...
-----------
環(huán)繞通知實(shí)現(xiàn)的前置通知。。。
目標(biāo)方法名:deleteUser
deleteUser...
環(huán)繞通知實(shí)現(xiàn)的后置通知。。。
目標(biāo)對(duì)象:com.lee.service.UserServiceImpl@1a0dcaa
返回值:true
-------------
updateUser...

如果想查看環(huán)繞通知實(shí)現(xiàn)的異常通知,將目標(biāo)方法設(shè)計(jì)一個(gè)異常就可以了。

3. 基于注解的aop

將一個(gè)類變成有特定功能的類,有四種做法:

  • 繼承類

  • 實(shí)現(xiàn)接口

  • 注解

  • 配置文件

下面我們說一下用注解方式實(shí)現(xiàn)的的aop:

3.1 前置通知

① 實(shí)現(xiàn)步驟

第一步:導(dǎo)入jar包

  • aspectjweaver.jar

第二步:編寫業(yè)務(wù)類

  • UserService.java
public interface UserService {
    void addUser(User student);
    void updateUser(User student);
    boolean deleteUser(int no);
}
  • UserServiceImpl
@Service("userService")
public class UserServiceImpl implements UserService {

    @Override
    public void addUser(User user) {
        System.out.println("addUser..." + user);
    }

    @Override
    public void updateUser(User user) {
        System.out.println("updateUser..." + user);
    }

    @Override
    public boolean deleteUser(int no) {
        System.out.println("deleteUser...學(xué)號(hào)是:" + no );
        return true;
    }
}

第三步:編寫通知類AnnotationAdvice.java

@Component
@Aspect
public class AnnotationAdvice {

  @Before("execution(* com.lee.service.*.addUser(..))")
  public void before() {
    System.out.println("前置通知執(zhí)行。。。");
  }
}

在類上加注解@Aspect,就表示把普通的類變成了通知類。

在方法上加注解@Before,就表示把普通的方法變成了前置方法。@Before里面需要些切入點(diǎn)的表達(dá)式。

第四步:編寫配置文件

<?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: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/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- 開啟包掃描 -->
    <context:component-scan base-package="com.lee"></context:component-scan>
    
    <!-- 開啟aop支持 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    
</beans>

如果要用注解開發(fā)aop,配置文件里必須開啟對(duì)aop的支持。

第五步:測(cè)試

@Test
public void testBeforeAdvice() {
  //1.加載配置文件
  ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
  //2. 獲取bean
  UserService userService  = (UserService)context.getBean("userService");
  User user = new User(1001, "zs");
  userService.addUser(user);
  System.out.println("-----------");
  userService.deleteUser(1);
  System.out.println("-------------");
  userService.updateUser(user);
}

輸出:

前置通知執(zhí)行。。。
addUser...User [no=1001, name=zs]
-----------
deleteUser...學(xué)號(hào)是:1
-------------
updateUser...User [no=1001, name=zs]

② 注意事項(xiàng)

  • 普通的類要變成通知類,需要在類上加注解@Aspect

  • 通知類里面的方法要變成前置通知方法,需要在方法上@Before注解,而且注解里面需要配置切入點(diǎn)表達(dá)式。

  • 如果想讓注解配置的aop生效,需要在配置文件中開啟對(duì)aop的支持。

  • 此案例我們將目標(biāo)類和通知類加入到IOC容器的方式是:注解掃描方式。也可以用xml配置方式。

③ JoinPoint

怎么可以像實(shí)現(xiàn)接口那樣,獲取到一些關(guān)于切入點(diǎn)的相關(guān)信息呢?用JoinPoint。

注意是org.aspectj.lang.JoinPoint。


通知類:

@Component
@Aspect
public class AnnotationAdvice {

    @Before("execution(* com.lee.service.*.addUser(..))")
    public void before(JoinPoint jp) {
        
    System.out.println("目標(biāo)方法相關(guān)信息:" + jp.getSignature());
        System.out.println("目標(biāo)方法名:" + jp.getSignature().getName());
        System.out.println( "方法參數(shù):" + Arrays.toString(jp.getArgs()));
        System.out.println("目標(biāo)對(duì)象:" + jp.getThis());
        
        System.out.println("前置通知執(zhí)行。。。");
    }
}

輸出:

目標(biāo)方法簽名:void com.lee.service.UserService.addUser(User)
目標(biāo)方法名:addUser
方法參數(shù):[User [no=1001, name=zs]]
目標(biāo)對(duì)象:com.lee.service.UserServiceImpl@7c7b252e
前置通知執(zhí)行。。。
addUser...User [no=1001, name=zs]
-----------
deleteUser...學(xué)號(hào)是:1
-------------
updateUser...User [no=1001, name=zs]

④ 相關(guān)方法

方法 解釋
getSignature() 獲取目標(biāo)方法相關(guān)信息
getSignature().getName() 獲取目標(biāo)方法名
getArgs() 獲取目標(biāo)方法參數(shù)
getThis() 獲取目標(biāo)對(duì)象
getTarget() 獲取目標(biāo)對(duì)象

3.2 后置通知

后置通知的注解是@AfterReturning

@Component
@Aspect
public class AnnotationAdvice {
    
  @AfterReturning( pointcut  = "execution(* com.lee.service.*.deleteUser(..))" , returning = "returnVal")
  public void after(JoinPoint jp , Object returnVal) {

    System.out.println("目標(biāo)方法相關(guān)信息:" + jp.getSignature());
    System.out.println("目標(biāo)方法名:" + jp.getSignature().getName());
    System.out.println( "方法參數(shù):" + Arrays.toString(jp.getArgs()));
    System.out.println("目標(biāo)對(duì)象:" + jp.getThis());

    System.out.println("返回值:" + returnVal);

    System.out.println("后置通知執(zhí)行。。。");
  }
}

注意:

  • 如果想查看返回值的回話,需要在@AfterReturning里聲明returning = "xxx"。

  • @AfterReturning( pointcut = "execution(* com.lee.service.*.deleteUser(..))" , returning = "returnVal")中,既可以用 pointcut = "xxx",也可以用 value = "xxx"。

3.3 異常通知

異常通知用注解@AfterThrowing。

如果需要打印異常信息需要在@AfterThrowing里加一個(gè)throwing = "xxx"

@Component
@Aspect
public class AnnotationAdvice {

    @AfterThrowing(pointcut = "execution(* com.lee.service.*.addUser(..))" , throwing = "th")
    public void myException(JoinPoint jp , Exception th) {
        
        System.out.println("目標(biāo)方法相關(guān)信息:" + jp.getSignature());
        System.out.println("目標(biāo)方法名:" + jp.getSignature().getName());
        System.out.println( "方法參數(shù):" + Arrays.toString(jp.getArgs()));
        System.out.println("目標(biāo)對(duì)象:" + jp.getThis());

        System.out.println("異常信息:" + th.getMessage());
        
        System.out.println("異常通知執(zhí)行。。。");
    }
}

輸出:

目標(biāo)方法相關(guān)信息:void com.lee.service.UserService.addUser(User)
目標(biāo)方法名:addUser
方法參數(shù):[User [no=1001, name=zs]]
目標(biāo)對(duì)象:com.lee.service.UserServiceImpl@48f2bd5b
異常信息:/ by zero
異常通知執(zhí)行。。。

擴(kuò)展:

上面方法捕獲的異常級(jí)別是Exception,如果我只想捕獲特定異常,比如:ArithmeticException。當(dāng)程序產(chǎn)生其他異常時(shí),就會(huì)捕捉不到。

@Aspect
public class AnnotationAdvice {

    @AfterThrowing(pointcut = "execution(* com.lee.service.*.addUser(..))" , throwing = "th")
    public void myException(JoinPoint jp , NullPointerException th) {
        
        System.out.println("目標(biāo)方法相關(guān)信息:" + jp.getSignature());
        System.out.println("目標(biāo)方法名:" + jp.getSignature().getName());
        System.out.println( "方法參數(shù):" + Arrays.toString(jp.getArgs()));
        System.out.println("目標(biāo)對(duì)象:" + jp.getThis());

        System.out.println("異常信息:" + th.getMessage());
        
        System.out.println("異常通知執(zhí)行。。。");
    }
}

異常信息就沒有被捕獲到,是由于程序中異常通知僅僅捕獲的是NullPointerException,而目標(biāo)方法里面出現(xiàn)的異常時(shí)ArithmeticException,所以沒有捕獲到

3.4 最終通知

最終通知是無論程序正常執(zhí)行還是異常執(zhí)行,都會(huì)執(zhí)行的一個(gè)通知。類似于try、catch、finally里面的finally。

最終通知用的注解是@After。

@Component
@Aspect
public class AnnotationAdvice {
    
    @After("execution(* com.lee.service.*.addUser(..))" )
    public void zuizhong() {
        System.out.println("最終通知執(zhí)行。。。");
    }
}

3.5 環(huán)繞通知

環(huán)繞通知用的注解是@Around

@Component
@Aspect
public class AnnotationAdvice {

    @Around(value = "execution(* com.lee.service.*.addUser(..))")
  //獲取目標(biāo)對(duì)象的詳細(xì)信息不能再用JoinPoint,需要用子接口ProceedingJoinPoint。
    public void around(ProceedingJoinPoint jp) {
        try {
            //執(zhí)行目標(biāo)方法之前,前置通知
            System.out.println("前置通知執(zhí)行。。。");
            jp.proceed();//執(zhí)行目標(biāo)方法
            //執(zhí)行目標(biāo)方法之后,后置通知
            System.out.println("后置通知執(zhí)行。。。");
        }catch (Throwable e) {
            System.out.println("異常通知執(zhí)行。。。");
        }finally {
            System.out.println("最終通知執(zhí)行。。。");
        }
        
    }
}

目標(biāo)方法沒有異常,輸出:

前置通知執(zhí)行。。。
addUser...User [no=1001, name=zs]
后置通知執(zhí)行。。。
最終通知執(zhí)行。。。

目標(biāo)方法存在異常,輸出:

前置通知執(zhí)行。。。
addUser...User [no=1001, name=zs]
異常通知執(zhí)行。。。
最終通知執(zhí)行。。。

執(zhí)行順序

  • 目標(biāo)方法沒有異常:

    前置通知-->目標(biāo)方法-->后置通知-->最終通知

  • 目標(biāo)方法存在異常:

    前置通知-->目標(biāo)方法-->異常通知-->最終通知

注意事項(xiàng)

  • 獲取目標(biāo)對(duì)象的詳細(xì)信息不能再用JoinPoint,需要用子接口ProceedingJoinPoint。

  • catch塊里異常信息的捕獲級(jí)別必須是Throwable,不能是其他。

  • 如果目標(biāo)方法沒有返回值,則環(huán)繞通知方法的返回值也可以是void。如果目標(biāo)方法有返回值,則環(huán)繞通知方法也必須有返回值,否則會(huì)報(bào)錯(cuò):Null return value from advice does not match primitive return type for: ....

  @Aspect
public class AnnotationAdvice {

    @Around(value = "execution(* com.lee.service.*.deleteUser(..))")
    public Object around(ProceedingJoinPoint jp) {
        Object result = null ;
        try {
            //執(zhí)行目標(biāo)方法之前,前置通知
            System.out.println("前置通知執(zhí)行。。。");
            result =  jp.proceed();//執(zhí)行目標(biāo)方法
            //執(zhí)行目標(biāo)方法之后,后置通知
            System.out.println("后置通知執(zhí)行。。。");
        }catch (Throwable e) {
            System.out.println("異常通知執(zhí)行。。。");
        }finally {
            System.out.println("最終通知執(zhí)行。。。");
        }
        return result;
    }
}

4. AOP底層實(shí)現(xiàn)原理

AOP的底層原理實(shí)現(xiàn)就是Spring的動(dòng)態(tài)代理。

4.1 Spring創(chuàng)建的動(dòng)態(tài)代理類在哪?

Spring框架在運(yùn)?時(shí),通過動(dòng)態(tài)字節(jié)碼技術(shù),在JVM中創(chuàng)建的,運(yùn)?在JVM內(nèi)部,等程序結(jié)束后,會(huì)和 JVM ?起消失。

  • 什么叫動(dòng)態(tài)字節(jié)碼技術(shù):


    動(dòng)態(tài)字節(jié)碼技術(shù)

JVM是怎么創(chuàng)建對(duì)象的?

首先需要編寫 .java 的源文件,源文件編譯后會(huì)變成 .class的字節(jié)碼文件,JVM通過加載字節(jié)碼就能創(chuàng)建出這個(gè)類的對(duì)象。

那什么是動(dòng)態(tài)字節(jié)碼?

動(dòng)態(tài)字節(jié)碼就意味著不需要編寫 .java的源文件,也就不能編譯為 .class文件。但是虛擬機(jī)創(chuàng)建對(duì)象肯定需要的還是 .class格式的字節(jié)碼,此時(shí)的字節(jié)碼就是動(dòng)態(tài)創(chuàng)建的。

動(dòng)態(tài)字節(jié)碼是怎么創(chuàng)建的?

通過第三方的動(dòng)態(tài)字節(jié)碼框架創(chuàng)建,常見的動(dòng)態(tài)字節(jié)碼框架有 ASM、Javaassist、Cglib。這些框架可以動(dòng)態(tài)的生成字節(jié)碼。

所以,動(dòng)態(tài)代理類的本質(zhì)就是動(dòng)態(tài)字節(jié)碼技術(shù),比如 UserServiceProxy就會(huì)自動(dòng)的通過動(dòng)態(tài)字節(jié)碼在JVM中生成,而不需要去手動(dòng)編寫。

  • 結(jié)論:

    動(dòng)態(tài)代理不需要定義類?件,都是JVM運(yùn)?過程中動(dòng)態(tài)創(chuàng)建的,所以不會(huì)造成靜態(tài)代理,類?件數(shù)量過多,影響項(xiàng)?管理的問題。

4.2 Spring工廠如何加工原始對(duì)象

  • 思路分析:

就是使用后置bean去對(duì)原始對(duì)象進(jìn)行再加工,再加工階段加上額外功能。

再加工的過程使用的是動(dòng)態(tài)代理。


Spring工廠創(chuàng)建對(duì)象.png
  • 核心編碼

UserService.java接口:

package com.lee.factory;

public interface UserService {

    void register();
    void login();
}

UserServiceImpl.java 實(shí)現(xiàn)類:

package com.lee.factory;

public class UserServiceImpl implements UserService {

    @Override
    public void register() {
        System.out.println("注冊(cè)。。。。。");
    }

    @Override
    public void login() {
        System.out.println("登錄。。。。");
    }

}

將UserServiceImpl納入到IoC容器:

<bean id="userService" class="com.lee.factory.UserServiceImpl"></bean>

創(chuàng)建ProxyBeanPostProcessor.java,實(shí)現(xiàn)代理:

package com.lee.factory;

public class ProxyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String args) throws BeansException {
        
        ClassLoader loader = ProxyBeanPostProcessor.class.getClassLoader();
        Class<?>[] interfaces = bean.getClass().getInterfaces();
        InvocationHandler invocationHandle = new InvocationHandler() {
            
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("---------log.....-----");
                Object invoke = method.invoke(bean, args);
                return invoke;
            }
        };
        //使用JDK代理創(chuàng)建對(duì)象
        Object ret = Proxy.newProxyInstance(loader, interfaces, invocationHandle);
        return ret;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String args) throws BeansException {
        return bean;
    }

}

測(cè)試:

public class TestAopProxy {

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

輸出:

---------log.....-----
注冊(cè)。。。。。
---------log.....-----
登錄。。。。

5. AOP開發(fā)中的?個(gè)坑

先看一個(gè)案例:

業(yè)務(wù)里有兩個(gè)方法,分別是a() 和 b():

package com.lee.spring.service.impl;

public class UserServiceImpl {

    public void a() {
        System.out.println("a方法執(zhí)行");
    }
    
    public void b() {
        System.out.println("b方法執(zhí)行");
    }
}

有一個(gè)額外方法,通過 Spring 進(jìn)行增強(qiáng):

package com.lee.spring.aop;

public class Before implements MethodBeforeAdvice {

    @Override
    public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
        System.out.println("前置增強(qiáng)執(zhí)行");
    }   
}

配置文件:

<?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.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- 將UserService納入到IOC容器 -->
    <bean id = "userService" class="com.lee.spring.service.impl.UserServiceImpl"></bean>
    
    <!-- 將Before增強(qiáng)納入到IOC容器 -->
    <bean id = "before" class="com.lee.spring.aop.Before"></bean>

    <!-- 配置增強(qiáng) -->
    <aop:config>
        <aop:pointcut expression="execution(* *..UserServiceImpl.*(..))" id="pointcut"/>
        <aop:advisor advice-ref="before" pointcut-ref="pointcut"/>
    </aop:config>
</beans>

編寫測(cè)試類進(jìn)行測(cè)試:

@Test
public void test01() {
  ApplicationContext context =  new ClassPathXmlApplicationContext("applicationContext.xml");
  UserServiceImpl userService =  (UserServiceImpl)context.getBean("userService");
  userService.a();
  userService.b();
}

控制臺(tái)輸出:

前置增強(qiáng)執(zhí)行
a方法執(zhí)行
前置增強(qiáng)執(zhí)行
b方法執(zhí)行

以上操作都是正常的 aop 前置增強(qiáng),如果業(yè)務(wù)邏輯變?yōu)椋?/p>

package com.lee.spring.service.impl;

public class UserServiceImpl {

    public void a() {
        System.out.println("a方法執(zhí)行");
        b();
    }
    
    public void b() {
        System.out.println("b方法執(zhí)行");
    }
}

測(cè)試變?yōu)椋?/p>

@Test
public void test01() {
  ApplicationContext context =  new ClassPathXmlApplicationContext("applicationContext.xml");
  UserServiceImpl userService =  (UserServiceImpl)context.getBean("userService");
  userService.a();
}

控制臺(tái)輸出:

前置增強(qiáng)執(zhí)行
a方法執(zhí)行
b方法執(zhí)行

很明顯,在方法a() 中 調(diào)用b()方法,b()方法的前置增強(qiáng)就不生效了。這就是aop中的一個(gè)坑。

原因分析:

在執(zhí)行 userService.a();時(shí),a()方法調(diào)用的時(shí)候userService對(duì)象,而userService對(duì)象是從ioc容器中獲取的代理對(duì)象,所以可以進(jìn)行增強(qiáng)。但是在 a() 方法中調(diào)用 b() 方法,b() 方法是來自與 userService對(duì)象本身,而不是 Spring代理的,所以不具備增強(qiáng)功能。

所以可以這樣改進(jìn):

package com.lee.spring.service.impl;

public class UserServiceImpl {

    public void a() {
        System.out.println("a方法執(zhí)行");
        ApplicationContext context =  new ClassPathXmlApplicationContext("applicationContext.xml");
        UserServiceImpl userService =  (UserServiceImpl)context.getBean("userService");
        userService.b();
    }
    
    public void b() {
        System.out.println("b方法執(zhí)行");
    }
}

在調(diào)用 b() 方法的時(shí)候,不是直接調(diào)用,而是從ioc中獲取代理對(duì)象,然后調(diào)用代理對(duì)象的 b() 方法。

但是這樣有個(gè)問題:Spring工廠是重量級(jí)資源,會(huì)侵占內(nèi)存,所以一個(gè)應(yīng)用中只創(chuàng)建一個(gè)工廠就足夠了。

可以這樣更改:

package com.lee.spring.service.impl;

public class UserServiceImpl implements ApplicationContextAware {

    private ApplicationContext context;
    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        this.context = context;
    }
    public void a() {
        System.out.println("a方法執(zhí)行");
        UserServiceImpl userService =  (UserServiceImpl)context.getBean("userService");
        userService.b();
    }
    
    public void b() {
        System.out.println("b方法執(zhí)行");
    }
}

6. AOP階段知識(shí)總結(jié)

AOP相關(guān)知識(shí)總結(jié).png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容