在介绍AOP之前,我们先来解决一个问题。假如我想在一个方法逻辑执行之前对它的参数进行验证,在它执行之后对它的结果进行日志记录,我们的做法一般如下代码
package com.bupt.springtest.aop; public interface ArithmeticCalculator { public int add(int i, int j); }
package com.bupt.springtest.aop; //直接在源代码中增加验证和日志逻辑 public class ArithmeticCalculatorImp implements ArithmeticCalculator { @Override public int add(int i, int j) {
//验证 System.out.println("The param of add method is " + i + ", " + j); int result = i + j;
//日志 System.out.println("The result of add method is " + result); return result; } }
直接在源代码里增加上述功能虽然能达到效果,但如此编写会带来很多问题:
1. 代码混乱:随着越来越多的非业务逻辑需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还须兼顾其它多个关注点。
2. 代码分散:以日志需求为例,指示为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块。
当然,如果你学过动态代理,应该能想到可以使用动态了代理来消除以上缺陷。动态代理的思想就是可以把业务逻辑交由一个代理类来实现,同时在实现的前后我们可以增加自己的逻辑。对应于这里就是,我们可以在代理的前后加上验证和日志逻辑。
//此时的核心业务逻辑中不再包含其他非业务逻辑,代码更简洁
public class ArithmeticCalculatorImp implements ArithmeticCalculator { @Override public int add(int i, int j) { int result = i + j; return result; } }
package com.bupt.springtest.aop; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; public class ArithmeticCalculatorProxy { //被代理对象 private ArithmeticCalculator target; public ArithmeticCalculatorProxy(ArithmeticCalculator target) { this.target = target; } public ArithmeticCalculator getProxy() { //定义一个与被代理类同类型的代理对象 ArithmeticCalculator proxy = null; //被代理对象由哪个类加载器加载 ClassLoader loader = target.getClass().getClassLoader(); //代理对象的类型,即其中有哪些方法 Class[] interfaces = new Class[]{ArithmeticCalculator.class}; //当调用被代理对象的方法时,程序交由invoke方法处理 InvocationHandler h = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("The params of " + method.getName() + " method is " + Arrays.asList(args)); Object result = method.invoke(target, args); System.out.println("The result of " + method.getName() + " method is " + result); return result; } }; //生成代理对象 proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h); return proxy; } public static void main(String[] args) { ArithmeticCalculator target = new ArithmeticCalculatorImp(); ArithmeticCalculator proxy = new ArithmeticCalculatorProxy(target).getProxy(); //执行此方法时,程序转由invoke方法处理 proxy.add(2, 1); } }
由结果可以看到,我们在不往源代码中添加功能的情况下同样做到了验证和日志功能。但是,在实际开发中一个项目中可能有成百上千个业务需要增加验证和日志,这就导致使用自定义的动态代理来实现上述功能非常不现实,而Spring框架的 AOP 功能为我们很好的解决了这些问题。而且 Spring AOP 就是基于动态代理的原理来实现的,我们只需要配置相应的文件以及注解就能轻松地达到上面一大串代码所实现的功能。
AOP简介
AOP(Aspect-Oriented Programming, 面向切面编程):是一种新的方法论,是对传统OOP(Object-Oriented Programming, 面型对象编程)的补充。它主要的编程对象是切面(aspect),它是横切关注点的模块化。在应用AOP编程时,仍然需要定义公共功能(如:日志或验证),但可以明确的定义这个功能在哪里,以什么方式应用,并且不必修改受影响类。这样一来横切关注点就被模块化到特殊的对象(切面)里去了。由此带来的好处包括:1. 每个事物逻辑位于一个位置,代码不分散,便于维护和升级;2. 业务模块更简洁,只包含核心业务代码。
我们先来介绍几个AOP的概念
1. 切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
2. 通知(Advice):切面必须完成的工作,这里的验证和日志就可以看成是两个通知
3. 目标(Target):被通知的对象
4. 代理(Proxy):向目标对象应用通知之后创建的对象
5. 连接点(Joinpoint):程序执行的某个特定位置;如类的某个方法调用前、后或方法抛出异常后等。连接点由两个信息确定:方法表示的程序执行点;相对点表示的方位。例如ArithmethicCalculator.add() 方法执行前的连接点,执行点为 ArithmethicCalculator.add(),方位为该方法执行前的位置。
6. 切点(Pointcut):每个类拥有多个连接点。例如:ArithmethicCalculator 的所有方法实际上都是连接点,即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点,类似于:连接点相当于数据库中的记录,切点相当于查询条件。切点和连接点不是一对一的关系,一个切点匹配多个连接点,切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
7. 引入(Introduction):用来给一个类型声明额外的方法或属性(也称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象
8. 织入(Weaving):织入是将切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。
编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
类加载时期:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5 的 LTW(load-time weaving)就支持以这种方式织入切面。
运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的。
Spring aop vs AspectJ
Spring除了有自带的spring aop框架之外,Spring也支持使用AspecJ注解的方式进行AOP编程,一般来说推荐使用AspectJ的形式进行开发。
Spring aop是aop实现方案的一种,它支持在运行期基于动态代理的方式将aspect织入目标代码中来实现aop。但是spring aop的切入点支持有限,而且对于static和final的方法都无法支持aop(因为此类方法无法生成代理类);另外 spring aop 只支持被IoC管理的 bean,其他普通java类无法支持aop。
AspectJ是一个代码生成工具,其中AspectJ语法就是用来定义代码生成规则的语法。基于自己的语法编译工具,编译结果是Java Class文件,运行时classpath需要包含AspectJ的一个jar文件,支持编译时织入切面,即所谓的CTW机制,可以通过一个Ant或Maven任务完成这个操作。AspectJ有自己的类装载器,支持在类装载时织入切面,即所谓的LTW机制。AspectJ同样支持运行时织入,运行时织入是基于动态代理的机制。
区别:Spring aop采用动态织入,而 AspectJ 是静态织入。静态织入:指在编译时期就织入,即编译出来的calss文件,字节码就已经被织入。动态织入又分静态和动态两种,静态指织入过程只在第一次调用时执行;动态指根据代码动态运行的中间态来决定如何操作,每次调用Target的时候都执行。
AspectJ 注解声明切面
要在Spring应用中使用AspectJ注解,必须在classpath下包含AspectJ类库:com.springsource.org.aopalliance.jar 和 com.springsource.org.aspectj.weaver.jar;将aop schema添加到 <beans> 根元素中;在Spring IoC 容器中启用 AspectJ直接支持,只要在 bean 配置文件中定义一个空的XML元素 <aop:aspectj-autoproxy>;当Spring IoC 容器检测到 bean 配置文件中的 <aop:aspectj-autoproxy> 元素时,就会自动为与 AspectJ 切面匹配的 bean 创建代理。
要在Spring中声明 AspectJ 切面,只需要在IoC容器中将切面声明为 bean 实例。当在Spring IoC 容器中初始化 AspectJ切面之后,Spring IoC 容器就会为那些与 AspectJ切面匹配的 bean 创建代理。在AspectJ注解中,切面只是一个带有 @AspectJ 注解的java类。
通知是标有某种注解的简单地java方法,AspectJ 支持5种类型的通知注解:
1. @Before:前置通知,在方法执行之前执行
2. @After:后置通知,在方法执行之后执行
3. @AfterReturning:返回通知,在方法返回结果之后执行
4. @AfterThrowing:异常通知,在方法抛出异常之后
5. @Around:环绕通知,围绕着方法的执行
典型的切入点表达式是根据方法的签名来匹配各种方法:
- execution (* *.*(..)):第一个 * 代表匹配任意修饰符和返回值,第二个 * 代表任意类的对象,第三个 * 代表任意方法,参数列表中的 .. 匹配任意数量的参数
- execution (* com.bupt.springtest.aop.ArithmeticCalculator.*(..)):匹配 ArithmeticCalculator 中声明的所有方法,第一个 * 代表任意修饰符及任意返回值;第二个 * 代表任一方法; .. 匹配任意数量的参数。若目标类或接口与该切面在同一个包中,可以省略包名。
- execution (public double ArithmeticCalculator.*(double, ..)):匹配第一个参数为double类型的方法, .. 匹配任意数量任意类型的参数
- execution (public double ArithmeticCalculator.*(double, double)):匹配参数类型为double, double类型的方法
前置通知:在方法执行之前执行的通知。
前置通知使用 @Before 注解,并将切入点表达式的值作为注解值。可以在通知方法中声明一个类型为 JointPoint 的参数。然后就能访问链接细节,如方法名称和参数值等。
后置通知:是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候。无论连接点是否正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。
下面看代码示例
package com.bupt.springtest.aop; public interface ArithmeticCalculator { public int divide(int i, int j); }
package com.bupt.springtest.aop; import org.springframework.stereotype.Component; //主业务类 @Component public class ArithmeticCalculatorImp implements ArithmeticCalculator { @Override public int divide(int i, int j) { int result = i / j; return result; } }
package com.bupt.springtest.aop; import java.util.Arrays; import java.util.List; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; /* * 创建一个日志切面类
* 把横切关注点的代码抽象到切面类中
* 切面首先是个IoC中的 bean,即加入 @Component 注解,其次还需加入 @Aspect 注解 */ @Aspect @Component public class LoggingAspect { //声明一个前置通知,指定我要在哪个方法前面增加日志信息 @Before("execution(public int com.bupt.springtest.aop.ArithmeticCalculatorImp.divide(int, int))") public void beforeMethod() { System.out.println("before method"); } /* //还可以通过 JoinPoint 可以获取方法的信息 @Before("execution(public int com.bupt.springtest.aop.ArithmeticCalculatorImp.divide(int, int))") public void beforeMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getDeclaringTypeName(); List<Object> args = Arrays.asList(joinPoint.getArgs()); System.out.println("The params of " + methodName + " method is " + args); } */
/*
* 后置通知:在目标执行后(无论是否发生异常),执行的通知
* 在后置通知中还不能访问目标方法执行的结果(返回结果在返回通知中访问)
*/
@After("execution(public int com.bupt.springtest.aop.ArithmeticCalculatorImp.divide(int, int))")
public void afterMethod(JoinPoint joinPoint)
{
System.out.println("after method");
}
}
<!-- 配置自动扫描的包 --> <context:component-scan base-package="com.bupt.springtest.aop"/> <!-- 使AspectJ注解起作用:自动为匹配的类生成代理对象 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
package com.bupt.springtest.aop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; //测试类 public class AspectTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("ApplicationContext.xml"); ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class); int result = ac.divide(2, 1); System.out.println("result : " + result); } }
打印结果:
before method
after method
result : 2
返回通知:返回通知中访问节点的返回值。在返回通知中,只要将 returning 属性加到 @AfterReturning 注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称。必须在通知方法的签名中添加一个同名参数。在运行时, Spring AOP 会通过这个参数传递返回值。原始的切点表达式需要出现在 value/pointcut 属性中。
异常通知:只在连接点抛出异常时才执行异常通知。将 throwing 属性添加到 @AfterThrowing 注解中,也可以访问连接点抛出的异常。 Throwable 是所有错误和异常类的父类,所以在异常通知方法可以捕获到任何错误和异常。如果只对某个特殊的异常类感兴趣,可以将参数声明为对应其异常的参数类型,然后通知就只在这个类型及其子类的异常时才会被执行。
下面是代码演示
package com.bupt.springtest.aop; 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; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; /* * 创建一个日志切面类 */ @Aspect @Component public class LoggingAspect { //声明一个前置通知,指定我要在哪个方法前面增加日志信息 @Before("execution(public int com.bupt.springtest.aop.ArithmeticCalculatorImp.divide(int, int))") public void beforeMethod() { System.out.println("before method"); } /* * 后置通知:在目标执行后(无论是否发生异常),执行的通知 * 在后置通知中还不能访问目标方法执行的结果(返回结果在返回通知中访问),因为方法可能会出现异常 */ @After("execution(public int com.bupt.springtest.aop.ArithmeticCalculatorImp.divide(int, int))") public void afterMethod(JoinPoint joinPoint) { System.out.println("after method"); } /* * 在方法正常结束时执行的代码 * 返回通知是可以访问到方法的返回值的,也可以将 value 换成 pointcut */ @AfterReturning(value="execution(public int com.bupt.springtest.aop.ArithmeticCalculatorImp.divide(int, int))", returning="result") public void afterReturning(JoinPoint joinPoint, Object result) { String methodName = joinPoint.getSignature().getName(); System.out.println("The result of " + methodName + " method is " + result); } /* * 在目标方法出现异常时会执行的代码 * 可以访问到异常对象,且可以指定在出现特定异常时执行通知代码 */ @AfterThrowing(value="execution(public int com.bupt.springtest.aop.ArithmeticCalculatorImp.divide(int, int))", throwing="ex") public void afterThrowing(JoinPoint joinPoint, Exception ex) { String methodName = joinPoint.getSignature().getName(); System.out.println("The " + methodName + " method occurs excpetion: " + ex); } }
public class AspectTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("ApplicationContext.xml"); ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class); int result = ac.divide(2, 1); System.out.println("result : " + result); } }
打印结果: before method after method The result of divide method is 2 result : 2
//出现异常时 public class AspectTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("ApplicationContext.xml"); ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class); int result = ac.divide(2, 0); System.out.println("result : " + result); } }
打印结果:
before method
after method
The divide method occurs excption: java.lang.ArithmeticException: / by zero
环绕通知:它是所有通知类型中功能最强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。对于环绕通知来说,连接点的参数必须是 ProceedingJoinPoint。它是 JointPoint 的子接口,允许控制何时执行,是否执行连接点。在环绕通知中需要明确调用 ProceedingJoinPoint 的 proceed() 方法来执行被代理的方法,如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed() 的返回值,否则会出现空指针异常。
package com.bupt.springtest.aop; import java.util.Arrays; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { /* * 环绕通知需要携带 ProceedingJoinPoint 类型的参数,环绕通知类似于动态代理的全过程 * ProceedingJoinPoint 类型参数可以决定是否执行目标方法 且环通知必须有返回值,返回值即为最终目标方法的返回值 */ @Around("execution(public int com.bupt.springtest.aop.ArithmeticCalculatorImp.divide(int, int))") public Object aroundMethod(ProceedingJoinPoint pjd) { Object result = null; String methodName = pjd.getSignature().getName(); try { // 前置通知 System.out.println("The params of method " + methodName + " is " + Arrays.asList(pjd.getArgs())); // 执行目标方法 result = pjd.proceed(); // 返回通知 System.out.println("The result of " + methodName + " method is " + result); } catch (Throwable e) { //异常通知 System.out.println("The " + methodName + " occurs exception : " + e); } //后置通知 System.out.println("The " + methodName + " method ends"); return result; } }
public class AspectTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("ApplicationContext.xml"); ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class); int result = ac.divide(2, 1); System.out.println("result : " + result); } }
打印结果: The params of method divide is [2, 1] The result of divide method is 2 The divide method ends result : 2
//出现异常时 public class AspectTest { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("ApplicationContext.xml"); ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class); int result = ac.divide(2, 0); System.out.println("result : " + result); } }
打印结果: The params of method divide is [2, 0] The divide occurs exception : java.lang.ArithmeticException: / by zero The divide method ends
在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解来指定。
实现 Ordered 接口,getOrder() 方法返回值越小,优先级越高;若使用 @Ordered 注解,序号标注在注解中,序号越小优先级越高
@Aspect @Order(0) public class CalculatorValidationAspect{ @Aspect @Order(1) public class CalculatorLoggingAspect{
重用切入点定义
在编写AspectJ切面时,可以直接在通知注解中书写切入点表达式,但同一个切点表达式可能会在多个通知中重复出现。在 AspectJ 切面中,可以通过 @Pointcut 注解将一个切入点声明成简单的方法。切入点的方法通常是空的,因为将切入点定义与应用程序逻辑混在一起是不合理的。切入点访问控制符同时也控制着这个切入点的可见性,如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切点时,必须将类名包括在内。如果类没有与这个切面放在一个包中,还必须包含包名。
@Aspect @Component public class LoggingAspect { /* * 定义一个方法,用于声明切入点表达式。一般来说,该方法不需要添加其他代码 * 使用 @Poincut 来声明切入点表达式 * 若不在同一个包或类下在使用时需要加上包名或类名 */ @Pointcut("execution(public int com.bupt.springtest.aop.ArithmeticCalculatorImp.divide(int, int))") public void declareJoinPoint(){} @Before("declareJoinPoint()") public void beforeMethod() { System.out.println("before method"); } @AfterReturning(pointcut="declareJoinPoint()", returning="result") public void afterReturning(JoinPoint joinPoint, Object result) { String methodName = joinPoint.getSignature().getName(); System.out.println("The " + methodName + " method result is " + result); } }
基于配置文件的形式配置AOP
除了使用AspectJ注解声明切面,Spring也支持在 bean 配置文件中声明切面。这种声明是通过 aop schema 中的 xml 元素完成的。
正常情况下,基于注解的声明要优先于基于 xml 的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于 xml 的配置则是 Spring 专有的。
当使用 xml 声明切面时,需要在 <beans> 根元素中导入 aop Schema,在 bean 配置文件中,所有的 Spring aop 配置都必须定义在 <aop:config> 元素内部。对于每个切面而言,都要创建一个 <aop:aspect> 元素来为具体的切面实现引用后端 bean 实例,切面 bean 必须有一个标示符,供 <aop:aspect> 元素引用。
切入点使用 <aop:pointcut> 元素声明或使用 <pointcut-ref> 来引用切入点,切入点必须定义在 <aop:aspect> 元素下,或者直接定义在 <aop:config>元素下
基于 xml 的 aop 配置不允许在切入点表达式中用名称引用其它切入点
<!-- 配置bean -->
<bean id="arithmeticCalculator" class="com.bupt.springtest.aop.ArithmeticCalculatorImp" />
<!-- 配置切面bean --> <bean id="loggingAspect" class="com.bupt.springtest.aop.LoggingAspect" /> <bean id="validationAspect" class="com.bupt.springtest.aop.ValidationAspect" /> <!-- 配置AOP --> <aop:config>
<!-- 配置切点表达式 --> <aop:pointcut expression="execution(* com.bupt.springtest.aop.ArithmeticCalculator.*(..))" id="pointcut" />
<!-- 配置切面及通知 --> <aop:aspect ref="loggingAspect" order="2"> <aop:before method="beforeMethod" pointcut-ref="pointcut" /> <aop:after method="afterMethod" pointcut-ref="pointcut" /> <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ex" /> <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result" /> <aop:around method="aroundMethod" pointcut-ref="pointcut" /> </aop:aspect> <aop:aspect ref="validationAspect" order="1">
<aop:pointcut
expression="execution(* com.bupt.springtest.aop.ArithmeticCalculator.*(..))"
id="pointcut1"/>
<aop:before method="validationArgs" pointcut-ref="pointcut1" /> </aop:aspect>
</aop:config>