春天AOP示例教程-方面、建议、切点、连接点、注释、XML配置
Spring Framework是基于两个核心概念——依赖注入和面向切面编程(Spring AOP)来开发的。
春天的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代理来使用CGLIB代理。
编织:它是将方面与其他对象链接以创建通知的代理对象的过程。这可以在编译时、加载时或运行时完成。Spring AOP在运行时进行编织。
面向切面编程——建议类型
根据建议的执行策略,它们可以分为以下类型。
-
- 前置通知:这些建议在连接点方法执行之前运行。我们可以使用@Before注释将建议类型标记为前置建议。
-
- 后置(最终)通知:一个在连接点方法完成执行后执行的建议,无论是正常完成还是抛出异常。我们可以使用@After注释创建后置建议。
-
- 返回后通知:有时我们希望只有在连接点方法正常执行时才执行建议方法。我们可以使用@AfterReturning注释将方法标记为返回后建议。
-
- 抛出后通知:这个建议仅在连接点方法抛出异常时执行,我们可以使用它在声明式地回滚事务。我们使用@AfterThrowing注释来表示这种类型的建议。
- 环绕通知:这是最重要和强大的建议。这个建议围绕连接点方法,并且我们也可以选择是否执行连接点方法。我们可以编写建议代码,在执行连接点方法之前和之后执行。在环绕建议中,调用连接点方法和返回值(如果方法返回值)是环绕建议的责任。我们使用@Around注释创建环绕建议方法。
上述提到的几点可能听起来很令人困惑,但当我们深入研究Spring AOP的实现时,事情就会更加清楚了。让我们从创建一个简单的Spring项目并添加AOP实现开始。Spring支持使用AspectJ注解来创建切面,为了简单起见,我们将使用这种方式。所有上述的AOP注解都定义在org.aspectj.lang.annotation包中。Spring Tool Suite提供有关切面的有用信息,所以我建议你使用它。如果你对STS不熟悉,我建议你参考一下Spring MVC教程,其中有解释如何使用它。
春季AOP示例
创建一个新的简单Spring Maven项目,使得所有的Spring Core库都包含在pom.xml文件中,我们不需要显式地引入它们。我们最终的项目将类似如下图,我们将详细了解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>
请注意,我已在项目中添加了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 beans的入口点。
使用AOP的春季Bean配置
如果您在使用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">
<!-- Enable AspectJ style of Spring AOP -->
<aop:aspectj-autoproxy />
<!-- Configure Employee Bean and initialize it -->
<bean name="employee" class="com.Olivia.spring.model.Employee">
<property name="name" value="Dummy Name"></property>
</bean>
<!-- Configure EmployeeService bean -->
<bean name="employeeService" class="com.Olivia.spring.service.EmployeeService">
<property name="employee" ref="employee"></property>
</bean>
<!-- Configure Aspect Beans, without this Aspects advices wont execute -->
<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支持自动代理
将Aspect类配置为其他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("Executing Advice on getName()");
}
@Before("execution(* com.Olivia.spring.service.*.get*())")
public void getAllAdvice(){
System.out.println("Service method getter called");
}
}
上述方面课程中的重要要点有:
- Aspect classes are required to have @Aspect annotation.
- @Before annotation is used to create Before advice
- The string parameter passed in the @Before annotation is the Pointcut expression
- getNameAdvice() advice will execute for any Spring Bean method with signature public String getName(). This is a very important point to remember, if we will create Employee bean using new operator the advices will not be applied. Only when we will use ApplicationContext to get the bean, advices will be applied.
- We can use asterisk (*) as wild card in Pointcut expressions, getAllAdvice() will be applied for all the classes in com.Olivia.spring.service package whose name starts with get and doesn’t take any arguments.
在我们研究了所有不同类型的建议之后,我们将在一堂测试课中观察这些建议的实施情况。
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("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.Olivia.spring.service.*)")
public void allMethodsPointcut(){}
}
以上例子非常清楚,我们在建议注解参数中使用的是方法名称,而不是表达式。
Spring AOP的JoinPoint和Advice参数。
我们可以在通知方法中将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("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.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("Running After Advice. String argument passed="+name);
}
@AfterThrowing("within(com.Olivia.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.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("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()方法调用被通知方法的目标对象。如果被通知方法有返回值,那么它的责任就是将返回值返回给调用程序。对于无返回值的方法,通知方法可以返回null。由于环绕通知切入了被通知方法,因此我们可以控制方法的输入、输出以及执行行为。
使用自定义注释切入点的春季建议
如果您查看以上所有的建议切入点表达式,有可能它们会应用到一些不应该应用的其他Bean上。例如,某人可以定义一个具有getName()方法的新Spring Bean,即使原意不是如此,建议也会开始应用到该Bean上。这就是为什么我们应该尽可能将切入点表达式的范围保持狭窄的原因。另一种方法是创建一个自定义注解,将我们希望应用建议的方法注释起来。这就是Employee的setName()方法被@Loggable注解标注的目的。Spring框架的@Transactional注解就是Spring事务管理的一个很好的例子。Loggable.java的代码如下:
package com.Olivia.spring.aspect;
public @interface Loggable {
}
EmployeeAnnotationAspect.java代码:
员工注解切面.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("Executing myAdvice!!");
}
}
只有在setName()方法中,myAdvice()方法会进行通知。这种方法非常安全,并且当我们想要在任何方法上应用通知时,只需要用Loggable注解进行注释即可。
Spring的AOP XML配置
我总是更喜欢使用注解,但我们也有在春配置文件中配置方面的选项。例如,假设我们有一个如下的类。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 示例
让我们编写一个简单的春季程序,看看所有这些方面是如何穿插在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的基础知识,并可以从实例中学到更多。从下方链接下载示例项目并进行试验。
下载Spring AOP项目