基于注解的声明式AspectJ
与基于代理类的AOP实现相比,基于XML的声明式ApectJ要便捷得多,但是它也存在着一些缺点,那就是要在Spring文件中配置大量的代码信息。为了解决这个问题,AspectJ框架为AOP的实现提供了一套注解,用以取代Spring配置文件中为实现AOP功能所配置的臃肿代码。
关于AspectJ注解的介绍,如表1所示。
表1 AspectJ的注解及其描述
注解名称 | 描述 |
---|---|
@AspectJ | 用于定义一个切面。 |
@Pointcut | 用于定义切入点表达式。在使用时还需定义一个包含名字和任意参数的方法签名来表示切入点名称。实际上,这个方法签名就是一个返回值为void,且方法体为空的普通的方法。 |
@Before | 用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。 |
@AfterReturning | 用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut/value和returning属性,其中pointcut/value这两个属性的作用一样,都用于指定切入点表达式。returning属性值用于表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法的返回值。 |
@Around | 用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被织入的切入点。 |
@AfterThrowing | 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut/value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。 |
@After | 用于定义最终final通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被织入的切入点。 |
@DeclareParents | 用于定义引介通知,相当于IntroductionInterceptor(不要求掌握)。 |
为了使读者可以快速的掌握这些注解,接下来重新使用注解的形式来实现上一节中的案例,具体步骤如下。
(1)在chapter03项目的src目录下,创建com.itheima.aspectj.annotation包,将切面类MyAspect拷贝到该包下,并对该文件进行编辑,如文件1所示。
文件1 MyAspect.java
1 package com.itheima.aspectj.annotation;
2 import org.aspectj.lang.JoinPoint;
3 import org.aspectj.lang.ProceedingJoinPoint;
4 import org.aspectj.lang.annotation.After;
5 import org.aspectj.lang.annotation.AfterReturning;
6 import org.aspectj.lang.annotation.AfterThrowing;
7 import org.aspectj.lang.annotation.Around;
8 import org.aspectj.lang.annotation.Aspect;
9 import org.aspectj.lang.annotation.Before;
10 import org.aspectj.lang.annotation.Pointcut;
11 import org.springframework.stereotype.Component;
12 /**
13 * 切面类,在此类中编写通知
14 */
15 @Aspect
16 @Component
17 public class MyAspect {
18 // 定义切入点表达式
19 @Pointcut("execution(* com.itheima.jdk.*.*(..))")
20 // 使用一个返回值为void、方法体为空的方法来命名切入点
21 private void myPointCut(){}
22 // 前置通知
23 @Before("myPointCut()")
24 public void myBefore(JoinPoint joinPoint) {
25 System.out.print("前置通知 :模拟执行权限检查...,");
26 System.out.print("目标类是:"+joinPoint.getTarget() );
27 System.out.println(",被织入增强处理的目标方法为:"
28 +joinPoint.getSignature().getName());
29 }
30 // 后置通知
31 @AfterReturning(value="myPointCut()")
32 public void myAfterReturning(JoinPoint joinPoint) {
33 System.out.print("后置通知:模拟记录日志...," );
34 System.out.println("被织入增强处理的目标方法为:"
35 + joinPoint.getSignature().getName());
36 }
37 // 环绕通知
38 @Around("myPointCut()")
39 public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
40 throws Throwable {
41 // 开始
42 System.out.println("环绕开始:执行目标方法之前,模拟开启事务...");
43 // 执行当前目标方法
44 Object obj = proceedingJoinPoint.proceed();
45 // 结束
46 System.out.println("环绕结束:执行目标方法之后,模拟关闭事务...");
47 return obj;
48 }
49 // 异常通知
50 @AfterThrowing(value="myPointCut()",throwing="e")
51 public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
52 System.out.println("异常通知:" + "出错了" + e.getMessage());
53 }
54 // 最终通知
55 @After("myPointCut()")
56 public void myAfter() {
57 System.out.println("最终通知:模拟方法结束后的释放资源...");
58 }
59 }
在文件1中,首先使用@Aspect注解定义了切面类,由于该类在Spring中是作为组件使用的,所以还需要添加@Component注解才能生效。然后使用了@Poincut注解来配置切入点表达式,并通过定义方法来表示切入点名称。接下来在每个通知相应的方法上添加了相应的注解,并将切入点名称“myPointCut”作为参数传递给需要执行增强的通知方法。如果需要其他参数(如异常通知的异常参数),可以根据代码提示传递相应的属性值。
(2)在目标类com.itheima.jdk.UserDaoImpl中,添加注解@Repository("userDao")。
(3)在com.itheima.aspectj.annotation包下,创建配置文件applicationContext.xml,并对该文件进行编辑,如文件2所示。
文件2 applicationContext.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xmlns:aop="http://www.springframework.org/schema/aop"
5 xmlns:context="http://www.springframework.org/schema/context"
6 xsi:schemaLocation="http://www.springframework.org/schema/beans
7 http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
8 http://www.springframework.org/schema/aop
9 http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
10 http://www.springframework.org/schema/context
11 http://www.springframework.org/schema/context/spring-context-4.3.xsd">
12 <!-- 指定需要扫描的包,使注解生效 -->
13 <context:component-scan base-package="com.itheima" />
14 <!-- 启动基于注解的声明式AspectJ支持 -->
15 <aop:aspectj-autoproxy />
16 </beans>
在文件2中,首先引入了context约束信息,然后使用<context>元素设置了需要扫描的包,使注解生效。由于此案例中的目标类位于com.itheima.jdk包中,所以这里设置base-package的值为“com.itheima”。最后,使用<aop:aspectj-autoproxy />来启动Spring对基于注解的声明式AspectJ的支持。
(4)在com.itheima.aspectj.annotation包中,创建测试类TestAnnotation,如文件3所示。
文件3 TestAnnotation.java
1 package com.itheima.aspectj.annotation;
2 import org.springframework.context.ApplicationContext;
3 import
4 org.springframework.context.support.ClassPathXmlApplicationContext;
5 import com.itheima.jdk.UserDao;
6 // 测试类
7 public class TestAnnotationAspectj {
8 public static void main(String args[]) {
9 String xmlPath =
10 "com/itheima/aspectj/annotation/applicationContext.xml";
11 ApplicationContext applicationContext =
12 new ClassPathXmlApplicationContext(xmlPath);
13 // 1 从spring容器获得内容
14 UserDao userDao = (UserDao) applicationContext.getBean("userDao");
15 // 2 执行方法
16 userDao.addUser();
17 }
18 }
执行程序后,控制台的输出结果如图1所示。
图1 运行结果
以上一节的方式来演示异常通知的执行,控制台的输出结果如图2所示。
图2 运行结果
从图1和图2可以看出,基于注解的方式与基于XML的方式的执行效果相同,只是在目标方法前后通知的执行顺序发生了变化。相对来说,使用注解的方式更加简单、方便,所以在实际开发中推荐使用注解的方式进行AOP开发。
注意:
如果在同一个连接点有多个通知需要执行,那么在同一切面中,目标方法之前的前置通知和环绕通知的执行顺序是未知的,目标方法之后的后置通知和环绕通知的执行顺序也是未知的。