Spring AOP示例教程 - Aspect,Advice,Pointcut,JoinPoint,Annotations,XML Configuration

 

Spring AOP示例教程 - Aspect,Advice,Pointcut,JoinPoint,Annotations,XML Configuration

 

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

目录[ 隐藏 ]

春天AOP

Spring AOP示例教程 - Aspect,Advice,Pointcut,JoinPoint,Annotations,XML Configuration

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

Spring AOP概述

大多数企业应用程序都有一些常见的横切关注点,适用于不同类型的对象和模块。一些常见的横切关注点是日志记录,事务管理,数据验证等。在面向对象编程中,应用程序的模块化由类实现,而在面向方面编程应用程序中,模块化由Aspects实现,并且它们被配置为跨越不同的类。

Spring AOP从通过普通面向对象编程模型无法实现的类中获取横切任务的直接依赖性。例如,我们可以有一个单独的日志记录类,但功能类必须再次调用这些方法来实现跨应用程序的日志记录。

面向方面的编程核心概念

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

 

  1. 方面:方面是一个实现跨越多个类的企业应用程序问题的类,例如事务管理。方面可以是通过Spring XML配置配置的普通类,也可以使用Spring AspectJ集成将类定义为使用@Aspect注释的Aspect 。
  2. 连接点:连接点是应用程序中的特定点,例如方法执行,异常处理,更改对象变量值等。在Spring AOP中,连接点始终是方法的执行。
  3. 建议:建议是针对特定连接点采取的操作。在编程方面,它们是在应用程序中达到具有匹配切入点的特定连接点时执行的方法。您可以将Advices视为Struts2拦截器Servlet过滤器
  4. 切入点:切入点是与连接点匹配的表达式,用于确定是否需要执行建议。Pointcut使用与连接点匹配的不同类型的表达式,Spring框架使用AspectJ切入点表达式语言。
  5. 目标对象:它们是应用建议的对象。Spring AOP是使用运行时代理实现的,因此该对象始终是代理对象。意味着在运行时创建子类,其中覆盖目标方法并根据其配置包含建议。
  6. AOP代理:Spring AOP实现使用JDK动态代理来创建具有目标类和通知调用的Proxy类,这些类称为AOP代理类。我们也可以通过将它添加为Spring AOP项目中的依赖项来使用CGLIB代理。
  7. 编织:将方面与其他对象链接以创建建议的代理对象的过程。这可以在编译时,加载时或在运行时完成。Spring AOP在运行时执行编织。

AOP建议类型

基于建议的执行策略,它们具有以下类型。

  1. 建议之前:这些建议在执行连接点方法之前运行。我们可以使用@Before注释将建议类型标记为Before advice。
  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 Tutorial,我已经解释了如何使用它。

Spring AOP示例

创建一个新的Simple Spring Maven项目,以便所有Spring Core库都包含在pom.xml文件中,我们不需要显式包含它们。我们的最终项目将如下图所示,我们将详细介绍Spring核心组件和Aspect实现。

Spring AOP示例教程 - Aspect,Advice,Pointcut,JoinPoint,Annotations,XML Configuration

Spring AOP AspectJ依赖项

Spring框架默认提供AOP支持,但由于我们使用AspectJ注释来配置方面和建议,我们需要将它们包含在pom.xml文件中。


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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>

我已经添加的通知aspectjrt,并aspectjtools在项目依赖(版本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;
	}
}

我本可以使用Spring注释将其配置为Spring组件,但我们将在此项目中使用基于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="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-4.0.xsd
		http://www.springframework.org/schema/aop http://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 =“http://www.springframework.org/schema/aop”
  2. 添加aop:aspectj-autoproxy元素以在运行时启用带有自动代理的Spring AspectJ支持
  3. 将Aspect类配置为其他Spring bean

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

在Aspect示例之前的Spring AOP

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注释。
  • @Before注释用于创建Before建议
  • @Before注释中传递的字符串参数是Pointcut表达式
  • 对于任何带签名的Spring Bean方法,都会执行getNameAdvice()建议public String getName()。这是一个非常重要的要点,如果我们使用new运算符创建Employee bean,则不会应用建议。只有当我们使用ApplicationContext来获取bean时,才会应用建议。
  • 我们可以在Pointcut表达式中使用星号(*)作为通配符,getAllAdvice()将应用于com.journaldev.spring.service名称get以及不带任何参数的包中的所有类。

在我们查看了所有不同类型的建议后,我们将在测试类中查看这些建议。

Spring AOP切入点方法和重用

有时我们必须在多个地方使用相同的Pointcut表达式,我们可以使用@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 to execute on all the methods of classes in a package
	@Pointcut("within(com.journaldev.spring.service.*)")
	public void allMethodsPointcut(){}
	
}

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

Spring AOP JoinPoint和建议参数

我们可以使用JoinPoint作为通知方法中的参数,并使用它获取方法签名或目标对象。

我们可以args()在切入点中使用表达式来应用于与参数模式匹配的任何方法。如果我们使用它,那么我们需要在确定参数类型的advice方法中使用相同的名称。我们也可以在advice参数中使用Generic对象

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 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,After Throwing和After Returning。

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

Spring AOP围绕Aspect示例

如前所述,我们可以使用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方法,advice方法可以返回null。由于围绕建议方法的建议,我们可以控制方法的输入和输出以及它的执行行为。

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

如果您查看上述所有建议切入点表达式,则有可能将它们应用于其他非预期的bean。例如,某人可以使用getName()方法定义一个新的spring bean,并且即使它不是预期的,也会开始应用该建议。这就是我们应该尽可能缩小切入点表达范围的原因。

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

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配置

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

EmployeeXMLConfigAspect.java代码:


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[email protected]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项目