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(*)) &amp;&amp; args(user,..)"/>

参考