Spring AOP
在软件中,有些行为对于大多数应用都是通用的。日志、安全和事务管理的确很重要,但它们是否是应用对象主动参与的行为呢?如果让应用对象只关注于自己所针对的业务领域问题,而其他方面的问题有其他应用对象来处理,这样会更好
在软件开发中,分布于应用中多处的功能被称为横切关注点(cross-cutting coners)。通常,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往直接嵌入到应用的业务逻辑之中)。将这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的
依赖注入(DI)有助于应用对象之间的解耦,而AOP可以实现横切关注点与它们所影响的对象之间的解耦
AOP基础
如下的ArithmeticCalculator
接口,定义加减乘除方法:
public interface ArithmeticCalculator {
//加
int add(int i, int j);
//减
int sub(int i, int j);
//乘
int mul(int i, int j);
//除
int div(int i, int j);
}
假设程序现在有如下的需求:
- 日志-在程序执行期间追踪正在发生的活动
- 验证-希望计算器只能处理正确的运算
根据需求,实现ArithmeticCalculator
接口如下:
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
System.out.println("The method add begins with [" + i + "," + j + "]");
int result = i + j;
System.out.println("The method add ends with " + result);
return result;
}
......
}
这样做会发现:
- 加减乘除方法中都要添加类似的输出语句
- 方法中的逻辑会显的很混乱
总结起来,这样做有如下的问题:
- 代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点
- 代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块
如何解决上述问题呢?
可以使用动态代理,代理设计模式的原理是:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
如下,创建一个计算器的日志代理类ArithmeticCalculatorLogginProxy
,详细说明见注释:
package com.wz.spring.aop.helloworld;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class ArithmeticCalculatorLogginProxy {
//要代理的对象
private ArithmeticCalculator target;
public ArithmeticCalculatorLogginProxy(ArithmeticCalculator target) {
this.target = target;
}
public ArithmeticCalculator getLoggingProxy(){
ArithmeticCalculator proxy = null;
//代理对象由哪一个类加载器负责加载
ClassLoader loader = target.getClass().getClassLoader();
//代理对象的类型,即其中有哪些方法
Class[] interfaces = new Class[] {ArithmeticCalculator.class};
//当调用代理对象其中的方法时,执行代码
InvocationHandler h = new InvocationHandler() {
/**
* proxy - 正在返回的那个代理对象,一般情况下,在invoke方法中都不使用该对象
* method - 正在被调用的方法
* args - 调用方法时,传入的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//日志
System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
//执行方法
Object result = method.invoke(target, args);
//日志
System.out.println("The method " + methodName + " ends with " + result);
return result;
}
};
proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h);
return proxy;
}
}
进行如下的测试,代码如下:
ArithmeticCalculator target = new ArithmeticCalculatorImpl();
ArithmeticCalculator proxy = new ArithmeticCalculatorLogginProxy(target).getLoggingProxy();
int result = proxy.add(1, 2);
System.out.println("-->" + result);
测试控制台输出如下:
The method add begins with [1, 2]
The method add ends with 3
-->3
可以发现,用动态代理实现的日志方式,比原来的那种方式,更简洁,更方便
但如果每个需求都要写个动态代理,其实还是有很多的工作量,能不能用一个更简单的方式来实现呢?答案就是使用AOP
AOP简介
AOP(Aspect-Oriented Programming)面向切面编程。AOP的主要编程对象是切面(aspect),而切面模块化横切关注点
在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,以什么方式应用,并且在不必修改受影响的类。这样一来横切关注点就被模块化到特殊的对象(切面)里
AOP的好处:
- 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
- 业务模块更简洁,只包含核心业务代码
AOP术语
切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象。(如上图中的验证,日志等)。
通知(Advice):切面必须要完成的工作(可以把切面里的每一个方法称为通知)
目标(Target):被通知的对象
代理(Proxy):向目标对象应用通知之后创建的对象
连接点(Joinpoint):程序执行的某个特定位置。如类某个方法调用前、调用后、方法抛出异常后等。连接点由两个信息确定:方法(表示程序的执行点),相对点(表示方位)。例如,ArithmeticCalculator#add()
方法执行前的连接点,其中执行点为ArithmeticCalculator#add()
,方位为该方法执行前的位置
切点(pointcut):每个都有多个连接点,例如ArithmeticCalculator
的所有方法都是连接点,即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。类比:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过org.springframework.aop.Pointcut
接口进行描述,它使用类和方法作为连接点的查询条件
在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。AspectJ是Java社区里最完整最流行的AOP框架。
简单的理解:
- 切面是通知和切点的集合
- 通知定义了切面是什么以及何时使用
- 如果通知定义了切面的”什么”和”何时”,那么切点就定义了”何处”。切点的定义会匹配通知所要织入的一个或多个连接点
在Spring中启用AspectJ注解支持
要在Spring应用中使用AspectJ注解,必须在classpath下包含AspectJ类库:aopalliance.jar
、aspectj.weaver.jar
和spring-aspects.jar
。全部的包如下:
创建Spring
的配置文件applicationContext.xml
,加入beans
、aop
、context
命令空间
现在要做的是还是一样在调用ArithmeticCalculator
接口中的方法时加入日志功能
基本步骤如下:
1.给ArithmeticCalculatorImpl
添加@Component
注解
@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
......
}
2.从业务逻辑中抽取横切关注点,有关联的、相似的横切关注点可组成切面。这里,创建日志切面LoggingAspect
把LoggingAspect
声明为一个切面需要如下的步骤:
- 需要把该类放入IoC容器中,使用
@Component
注解 - 声明为一个切面,使用
@Aspect
注解
LoggingAspect
结构如下:
//把该类声明为一个切面
//1.需要把该类放入ioc容器中
//2.再声明为一个切面
@Component
@Aspect
public class LoggingAspect {
......
}
3.applicationContext.xml
中的配置,这里是基于注解的方式
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.wz.spring.aop.impl"></context:component-scan>
<!-- 使AspectJ注解起作用:自动为匹配的类生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
通知
Spring切面可以应用5中类型的通知
Before
-在方法被调用之前调用通知After
-在方法完成之后调用通知,无论方法执行是否成功After-returning
-在方法成功执行之后调用通知After-throwing
-在方法抛出异常后调用通知Around
-通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
前置通知
在LoggingAspect
中创建一个beforeMethod
方法,这里主要是用来在调用ArithmeticCalculator
接口的方法前输出日志
@Before
声明方法是一个前置通知,在目标方法开始之前执行
//声明该方法是一个前置通知,在目标方法开始之前执行
@Before("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.add(int, int))")
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();//方法名称
List<Object> args = Arrays.asList(joinPoint.getArgs());//参数
System.out.println("The method " + methodName + " begins" + args);
}
一些说明:
1.@Before("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.add(int, int))")
表示在AthimeticCalculator
的add
方法调用前执行
如果想作用于AthimeticCalculator
下的所有方法,可以使用*
@Before("execution(public int com.wz.spring.aop.impl.AthimeticCalculator.*(int, int))")
execution
用于匹配连接点的执行方法
execution * com.wz.spring.aop.impl.AthimeticCalculator.*(..)
匹配AthimeticCalculator
中声明的所有方法,第一个*
代表任意修饰符及任意返回值,第二个*
代表任意方法,..
匹配任意数量的参数。若目标类与接口与该切面在同一个包中,可以省略包名execution public * com.wz.spring.aop.impl.AthimeticCalculator.*(..)
匹配AthimeticCalculator
接口的所有公共方法execution public double com.wz.spring.aop.impl.AthimeticCalculator.*(..)
匹配AthimeticCalculator
接口中返回double
类型数值的方法execution public double com.wz.spring.aop.impl.AthimeticCalculator.*(double,..)
匹配第一个参数为double
类型的方法,..
匹配任意数量的参数execution public double com.wz.spring.aop.impl.AthimeticCalculator.*(double,double)
匹配参数类型为double
,double
类型的方法
这里需要注意的是,一定要在applicationContext.xml
中,添加上:
<!-- 使AspectJ注解起作用:自动为匹配的类生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
其作用是:当我们调用一个目标方法,而这个目标方法跟注解声明的这个方法相匹配时,AOP框架会自动为方法所在的类生成一个代理。
2.方法的参数JoinPoint
,主要用来获取方法的名称和参数
3.测试如下
public static void main(String[] args) {
//1.创建spring ioc容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//2.从ioc容器中获取bean的实例
ArithmeticCalculator arithmeticCalculator = ctx.getBean(ArithmeticCalculator.class);
//3.使用bean
int result = arithmeticCalculator.add(1, 2);
System.out.println("result = " + result);
}
控制台输出结果如下:
The method add begins[1, 2]
result = 3
后置通知
后置通知在目标方法执行后(无论是否发生异常),执行通知。
在后置方法中还不能访问目标方法执行的结果。
//后置通知在目标方法执行后(无论是否发生异常),执行通知
@After("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends");
}
如下一个数除以0,如果抛出异常,任然会执行通知:
result = arithmeticCalculator.div(1, 0);
System.out.println("result = " + result);
控制台输出结果为:
The method div begins[1, 0]
The method div ends
返回通知
在方法正常结束后执行的代码,返回通知是可以访问到方法的返回值的。
- 在返回通知中,只要将
returning
属性添加到注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称 - 必须在通知方法的签名中添加一个同名参数。在运行时,Spring AOP会通过这个参数传递返回值
@AfterReturning(value="execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))", returning="result")
public void afterReturning(JoinPoint joinPoint, Object result ) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method "+methodName +" return with "+result);
}
异常通知
在方法出现异常时会执行的代码,可以访问到异常的对象,且可以指定在出现特定异常时在执行通知代码
- 将
throwing
属性添加到@AfterThrowing
注解中 - 如果只对某种特殊类型的异常类型感兴趣,可以将参数声明为其他异常的参数类型,然后通知就只在抛出这个类型及其子类的异常时才被执行
@AfterThrowing(value="execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))", throwing="ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("The method "+methodName +" throw with "+ex);
}
一个数除以0,此时测试,控制输出如下的接口:
The method div begins[1, 0]
The method div ends
The method div throw with java.lang.ArithmeticException: / by zero
环绕通知
环绕通知需要携带ProceedingJoinPoint
类型的参数。环绕通知类似于动态代理的全过程,ProceedingJoinPoint
类型的参数可以决定是否执行目标方法。且环绕通知必须有返回值,返回值即为目标方法的返回值。
@Around("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))")
public Object around(ProceedingJoinPoint pjd) {
System.out.println("around");
Object result = null;
String methodName = pjd.getSignature().getName();
//执行目标方法
try {
//前置通知
System.out.println("The method " + methodName + " begins with " + Arrays.asList(pjd.getArgs()));
result = pjd.proceed();
//后置通知
System.out.println("The method "+methodName +" ends with "+Arrays.asList(pjd.getArgs()));
} catch (Throwable e) {
//异常通知
e.printStackTrace();
}
//后置通知
return result;
}
切面的优先级
现在再添加一个验证切面ValidationAspect
,如下:
@Aspect
@Component
public class ValidationAspect {
@Before("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))")
public void validateArgs(JoinPoint joinPoint){
System.out.println("validate: " + Arrays.asList(joinPoint.getArgs()));
}
}
那现在就有一个问题是,日志通知和验证通知,哪一个在前,哪一个在后呢?
可以指定切面的优先级,指定哪个切面在前,哪个切面在后。
使用@Order
注解指定切面的优先级,值越小优先级越高
如下:
@Order(1)
@Aspect
@Component
public class ValidationAspect {
@Before("execution(public int com.wz.spring.aop.impl.ArithmeticCalculator.*(int, int))")
public void validateArgs(JoinPoint joinPoint){
System.out.println("validate: " + Arrays.asList(joinPoint.getArgs()));
}
}
此时,控制台就会先输出验证的通知:
validate: [1, 2]
The method add begins[1, 2]
重用切点表达式
@Pointcut
来声明切入点表达式,后面的其他通知直接使用方法名来引用当前的切入点表达式。
/**
* 定义一个方法,用于声明切入点表达式
* 一般的,该方法中不需要添入其他的代码
*/
@Pointcut("execution(public int aop.impl.AthimeticCalculator.*(int, int))")
public void declareJointPoinExpression() {}
@Before("declareJointPoinExpression()")
public void beforeMethod(JoinPoint joinpoint) {
String methodName = joinpoint.getSignature().getName();
List<Object> args = Arrays.asList(joinpoint.getArgs());
System.out.println("The method "+methodName +" begins with "+args);
}
Spring基于配置文件的方式来配置AOP
Spring也支持在Bean配置文件中声明切面。这种声明是通过aop schema中的XML元素完成的。
正常情况下,基于注解的声明要优于基于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.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd">
<!-- 配置Bean -->
<bean id="athimeticCalculator" class="aop.impl.xml.AthimeticCalculatorImpl"></bean>
<!-- 配置切面的bean -->
<bean id="loggingAspect" class="aop.impl.xml.LoggingAspect"></bean>
<bean id="validationAspect" class="aop.impl.xml.ValidationAspect"></bean>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切点表达式 这里可以服用,避免重复 -->
<aop:pointcut expression="execution(* aop.impl.xml.*.*(..))" id="pointcut"/>
<!-- 配置切面及通知 -->
<aop:aspect ref="loggingAspect" order="2">
<aop:before method="beforeMethod" pointcut-ref="pointcut"/>
<aop:after method="afterMethod" pointcut-ref="pointcut"/>
</aop:aspect>
<aop:aspect ref="validationAspect" order="1">
<aop:before method="validateArgs" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
</beans>
需要注意的是:
1.大多数的AOP配置元素必须在<aop:config>
元素的上下文内使用
2.在<aop:config>
元素内,可以声明一个或多个通知器、切面或者切点。aop:aspect
的ref
元素引用了一个POJO Bean
3.aop:before
表示前置通知,pointcut
属性定义了通知所应用的切点。
4.aop:pointcut
定义了一个命名切点,可以避免重复定义切点
Spring的AOP配置元素简化了基于POJO切面的声明
AOP配置元素 | 描述 |
---|---|
<aop:advisor> | 定义AOP通知器 |
<aop:after> | 定义AOP后置通知(不管被通知的方法是否执行成功) |
<aop:after-returning> | 定义AOP after-returning通知 |
<aop:after-throwing> | 定义after-throwing通知 |
<aop:around> | 定义AOP环绕通知 |
<aop:aspect> | 定义切面 |
<aop:aspectj-autoproxy> | 启用@AspectJ注解驱动的切面 |
<aop:before> | 定义AOP前置通知 |
<aop:config> | 顶层AOP配置元素。大多数的<aop:*> 元素必须包含在<aop:config> 元素内 |
<aop:declare-parents> | 为被通知的对象引入额外的接口,并透明的实现 |
<aop:pointcut> | 定义切点 |