本文介绍通过注解@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方式实现。