Spring AOP示例教程–Aspect,Advice,Pointcut,JoinPoint,注释,XML配置

时间:2020-02-23 14:35:44  来源:igfitidea点击:

Spring Framework是基于两个核心概念开发的:依赖注入和面向方面的编程(Spring AOP)。

SpringAOP

我们已经了解了Spring Dependency Injection的工作原理,今天我们将研究面向方面的编程的核心概念以及如何使用Spring Framework来实现它。

Spring AOP概述

大多数企业应用程序都有一些共同的横切关注点,这些关注点适用于不同类型的对象和模块。
一些常见的横切关注点是日志记录,事务管理,数据验证等。

在面向对象编程中,应用程序的模块化是通过类实现的,而在面向方面编程中,应用程序的模块化是通过Aspects实现的,它们被配置为跨越不同的类。

Spring AOP从我们无法通过常规的面向对象的编程模型实现的类中删除横切任务的直接依赖关系。
例如,我们可以有一个单独的日志记录类,但功能类也必须调用这些方法以实现整个应用程序的日志记录。

面向方面的编程核心概念

在深入探讨Spring AOP实现的实现之前,我们应该了解AOP的核心概念。

  • 方面:方面是实现跨多个类的企业应用程序关注点的类,例如事务管理。
    Aspects可以是通过Spring XML配置配置的普通类,或者我们可以使用Spring AspectJ集成使用@Aspect批注将类定义为Aspect。

  • 连接点:连接点是应用程序中的特定点,例如方法执行,异常处理,更改对象变量值等。
    在Spring AOP中,连接点始终是方法的执行。

  • 建议:建议是针对特定连接点采取的操作。
    就编程而言,它们是在应用程序中达到具有匹配切入点的特定连接点时执行的方法。
    您可以将建议视为Struts2拦截器或者Servlet过滤器。

  • 切入点:切入点是与连接点匹配的表达式,用于确定是否需要执行建议。
    Pointcut使用与联接点匹配的不同种类的表达式,而Spring框架使用AspectJ Pointcut表达式语言。

  • 目标对象:它们是应用建议的对象。
    Spring AOP是使用运行时代理实现的,因此此对象始终是代理对象。
    这意味着将在运行时创建一个子类,在该子类中将覆盖目标方法,并根据其配置包含建议。

  • AOP代理:Spring AOP实现使用JDK动态代理来创建具有目标类和建议调用的Proxy类,这些被称为AOP代理类。
    我们还可以通过将CGLIB代理添加为Spring AOP项目中的依赖项来使用它。

  • 编织:这是将各方面与其他对象链接以创建建议的代理对象的过程。
    这可以在编译时,加载时或者运行时完成。
    Spring AOP在运行时执行编织。

AOP咨询类型

根据建议的执行策略,它们属于以下类型。

  • 咨询之前:这些咨询在执行连接点方法之前运行。
    我们可以使用@Before注释将建议类型标记为Before建议。

  • 之后(最终)建议:在连接点方法完成执行后(无论是正常还是通过引发异常)执行的建议。
    我们可以使用@After注释来创建事后建议。

  • 返回建议后:有时我们希望建议方法仅在联接点方法正常执行时才执行。
    我们可以使用@ AfterReturning注释将方法标记为返回建议之后。

  • 投掷建议后:仅在连接点方法抛出异常时才执行此建议,我们可以使用它以声明方式回滚事务。
    对于此类建议,我们使用@AfterThrowing注解。

  • 围绕建议:这是最重要和最有力的建议。
    该建议围绕着连接点方法,我们还可以选择是否执行连接点方法。
    我们可以编写在执行连接点方法之前和之后执行的建议代码。
    周围建议的责任是调用连接点方法并在该方法返回某些值时返回值。
    我们使用" @Around"注释来创建建议方法。

上面提到的几点听起来可能令人困惑,但是当我们看一下Spring AOP的实现时,情况将会更加清楚。
让我们开始创建一个具有AOP实现的简单Spring项目。
Spring提供了对使用AspectJ注释来创建方面的支持,为简单起见,我们将使用它。
以上所有AOP注释都在org.aspectj.lang.annotation包中定义。

Spring Tool Suite提供了有关各方面的有用信息,因此我建议您使用它。
如果您不熟悉STS,建议您看一看Spring MVC教程,那里已经解释了如何使用它。

Spring AOP示例

创建一个新的Simple Spring Maven项目,以便所有的Spring Core库都包含在pom.xml文件中,而我们不需要显式地包含它们。
我们的最终项目将如下图所示,我们将详细研究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.theitroad.spring.model;

import com.theitroad.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.theitroad.spring.service;

import com.theitroad.spring.model.Employee;

public class EmployeeService {

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

我本可以使用Spring注释将其配置为Spring Component,但是在此项目中我们将使用基于XML的配置。
EmployeeService类非常标准,它只是为我们提供了Employee bean的访问点。

使用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.theitroad.spring.model.Employee">
	<property name="name" value="Dummy Name"></property>
</bean>

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

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

</beans>

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

  • 声明AOP命名空间,例如xmlns:aop =" https://www.springframework.org/schema/aop"
  • 添加aop:aspectj-autoproxy元素以在运行时启用具有自动代理的Spring AspectJ支持
  • 与其他Spring Bean一样配置Aspect类

可以看到,我在spring bean配置文件中定义了很多方面,是时候一个个地研究它们了。

Aspect示例之前的Spring AOP

EmployeeAspect.java代码:

package com.theitroad.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.theitroad.spring.service.*.get*())")
	public void getAllAdvice(){
		System.out.println("Service method getter called");
	}
}

上述方面类中的重要点是:

  • 方面类必须具有@Aspect批注。

  • @Before注释用于创建Before建议

  • @Before注释中传递的字符串参数是Pointcut表达式

  • 对于签名为public String getName()的任何Spring Bean方法,都会执行getNameAdvice()建议。
    要记住这一点非常重要,如果我们将使用新的运算符创建Employee bean,则建议将不会应用。
    只有当我们使用ApplicationContext来获取bean时,才会应用建议。

  • 我们可以在Pointcut表达式中使用星号(*)作为通配符,getAllAdvice()将应用于com.theitroad.spring.service包中所有名称以get开头且不带任何参数的类。

在研究了所有不同类型的建议之后,我们将在测试程序中查看实际的建议。

Spring AOP切入点方法和重用

有时我们必须在多个地方使用相同的Pointcut表达式,我们可以使用@ Pointcut注释创建一个空方法,然后将其用作建议中的表达式。

EmployeeAspectPointcut.java代码:

package com.theitroad.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 to execute on all the methods of classes in a package
	@Pointcut("within(com.theitroad.spring.service.*)")
	public void allMethodsPointcut(){}
	
}

上面的示例非常清楚,我们在通知注释参数中使用的是方法名称,而不是表达式。

Spring AOP JoinPoint和建议参数

我们可以在咨询方法中使用JoinPoint作为参数,并使用它获取方法签名或者目标对象。

我们可以在切入点中使用args()表达式,以将其应用于与参数模式匹配的任何方法。
如果使用此参数,则需要在确定参数类型的地方的advice方法中使用相同的名称。
我们也可以在建议参数中使用通用对象。

EmployeeAspectJoinPoint.java代码:

package com.theitroad.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.theitroad.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 arguments, will be applied to bean methods with single String argument
	@Before("args(name)")
	public void logStringArguments(String name){
		System.out.println("String argument passed="+name);
	}
}

建议后的Spring AOP示例

让我们看一个简单的方面类,其中包含After,Throwing和After Return建议的示例。

EmployeeAfterAspect.java代码:

package com.theitroad.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.theitroad.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()方法来展示After Throwing建议的用法。

围绕Aspect示例的Spring AOP

如前所述,我们可以使用Around方面来减少前后的方法执行。
我们可以使用它来控制建议的方法是否执行。
我们还可以检查返回的值并进行更改。
这是最有力的建议,需要正确应用。

EmployeeAroundAspect.java代码:

package com.theitroad.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.theitroad.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作为参数,我们应该使用它的progress()方法来调用目标对象的建议方法。

如果建议的方法返回了某些内容,则建议将其返回给调用者程序。
对于void方法,advice方法可以返回null。

由于围绕建议的建议是围绕建议方法的,因此我们可以控制该方法的输入和输出以及其执行行为。

带有自定义注释切入点的Spring建议

如果您查看以上所有建议切入点表达式,它们很可能会被应用到其他一些不希望使用的bean。
例如,某人可以使用getName()方法定义一个新的spring bean,即使该建议不是故意的,该建议也将开始被应用。
这就是为什么我们应该使切入点表达的范围尽可能地窄。

另一种方法是创建自定义注释,并在我们希望应用建议的地方注释方法。
这是使用@Loggable批注对Employee setName()方法进行批注的目的。

Spring Framework @Transactional注释是这种用于Spring事务管理的方法的很好的例子。

Loggable.java代码:

package com.theitroad.spring.aspect;

public @interface Loggable {

}

EmployeeAnnotationAspect.java代码:

package com.theitroad.spring.aspect;

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

@Aspect
public class EmployeeAnnotationAspect {

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

myAdvice()方法将仅建议setName()方法。
这是一种非常安全的方法,每当我们要将建议应用于任何方法时,我们所需要做的就是使用Loggable注释对其进行注释。

Spring AOP XML配置

我总是喜欢注释,但是我们也可以选择在spring配置文件中配置方面。
例如,假设我们有一个如下的程序。

EmployeeXMLConfigAspect.java代码:

package com.theitroad.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.theitroad.spring.aspect.EmployeeXMLConfigAspect" 

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

AOP xml config元素的用途很明确,因此我将不对其进行详细介绍。

Spring AOP示例

让我们有一个简单的Spring程序,看看所有这些方面如何贯穿bean方法。

SpringMain.java代码:

package com.theitroad.spring.main;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.theitroad.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("hyman");
		
		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=hyman
Before running loggingAdvice on method=execution(void com.theitroad.spring.model.Employee.setName(String))
Agruments Passed=[hyman]
Executing myAdvice!!
Running After Advice. String argument passed=hyman
Service method getter called
Before executing service method
Exception thrown in Employee Method=execution(void com.theitroad.spring.model.Employee.throwException())
Exception in thread "main" java.lang.RuntimeException: Dummy Exception
	at com.theitroad.spring.model.Employee.throwException(Employee.java:19)
	at com.theitroad.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.theitroad.spring.model.Employee$$EnhancerBySpringCGLIB$f881964.throwException(<generated>)
	at com.theitroad.spring.main.SpringMain.main(SpringMain.java:17)

您可以看到建议正在根据切入点配置一个接一个地执行。
您应该一个一个地配置它们,以避免混淆。