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

前言介紹

附錄: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找到下載鏈接:鏈接

附錄參考

優質博客參考:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章