Spring AOP 示例教程 – 切面(Aspect)、建议(Advice)、切入点(Pointcut)、连接点(JoinPoint)、注解(Annotations)、XML配置

Spring框架基于两个核心概念进行开发 – 依赖注入和面向切面编程(Spring AOP)。

Spring AOP

我们已经了解了Spring依赖注入的工作原理,今天我们将深入研究面向切面编程的核心概念以及如何使用Spring框架实现它。

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的实现时,事情会更加清楚。让我们开始创建一个简单的Spring项目,并实现AOP。Spring提供了使用AspectJ注解创建方面的支持,我们将使用它来简化。所有上述AOP注解都定义在org.aspectj.lang.annotation包中。Spring工具套件提供了有关方面的有用信息,所以我建议您使用它。如果您对STS不熟悉,我建议您查看Spring MVC教程,我在其中解释了如何使用它。

Spring AOP示例

创建一个新的简单的Spring Maven项目,以便在pom.xml文件中包含所有的Spring Core库,而不需要显式地引入它们。我们的最终项目将如下图所示,我们将详细研究Spring核心组件和切面实现。

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注解。我们稍后会查看它的使用。

服务类

让我们创建一个服务类来处理 Employee 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 beans中使用Spring AOP,我们需要执行以下步骤:

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

你可以看到我在Spring Bean配置文件中定义了许多方面,现在是时候逐个查看它们了。

Spring AOP Before Aspect示例

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注解的空方法,然后在通知中使用它作为表达式。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("within(com.journaldev.spring.service.*)")
	public void allMethodsPointcut(){}
	
}

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

Spring AOP JoinPoint 和 Advice 参数

我们可以在通知方法中使用 JoinPoint 作为参数,并使用它获取方法签名或目标对象。我们可以在切点中使用 args() 表达式,以应用于与参数模式匹配的任何方法。如果我们使用这个,那么我们需要在通知方法中使用相同的名称,从中确定参数类型。我们还可以在通知参数中使用通用对象。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环绕通知示例

如前所述,我们可以使用环绕通知在方法执行前后切入。我们可以用它来控制被通知方法是否执行。我们还可以检查返回的值并进行更改。这是最强大的建议,需要正确应用。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上。例如,某人可以定义一个带有getName()方法的新Spring bean,并且尽管没有意图,但建议将开始应用于该bean。这就是为什么我们应该尽可能地将切入点表达式的范围保持狭窄的原因。另一种方法是创建一个自定义注解,并在我们想要应用建议的方法上进行注解。这就是将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