Spring5.0原始碼學習系列之Spring AOP簡述

smileNicky發表於2020-11-26

前言介紹

附錄:Spring原始碼學習專欄

在前面章節的學習中,我們對Spring框架的IOC實現原始碼有了一定的瞭解,接著本文繼續學習Springframework一個核心的技術點AOP技術。

在學習Spring AOP原始碼之前,您是否對AOP有足夠熟悉的理解?在對應用都不熟悉之前就去學習原始碼,肯定是很難理解的,所以本文先不描述原始碼的實現,先通過本篇部落格瞭解熟悉Spring AOP,然後再學習原始碼

1、什麼是AOP技術?

引用Spring官網對AOP技術的概述:

Aspect-Oriented Programming (AOP) complements Object-Oriented Programming (OOP) by providing another way of thinking about program structure. The key unit of modularity in OOP is the class, whereas in AOP the unit of modularity is the aspect. Aspects enable the modularization of concerns such as transaction management that cut across multiple types and objects. (Such concerns are often termed crosscutting concerns in AOP literature.)

挑重點來說,所謂AOP(Aspect-Oriented Programming)也即面向方面的程式設計,是指通過跨領域關注點的分離來實現模組化的一種物件導向技術。

  • 跨領域也即跨多種型別和物件的事務管理等等;
  • 關注點通常被稱為橫切關注點,OOP中模組化的關鍵單元是類,而在AOP中模組化是方面關注點通常被稱為橫切關注點

2、AOP的本質目的

AOP本質:在不改變原有業務邏輯的情況下增強橫切邏輯,這個橫切邏輯可以是許可權校驗邏輯、日誌監控、事務控制等等

AOP相關知識詳情可以參考:Spring AOP官方文件

3、AOP的相關術語

名詞 描述
連線點(Joinpoint) 連線點是一個程式的執行,如方法的執行或異常的處理過程中的一個點
切入點(Pointcut) 指的是將增強程式碼織入到業務主線進來之後的連線點
通知/增強(Advice) Advice可以翻譯為通知或者增強,指的是切面類中用於提供增強功能的方法
目標物件(Target) 指代理的目標物件,即被代理物件
代理(Proxy) 指一個類被AOP織入增強之後,產生的代理類,即代理物件
織入(Weaving) 指的是將增強(Advice)應用到目標物件(Target)產生代理物件(Proxy)的過程。ps:AspectJ採用的是編譯期織入和類裝載期織入,而Spring AOP採用的是動態代理織入
切面(Aspect) 切面也就是AOP的關注點,也就是說是Advice程式碼的關注點,切面是對上述概念的一個綜合。將這些Advice程式碼放在一個類中,這個類就是切面類,切面類是跨多個類的關注點的模組化類

看了前面這些理論,您可能不是很理解,所以引用國外網站的圖例進行說明AOP概念,圖來自連結
在這裡插入圖片描述

綜上所述,其實所謂的目的其實只是要鎖定在某個切入點(Pointcut)織入(Weaving)特定的增強邏輯(Advice)

4、Spring AOP和AspectJ

有了前面對AOP概念的概述之後,我們能夠大致理解AOP了,不過本文還是要理理Spring AOP和AspectJ的關係

在這裡插入圖片描述

Spring官網給指出了Spring AOP和AspectJ的關係,官網明確指出其立場,表明Spring AOP不會和AspectJ專案競爭哪個專案能提供更成熟的AOP解決方案,Spring AOP和AspectJ是一種互補的關係,Spring AOP無縫結合了IOC和AspectJ,從而使Spring AOP能夠符合大部分的AOP需求,這是一種能提供代理物件的模式

從官網也可以知道了Spring AOP和AspectJ的大致關係,其實這是兩種AOP的實現技術,Spring AOP是整合了AspectJ部分功能,同時結合IOC實現的,AspectJ則是一種比較完善成熟的AOP解決方案,接著本文做下簡單對比

  • Spring AOP

    • Spring AOP是SpringFramework的元件,屬於Springframework的一個比較核心的功能,Spring AOP是結合了AspectJ和IOC實現的,提供了AspectJ的功能
    • Spring AOP 致力於解決的是企業級開發中最普遍的 AOP 需求(方法織入),Spring AOP不會和AspectJ競爭
    • Spring AOP 只能作用於 Spring 容器中的 Bean
    • 效能方面,Spring AOP是在執行時進行動態織入的,所以效能比不上AspectJ
    • Spring AOP使用動態代理的方式進行方法織入,有兩種動態代理方法,一種是CGLIB,另外一種是JDK提供的動態代理
      引用https://www.baeldung.com/spring-aop-vs-aspectj的圖進行說明
      在這裡插入圖片描述
  • AspectJ

    • AspectJ來自Eclipse的開源專案,連結:https://www.eclipse.org/aspectj
    • AspectJ是一種比較成熟的AOP解決方案,能夠提供比Spring AOP更多的AOP功能
    • AspectJ的方法織入屬於靜態織入,它的織入時機可以是:compile-time(編譯期)、post-compile(編譯後)、load-time(JVM類載入器載入時候)
    • AspectJ在編譯時進行方法織入,所以效能比Spring AOP好

ok,前面已經簡單列舉了Spring AOP和AspectJ的主要不同,現在可以用表格列舉出不同點對比,表格參考自國外網站

對比 Spring AOP AspectJ
實現語言 使用存Java語言 使用Java程式語言的擴充套件實現
編譯過程 無需單獨的編譯過程 除非設定了LTW,否則需要AspectJ編譯器(ajc)
織入時機 動態代理,在執行時織入 靜態織入,在編譯過程織入,它的織入時機可以是:compile-time(編譯期)、post-compile(編譯後)、load-time(JVM類載入器載入時候)
功能 基本的方法織入 可以編織欄位,方法,建構函式,靜態初始值設定項,最終類/方法等…
範圍 只能作用於Spring容器管理的bean上 可以作用於所有領域物件上實施
效能 比AspectJ慢得多 更好的效能(編譯時編織比執行時編織要快得多)
學習 易於學習和應用 比Spring AOP複雜

補充,AspectJ靜態織入時機:

  • compile-time weaving:編譯期織入,在編譯時候就直接進行方法織入,直接編譯出包含織入程式碼的 .class 檔案
  • post-compile weaving:編譯後織入,也可以稱之為二進位制織入,它將Advice織入於編織後現有的類檔案和JAR檔案
  • Load-time weaving(LTW):指的是在載入類的時候進行織入,與以前的二進位制編織完全一樣,不同之處在於編織被推遲到類載入器將類檔案載入到JVM的過程

5、Spring中AOP代理選擇

在前面知識,我們知道Spring AOP是使用動態代理技術實現Spring AOP中的代理選擇,方法織入實現有兩種方法,一種是JDK動態代理,一種是CGLIB

  • 預設情況下,Spring框架會根據被代理物件(Target Object)是否實現介面來選擇JDK還是CGLIB。如果被代理物件沒有實現任何介面,Spring會選擇CGLIB,如果被代理物件有實現介面,Spring會選擇JDK提供的動態代理。
  • ps:雖然預設情況是這樣的,不過我們可以通過配置的方式來自定義選擇動態代理方式

6、實驗環境準備參考

學習了前面的理論知識之後,現在可以通過例子進行實踐,實踐之前,您需要如下的環境準備,實驗環境參考:

  • SpringFramework版本
    • Springframework5.0.x
  • 開發環境
    • JAR管理:gradle 4.9/ Maven3.+
    • 開發IDE:IntelliJ IDEA 2018.2.5
    • JDK:jdk1.8.0_31
    • Git Server:Git fro window 2.8.3
    • Git Client:SmartGit18.1.5(可選)

7、Spring AOP實現方式

在Spring AOP中,主要提供了三種配置方式:

  • Spring1.2 基於介面的配置:Spring最早的AOP實現是基於Spring提供的AOP介面實現的,通過實現介面,進行Advice邏輯程式碼編寫等等
  • Spring2.0+ schema-based 配置 :Spring2.0之後,提供了 schema-based 配置,也就是xml型別的配置,使用名稱空間<aop>
  • Spring2.0+ @Aspect配置:Spring2.0之後,也提供了@Aspect這種方法,@Aspect是用AspectJ的jar,但是實現是Spring AOP自己實現的

8、Spring AOP例子參考

前面介紹了Spring AOP實現的三種方式,接著本文通過程式碼例子進行驗證:

maven配置

<properties>
    <springframework.version>5.0.19.RELEASE</springframework.version>
    <aspectj.version>1.9.4</aspectj.version>
</properties>

<dependencies>
	<!-- Spring aop配置-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${springframework.version}</version>
    </dependency>
    <!-- 本文的測試類需要 ioc配置-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${springframework.version}</version>
    </dependency>
    <!-- @Aspect才需要加上-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>${aspectj.version}</version>
    </dependency>


</dependencies>

8.1、Spring1.2 基於介面的配置

8.1.1、基礎類編寫

User.java

package com.example.spring.aop.bean;

public class User {
    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

UserService .java:

package com.example.spring.aop.service;

import com.example.spring.aop.bean.User;

/**
 * <pre>
 *      UserService
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/11/20 18:02  修改內容:
 * </pre>
 */
public interface UserService {

    User addUser(User user);

    User getUser();
}

UserServiceImpl .java

package com.example.spring.aop.service.impl;

import com.example.spring.aop.bean.User;
import com.example.spring.aop.service.UserService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

/**
 * <pre>
 *		UserServiceImpl 
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/11/20 17:57  修改內容:
 * </pre>
 */
@Service
public class UserServiceImpl implements UserService {

    private static User user = null;

    @Override
    public User addUser(User userDto) {
        user = new User();
        BeanUtils.copyProperties(userDto,user);
        return user;
    }

    @Override
    public User getUser() {
        return user;
    }
}

8.1.2、使用Advice介面

LogMethodBeforeAdvice .java

package com.example.spring.aop.core.advice;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * <pre>
 *      LogMethodBeforeAdvice
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/11/20 17:38  修改內容:
 * </pre>
 */
public class LogMethodBeforeAdvice implements MethodBeforeAdvice {
	@Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(String.format("執行方法:%s,引數列表:%s", method.getName(), Arrays.toString(args) ));
    }
}

LogAfterReturningAdvice .java

package com.example.spring.aop.core.advice;

import org.springframework.aop.AfterReturningAdvice;

import java.lang.reflect.Method;

/**
 * <pre>
 *      LogAfterReturningAdvice
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/11/20 17:41  修改內容:
 * </pre>
 */
public class LogAfterReturningAdvice implements AfterReturningAdvice {
	@Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println(String.format("方法返回:%s", returnValue ));
    }
}

spring_interfaces_config.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 具體業務實現類(target Object)-->
    <bean id="userServiceTarget" class="com.example.spring.aop.service.impl.UserServiceImpl"></bean>

    <!-- 實現MethodBeforeAdvice-->
    <bean id="logMethodBeforeAdvice" class="com.example.spring.aop.core.advice.LogMethodBeforeAdvice"></bean>
    <!-- 實現AfterReturningAdvice-->
    <bean id = "logAfterReturningAdvice" class="com.example.spring.aop.core.advice.LogAfterReturningAdvice"></bean>

    <bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!-- 配置代理介面 proxy-->
        <property name="proxyInterfaces">
            <list>
                <value>com.example.spring.aop.service.UserService</value>
            </list>
        </property>
        <!-- 配置目標物件,也就是被代理類,具體業務實現類-->
        <property name="target" ref="userServiceTarget"></property>
        <!-- 配置攔截器,可以配置advice、advisor、interceptor -->
        <property name="interceptorNames">
            <list>
                <value>logMethodBeforeAdvice</value>
                <value>logAfterReturningAdvice</value>
            </list>
        </property>
    </bean>

</beans>

TestApplication.java:

package com.example.spring.aop;


import com.example.spring.aop.bean.User;
import com.example.spring.aop.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestApplication {

    public static void testAopProxy() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("classpath:spring_interfaces_config.xml");
        UserService userService = (UserService) ioc.getBean("userServiceProxy");
        User userDto = new User();
        userDto.setUsername("tom");
        userDto.setPassword("11");
        userService.addUser(userDto);
        System.out.println(String.format("使用者資料列印:%s",userService.getUser().toString()));
    }

    public static void main(String[] args) {
        testAopProxy();
    }
}

執行方法:addUser,引數列表:[User{username='tom', password='11'}]
方法返回:User{username='tom', password='11'}
執行方法:getUser,引數列表:[]
方法返回:User{username='tom', password='11'}
使用者資料列印:User{username='tom', password='11'}

8.1.3、使用Advisor介面

  • NameMatchMethodPointcutAdvisor使用
    定義一個只會攔截查詢方法的Advisor,修改配置:
<!-- 定義一個只會攔截查詢方法的Advisor -->
<bean id="logOnlyObtainQueryAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
    <!-- 定義Advice類 -->
    <property name="advice" ref="logMethodBeforeAdvice"></property>
    <!-- 只有查詢方法才會被攔截 -->
    <property name="mappedNames" value="getUser"></property>
</bean>

<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <!-- 配置代理介面 proxy-->
    <property name="proxyInterfaces">
        <list>
            <value>com.example.spring.aop.service.UserService</value>
        </list>
    </property>
    <!-- 配置目標物件,也就是被代理類,具體業務實現類-->
    <property name="target" ref="userServiceTarget"></property>
    <!-- 配置攔截器,可以配置advice、advisor、interceptor -->
    <property name="interceptorNames">
        <list>
            <value>logOnlyObtainQueryAdvisor</value>
        </list>
    </property>
</bean>

執行方法:getUser,引數列表:[]
使用者資料列印:User{username='tom', password='11'}

  • RegexpMethodPointcutAdvisor使用
    前面介紹了NameMatchMethodPointcutAdvisor,不過不夠通用,所以Spring aop還提供了RegexpMethodPointcutAdvisor,可以支援正規表示式
 <!-- 定義支援正則匹配的Advisor,只攔截查詢方法-->
 <bean id="regexpMethodAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
     <property name="advice" ref="logMethodBeforeAdvice"></property>
     <property name="pattern" value="com.example.spring.aop.*.service.*.get.*"></property>
 </bean>

<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
   <!-- 配置代理介面 proxy-->
   <property name="proxyInterfaces">
       <list>
           <value>com.example.spring.aop.service.UserService</value>
       </list>
   </property>
   <!-- 配置目標物件,也就是被代理類,具體業務實現類-->
   <property name="target" ref="userServiceTarget"></property>
   <!-- 配置攔截器,可以配置advice、advisor、interceptor -->
   <property name="interceptorNames">
       <list>
           <value>regexpMethodAdvisor</value>
       </list>
   </property>
</bean>

執行方法:getUser,引數列表:[]
使用者資料列印:User{username='tom', password='11'}

8.1.4、Interceptor介面使用

TestMethodInterceptor .java:

package com.example.spring.aop.core.interceptor;


import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

/**
 * <pre>
 *      TestMethodInterceptor
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/11/23 10:28  修改內容:
 * </pre>
 */
public class TestMethodInterceptor  implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println(String.format("方法呼叫前(before method invoke) :%s",methodInvocation));
        Object implObj = methodInvocation.proceed();
        System.out.println(String.format("方法呼叫後(after method invoke) :%s",implObj));
        return implObj;
    }
}


修改配置檔案:

<!-- 定義MethodInterceptor -->
<bean id="methodInterceptor" class="com.example.spring.aop.core.interceptor.TestMethodInterceptor"></bean>

<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
   <!-- 配置代理介面 proxy-->
    <property name="proxyInterfaces">
        <list>
            <value>com.example.spring.aop.service.UserService</value>
        </list>
    </property>
    <!-- 配置目標物件,也就是被代理類,具體業務實現類-->
    <property name="target" ref="userServiceTarget"></property>
    <!-- 配置攔截器,可以配置advice、advisor、interceptor -->
    <property name="interceptorNames">
        <list>
            <value>logMethodBeforeAdvice</value>
            <value>logAfterReturningAdvice</value>
            <value>methodInterceptor</value>
        </list>
    </property>
</bean>

挑addUser方法的日誌資訊:

方法呼叫前(before method invoke) :ReflectiveMethodInvocation: public abstract com.example.spring.aop.bean.User com.example.spring.aop.service.UserService.addUser(com.example.spring.aop.bean.User); target is of class [com.example.spring.aop.service.impl.UserServiceImpl]


方法呼叫後(after method invoke) :User{username='tom', password='11'}

8.1.5、beanNameAutoProxy使用

前面介紹了ProxyFactoryBean配置對應的業務代理進行呼叫,不過不夠靈活,所以Spring中還提供了beanNameAutoProxy,這種方式是自動匹配beanName id,不需要每個業務都配對應的proxy進行代理

新建一個新的配置檔案,spring_beanNameAutoProxy_config.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 具體業務實現類(target Object)-->
    <bean id="userServiceTarget" class="com.example.spring.aop.service.impl.UserServiceImpl"></bean>

    <!-- 實現MethodBeforeAdvice-->
    <bean id="logMethodBeforeAdvice" class="com.example.spring.aop.core.advice.LogMethodBeforeAdvice"></bean>
    <!-- 實現AfterReturningAdvice-->
    <bean id = "logAfterReturningAdvice" class="com.example.spring.aop.core.advice.LogAfterReturningAdvice"></bean>

    <!-- 定義MethodInterceptor -->
    <bean id="methodInterceptor" class="com.example.spring.aop.core.interceptor.TestMethodInterceptor"></bean>

    <!-- 定義BeanNameAutoProxyCreator -->
    <bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <!-- interceptorNames可以配置advice、advisor、interceptor-->
        <property name="interceptorNames">
            <list>
                <value>logMethodBeforeAdvice</value>
                <value>logAfterReturningAdvice</value>
                <value>methodInterceptor</value>
            </list>
        </property>
        <!-- 注意:beanNames這裡是攔截對應的實現類bean id,eg:userServiceTarget-->
        <property name="beanNames" value="*ServiceTarget"></property>
    </bean>


</beans>

TestApplication.java:

package com.example.spring.aop;


import com.example.spring.aop.bean.User;
import com.example.spring.aop.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestApplication {

    public static void testBeanNameAutoProxy() {
        ApplicationContext ioc = new ClassPathXmlApplicationContext("classpath:spring_beanNameAutoProxy_config.xml");
        UserService userService = ioc.getBean(UserService.class);
        User userDto = new User();
        userDto.setUsername("tom");
        userDto.setPassword("11");
        userService.addUser(userDto);
        System.out.println(String.format("使用者資料列印:%s",userService.getUser().toString()));
    }

    public static void main(String[] args) {
        // BeanNameAutoProxyCreator
        testBeanNameAutoProxy();
    }
}

8.2、Spring2.0+ @Aspect配置

@Aspect這種方式是比較常用的,pom需要加上aspectjweaver配置,spring aop引用了aspectJ的api,但是實現是spring自己進行實現擴充的

8.2.1、啟用@AspectJ支援

註解方式,使用@EnableAspectJAutoProxy開啟

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

xml方式,可以使用<aop:aspectj-autoproxy/>開啟

<?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 https://www.springframework.org/schema/aop/spring-aop.xsd">
    <aop:aspectj-autoproxy/>
</beans>

8.2.2、宣告方面

xml方式:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of aspect here as normal -->
</bean>

java方式,使用註解@Aspect

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class NotVeryUsefulAspect {

}

8.2.3、宣告切入點

切入點(Pointcut)的型別,Spring官網給出了比較詳情的介紹:
在這裡插入圖片描述
在官網的建議是宣告一個通用的SystemArchitecture

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

    /**
     * A join point is in the web layer if the method is defined
     * in a type in the com.xyz.someapp.web package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.web..*)")
    public void inWebLayer() {}

    /**
     * A join point is in the service layer if the method is defined
     * in a type in the com.xyz.someapp.service package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.service..*)")
    public void inServiceLayer() {}

    /**
     * A join point is in the data access layer if the method is defined
     * in a type in the com.xyz.someapp.dao package or any sub-package
     * under that.
     */
    @Pointcut("within(com.xyz.someapp.dao..*)")
    public void inDataAccessLayer() {}

    /**
     * A business service is the execution of any method defined on a service
     * interface. This definition assumes that interfaces are placed in the
     * "service" package, and that implementation types are in sub-packages.
     *
     * If you group service interfaces by functional area (for example,
     * in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
     * the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
     * could be used instead.
     *
     * Alternatively, you can write the expression using the 'bean'
     * PCD, like so "bean(*Service)". (This assumes that you have
     * named your Spring service beans in a consistent fashion.)
     */
    @Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
    public void businessService() {}

    /**
     * A data access operation is the execution of any method defined on a
     * dao interface. This definition assumes that interfaces are placed in the
     * "dao" package, and that implementation types are in sub-packages.
     */
    @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
    public void dataAccessOperation() {}

}

因為官網的介紹比較詳細,所以本部落格只挑部分比較重要的進行介紹:

  • execution
    execution:執行,這是最基本的切入點型別:
// 匹配UserService裡的任何方法
 @Pointcut("execution(* com.example.spring.aop.service.UserService.*(..))")
    public void regexpExecution(){}

ps:第一個*表示匹配任何返回值,第二個*表示匹配任何方法,(..)表示匹配任何數量的方法引數

eg:execution(* *..find*(Long,..))用於匹配方法名為find...,而第一個引數是long型別的

 @Pointcut("within(com.example.spring.aop..*) && execution(* *..find*(Long,..))")
    public void regexpExecutionByMethodName(){}
  • within
    within:表示服務包中的任何連線點(僅在Spring AOP中執行方法)
@Pointcut("within(com.example.spring.aop..*)")
  • this和target
    前者在Spring AOP建立基於CGLIB的代理時起作用,而後者在建立基於JDK的代理時使用

如下例項程式碼:

public class UserServiceImpl implements UserService {
    //...
}

對於UserService,使用target,這種情況是基於CGLIB的代理

@Pointcut("target(com.example.spring.aop.service.UserService)")

對於UserService,使用this,這種情況是基於JDK的代理

@Pointcut("this(com.example.spring.aop.service.impl.UserServiceImpl)")
  • args
    限制匹配點(引數是給定型別的例項)的連線點(使用Spring AOP時方法的執行)

  • @target
    @target不要和target混淆了,其中類的執行的物件的具有給定型別的註釋

@Pointcut("@target(org.springframework.stereotype.Repository)")
  • @args
    @args其中傳遞的實際引數的執行時型別具有給定型別的註釋
package com.example.spring.aop.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
// 作用於類
@Target(ElementType.TYPE)
public @interface Entity {
}

@Pointcut("within(com.example.spring.aop..*) && @args(com.example.spring.aop.annotation.Entity)")
    public void argsMethod(){}
  • @within
    將匹配限制為具有給定註釋的型別內的連線點
Pointcut("@within(org.springframework.stereotype.Repository)")

等效於:

@Pointcut("within(@org.springframework.stereotype.Repository *)")
  • @annotation
    這個@annotation是用於匹配註解的,比較常用,我們可以自己寫個註解類:
package com.example.spring.aop.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * <pre>
 *      EnableLog
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/11/23 18:27  修改內容:
 * </pre>
 */
@Retention(RetentionPolicy.RUNTIME)
// 作用於方法
@Target(ElementType.METHOD)
public @interface EnableLog {
}

然後,在對應方法加上@EnableLog 的方法都能被攔截到:

@Pointcut("within(com.example.spring.aop..*) && @annotation(com.example.spring.aop.annotation.EnableLog)")
    public void annotationMethod(){}
  • 組合表示式
    組合表示式可以使用&&,||和!進行組合
 @Pointcut("within(com.example.spring.aop..*) && execution(public String com.example.spring.aop.service.UserService.*(..))")

8.2.4、宣告Advice型別

Advice型別,Spring官網也有比較詳細的介紹:
在這裡插入圖片描述
歸納一下通知型別:

  • 前置通知(@Before):在方法執行之前,使用@Before註釋宣告
  • 後置通知(@AfterReturning):當匹配的方法執行正常返回時執行建議。它使用@AfterReturning註釋宣告
  • 異常通知(@AfterThrowing):程式丟擲異常後執行,不拋異常不會呼叫,使用@AfterThrowing註釋宣告
  • 最後通知(@After):匹配的方法執行退出,如果是有try...catch,一般是在finally執行完成後,使用@After註釋宣告
  • 環繞通知(@Around):圍繞建議在匹配的方法執行過程中“圍繞”執行。它有機會在該方法執行之前和之後進行工作,並確定該方法何時,如何以及什至完全可以執行,使用@Around註釋宣告
package com.example.spring.aop.config;

import com.example.spring.aop.service.UserService;
import com.example.spring.aop.service.impl.UserServiceImpl;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

/**
 * <pre>
 *      SpringAspectJConfiguration
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/11/24 10:52  修改內容:
 * </pre>
 */
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false)
@Aspect
public class SpringAspectJConfiguration {

    @Bean
    public UserService userService(){
        return new UserServiceImpl();
    }

    private ThreadLocal<SimpleDateFormat> simpleDateFormat =new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            //return super.initialValue();
            return new SimpleDateFormat("[yyyy-mm-dd hh:mm:ss:SSS]");
        }
    };

    //---------------------------------------------------------------------
    // Types of pointcut
    //---------------------------------------------------------------------

    @Pointcut("within(com.example.spring.aop..*) && execution(public String com.example.spring.aop.service.UserService.*(..))")
    public void regexpExecution(){}

    @Pointcut("within(com.example.spring.aop..*) && execution(* *..find*(Long,..))")
    public void regexpExecutionByMethodName(){}

    @Pointcut("within(com.example.spring.aop..*) && target(com.example.spring.aop.service.UserService)")
    public void targetInterface(){}

    @Pointcut("within(com.example.spring.aop..*) && @args(com.example.spring.aop.annotation.Entity)")
    public void argsMethod(){}

    @Pointcut("within(com.example.spring.aop..*) && @annotation(com.example.spring.aop.annotation.EnableLog)")
    public void annotationMethod(){}

    //---------------------------------------------------------------------
    // Types of advice
    //---------------------------------------------------------------------

    @Before(value = "regexpExecution()")
    public void beforeAdvice(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        String methodName = joinPoint.getSignature().getName();
        System.out.println(String.format("前置通知:beforeAdvice,引數是:%s", Arrays.toString(args)));
        System.out.println(simpleDateFormat.get().format(new Date()) + methodName);
    }

    @AfterReturning(value = "regexpExecution()", returning = "returnVal")
    public void afterReturningAdvice(Object returnVal){
        System.out.println(String.format("後置通知:afterReturningAdvice,返回引數是:%s", returnVal));
    }

    @AfterThrowing(value = "regexpExecution()", throwing = "e")
    public void afterThrowingAdvice(Throwable e) {
        System.out.println(String.format("異常通知:afterThrowingAdvice,異常資訊:%s", e));
    }

    @After(value = "regexpExecution()")
    public void afterAdvice() {
        System.out.println(String.format("最後通知:afterAdvice"));
    }

    @Around(value = "regexpExecution()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
        Object rtValue = null;
        try {
            System.out.println("aroundAdvice前置通知!");
            // 獲取引數
            Object[] args = proceedingJoinPoint.getArgs();
            // 執行切入點方法
            rtValue = proceedingJoinPoint.proceed(args);

            System.out.println("aroundAdvice後置通知!");
        } catch (Throwable e) {
            System.out.println("aroundAdvice異常通知!");
            e.printStackTrace();
        } finally {
            System.out.println("aroundAdvice最後通知!");
        }
        return rtValue;
    }

}

aroundAdvice前置通知!
前置通知:beforeAdvice,引數是:[1]
[2020-13-24 02:13:48:509]findUserNameById
aroundAdvice後置通知!
aroundAdvice最後通知!
最後通知:afterAdvice
後置通知:afterReturningAdvice,返回引數是:tom

8.2.5、例子:實現日誌監控

Service加個方法:
在這裡插入圖片描述
在這裡插入圖片描述
AopConfiguration.java:

package com.example.spring.aop.config;

import com.example.spring.aop.core.interceptor.TestMonitoringInterceptor;
import com.example.spring.aop.service.UserService;
import com.example.spring.aop.service.impl.UserServiceImpl;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * <pre>
 *      AOP日誌監控配置類
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本: V1.0.0    修改人:mazq  修改日期: 2020/11/23 14:30  修改內容: 新增配置類
 * </pre>
 */
@Configuration
@Aspect
@EnableAspectJAutoProxy
public class AopLogMonitorConfiguration {

    @Pointcut("within(com.example.spring.aop..*) && execution(public String com.example.spring.aop.service.UserService.findUserNameById(Long, ..))")
    public void monitor(){ }

    @Bean
    public TestMonitoringInterceptor monitoringInterceptor() {
        return new TestMonitoringInterceptor(true);
    }

    @Bean
    public Advisor monitoringAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("com.example.spring.aop.config.AopConfiguration.monitor()");
        return new DefaultPointcutAdvisor(pointcut, monitoringInterceptor());
    }

    @Bean
    public UserService userService(){
        return new UserServiceImpl();
    }


}


TestMonitoringInterceptor.java

package com.example.spring.aop.core.interceptor;

import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.springframework.aop.interceptor.AbstractMonitoringInterceptor;

/**
 * <pre>
 *      TestMonitoringInterceptor
 * </pre>
 *
 * <pre>
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/11/23 16:39  修改內容:
 * </pre>
 */
public class TestMonitoringInterceptor extends AbstractMonitoringInterceptor {

    public TestMonitoringInterceptor(){}

    public TestMonitoringInterceptor (boolean useDynamicLogger) {
        setUseDynamicLogger(useDynamicLogger);
    }

    @Override
    protected Object invokeUnderTrace(MethodInvocation methodInvocation, Log log) throws Throwable {
        String name = createInvocationTraceName(methodInvocation);
        long start = System.currentTimeMillis();
        try {
            return methodInvocation.proceed();
        } finally {
            long end = System.currentTimeMillis();
            long time = end - start;
            log.info(String.format("方法名:%s,執行時間:%s ms",name,time));
            if (time > 10) {
                log.warn(String.format("方法名:%s,執行時間超過10 ms! ",name));
            }
        }
    }
}

pom.xml加上logback配置:

<properties>
    <slf4j.version>1.7.25</slf4j.version>
    <logback.version>1.2.3</logback.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-core</artifactId>
        <version>${logback.version}</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-access</artifactId>
        <version>${logback.version}</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
    </dependency>
</dependencies>

logback.xml,copy @https://github.com/eugenp/tutorials/blob/master/spring-aop/src/main/resources/logback.xml,進行一點改寫:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- copy @https://github.com/eugenp/tutorials/blob/master/spring-aop/src/main/resources/logback.xml-->

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
            </pattern>
        </encoder>
    </appender>

    <logger name="org.springframework" level="WARN" />
    <logger name="org.springframework.transaction" level="WARN" />

    <!-- in order to debug some marshalling issues, this needs to be TRACE -->
    <logger name="org.springframework.web.servlet.mvc" level="WARN" />

    <logger name="com.example.spring.aop.core.interceptor.TestMonitoringInterceptor" level="INFO" />

    <logger name="org.springframework.aop.interceptor.PerformanceMonitorInterceptor" level="TRACE" />

    <root level="TRACE">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>
package com.example.spring.aop;


import com.example.spring.aop.bean.User;
import com.example.spring.aop.config.AopConfiguration;
import com.example.spring.aop.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TestApplication {

    public static void testLogMonitoring() {
        AnnotationConfigApplicationContext ioc = new AnnotationConfigApplicationContext();
        // 註冊配置類
        ioc.register(AopConfiguration.class);
        // 啟動IOC容器
        ioc.refresh();
        UserService userService = (UserService) ioc.getBean("userService");
        System.out.println(userService.findUserNameById(1L));
    }

    public static void main(String[] args) {
        // logging monitoring
        testLogMonitoring();
    }
}

17:54:05.553 [main] INFO c.e.s.a.service.impl.UserServiceImpl - 方法名:com.example.spring.aop.service.UserService.findUserNameById,執行時間:2531 ms
17:54:05.559 [main] WARN c.e.s.a.service.impl.UserServiceImpl - 方法名:com.example.spring.aop.service.UserService.findUserNameById,執行時間超過10 ms!

8.3、Spring2.0+ schema-based 配置

Spring2.0之後提供了基於 <aop /> 名稱空間的 XML 配置,這也就是本文介紹的schema-based 配置

SchemaBasedAspect .java:

package com.example.spring.aop.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class SchemaBasedAspect {

    private ThreadLocal<SimpleDateFormat> simpleDateFormat =new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            //return super.initialValue();
            return new SimpleDateFormat("[yyyy-mm-dd hh:mm:ss:SSS]");
        }
    };

    public void beforeAdvice(JoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        String methodName = joinPoint.getSignature().getName();
        System.out.println(String.format("前置通知:beforeAdvice,引數是:%s", Arrays.toString(args)));
        System.out.println(simpleDateFormat.get().format(new Date()) + methodName);
    }

    public void afterReturningAdvice(Object returnVal){
        System.out.println(String.format("後置通知:afterReturningAdvice,返回引數是:%s", returnVal));
    }

    public void afterThrowingAdvice(Throwable e) {
        System.out.println(String.format("異常通知:afterThrowingAdvice,異常資訊:%s", e));
    }

    public void afterAdvice() {
        System.out.println(String.format("最後通知:afterAdvice"));
    }

    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
        Object rtValue = null;
        try {
            System.out.println("aroundAdvice前置通知!");
            // 獲取引數
            Object[] args = proceedingJoinPoint.getArgs();
            // 執行切入點方法
            rtValue = proceedingJoinPoint.proceed(args);

            System.out.println("aroundAdvice後置通知!");
        } catch (Throwable e) {
            System.out.println("aroundAdvice異常通知!");
            e.printStackTrace();
        } finally {
            System.out.println("aroundAdvice最後通知!");
        }
        return rtValue;
    }
}

spring_schemaBased_config.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 具體業務實現類(target Object)-->
    <bean id="userService" class="com.example.spring.aop.service.impl.UserServiceImpl"></bean>
    <!-- SchemaBased Aspect-->
    <bean id="LoggingAspect" class="com.example.spring.aop.aspect.SchemaBasedAspect"></bean>

    <aop:aspectj-autoproxy/>

    <!--開始aop的配置-->
    <aop:config>
        <aop:pointcut id="executionPointcut" expression="execution(* com.example.spring.aop.service.UserService.*(..))" />
        <!--配置切⾯-->
        <aop:aspect id="logAspect" ref="LoggingAspect">
            <!--配置前置通知-->
            <aop:before method="beforeAdvice"
                        pointcut-ref="executionPointcut"></aop:before>
            <!--配置後置通知-->
            <aop:after-returning method="afterReturningAdvice"
                                 pointcut-ref="executionPointcut" returning="returnVal"></aop:after-returning>
            <!-- 配置異常通知-->
            <aop:after-throwing method="afterThrowingAdvice" pointcut-ref="executionPointcut" throwing="e"
            ></aop:after-throwing>
            <!-- 配置最後通知-->
            <aop:after method="afterAdvice" pointcut-ref="executionPointcut"></aop:after>
            <!-- 配置環繞通知-->
            <aop:around method="aroundAdvice" pointcut-ref="executionPointcut"></aop:around>
        </aop:aspect>
    </aop:config>

</beans>

TestApplication.java:

package com.example.spring.aop;

import com.example.spring.aop.bean.User;
import com.example.spring.aop.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestApplication {

    public static void testSchemaBasedAop(){
        ApplicationContext ioc = new ClassPathXmlApplicationContext("classpath:spring_schemaBased_config.xml");
        UserService userService = (UserService) ioc.getBean("userService");
        User userDto = new User();
        userDto.setUsername("tom");
        userDto.setPassword("11");
        userService.addUser(userDto);
        System.out.println(String.format("使用者資料列印:%s",userService.getUser().toString()));
    }

    public static void main(String[] args) {
        // schema Based config
        testSchemaBasedAop();
    }
}

本文的程式碼例子可以在github找到下載連結:連結

附錄參考

優質部落格參考:

相關文章