Spring AOP(二)--注解方式

本文介绍通过注解@AspectJ实现Spring AOP,这里要重点说明一下这种方式实现时所需的包,因为Aspect是第三方提供的,不包含在spring中,所以不能只导入spring-aop的包,为了安全起见我导入的包有(我是maven方式添加依赖):

步骤如下:

一、创建连接点

spring是方法级别的拦截器,所以连接点就是某个类中的某个方法,从动态代理的角度来看就是将要拦截的方法织入AOP通知。

1⃣️创建一个接口

1 public interface EmployeeService {
2 
3     public void getEmployeeInfo(Employee employee);
4     
5     public void getEmployeeSex(Employee employee);
6 }

这个接口中提供了两个方法,后续会用来测试连接点的概念,因为只有将方法织入到AOP才会执行完整的拦截流程。

2⃣️创建接口实现类,增加注解@Component

 1 @Component
 2 public class EmployeeServiceImpl implements EmployeeService {
 3 
 4     @Override
 5     public void getEmployeeInfo(Employee employee) {
 6         System.out.println("name:" + employee.getUsername() + ";sex:" + employee.getSex());
 7     }
 8 
 9     @Override
10     public void getEmployeeSex(Employee employee) {
11         System.out.println("性别:"+employee.getSex());
12     }
13 }

 二、创建切面

创建好了连接点之后就可以创建切面了,它就相当于是一个拦截器,在spring中只要使用@Aspect注解一个类,spring ioc容器就会将它视为一个切面处理。

 1 package com.hyc.aop.aspect;
 2 
 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.DeclareParents;
11 import org.aspectj.lang.annotation.Pointcut;
12 
13 import com.hyc.pojo.Employee;
14 
15 /**
16  * 定义一个切面
17  * 
18  * @Aspect 该注解表示这个类就是一个切面了
19  */
20 @Aspect
21 public class EmployeeAspect {
22 
29     /**
30      * 定义一个切点,通知aop什么时候启动拦截并织入对应流程
31      *  注意以下几点: 
32      * 1、方法返回类型* 和方法之间有空格
33      * 2、在下面的四个方法中引用这个切点时方法名要加括号
34      * 3、execution正则表达式中的方法就是一个连接点,将代理对象和切面相连,如果不定义这个连接点,则不会将代理对象的方法和切面相连
35      */
36     @Pointcut("execution(* com.hyc.aop.aspect.EmployeeServiceImpl.getEmployeeInfo(..))")
37     public void getInfo() {
38 
39     }
40 
41     @Before("getInfo()")
42     public void before() {
43         System.out.println("before:代理方法执行之前");
44     }
45 
46     @After("getInfo()")
47     public void after() {
48         System.out.println("after:代理方法执行完毕");
49     }
50 
51     @AfterReturning("getInfo()")
52     public void afterReturning() {
53         System.out.println("afterReturning:代理方法执行完毕,执行成功");
54     }
55 
56     @AfterThrowing("getInfo()")
57     public void afterThrowing() {
58         System.out.println("afterThrowing:代理方法执行完毕,执行过程出现异常");
59     }
60 }

 上面的代码中红色加粗部分所代表的意思如下:

  • @Aspect注解:告诉spring,这个类是一个切面;
  • @Pointcut注解:定义一个切点,并告诉AOP什么时候启动拦截并织入对应流程;
  • @before、@after、@afterReturning、@afterThrowing分别是四种通知,它们可以引用之前定义的切点,也可以有自己的切点;

有必要解释一下定义切点注解@Pointcut中的内容:

1 @Pointcut("execution(* com.hyc.aop.aspect.EmployeeServiceImpl.getEmployeeInfo(..))") 

 在上面的注解中,定义了execution的正则表达式,spring是通过这个正则表达式判断是否需要拦截你所定义的方法,即被代理对象的方法。

  • execution:代表执行方法的时候会触发;
  • *:代表方法的返回类型任意;
  • com.hyc.aop.aspect.EmployeeServiceImpl:被代理类的全限定名,注意它和前面的返回类型*之间有一个空格;
  • getEmployeeInfo:被拦截方法名称;
  • (..):方法中的参数,类型任意;

通过上面的描述,上述注解及内部正则表达式的意思就是:全限定名为com.hyc.aop.aspect.EmployeeServiceImpl的类中的getEmployeeInfo方法被当做一个切点,当程序执行这个方法的时候对它进行拦截,这样就能按照AOP通知的规则把这个方法织入流程中。

三、创建配置类,采用注解java配置

 1 /*
 2  * 定义一个配置类,通过java配置的方式获取切面
 3  */
 4 @Configuration
 5 @ComponentScan(basePackages= {"com.hyc.aop.aspect","com.hyc.pojo"})
 6 @EnableAspectJAutoProxy //自动代理,代替了动态代理的实现
 7 public class AspectConfig {
 8     //返回一个切面
 9     @Bean
10     public EmployeeAspect getAspect() {
11         return new EmployeeAspect();
12     }
13 
14 }

这个配置就是之前介绍过的注解方式装配bean的配置方法,不过对AOP有效的注解是@EnableAspectJAutoProxy,从字面意思理解它是开启了切面自动代理功能,其实就是启用了AspectJ框架的自动代理,这样spring就会生成一个代理对象,进而使用AOP,其中的getAspect方法是生成了一个切面。

四、测试

完成了上面的配置之后,我就可以进行测试了,测试方法如下:

 1     @Test
 2     public void testAopByConfig() {
 3         @SuppressWarnings("resource")
 4         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AspectConfig.class);
 5         EmployeeService es = (EmployeeService) context.getBean(EmployeeService.class);
 6         Employee employee = (Employee) context.getBean("employee");
 7         es.getEmployeeInfo(employee);
 8         employee = null;
 9         es.getEmployeeInfo(employee);
10

 上面的单元测试方法中红色部分:

第四行:首先是获取配置类的上下文,此时已经启动AspectJ的自动代理,并生成了一个切面;

第五行:通过上下文就能生成一个代理对象,如果被代理类有接口则采用jdk动态代理,否则就是CGLIB动态代理;

第七行:测试方法,因为employee不为空,所以可以正常返回,并执行afterReturning方法;

第八、九行:将employee设置为null,执行过程出现异常,所以会执行aferThrowing方法;

查看测试结果:

 从上面的运行结果来看,完全符合刚开始的流程图:

1、首先执行before方法;

2、执行被代理对象的方法;

3、执行完被代理对象方法后,不管成功与否都会执行after方法;

4、如果被代理对象的方法正常返回,则执行afterReturning方法,如果返回异常,则执行afterTrowing方法(结果中的afterException是书写错误,其实都一个意思啦😄)

以上就是通过注解@Aspect的方式实现一个Spring AOP的过程,其实这只是一个基本过程,下面来介绍一些更高级的玩法:比如环绕通知、给通知传递参数、引入新的类等。

五、环绕通知

环绕通知是spring aop中最强大的功能,它可以同时实现前置通知和后置通知,并且保留了调度被代理对象原有方法的功能,在之前的切面类中加入以下环绕通知:

 1     @Around("getInfo()")
 2     public void around(ProceedingJoinPoint pjp) {
 3         System.out.println("around before...");
 4         try {
 5             pjp.proceed();
 6         } catch (Throwable e) {
 7             //此处将异常打印出来
 8             System.out.println(e.getLocalizedMessage());
 9         }
10         System.out.println("around after...");
11     }

 在切面中通过@Around注解加入了切面环绕通知,这个通知里有一个参数ProceedingJoinPoint,这个参数是spring提供的,使用它可以反射连接点方法。

下面来看一下加入环绕通知之后的执行结果:

 1 around before...
 2 before:代理方法执行之前
 3 name:张三;sex:男
 4 around after...
 5 after:代理方法执行完毕
 6 afterReturning:代理方法执行完毕,执行成功
 7 ----分割线----
 8 around before...
 9 before:代理方法执行之前
10 null
11 around after...
12 after:代理方法执行完毕
13 afterReturning:代理方法执行完毕,执行成功

可以看到执行的顺序,注意⚠️,使用这种方式时around before在before之前执行,但是XML方式它在before方法执行,这个下一篇文文章验证。

但是使用环绕方法的时候,因为执行被代理方法时空指针异常被捕获了,所以当我在测试代码中把employee设置为null时还是会执行afterReturning方法。

六、给通知传参数

有时候我们希望能给某个通知传递一些参数,当然这些参数就是通过连接点方法传进去的,比如之前的连接点方法getEmployeeInfo(Employee employee);它里面有一个参数,现在我想把这个参数只传递给前置通知before,那么此时before的切点就不能引用公共方法,而是重写自己的,并传入参数,如下代码所示:

1     @Before("execution(* com.hyc.aop.aspect.EmployeeServiceImpl.getEmployeeInfo(..)) && args(employee)")
2     public void before(Employee employee) {
3         System.out.println("before:代理方法执行之前,username:" + employee.getUsername());
4     }

上面代码中黄色背景部分就是传递参数的方式,注意它要写在execution()的外面

下面来看测试结果:

 1 around before...
 2 before:代理方法执行之前,username:张三
 3 name:张三;sex:男
 4 around after...
 5 after:代理方法执行完毕
 6 afterReturning:代理方法执行完毕,执行成功
 7 ----分割线----
 8 around before...
 9 null
10 around after...
11 after:代理方法执行完毕
12 afterReturning:代理方法执行完毕,执行成功

从结果可以看出,当参数不为空时,可以正常传递并执行before通知,但是如果参数为空,则不会执行before.

其实这种方式还能引申出另一个问题,每个同通知是否可以配置不同的连接点呢?比如我的before通知和其他通知定义不同的连接点,之前在定义连接点类的时候还定义了一个getEmployeeSex(Employee employee)方法,现在正是用它的时候,我现在把它作为before通知的连接点,所以before的配置改为如下:

1    @Before("execution(* com.hyc.aop.aspect.EmployeeServiceImpl.getEmployeeSex(..)) && args(employee)")
2     public void before(Employee employee) {
3         System.out.println("before:代理方法执行之前,username:" + employee.getUsername());
4     }

其他配置不变,测试代码改为:

 1     @Test
 2     public void testAopByConfig() {
 3         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AspectConfig.class);
 4         // 要获取真实对象bean,必须要在类上使用Componetn注解
 5         EmployeeService es = (EmployeeService) context.getBean(EmployeeService.class);
 6         Employee employee = (Employee) context.getBean("employee");
 7         es.getEmployeeInfo(employee);
 8         es.getEmployeeSex(employee);
 9         System.out.println("----分割线----");
10         employee = null;
11         es.getEmployeeInfo(employee);
12     }

增加对方法getEmployeeSex的调用,测试结果如下:

 1 around before...
 2 name:张三;sex:男
 3 around after...
 4 after:代理方法执行完毕
 5 afterReturning:代理方法执行完毕,执行成功
 6 before:代理方法执行之前,username:张三
 7 性别:男
 8 ----分割线----
 9 around before...
10 null
11 around after...
12 after:代理方法执行完毕
13 afterReturning:代理方法执行完毕,执行成功

从上面的结果可以看出,执行第一个连接点方法时,没有执行before通知,执行第二个连接点方法时,只执行了before通知;由此可见我们可以为不同的通知创建不同的连接点方法。比如在一个包含数据库的业务逻辑中,在before中进行数据库连接处理,在after中进行数据库连接关闭等等。

七、引入

spring aop只是通过动态代理的方式把不同的类织入到它所约定的流程中,有时候我们也希望通过引入一些新的类来完善被织入的类,比如上面的连接点中,获取属性值之前没有判断employee对象是否为空,现在有一个类进行了对象是否为空的判断,我想用它来完善之前的代码,可是怎么能使用aop引用它呢?要知后事如何,且看下面分解:

第一步:新建一个接口和实现类,完成是否为空的判断逻辑

1⃣️接口

1 public interface EmployeeCheckService {
2     public boolean isPass(Employee employee);
3 }

2⃣️实现类

1 public class EmployeeCheckServiceImpl implements EmployeeCheckService {
2 
3     @Override
4     public boolean isPass(Employee employee) {
5         return employee != null;
6     }
7 
8 }

第二步:在切面中增加一个接口类的属性,并使用注解@DeclaredParents

1     /**
2      * 定义一个EmployeeCheckService类的成员变量作为引入对象
3      */
4     @DeclareParents(value = "com.hyc.aop.aspect.EmployeeServiceImpl+", defaultImpl = EmployeeCheckServiceImpl.class)
5     public EmployeeCheckService employeeCheckService;

上面h红色部分的注解有两个属性:

  • value="com.hyc.aop.aspect.EmployeeServiceImpl+" 表示对类EmployeeServiceImpl进行增强,有了这个定义之后,就能将EmployeeServiceImpl类强制转化成要引入的类型了,可以理解为让EmployeeServiceImpl实现了EmployeeCheckService接口,毕竟看这个注解的名称就是让属性成为注解值的接口嘛!
  • defaultImpl代表它默认的实现类

有了这个配置之后,可以将测试类修改成下面这样:

 1     @Test
 2     public void testAopByConfig() {
 3         AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AspectConfig.class);
 4         // 要获取真实对象bean,必须要在类上使用Componetn注解
 5         EmployeeService es = (EmployeeService) context.getBean(EmployeeService.class);
 6         Employee employee = (Employee) context.getBean("employee");
 7         EmployeeCheckService ecs = (EmployeeCheckService) es;
 8         if (ecs.isPass(employee)) {
 9             es.getEmployeeInfo(employee);
10             es.getEmployeeSex(employee);
11         }
12         employee = null;
13         if (ecs.isPass(employee)) {
14             es.getEmployeeInfo(employee);
15             es.getEmployeeSex(employee);
16         }
17     }

看上面代码中的加粗红色部分:

第七行:将被代理对象强制转化成要引入的类型,这样就能调用它的方法了,因为此时已经将这个类引入到AOP中了。查看测试结果:

1 around before...
2 before:代理方法执行之前
3 name:张三;sex:男
4 around after...
5 after:代理方法执行完毕
6 afterReturning:代理方法执行完毕,执行成功
7 性别:男

可以看到到对象employee为空时,直接返回,没有进入到方法执行,所以引入成功。

以上就是通过注解+java配置方式实现sprin aop的方法,下一篇将以本文中所有功能为例,用XML方式实现。

posted @ 2018-09-29 11:42  bug改了我  阅读(1034)  评论(0编辑  收藏  举报