浅谈AOP

AOP简介

AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB的动态代理。

AOP(面向切面编程)

AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 Spring 框架中的一个重要内容。利用 AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

Spring的AOP原理图


AOP编程术语

切面(Aspect)

切面泛指交叉业务逻辑。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。

连接点(JoinPoint)

连接点指可以被切面织入的具体方法。通常业务接口中的方法均为连接点。

切入点(Poincut)

切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。

目标对象(Target)

目 标 对 象 指 将 要 被 增 强 的 对 象 。 即 包 含 主 业 务 逻 辑 的 类 的 对 象 。 上 例 中 的StudentServiceImpl 的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然,不被增强,也就无所谓目标不目标了。

通知(Advice)

通知表示切面的执行时间,Advice 也叫增强。上例中的 MyInvocationHandler 就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。切入点定义切入的位置,通知定义切入的时间。


AspectJ 对 AOP 的实现

AspectJ 中常用的五种通知类型:

(1)前置通知
(2)后置通知
(3)环绕通知
(4)异常通知
(5)最终通知

AspectJ 的切入点表达式

AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)throws-pattern?)

解释:

modifiers-pattern] 访问权限类型
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型
?表示可选的部分以上表达式共 4 个部分。
execution(访问权限 方法返回值 方法声明(参数) 异常类型)

举例:

符号 意义
* 0至多个任意字符
.. 用在方法参数中,表示任意多个参数;用在包名后,表示当前包及子包路径
+ 用在类名后,表示当前类及其子类;用在接口后,表示当前接口及其实现类

execution(public * (..))
指定切入点为:任意公共方法。
execution(
set*(..))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service..(..))
指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* com.xyz.service...(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“”,表示包、子包下的所有类。
execution(
..service..(..))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(
.service..(..))
指定只有一级包下的 serivce 子包下所有类(接口)中所有方法为切入点
execution(
.ISomeService.(..))
指定只有一级包下的 ISomeSerivce 接口中所有方法为切入点
execution(* ..ISomeService.(..))
指定所有包下的 ISomeSerivce 接口中所有方法为切入点
execution(* com.xyz.service.IAccountService.(..))
指定切入点为:IAccountService 接口中的任意方法。
execution(
com.xyz.service.IAccountService+.(..))
指定切入点为:IAccountService 若为接口,则为接口中的任意方法及其所有实现类中的任意方法;若为类,则为该类及其子类中的任意方法。
execution(
joke(String,int)))
指定切入点为:所有的 joke(String,int)方法,且 joke()方法的第一个参数是 String,第二个参数是 int。如果方法中的参数类型是 java.lang 包下的类,可以直接使用类名,否则必须使用全限定类名,如 joke( java.util.List, int)。
execution(* joke(String,)))
指定切入点为:所有的 joke()方法,该方法第一个参数为 String,第二个参数可以是任意类型,如joke(String s1,String s2)和joke(String s1,double d2)都是,但joke(String s1,double d2,String s3)不是。
execution(
joke(String,..)))
指定切入点为:所有的 joke()方法,该方法第一个参数为 String,后面可以有任意个参数且参数类型不限,如 joke(String s1)、joke(String s1,String s2)和 joke(String s1,double d2,String s3)都是。
execution(* joke(Object))
指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型。joke(Object ob)是,但,joke(String s)与 joke(User u)均不是。
execution(* joke(Object+)))
指定切入点为:所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型或该类的子类。不仅 joke(Object ob)是,joke(String s)和 joke(User u)也是。


AspectJ基于注解的AOP实现–Maven

搭建号Maven环境

引入依赖
引入AOP约束

定义业务接口与实现类

1
2
3
4
5
6
7
public interface SomeService {
void doSome(String name,int age);
String doOther(String name , int age);
String doFirst(String name,int age);
void doSecond(String name,int age);
void doThird();
}
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
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String naem,int age) {
System.out.println("SomeServiceImpl的业务方法doSome");
}

@Override
public String doOther(String name, int age) {
System.out.println("SomeServiceImpl的业务方法doOther");
return "abcd";
}

@Override
public String doFirst(String name, int age) {
System.out.println("SomeServiceImpl的业务方法doFirst");
return "doFirst";
}

@Override
public void doSecond(String name, int age) {
System.out.println("SomeServiceImpl的业务方法doSecond");
int i =10/0;

}

@Override
public void doThird() {
System.out.println("SomeServiceImpl的业务方法doThird");
}
}

定义切面类

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

/*
* 切面类:是用来给业务方法增强功能的
* @Aspect:是aspectj框架中的,表示当前类是切面类
* 位置:在类的定义上面使用
* */
@Aspect
public class MyAspect {
//有功能增强的方法

/*
* 定义方法实现功能增强,方法的定义格式
* 1.public方法
* 2.一般都是void返回值(环绕通知除外)
* 3.方法名称自定义
* 4.方法可以有参数,参数的类型有限制
* */

/*
* @Before:前置通知
* 属性:value表示切入点表达式,表示切面执行的位置
* 位置:方法定义的上面
*
* 特点:1.在目标方法之前先执行的
* 2.不会影响目标方法的执行
* 3.不会改变目标方法的执行结果
* */
/*
* 获取doSome方法在执行时候的信息
* 参数:JoinPoint表示连接点(业务方法),
* 连接点是切入点中的一个方法
* */
@Before(value = "execution(* com.bjpowernode.service.SomeServiceImpl.doSome(..))")
public void myBefore(JoinPoint jp){
//在方法中,实现功能的增强,例如日志代码
System.out.println("前置通知:在目标方法之前,执行日志的功能");
//获取方法定义
System.out.println("连接点的方法定义:"+jp.getSignature());
System.out.println("连接点的方法名称:"+jp.getSignature().getName());
//获取方法执行时的参数
Object args [] = jp.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
}

/*
* @AfterReturning:后置通知
* 属性:value:切入点表达式
* returning:自定义的变量,表示目标方法的放回值的
* 变量的名称必须和通知方法的参数名一样
* 位置:方法的上面
* 特点:
* 1.在目标方法之后执行的
* 2.能够获取到目标方法的执行结果
* 3.修改目标方法不会影响最后的执行结果
* */
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",returning = "result")
public void afterReturing(Object result){
//修改目标方法返回置
if(result != null){
String st = (String)result;
result = st.toUpperCase();
}
System.out.println("后置通知,在目标方法之后执行的。能够获取到目标方法的执行结果:"+result);
}

/*
* @Around:环绕通知
* 属性:value切入点表达式
* 位置:方法定义的上面
* 特点:
* 1.在目标方法的前和后都能增强功能
* 2.控制目标方法是否执行
* 3.修改目标方法的执行结果
*
* 环绕通知方法的定义:
* 1.参数proceedingJoinPoint
* 2.有返回值,推荐使用object
* */
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
String name= "";
Object args [] = pjp.getArgs();
if(args.length>0){
name = (String)args[0];
}
Object result = null;
System.out.println("环绕通知:在目标方法之前加入日志");
//控制目标是否执行
if("zs".equals(name)){
//执行目标方法
result = pjp.proceed();//doFirst result = method.invoke(target,args);
}
System.out.println("环绕通知:在目标方法之后加入事务处理");
//返回目标方法的执行结果(可以是修改后的结果)
if(result != null){
result = "Hello Aspectj";
}
return result;
}

/*
* @AfterThrowing:异常通知
* 属性: value:切入点表达式
* throwing:自定义的变量,表示目标方法抛出异常现象,必须和通知方法的参数名一样
* 位置:方法定义的上面
* 特点:
* 1.在目标方法抛出异常时执行的,可以看作是对目标方法监控
* 2.不是异常处理程序,异常还是抛出
* */
//@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex")
@AfterThrowing(value = "mypt()",throwing = "ex")
public void myAfterThrowing(Throwable ex){
//能够获取异常信息
//1.把异常记录下来 2.发送通知
System.out.println("异常通知:在目标方法抛出异常时执行的,异常原因:"+ex.getMessage());
}

/*
* @After:最终通知
* 属性:value切入点表达式
* 位置:在方法的定义的上面
* 特点:
* 1.在目标方法之后执行
* 2.总是会被执行
* */
@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter(){
System.out.println("最终通知,总是会被被执行的,可以做收尾工作");
}

/*
* @Pointcut:定义和管理切入点
* 属性:value切入点表达式
* 位置:在自定义的方法上面
* 作用:@Pointcut定义在方法的上面,这个方法的名称就是切入点的别名
* 其他的通知注解的value属性可以使用方法名称,表示切入点
* */
@Pointcut(value = "execution(* *..SomeServiceImpl.doSecond(..))")
public void mypt(){
//无需代码
}
}

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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"
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">

<!--申明目标类对象-->
<bean id="SomeServiceTarget" class="com.bjpowernode.service.SomeServiceImpl"/>

<!--声明切面类对象-->
<bean id="myAspect" class="com.bjpowernode.aspect.MyAspect"/>

<!--声明自动代理生成器:使用aspectj把spring容器中目标类对象生成代理对象-->
<aop:aspectj-autoproxy/>

</beans>

测试类

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class AppTest {
@Test
public void test01(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);

//从spring的容器中获取目标对象(代理对象)
SomeService proxy = (SomeService) ctx.getBean("SomeServiceTarget");

//通过代理对象执行业务方法,实现功能的增强
proxy.doSome("zhangsan",20);
}

@Test
public void test02(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);

//从spring的容器中获取目标对象(代理对象)
SomeService proxy = (SomeService) ctx.getBean("SomeServiceTarget");

//通过代理对象执行业务方法,实现功能的增强
String str;
str = proxy.doOther("zs",21);
System.out.println("str:"+str);
}

@Test
public void test03(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);

//从spring的容器中获取目标对象(代理对象)
SomeService proxy = (SomeService) ctx.getBean("SomeServiceTarget");

//通过代理对象执行业务方法,实现功能的增强
String str;
str = proxy.doFirst("zs",21);
System.out.println("str:"+str);
}

@Test
public void test04(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);

//从spring的容器中获取目标对象(代理对象)
SomeService proxy = (SomeService) ctx.getBean("SomeServiceTarget");

//通过代理对象执行业务方法,实现功能的增强
proxy.doSecond("zs",21);
}

@Test
public void test05(){
String config = "applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);

//从spring的容器中获取目标对象(代理对象)
SomeService proxy = (SomeService) ctx.getBean("SomeServiceTarget");

//通过代理对象执行业务方法,实现功能的增强
proxy.doThird();
}
}

优点

面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。
若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变的混杂不清。