上一篇文章最后讲到了动态代理,在spring中有个特性也是利用动态代理实现的,那就是面向切面编程(aop)。Spring aop有两种实现方式:一种是spring aop,另一种是aspectj。这两种实现方式的主要区别在于:spring aop采用的是动态织入(运行期期植入),而aspectJ是静态织入(编译期植入)。
本文只阐述spring aspectj在实际项目中如何使用,不关乎具体实现,后续可能会写写一些spring aop的或者他们的实现原理。
首先是引入依赖:
pom.xml,测试项目我用maven来管理依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.0.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.3.RELEASE</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12-beta-1</version> <scope>test</scope> </dependency> </dependencies>
|
然后是配置文件,我觉得spring会写配置文件差不多就成功了一大半了。
aop.xml,这里我使用了注解配置的方式,因为这样感觉测试代码会清晰很多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <?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" xmlns:context="http://www.springframework.org/schema/context" 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.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd"> <!--自动扫描bean--> <context:component-scan base-package="aop" /> <!--开启aspectj--> <aop:aspectj-autoproxy/> </beans>
|
写一个简单的Service,其实就是一个bean,只不过我喜欢写成@Service,这样更好理解吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package aop; import org.springframework.stereotype.Service; @Service public class UserService { public String add(Integer id, String name) { String str = "add user id = " + id + " name = " + name; System.out.println(str); return str; } public void delete(Integer id) { System.out.println("delete user id = " + id); } public void update(Integer id) throws Exception { throw new Exception("更新出错"); } }
|
然后写一个切面,切面也是一个bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| package aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Service; @Service @Aspect public class OperateService { @Pointcut("execution(* aop.UserService.add(..))") public void pointCut(){}; @Before(value = "execution(* aop.UserService.add(..)) && args(id,name)", argNames = "id,name") public void beforeAdd(Integer id, String name) { System.out.println("插入前..." + "args.id = " + id + ";name=" + name); } @After("execution(* aop.UserService.add(..))") public void afterAdd() { System.out.println("插入后...1"); } @AfterReturning(pointcut = "pointCut()", returning = "ret") public void afterAdd2(JoinPoint joinPoint, Object ret) { System.out.println("插入后...2; return = " + ret); } @AfterThrowing(value = "execution(* aop.UserService.*(..))", throwing = "e") public void afterThrowException(Exception e) { System.out.println("报错了;e=" + e.getMessage()); } @Around(value = "execution(* aop.UserService.add(..)) && args(id,name)", argNames = "id,name") public void aroundOperate(ProceedingJoinPoint point, Integer id, String name) { System.out.println("around1 id =" + id + " name=" + name); try { point.proceed(); } catch (Throwable e) { } System.out.println("around2"); } }
|
在这个切面中我尽可能地把通知和切点的各种形式都写了出来,怎么传参数,怎么获取返回值,代码中都能找到。
大致总结一下:
- 总共有五种通知 Before, Around, After, AfterReturning, AfterThrowing,至于他们的顺序见后面的图;
- 其中我觉得最难理解的是Around,他更加灵活,能实现其他4种通知,带参数ProceedingJoinPoint,且ProceedingJoinPoint.proceed就是原方法的执行。
最后我们来试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package aop; import org.springframework.context.support.ClassPathXmlApplicationContext; public class aopMain { public static void main(String[] args) throws Exception { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("aop.xml"); UserService userService = (UserService) context.getBean("userService"); userService.add(1, "lk"); try { userService.update(1); } catch (Exception e) { } } }
|
输出结果:
1 2 3 4 5 6 7
| around1 id =1 name=lk 插入前...args.id = 1;name=lk add user id = 1 name = lk around2 插入后...1 插入后...2; return = null 报错了;e=更新出错
|
细心的人会发现afterReturning取的返回值是null,这是不是有问题,确实,查找资料发现afterReturning和Around共存时是拿不到返回值的,但是这个说法有点片面,因为around其实是代理了我们的方法,around没有return,拿到的自然是null,如果给around一个返回值,那么afterReturning也是能拿到值的。
我们将around改写一下:
1 2 3 4 5 6 7 8 9 10 11 12
| @Around(value = "execution(* aop.UserService.add(..)) && args(id,name)", argNames = "id,name") public String aroundOperate(ProceedingJoinPoint point, Integer id, String name) { System.out.println("around1 id =" + id + " name=" + name); String str = ""; try { str = (String) point.proceed(); } catch (Throwable e) { System.out.println("报错了"); } System.out.println("around2"); return str; }
|
输出:
1 2 3 4 5
| 插入前...args.id = 1;name=lk add user id = 1 name = lk 插入后...1 插入后...2; return = add user id = 1 name = lk 报错了;e=更新出错
|
从这些输出结果中,我们能发现他们的执行顺序是这样的:
好了,到这里差不多会使用spring aspectj来写一些简单的东西了,可以动手把你项目中一些代码重构一下啦。