Spring AOP实战教程:详解切面、通知、切点、连接点及注解与XML配置
Spring Framework是基于两个核心概念——依赖注入和面向切面编程(Spring AOP)来开发的。
Spring AOP概述
我们已经看过了Spring的依赖注入是如何工作的,今天我们将深入了解面向切面编程的核心概念,以及如何使用Spring框架来实现它。
Spring AOP 概述
大多数企业应用程序具有一些通用的横切关注点,适用于不同类型的对象和模块。其中一些通用的横切关注点包括日志记录、事务管理、数据验证等。在面向对象编程中,应用程序的模块化是通过类来实现的,而在面向切面编程中,应用程序的模块化是通过切面来实现的,并且它们被配置为跨越不同的类。Spring AOP通过从类中删除直接依赖于横切任务的方式,实现了无法通过常规面向对象编程模型实现的功能。例如,我们可以为日志记录单独创建一个类,但是功能类仍然需要调用这些方法以实现在整个应用程序中的日志记录。
面向切面编程的核心概念
在我们深入研究Spring AOP实现之前,我们应该先理解AOP的核心概念。
- 切面(Aspect):切面是实现企业应用中跨越多个类的关注点的类,例如事务管理。切面可以是通过Spring XML配置进行配置的普通类,也可以使用Spring AspectJ集成通过@Aspect注解定义类作为切面。
- 连接点(Join Point):连接点是应用程序中的特定点,例如方法执行、异常处理、更改对象变量值等等。在Spring AOP中,连接点始终是方法的执行。
- 通知(Advice):通知是针对特定连接点采取的操作。从编程的角度来看,它们是在应用程序中达到某个匹配切入点时执行的方法。您可以将通知视为Struts2拦截器或Servlet过滤器。
- 切入点(Pointcut):切入点是与连接点匹配的表达式,用于确定是否需要执行通知。切入点使用不同类型的表达式与连接点匹配,Spring框架使用AspectJ切入点表达式语言。
- 目标对象(Target Object):它们是应用通知的对象。Spring AOP使用运行时代理来实现,因此该对象始终是代理对象。这意味着在运行时创建了一个子类,其中目标方法被重写,并根据配置包括了通知。
- AOP代理(AOP Proxy):Spring AOP实现使用JDK动态代理来创建带有目标类和通知调用的代理类,这些被称为AOP代理类。我们还可以通过在Spring AOP项目中添加CGLIB代理来使用CGLIB代理。
- 织入(Weaving):它是将切面与其他对象链接以创建通知的代理对象的过程。这可以在编译时、加载时或运行时完成。Spring AOP在运行时进行织入。
面向切面编程——通知类型
根据通知的执行策略,它们可以分为以下类型:
- 前置通知(Before Advice):这些通知在连接点方法执行之前运行。我们可以使用@Before注解将通知类型标记为前置通知。
- 后置(最终)通知(After (finally) Advice):一个在连接点方法完成执行后执行的通知,无论是正常完成还是抛出异常。我们可以使用@After注解创建后置通知。
- 返回后通知(After Returning Advice):有时我们希望只有在连接点方法正常执行时才执行通知方法。我们可以使用@AfterReturning注解将方法标记为返回后通知。
- 抛出后通知(After Throwing Advice):这个通知仅在连接点方法抛出异常时执行,我们可以使用它在声明式地回滚事务。我们使用@AfterThrowing注解来表示这种类型的通知。
- 环绕通知(Around Advice):这是最重要和强大的通知。这个通知围绕连接点方法,并且我们也可以选择是否执行连接点方法。我们可以编写通知代码,在执行连接点方法之前和之后执行。在环绕通知中,调用连接点方法和返回值(如果方法返回值)是环绕通知的责任。我们使用@Around注解创建环绕通知方法。
上述提到的几点可能听起来很令人困惑,但当我们深入研究Spring AOP的实现时,事情就会更加清楚了。让我们从创建一个简单的Spring项目并添加AOP实现开始。Spring支持使用AspectJ注解来创建切面,为了简单起见,我们将使用这种方式。所有上述的AOP注解都定义在org.aspectj.lang.annotation包中。Spring Tool Suite提供有关切面的有用信息,所以我建议你使用它。如果你对STS不熟悉,我建议你参考一下Spring MVC教程,其中有解释如何使用它。
Spring AOP示例
创建一个新的简单Spring Maven项目,使得所有的Spring Core库都包含在pom.xml文件中,我们不需要显式地引入它们。我们最终的项目将类似如下图,我们将详细了解Spring核心组件和切面实现。
Spring AOP AspectJ 依赖
这是文章《Spring AOP示例教程-方面、建议、切点、连接点、注解、XML配置》的第2部分(共5部分)。
内容片段: 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>
<!-- 通用属性 -->
<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>
<!-- 日志 -->
<logback.version>1.0.13</logback.version>
<slf4j.version>1.7.5</slf4j.version>
<!-- 测试 -->
<junit.version>4.11</junit.version>
<!-- AspectJ -->
<aspectj.version>1.7.4</aspectj.version>
</properties>
<dependencies>
<!-- Spring和事务 -->
<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>
<!-- 使用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依赖 -->
<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版本。
模型类
让我们创建一个简单的JavaBean,我们将在示例中使用一些额外的方法。Employee.java代码:
package com.Olivia.spring.model;
import com.Olivia.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注解。稍后我们会详细研究它的使用。
服务类
让我们创建一个服务类来与员工实体一起工作。EmployeeService.java的代码:
package com.Olivia.spring.service;
import com.Olivia.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配置
这是文章《春天AOP示例教程-方面、建议、切点、连接点、注释、XML配置》的第3部分(共5部分)。
如果您在使用STS(Spring Tool Suite),您可以选择创建”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">
<!-- 启用Spring AOP的AspectJ风格 -->
<aop:aspectj-autoproxy />
<!-- 配置Employee Bean并初始化 -->
<bean name="employee" class="com.Olivia.spring.model.Employee">
<property name="name" value="Dummy Name"></property>
</bean>
<!-- 配置EmployeeService bean -->
<bean name="employeeService" class="com.Olivia.spring.service.EmployeeService">
<property name="employee" ref="employee"></property>
</bean>
<!-- 配置切面Bean,没有这个切面通知将不会执行 -->
<bean name="employeeAspect" class="com.Olivia.spring.aspect.EmployeeAspect" />
<bean name="employeeAspectPointcut" class="com.Olivia.spring.aspect.EmployeeAspectPointcut" />
<bean name="employeeAspectJoinPoint" class="com.Olivia.spring.aspect.EmployeeAspectJoinPoint" />
<bean name="employeeAfterAspect" class="com.Olivia.spring.aspect.EmployeeAfterAspect" />
<bean name="employeeAroundAspect" class="com.Olivia.spring.aspect.EmployeeAroundAspect" />
<bean name="employeeAnnotationAspect" class="com.Olivia.spring.aspect.EmployeeAnnotationAspect" />
</beans>
在Spring beans中使用Spring AOP,我们需要进行以下操作:
- 声明AOP命名空间,如xmlns:aop=”https://www.springframework.org/schema/aop”
- 添加aop:aspectj-autoproxy元素以在运行时启用Spring AspectJ支持自动代理
- 将切面类配置为其他Spring bean。
你可以看到,在Spring的bean配置文件中我定义了很多切面,现在是时候逐个来看一下了。
Spring AOP 前置切面示例EmployeeAspect.java代码:
package com.Olivia.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("在getName()上执行通知");
}
@Before("execution(* com.Olivia.spring.service.*.get*())")
public void getAllAdvice(){
System.out.println("调用了服务方法getter");
}
}
上述切面类中的重要要点有:
- 切面类需要添加@Aspect注解。
- @Before注解用于创建前置通知。
- @Before注解中传入的字符串参数是切入点表达式。
- getNameAdvice()通知将对任何签名为public String getName()的Spring Bean方法执行。这是一个需要记住的重要点,如果我们使用new运算符创建Employee bean,通知将不会被应用。只有当我们使用ApplicationContext获取bean时,通知才会被应用。
- 我们可以在切入点表达式中使用星号(*)作为通配符,getAllAdvice()将应用于com.Olivia.spring.service包中所有名称以get开头且不接受任何参数的类。
在我们研究了所有不同类型的通知之后,我们将在一个测试类中观察这些通知的实施情况。
Spring AOP切入点方法和重用
有时我们不得不在多个地方使用相同的切入点表达式,我们可以创建一个带有@Pointcut注解的空方法,然后在通知中使用它作为表达式。EmployeeAspectPointcut.java代码:
package com.Olivia.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("在getName()上执行loggingAdvice");
}
@Before("getNamePointcut()")
public void secondAdvice(){
System.out.println("在getName()上执行secondAdvice");
}
@Pointcut("execution(public String getName())")
public void getNamePointcut(){}
@Before("allMethodsPointcut()")
public void allServiceMethodsAdvice(){
System.out.println("执行服务方法之前");
}
//切入点用于执行包中所有类的所有方法
@Pointcut("within(com.Olivia.spring.service.*)")
public void allMethodsPointcut(){}
}
以上例子非常清楚,我们在通知注解参数中使用的是方法名称,而不是表达式。
Spring AOP的JoinPoint和Advice参数
Spring AOP 连接点参数使用
我们可以在通知方法中将JoinPoint作为参数使用,并使用它获取方法签名或目标对象。我们可以在切点中使用args()表达式,以适用于匹配参数模式的任何方法。如果我们使用这个方法,那么我们需要在通知方法中使用相同的名称来确定参数类型。我们还可以在通知参数中使用泛型对象。EmployeeAspectJoinPoint.java代码:
package com.Olivia.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.Olivia.spring.model..set*(*))")
public void loggingAdvice(JoinPoint joinPoint){
System.out.println("在运行loggingAdvice方法之前="+joinPoint.toString());
System.out.println("传递的参数=" + Arrays.toString(joinPoint.getArgs()));
}
//通知参数,将应用于具有单个String参数的bean方法
@Before("args(name)")
public void logStringArguments(String name){
System.out.println("传递的字符串参数="+name);
}
}
Spring AOP 后置通知示例
让我们来看一个简单的切面类,其中包括After、After Throwing和After Returning的示例通知。EmployeeAfterAspect.java代码:
package com.Olivia.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("运行后置通知。传递的字符串参数="+name);
}
@AfterThrowing("within(com.Olivia.spring.model.Employee)")
public void logExceptions(JoinPoint joinPoint){
System.out.println("Employee方法中抛出异常="+joinPoint.toString());
}
@AfterReturning(pointcut="execution(* getName())", returning="returnString")
public void getNameReturningAdvice(String returnString){
System.out.println("getNameReturningAdvice已执行。返回的字符串="+returnString);
}
}
我们可以在切点表达式中使用”within”来将建议应用于类中的所有方法。我们可以使用@AfterReturning建议来获取被建议方法返回的对象。我们在Employee bean中有一个throwException()方法,用来展示使用After Throwing建议的用法。
Spring AOP 环绕切面示例
如前所述,我们可以使用Around切面在方法执行之前和之后进行切入。我们可以使用它来控制是否执行被建议的方法。我们还可以检查返回值并进行修改。这是最强大的建议,需要适当地应用。EmployeeAroundAspect.java代码:
package com.Olivia.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.Olivia.spring.model.Employee.getName())")
public Object employeeAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
System.out.println("调用getName()方法之前");
Object value = null;
try {
value = proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("调用getName()方法之后。返回值="+value);
return value;
}
}
在切面编程中,环绕通知总是需要将ProceedingJoinPoint作为参数,并且我们应该使用它的proceed()方法调用被通知方法的目标对象。如果被通知方法有返回值,那么它的责任就是将返回值返回给调用程序。对于无返回值的方法,通知方法可以返回null。由于环绕通知切入了被通知方法,因此我们可以控制方法的输入、输出以及执行行为。
使用自定义注释切入点的Spring建议
如果您查看以上所有的建议切入点表达式,有可能它们会应用到一些不应该应用的其他Bean上。例如,某人可以定义一个具有getName()方法的新Spring Bean,即使原意不是如此,建议也会开始应用到该Bean上。这就是为什么我们应该尽可能将切入点表达式的范围保持狭窄的原因。另一种方法是创建一个自定义注解,将我们希望应用建议的方法注释起来。这就是Employee的setName()方法被@Loggable注解标注的目的。Spring框架的@Transactional注解就是Spring事务管理的一个很好的例子。Loggable.java的代码如下:
package com.Olivia.spring.aspect;
public @interface Loggable {
}
EmployeeAnnotationAspect.java代码:
package com.Olivia.spring.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class EmployeeAnnotationAspect {
@Before("@annotation(com.Olivia.spring.aspect.Loggable)")
public void myAdvice(){
System.out.println("执行我的建议!!");
}
}
只有在setName()方法中,myAdvice()方法会进行通知。这种方法非常安全,并且当我们想要在任何方法上应用通知时,只需要用Loggable注解进行注释即可。
Spring的AOP XML配置
这是文章《Spring AOP示例教程-切面、通知、切入点、连接点、注解、XML配置》的第5部分(共5部分)。
我总是更喜欢使用注解,但我们也有在Spring配置文件中配置切面的选项。例如,假设我们有一个如下的类。EmployeeXMLConfigAspect.java代码:
package com.Olivia.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.Olivia.spring.aspect.EmployeeXMLConfigAspect" />
<!-- Spring AOP XML Configuration -->
<aop:config>
<aop:aspect ref="employeeXMLConfigAspect" id="employeeXMLConfigAspectID" order="1">
<aop:pointcut expression="execution(* com.Olivia.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.Olivia.spring.main;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.Olivia.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.Olivia.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.Olivia.spring.model.Employee.throwException())
Exception in thread "main" java.lang.RuntimeException: Dummy Exception
at com.Olivia.spring.model.Employee.throwException(Employee.java:19)
at com.Olivia.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.Olivia.spring.model.Employee$$EnhancerBySpringCGLIB$$3f881964.throwException(<generated>)
at com.Olivia.spring.main.SpringMain.main(SpringMain.java:17)
你可以看到根据它们的切入点配置,通知逐一执行。你应该逐一配置它们,以避免混淆。这就是Spring AOP示例教程的全部内容,希望通过本教程你已经了解了Spring AOP的基础知识,并能够从实例中学到更多。你可以从下方链接下载示例项目并进行实践。