Spring AOP 示例教程 – Aspect、Advice、Pointcut、JoinPoint、Annotations、XML 配置

Spring Framework 是建立在兩個核心概念上的 – 依賴注入 和面向切面編程(Spring AOP)。

Spring AOP

我們已經看過Spring 依賴注入是如何工作的,今天我們將深入研究面向切面編程的核心概念以及如何使用 Spring Framework 實現它。

Spring AOP 概述

大多數企業應用程式都有一些適用於不同類型的對象和模組的共同橫切關注點。一些常見的橫切關注點包括記錄、事務管理、數據驗證等。在面向對象編程中,通過類實現應用程式的模塊化,而在面向方面編程中,通過方面實現應用程式的模塊化,並且它們被配置為橫跨不同類的。Spring AOP消除了在普通面向對象編程模型中無法實現的橫切任務對類的直接依賴。例如,我們可以有一個獨立的類用於記錄,但是功能類仍然需要調用這些方法來實現應用程式中的記錄。

面向方面編程的核心概念

在深入了解Spring AOP實現之前,我們應該理解AOP的核心概念。

  1. 方面:一個方面是一個實現企業應用程式關注點(例如事務管理)的類,可以是通過Spring XML配置的普通類,也可以使用Spring AspectJ集成來定義一個帶有@Aspect註解的類作為方面。
  2. 連接點:連接點是應用程序中的特定點,如方法執行、異常處理、變更對象變量值等。在Spring AOP中,連接點始終是方法的執行。
  3. 建議:建議是針對特定連接點採取的行動。在編程方面,它們是在應用程序中到達具有匹配切入點的某個連接點時執行的方法。您可以將建議想像成Struts2攔截器Servlet篩檢器
  4. 切入點:切入點是與連接點匹配的表達式,用於確定是否需要執行建議。切入點使用不同類型的表達式與連接點匹配,Spring框架使用AspectJ切入點表達式語言。
  5. 目標對象:它們是應用建議的對象。Spring AOP使用運行時代理實現,因此該對象始終是代理對象。這意味著在運行時創建了一個子類,其中目標方法被覆蓋,並且根據其配置包含了建議。
  6. AOP 代理:Spring AOP 實現使用 JDK 動態代理來創建帶有目標類和通知調用的代理類,這些類被稱為 AOP 代理類。我們還可以通過將其添加為 Spring AOP 項目中的依賴項來使用 CGLIB 代理。
  7. 編織:這是將方面與其他對象鏈接起來創建建議的代理對象的過程。這可以在編譯時、加載時或運行時完成。Spring AOP 在運行時執行編織。

AOP 通知類型

根據通知的執行策略,它們分為以下類型。

  1. 前置通知:這些通知在連接點方法執行之前運行。我們可以使用 @Before 注釋將通知類型標記為前置通知。
  2. 後置(最終)通知:在連接點方法完成執行後執行的通知,無論是正常完成還是通過拋出異常。我們可以使用 @After 注釋創建後置通知。
  3. 返回後通知:有時我們希望通知方法僅在連接點方法正常執行時才執行。我們可以使用 @AfterReturning 注釋將方法標記為返回後通知。
  4. 拋出建議:此建議僅在連接點方法拋出異常時執行,我們可以使用它來宣告式地回滾事務。我們使用@AfterThrowing注釋來進行此類建議。
  5. 環繞建議:這是最重要和強大的建議。此建議包圍連接點方法,我們還可以選擇是否執行連接點方法。我們可以編寫在執行連接點方法之前和之後執行的建議代碼。環繞建議的責任是調用連接點方法並在方法返回某些值時返回值。我們使用@Around注釋來創建環繞建議方法。

上述提到的要點可能聽起來令人困惑,但當我們查看Spring AOP的實現時,事情將更加清晰。讓我們開始創建一個帶有AOP實現的簡單Spring項目。Spring支持使用AspectJ注釋來創建切面,我們將使用它來簡化操作。所有上述的AOP注釋都定義在org.aspectj.lang.annotation包中。Spring Tool Suite提供有關切面的有用信息,因此我建議您使用它。如果您不熟悉STS,我建議您參考Spring MVC教程,我在其中解釋了如何使用它。

Spring AOP示例

建立一個新的簡單的 Spring Maven 專案,以便在 pom.xml 檔案中包含所有的 Spring Core 庫,我們不需要明確地將它們包含進來。我們的最終專案將如下圖所示,我們將詳細研究 Spring 核心組件和 Aspect 實現。

Spring AOP AspectJ 依賴

Spring 框架默認提供 AOP 支持,但由於我們正在使用 AspectJ 注釋來配置方面和建議,因此我們需要在 pom.xml 文件中包含它們。

<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>org.springframework.samples</groupId>
	<artifactId>SpringAOPExample</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>

		<!-- Generic properties -->
		<java.version>1.6</java.version>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

		<!-- Spring -->
		<spring-framework.version>4.0.2.RELEASE</spring-framework.version>

		<!-- Logging -->
		<logback.version>1.0.13</logback.version>
		<slf4j.version>1.7.5</slf4j.version>

		<!-- Test -->
		<junit.version>4.11</junit.version>

		<!-- AspectJ -->
		<aspectj.version>1.7.4</aspectj.version>

	</properties>

	<dependencies>
		<!-- Spring and Transactions -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-tx</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>

		<!-- Logging with SLF4J & LogBack -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${slf4j.version}</version>
			<scope>compile</scope>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>${logback.version}</version>
			<scope>runtime</scope>
		</dependency>

		<!-- AspectJ dependencies -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>${aspectj.version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjtools</artifactId>
			<version>${aspectj.version}</version>
		</dependency>
	</dependencies>
</project>

請注意,我已在專案中添加了 aspectjrtaspectjtools 依賴項(版本為 1.7.4)。同時,我已將 Spring 框架版本更新為截至目前的最新版本,即 4.0.2.RELEASE。

模型類

讓我們創建一個簡單的 Java Bean,我們將在示例中使用它,並添加一些額外的方法。Employee.java 代碼:

package com.journaldev.spring.model;

import com.journaldev.spring.aspect.Loggable;

public class Employee {

	private String name;
	
	public String getName() {
		return name;
	}

	@Loggable
	public void setName(String nm) {
		this.name=nm;
	}
	
	public void throwException(){
		throw new RuntimeException("Dummy Exception");
	}	
}

你有注意到 setName() 方法被 Loggable 注解标注了吗?这是我们在项目中自定义的一个 Java 注解。我们稍后会查看它的使用情况。

服务类

让我们创建一个服务类来处理员工 bean。EmployeeService.java 代码:

package com.journaldev.spring.service;

import com.journaldev.spring.model.Employee;

public class EmployeeService {

	private Employee employee;
	
	public Employee getEmployee(){
		return this.employee;
	}
	
	public void setEmployee(Employee e){
		this.employee=e;
	}
}

I could have used Spring annotations to configure it as a Spring Component, but we will use XML based configuration in this project. EmployeeService class is very standard and just provides us an access point for Employee beans.

使用 AOP 的 Spring Bean 配置

如果你使用的是 STS,你可以选择创建“Spring Bean 配置文件”,并选择 AOP 模式的命名空间,但如果你使用其他 IDE,你可以简单地将其添加到 spring bean 配置文件中。我的项目 bean 配置文件如下。spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
	xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="https://www.springframework.org/schema/aop"
	xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-4.0.xsd
		https://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

<!-- Enable AspectJ style of Spring AOP -->
<aop:aspectj-autoproxy />

<!-- Configure Employee Bean and initialize it -->
<bean name="employee" class="com.journaldev.spring.model.Employee">
	<property name="name" value="Dummy Name"></property>
</bean>

<!-- Configure EmployeeService bean -->
<bean name="employeeService" class="com.journaldev.spring.service.EmployeeService">
	<property name="employee" ref="employee"></property>
</bean>

<!-- Configure Aspect Beans, without this Aspects advices wont execute -->
<bean name="employeeAspect" class="com.journaldev.spring.aspect.EmployeeAspect" />
<bean name="employeeAspectPointcut" class="com.journaldev.spring.aspect.EmployeeAspectPointcut" />
<bean name="employeeAspectJoinPoint" class="com.journaldev.spring.aspect.EmployeeAspectJoinPoint" />
<bean name="employeeAfterAspect" class="com.journaldev.spring.aspect.EmployeeAfterAspect" />
<bean name="employeeAroundAspect" class="com.journaldev.spring.aspect.EmployeeAroundAspect" />
<bean name="employeeAnnotationAspect" class="com.journaldev.spring.aspect.EmployeeAnnotationAspect" />

</beans>

要在 Spring bean 中使用 Spring AOP,我们需要进行以下操作:

  1. 声明 AOP 命名空间,类似 xmlns:aop=“https://www.springframework.org/schema/aop
  2. 添加 aop:aspectj-autoproxy 元素以在运行时启用 Spring AspectJ 支持的自动代理
  3. 配置 Aspect 类作为其他 Spring bean

你可以看到,在 Spring Bean 配置文件中我定義了很多方面,現在是時候一一查看它們了。

Spring AOP Before Aspect Example

EmployeeAspect.java 代碼:

package com.journaldev.spring.aspect;

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

@Aspect
public class EmployeeAspect {

	@Before("execution(public String getName())")
	public void getNameAdvice(){
		System.out.println("Executing Advice on getName()");
	}
	
	@Before("execution(* com.journaldev.spring.service.*.get*())")
	public void getAllAdvice(){
		System.out.println("Service method getter called");
	}
}

上面的 aspect 類中的重要點是:

  • Aspect 類需要有 @Aspect 注解。
  • @Before 注解用於創建 Before advice
  • @Before 注解中傳遞的字符串參數是 Pointcut 表達式
  • getNameAdvice() 建議將對具有簽名 public String getName() 的任何 Spring Bean 方法執行。這是一個非常重要的點要記住,如果我們使用 new 運算符創建 Employee bean,則不會應用建議。只有當我們使用 ApplicationContext 獲取 bean 時,才會應用建議。
  • 我們可以在 Pointcut 表達式中使用星號 (*) 作為通配符,getAllAdvice() 將應用於 com.journaldev.spring.service 包中名稱以 get 開頭並且不接受任何參數的所有類。

我們將在查看了所有不同類型的建議之後,在測試類中看到建議的作用。

Spring AOP Pointcut 方法和重用

有时我们必须在多个地方使用相同的Pointcut表达式,我们可以创建一个带有@Pointcut注解的空方法,然后在advices中使用它作为表达式。EmployeeAspectPointcut.java 代码:

package com.journaldev.spring.aspect;

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

@Aspect
public class EmployeeAspectPointcut {

	@Before("getNamePointcut()")
	public void loggingAdvice(){
		System.out.println("Executing loggingAdvice on getName()");
	}
	
	@Before("getNamePointcut()")
	public void secondAdvice(){
		System.out.println("Executing secondAdvice on getName()");
	}
	
	@Pointcut("execution(public String getName())")
	public void getNamePointcut(){}
	
	@Before("allMethodsPointcut()")
	public void allServiceMethodsAdvice(){
		System.out.println("Before executing service method");
	}
	
	//Pointcut 用于在包中的所有类的所有方法上执行
	@Pointcut("within(com.journaldev.spring.service.*)")
	public void allMethodsPointcut(){}
	
}

上面的例子很清晰,与其使用表达式,我们在advice注解参数中使用方法名。

Spring AOP JoinPoint 和 Advice 参数

我们可以在advice方法中使用JoinPoint作为参数,通过它获取方法签名或目标对象。我们可以在pointcut中使用args()表达式,以适用于匹配参数模式的任何方法。如果使用这个,那么我们需要在advice方法中使用相同的名称,从中确定参数类型。我们还可以在advice参数中使用通用对象。EmployeeAspectJoinPoint.java 代码:

package com.journaldev.spring.aspect;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class EmployeeAspectJoinPoint {
	
	@Before("execution(public void com.journaldev.spring.model..set*(*))")
	public void loggingAdvice(JoinPoint joinPoint){
		System.out.println("Before running loggingAdvice on method="+joinPoint.toString());
		
		System.out.println("Agruments Passed=" + Arrays.toString(joinPoint.getArgs()));

	}
	
	//Advice 参数,将应用于具有单个String参数的bean方法
	@Before("args(name)")
	public void logStringArguments(String name){
		System.out.println("String argument passed="+name);
	}
}

Spring AOP後置建議示例

讓我們看一個簡單的方面類別,帶有後置、拋出異常後和返回後建議的示例。EmployeeAfterAspect.java代碼:

package com.journaldev.spring.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class EmployeeAfterAspect {

	@After("args(name)")
	public void logStringArguments(String name){
		System.out.println("Running After Advice. String argument passed="+name);
	}
	
	@AfterThrowing("within(com.journaldev.spring.model.Employee)")
	public void logExceptions(JoinPoint joinPoint){
		System.out.println("Exception thrown in Employee Method="+joinPoint.toString());
	}
	
	@AfterReturning(pointcut="execution(* getName())", returning="returnString")
	public void getNameReturningAdvice(String returnString){
		System.out.println("getNameReturningAdvice executed. Returned String="+returnString);
	}
	
}

我們可以在切入點表達式中使用within來將建議應用於類中的所有方法。我們可以使用@AfterReturning建議來獲取被建議方法返回的對象。我們在Employee bean中有一個throwException()方法來展示後置拋出異常建議的使用。

Spring AOP環繞方面示例

如前所述,我們可以使用Around方面來在方法執行前後切入。我們可以使用它來控制被建議方法是否執行。我們還可以檢查返回的值並對其進行更改。這是最強大的建議,需要正確應用。EmployeeAroundAspect.java代碼:

package com.journaldev.spring.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class EmployeeAroundAspect {

	@Around("execution(* com.journaldev.spring.model.Employee.getName())")
	public Object employeeAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
		System.out.println("Before invoking getName() method");
		Object value = null;
		try {
			value = proceedingJoinPoint.proceed();
		} catch (Throwable e) {
			e.printStackTrace();
		}
		System.out.println("After invoking getName() method. Return value="+value);
		return value;
	}
}

環繞通知總是需要將ProceedingJoinPoint作為參數,並且我們應該使用它的proceed()方法來調用被通知的目標方法。如果被通知的方法有返回值,則由通知負責將其返回給調用方程序。對於void方法,通知方法可以返回null。由於環繞通知環繞在被通知的方法周圍,我們可以控制方法的輸入和輸出,以及其執行行為。

使用自定義註釋切入點的Spring通知

如果您查看上述所有建議切入點表達式,有可能它們會應用到一些其他未預期的Bean上。例如,某人可以定義一個新的Spring Bean並帶有getName()方法,即使不打算如此,建議也會開始應用於該方法。這就是為什麼我們應該將切入點表達式的範圍保持狹窄的原因。另一種方法是創建一個自定義註釋,並將要應用建議的方法進行註釋。這就是將Employee的setName()方法註釋為@Loggable註釋的目的。Spring Framework的@Transactional註釋是這種方法的一個很好的例子,用於Spring事務管理。Loggable.java代碼:

package com.journaldev.spring.aspect;

public @interface Loggable {

}

EmployeeAnnotationAspect.java代碼:

package com.journaldev.spring.aspect;

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

@Aspect
public class EmployeeAnnotationAspect {

	@Before("@annotation(com.journaldev.spring.aspect.Loggable)")
	public void myAdvice(){
		System.out.println("Executing myAdvice!!");
	}
}

myAdvice()方法將只建議setName()方法。這是一種非常安全的方法,每當我們想要在任何方法上應用建議時,我們只需用Loggable註釋進行註釋即可。

Spring AOP XML配置

I always prefer annotation but we also have the option to configure aspects in the spring configuration file. For example, let’s say we have a class as below. EmployeeXMLConfigAspect.java code:

package com.journaldev.spring.aspect;

import org.aspectj.lang.ProceedingJoinPoint;

public class EmployeeXMLConfigAspect {

	public Object employeeAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
		System.out.println("EmployeeXMLConfigAspect:: Before invoking getName() method");
		Object value = null;
		try {
			value = proceedingJoinPoint.proceed();
		} catch (Throwable e) {
			e.printStackTrace();
		}
		System.out.println("EmployeeXMLConfigAspect:: After invoking getName() method. Return value="+value);
		return value;
	}
}

我們可以通過在Spring Bean配置文件中包含以下配置來進行配置。

<bean name="employeeXMLConfigAspect" class="com.journaldev.spring.aspect.EmployeeXMLConfigAspect" />

<!-- Spring AOP XML Configuration -->
<aop:config>
	<aop:aspect ref="employeeXMLConfigAspect" id="employeeXMLConfigAspectID" order="1">
		<aop:pointcut expression="execution(* com.journaldev.spring.model.Employee.getName())" id="getNamePointcut"/>
		<aop:around method="employeeAroundAdvice" pointcut-ref="getNamePointcut" arg-names="proceedingJoinPoint"/>
	</aop:aspect>
</aop:config>

AOP xml 配置元素的目的從其名稱就很清楚,所以我不會對此進行太多細節解釋。

Spring AOP 示例

讓我們來看一個簡單的 Spring 程式,看看這些方面如何貫穿 bean 方法。SpringMain.java 代碼:

package com.journaldev.spring.main;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.journaldev.spring.service.EmployeeService;

public class SpringMain {

	public static void main(String[] args) {
		ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");
		EmployeeService employeeService = ctx.getBean("employeeService", EmployeeService.class);
		
		System.out.println(employeeService.getEmployee().getName());
		
		employeeService.getEmployee().setName("Pankaj");
		
		employeeService.getEmployee().throwException();
		
		ctx.close();
	}
}

現在,當我們執行上面的程式時,我們會得到以下輸出。

Mar 20, 2014 8:50:09 PM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4b9af9a9: startup date [Thu Mar 20 20:50:09 PDT 2014]; root of context hierarchy
Mar 20, 2014 8:50:09 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring.xml]
Service method getter called
Before executing service method
EmployeeXMLConfigAspect:: Before invoking getName() method
Executing Advice on getName()
Executing loggingAdvice on getName()
Executing secondAdvice on getName()
Before invoking getName() method
After invoking getName() method. Return value=Dummy Name
getNameReturningAdvice executed. Returned String=Dummy Name
EmployeeXMLConfigAspect:: After invoking getName() method. Return value=Dummy Name
Dummy Name
Service method getter called
Before executing service method
String argument passed=Pankaj
Before running loggingAdvice on method=execution(void com.journaldev.spring.model.Employee.setName(String))
Agruments Passed=[Pankaj]
Executing myAdvice!!
Running After Advice. String argument passed=Pankaj
Service method getter called
Before executing service method
Exception thrown in Employee Method=execution(void com.journaldev.spring.model.Employee.throwException())
Exception in thread "main" java.lang.RuntimeException: Dummy Exception
	at com.journaldev.spring.model.Employee.throwException(Employee.java:19)
	at com.journaldev.spring.model.Employee$$FastClassBySpringCGLIB$$da2dc051.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:711)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:58)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
	at com.journaldev.spring.model.Employee$$EnhancerBySpringCGLIB$$3f881964.throwException(<generated>)
	at com.journaldev.spring.main.SpringMain.main(SpringMain.java:17)

您可以看到,通過它們的切入點配置,建議逐個執行。您應該逐個配置它們,以避免混淆。這就是有關Spring AOP 示例教程的所有內容,希望您從中學會了 Spring 中 AOP 的基礎知識,並可以從示例中學到更多。從下面的鏈接下載示例項目並進行操作。

下載 Spring AOP 項目

Source:
https://www.digitalocean.com/community/tutorials/spring-aop-example-tutorial-aspect-advice-pointcut-joinpoint-annotations