AOP(Aspect-Oriented-Programming) 面向切面编程,其原理是使用动态代理去间接的调用接口,以方便代理者在代理调用过程前后加入私货(重复代码)的处理,以简化代码。也达到了在线性逻辑当中,以非入侵方式的切开线性逻辑加入自己的逻辑面(切面),是对面向对象的思维方式的一种有力的补充。
基本概念
Spring 基于 AspectJ 的动态代理的原理实现了 AOP,在正式开始之前先了解下面的几个概念:
- joinpoint, 连接点, 程序执行的某个特定位置
- Before
- After
- After-returning
- After-throwing
- Around
- pointcut, 切点, 如果连接点相当于数据中的记录,那么切点相当于查询条件,一个切点可以匹配多个连接点。
- advice, 增强,某个切面在特定连接点(joinpoint)处采取的操作即程序代码。
- aspect, 切面, 切面是由切点和增强(引介)组成的,它包括了对横切关注功能的定义,也包括了对连接点的定义。
注解式
预备的代码基本与 依赖注入篇 时候相同,这里以如何实现为 src/main/java/demo/service/UserService.java
的 add 方法加上日志切面作为目的,进行讲解。为此新建一个 LogIntercepter.java 文件内容如下:
1
2
3
4
5
6
7
8
9
10
|
// demo/aop/LogIntercepter.java
public class LogIntercepter {
void after() {
System.out.println("after user add");
}
void before() {
System.out.println("before user add");
}
}
|
先加上 AspectJ 的依赖:
1
2
3
4
5
|
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>RELEASE</version>
</dependency>
|
为使用的配置类加上 @EnableAspectJAutoProxy
注解:
1
2
3
4
5
6
|
@Configuration
@ComponentScan(basePackages = "demo")
@EnableAspectJAutoProxy
public class UserDaoConfig {
//...
}
|
为 LogIntercepter 类加上 @Component
注解以加入 spring 上下文,之后再加上 @Aspect
注解声明切面类,使用切入点语法为 after 和 before 方法分部加上 After 和 Before 的 advcie,最后以使得 spring 根据此类和其切入点语法创建自动代理。
1
2
3
4
5
6
7
8
9
10
11
12
|
@Component
@Aspect
public class LogIntercepter {
@After("execution(public void demo.service.UserService.addUser(demo.model.User))")
void after() {
System.out.println("after user add");
}
@Before("execution(public * demo..addUser(*))")
void before() {
System.out.println("before user add");
}
}
|
这样就完成了日志的织入,@After
和 @Before
里面的参数成为切入点语法。
切入点语法
executoin 表示是在执行方法时候切入,成为切入点指示符。用来指示切入点表达式目的,在 Spring AOP 中目前只有执行方法这一个连接点,Spring AOP 支持的 AspectJ 切入点指示符如下:
execution
:用于匹配方法执行的连接点;
within
:用于匹配指定类型内的方法执行;
this
:用于匹配当前 AOP 代理对象类型的执行方法;注意是 AOP 代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
target
:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
args
:用于匹配当前执行的方法传入的参数为指定类型的执行方法;
@within
:用于匹配所以持有指定注解类型内的方法;
@target
:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;
@args
:用于匹配当前执行的方法传入的参数持有指定注解的执行;
@annotation
:用于匹配当前执行方法持有指定注解的方法;
bean
:Spring AOP 扩展的,AspectJ 没有对于指示符,用于匹配特定名称的 Bean 对象的执行方法;
reference pointcut
:表示引用其他命名切入点,只有 @ApectJ 风格支持,Schema 风格不支持。
匹配模式和组合表达式
..
类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
*
匹配任何数量字符
- 可以使用与(&&)、或(||)、非(!)来衔接表达式
pointcut
在编写切入点语法点语法时候,发现这个大部分时候都是通用的,这时候就可以使用 @Pointcut
创建一个切入点(匹配模式),该注解利用被修饰的方法名作为标识符,这样就可以简化为下面的代码:
1
2
3
4
5
6
7
8
9
10
11
|
@Pointcut("execution(public * demo..addUser(*))")
void logMethod(){};
@After("logMethod()")
void after() {
System.out.println("after user add");
}
@Before("logMethod()")
void before() {
System.out.println("before user add");
}
|
advice
除了 @Before
和 @After
还可以有 @AfterReturning
@AfterThrowing
@Around
,其意义和名字一样,只不过需要注意的是 @Around
的用法,需要在切入点方法里调用责任链,这一点和拦截器的 dofilter 很相似。
1
2
3
4
5
6
|
@Around("myMethod()")
public void aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("method around start");
pjp.proceed();
System.out.println("method around end");
}
|
advice 参数
上面的 pjp 属于是自动代理的参数,在编写切面逻辑时候,时常会需要将被代理的方法的参数传递给切面,这时候可以这样实现:
1
2
3
4
|
@After("execution(public * demo..addUser(*)) && args(user))")
void after(User user) {
System.out.println("after user add " + user);
}
|
看似 && args(user)
是筛选条件的限定符,其实是将被代理的该名称的参数传递给切面逻辑,切面逻辑需要以同名的参数承接。args 里面可以挑选多个参数,以逗号分隔,如果全部传递的话可以使用 args(user, ..)
的语法简化。
DeclareParents
我们知道切面接口是由动态代理实现,即动态扫描被代理类的接口,然后动态构建一段代码,该段代码具有其所有的接口,在接口实现上添加上自己的业务逻辑,并使用反射包裹调用了原有的接口,然后动态编译,装载编译后 class 到内存中。既然动态包裹修改实现了一个类的接口,那么也是可以直接在该动态生成的代理类上直接 添加 接口的。
如想对一不可更改的类上面,添加一个自己的接口,就可以在这个代理类上实现。利用 AspectJ 的 @DeclareParents
就可以轻松实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public interface SayHi {
public void SayHai();
}
class SayHiImpl implements SayHi {
public void SayHai() { System.out.println("Hi"); }
}
@Component
@Aspect
public class LogIntercepter {
// value 表明需要被加上的类,defaultImpl 指明了添加的接口的实现类
@DeclareParents(value = "demo.service.UserService+", defaultImpl = SayHiImpl.class)
SayHi sayHi;
// ...
}
|
之后在上下文中获取的代理类就多了该接口,测试类中可以强制转化获取该接口
1
2
3
4
5
6
|
ApplicationContext ctx = new AnnotationConfigApplicationContext(UserDaoConfig.class);
UserService userService = ctx.getBean(UserService.class);
User user = new User();
user.setUsername("Scott");
((SayHi) userService).SayHai();
userService.addUser(user);
|
配置式
除了注解式的切面还可以使用 xml 配置的切面,首先修改 beans.xml 文件,添加 xsd 和 namespace ,然后添加上 <aop:aspectj-autoproxy/>
,最后配置文件的样子可能是这样的:
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
|
<?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:context="http://www.springframework.org/schema/context"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/srping-aop.xsd">
<context:annotation-config/>
<context:component-scan base-package="demo"/>
<!-- 加上 autoproxy -->
<aop:aspectj-autoproxy/>
<!--service 的组件已经使用注解装配完成-->
<bean id="userService" class="demo.service.UserService"/>
<!-- 切面类需要交由 spring 来管理 -->
<bean id="logInterceptor" class="demo.aop.LogIntercepter"/>
<aop:config>
<!-- ref 定义了切面类 -->
<aop:aspect id="logAspect" ref="logInterceptor">
<aop:pointcut id="logMethod" expression="execution(public * demo..addUser(*))"/>
<aop:before method="before" pointcut="execution(public * demo..addUser(*))"/>
<aop:after method="after" pointcut-ref="logMethod"/>
</aop:aspect>
</aop:config>
</beans>
|
结合之前的注解式很容易理解上面的一些点:
<aop:aspect>
对标 @Aspect
,ref 指定了其切面类 bean
<aop:before>
对标 @Before
,aop 后面的 before 指定了 advice 类型,method 指定了切面类对应的方法,pointcut 切入点语法
<aop:pointcut>
对标 @Pointcut
,使用 pointcut-ref 来指定
pointcut 切入点语法里如果需要使用 &
符号需要进行转义,如:
1
|
<aop:after method="after" pointcut="execution(public * demo..addUser(*)) && args(user,..)"/>
|
参考