上一篇文章最后讲到了动态代理,在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=更新出错

从这些输出结果中,我们能发现他们的执行顺序是这样的:
执行顺序.png
好了,到这里差不多会使用spring aspectj来写一些简单的东西了,可以动手把你项目中一些代码重构一下啦。