Spring AOP 示例教程 - 方面、建议、切入点、连接点、注释、XML 配置
Spring 框架基于两个核心概念开发 -依赖注入和面向方面编程(Spring AOP)。
Spring AOP
我们已经了解了Spring 依赖注入的工作原理,今天我们将研究面向方面编程的核心概念以及如何使用 Spring 框架实现它。
Spring AOP 概述
大多数企业应用程序都有一些常见的横切关注点,它们适用于不同类型的对象和模块。一些常见的横切关注点包括日志记录、事务管理、数据验证等。在面向对象编程中,应用程序的模块化是通过类实现的,而在面向方面编程中,应用程序的模块化是通过方面实现的,它们被配置为跨越不同的类。Spring AOP 从类中消除了横切任务的直接依赖关系,这是我们无法通过普通的面向对象编程模型实现的。例如,我们可以有一个单独的类用于日志记录,但功能类必须再次调用这些方法才能在整个应用程序中实现日志记录。
面向方面编程核心概念
在深入研究Spring AOP实现之前,我们应该了解AOP的核心概念。
- 方面:方面是实现跨多个类的企业应用程序关注点的类,例如事务管理。方面可以是通过 Spring XML 配置配置的普通类,也可以使用 Spring AspectJ 集成通过
@Aspect
注释将类定义为方面。 - 连接点:连接点是应用程序中的特定点,例如方法执行、异常处理、更改对象变量值等。在 Spring AOP 中,连接点始终是方法的执行。
- 建议:建议是针对特定连接点采取的操作。从编程角度来看,它们是在应用程序中到达具有匹配切入点的某个连接点时执行的方法。您可以将建议视为Struts2 拦截器或Servlet 过滤器。
- 切入点:切入点是与连接点匹配的表达式,用于确定是否需要执行建议。切入点使用与连接点匹配的不同类型的表达式,Spring 框架使用 AspectJ 切入点表达式语言。
- 目标对象:它们是应用建议的对象。Spring AOP 是使用运行时代理实现的,因此此对象始终是代理对象。这意味着在运行时创建一个子类,其中覆盖目标方法并根据其配置包含建议。
- AOP 代理:Spring AOP 实现使用 JDK 动态代理来创建具有目标类和建议调用的代理类,这些类称为 AOP 代理类。我们还可以通过在 Spring AOP 项目中将其添加为依赖项来使用 CGLIB 代理。
- 编织:将方面与其他对象链接以创建建议的代理对象的过程。这可以在编译时、加载时或运行时完成。Spring AOP 在运行时执行编织。
AOP 建议类型
根据advice的执行策略,主要有以下几种类型。
- 之前建议:这些建议在执行连接点方法之前运行。我们可以使用
@Before
注释将建议类型标记为之前建议。 - 最终(最终)建议:在连接点方法完成执行后执行的建议,无论是正常执行还是抛出异常。我们可以使用
@After
注释创建最终建议。 - 返回后建议:有时我们希望仅当连接点方法正常执行时才执行建议方法。我们可以使用
@AfterReturning
注释将方法标记为返回后建议。 - 抛出建议后:仅当连接点方法抛出异常时,才会执行此建议,我们可以使用它以声明方式回滚事务。我们
@AfterThrowing
对此类建议使用注释。 - 环绕建议:这是最重要和最强大的建议。此建议围绕连接点方法,我们还可以选择是否执行连接点方法。我们可以编写在执行连接点方法之前和之后执行的建议代码。环绕建议的职责是调用连接点方法并返回值(如果该方法返回某些内容)。我们使用
@Around
注释来创建环绕建议方法。
上面提到的要点可能听起来令人困惑,但是当我们看看 Spring AOP 的实现时,事情就会更加清晰。让我们开始创建一个带有 AOP 实现的简单 Spring 项目。Spring 提供了使用 AspectJ 注释创建方面的支持,我们将使用它来简化操作。所有上述 AOP 注释都在org.aspectj.lang.annotation
包中定义。Spring Tool Suite提供了有关方面的有用信息,因此我建议您使用它。如果您不熟悉 STS,我建议您查看Spring MVC 教程,我已在其中解释了如何使用它。
Spring AOP 示例
创建一个新的简单 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>
请注意,我在项目中添加了依赖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="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,我们需要执行以下操作:
- 声明 AOP 命名空间,如 xmlns:aop=“ https://www.springframework.org/schema/aop ”
- 添加 aop:aspectj-autoproxy 元素以在运行时启用 Spring AspectJ 支持自动代理
- 将 Aspect 类配置为其他 Spring bean
您可以看到我在 spring bean 配置文件中定义了很多方面,现在是时候逐一研究它们了。
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
注释。 - @Before注释用于创建 Before 建议
- 注解中传递的字符串参数
@Before
是Pointcut表达式 - getNameAdvice()建议将针对具有签名的任何 Spring Bean 方法执行
public String getName()
。这是需要记住的非常重要的一点,如果我们使用 new 运算符创建 Employee bean,则建议将不适用。只有当我们使用 ApplicationContext 获取 bean 时,建议才会被应用。 - 我们可以在切入点表达式中使用星号(*)作为通配符,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 to execute on all the methods of classes in a package
@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 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 围绕方面示例
如前所述,我们可以使用 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 Advice
如果你查看上述所有建议切入点表达式,它们可能会被应用于其他一些不期望的 bean。例如,有人可以使用 getName() 方法定义一个新的 spring bean,即使不是预期的,建议也会开始应用于该 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.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 项目