Spring aop

面向切面编程(AOP)通过提供另一种关于程序结构的思考来补充面向对象编程(OOP)。OOP中模块化的关键单元是类,而在AOP中,模块化单元是切面。切面能够实现跨越多种类型和对象的关注点(例如事务管理,日志处理,访问控制)的模块化。

简单点说就是:在一个方法的前后植入逻辑,这个方法就是一个切入点。原理是通过动态代理生成代理类,Spring中给我们使用的是他生成的代理类。

AOP中的术语

切面:跨越多个类别的关注点的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,方面是通过使用常规类(基于模式的方法)或使用@Aspect注释(@AspectJ样式)注释的常规类来实现的 。

连接点:程序执行期间的一个点,例如执行的方法或处理异常。在Spring AOP中,连接点始终表示执行的方法(目标对象的目标方法)。

通知:拦截到连接点之前或之后处理的一段逻辑,这段逻辑一般是切面中的方法

切入点:匹配连接点的定义(例如,执行具有特定名称的方法),Spring中用切入点表达式定义。由切入点表达式匹配的连接点的概念是AOP的核心,而Spring默认使用AspectJ切入点表达式语言。

引介:可以让程序员对现有的类添加新的接口,并确定该接口方法的实现

目标对象:由一个或多个切面切入的对象。由于Spring AOP是使用运行时代理实现的,因此该对象始终是代理对象。目标对象中的被代理的方法就是连接点

AOP代理:由AOP框架创建的对象,代理对象是将切面中的通知根据切入点表达式织入到目标对象的方法中。在Spring Framework中,AOP代理是JDK动态代理或CGLIB代理。

编织:将方面与其他应用程序类型或对象链接以创建通知对象。这可以在编译时(例如使用AspectJ编译器),加载时间或在运行时完成。与其他纯Java AOP框架一样,Spring AOP在运行时执行编织。

通知类型

Spring AOP包含以下类型的通知:
前置通知:在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常)。

后置通知:在连接点正常完成后运行的通知(例如,如果方法返回而不抛出异常)。

异常通知:如果方法通过抛出异常退出,则执行通知。

最终通知:无论连接点退出的方式(正常或异常返回),都要执行通知。

围绕通知:围绕连接点的通知,例如方法调用。围绕通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续执行连接点还是通过返回自己的返回值或抛出异常来快速通知的方法执行。

AOP的代理

Spring AOP默认使用AOP代理的标准JDK动态代理,如果目标类没有接口的话就会采用CGLIB.

简单例子

配置

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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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/spring-aop.xsd">

<context:component-scan base-package="com.qworldr">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--开启切面注解支持 -->
<aop:aspectj-autoproxy/>
</beans>

切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
@Aspect
public class Aspectj {
//切入点,切入TestService类的无参方法
@Pointcut("execution(* com.qworldr.service.TestService.*())")
public void pointcut(){};

//前置通知,使用pintcut方法上配置的切入点
@Before("pointcut()")
public void before(){
System.out.println("test执行前");
}
}

目标类

1
2
3
4
5
6
7
8
@Service
public class TestService {
//连接点
public void test(){
System.out.println("执行test");
}

}

测试

1
2
3
4
5
6
7
8
9
10
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Test01 {

@Autowired
private TestService testService;
@Test
public void test(){
testService.test();
}
}

运行结果

1
2
test执行前
执行test

从运行结果可以看出,前置通知再连接点之前执行。

切面配置

注解方式

上面例子就是通过注解方式实现的AOP。

注解方式除了依赖Spring-aop,还依赖下面两个包

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.1</version>
</dependency>

启动aop注解支持

注解方式启动

1
2
3
4
5
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

xml方式启动

1
<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
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
/**
* 切面声明,@Aspect不具有被Spring扫描注册的功能,所以还需要@Component注解配合。
*/
@Aspect
@Component
public class Aspectj {

//切入点声明
@Pointcut("execution(* com.qworldr.service.TestService.*())")
public void pointcut(){};

/**
* 前置通知 ,引用切入点。
*/
@Before("pointcut()")
public void before(){
System.out.println("前置通知 test执行前");
}

/**
* 后置通知 每种通知都有一个JoinPoint类型参数获取目标方法的信息
* @param joinPoint 可以获得目标方法和参数值
* @param val 目标方法返回值 参数名要和注解的returning一致
*/
@AfterReturning(pointcut = "pointcut()",returning = "val")
public void afterReturn(JoinPoint joinPoint, Object val){
System.out.println("后置通知:"+val);
}

/**
* 异常通知
* @param joinPoint 可以获得目标方法和参数值
* @param ex 目标方法抛出异常 参数名要和注解的throwing一致
*/
@AfterThrowing(pointcut = "pointcut()",throwing = "ex")
public void afterThrowing(JoinPoint joinPoint,Throwable ex){
System.out.println("异常:"+ex);
}

/**
* 最终通知
*/
@After("pointcut()")
public void after(){
System.out.println("最终通知 xml test执行后");
}

/**
* 环绕通知
* @param joinPoint ProceedingJoinPoint的proceed方法相当于invoke方法,调用目标类的目标方法
* @return
* @throws Throwable
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知 test执行前");
Object proceed = joinPoint.proceed();
System.out.println("环绕通知 xml test执行后");
return proceed;
}
}

xml配置

切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class XMLAspectj {
public void before(){
System.out.println("前置通知 test执行前");
}

public void afterReturn(JoinPoint joinPoint,Object val){
System.out.println("后置通知:"+val);
}
public void afterThrowing(JoinPoint joinPoint,Throwable ex){
System.out.println("异常:"+ex);
}
public void after(){
System.out.println("最终通知 xml test执行后");
}

public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知 test执行前");
Object proceed = joinPoint.proceed();
System.out.println("环绕通知 xml test执行后");
return proceed;
}
}

xml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<bean id="aspect" class="com.qworldr.aspectj.XMLAspectj"></bean>
<aop:config>
<aop:aspect id="aspect" ref="aspect">
<aop:pointcut id="pointcut" expression="execution(* com.qworldr.service.TestService.*())"/>
<aop:before method="before" pointcut-ref="pointcut"></aop:before>
<!-- 后置通知,通知方法可以有两个参数
JoinPoint point 可以获得目标方法和参数值
Object val 这里的名字要和returning=”val”中保持一致,指的是方法的返回值。
-->
<aop:after-returning method="afterReturn" pointcut-ref="pointcut" returning="val"></aop:after-returning>
<!-- 异常通知,通知方法可以有两个参数
JoinPoint point 可以获得目标方法和参数值
Throwable ex 这里的名字要和throwing="ex" 中保持一致,指的是方法的抛出的异常。
-->
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ex"></aop:after-throwing>
<aop:after method="after" pointcut-ref="pointcut" ></aop:after>
<!-- 环绕通知,通知方法可以有1个参数
ProceedingJoinPoin point 可以控制连接点的执行,ProceedingJoinPoint的proceed方法相当于invoke方法,调用目标类的目标方法
-->
<aop:around method="around" pointcut-ref="pointcut" ></aop:around>
</aop:aspect>
</aop:config>

切入点表达式配置

通过切入点表达式匹配感兴趣的连接点,控制通知何时执行。

切入点支持的指示符

execution:用于匹配方法执行的连接点;
within:用于匹配指定类型内的方法执行
this:用于匹配当前AOP代理对象类型的执行方法
target:用于匹配当前目标对象类型的执行方法
args:用于匹配当前执行的方法传入的参数为指定类型的执行方法
@within:用于匹配所以持有指定注解类型内的方法
@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
@args:用于匹配当前执行的方法传入的参数持有指定注解的执行
@annotation:用于匹配当前执行方法持有指定注解的方法;

execution配置

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 方法名 , 表示任务名字,例子:set 表示以set开头的
(param-pattern) 方法参数 ()匹配一个不带参数的方法,而(..)匹配任何数量(零个或多个)参数
throws-pattern? 异常类型,不必须

表达式通配符

*:匹配任何数量字符;
..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
+:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。

切入点运算

切入点直接可以通过 && ,|| ,! 进行运算。

com.xyz.service包或其子包中的参数是java.io.Serializable的连接点。

1
within(com.xyz.service..*)&&args(java.io.Serializable)

com.xyz.service包或其子包的任何连接点或者参数是java.io.Serializable的连接点。

1
within(com.xyz.service..*)||args(java.io.Serializable)

不在com.xyz.service包中的任何连接点。

1
!within(com.xyz.service..*)

例子

com.xyz.service包下的任意方法

1
execution(* com.xyz.service..*.*(..))

com.xyz.service包或其子包中的任何连接点

1
within(com.xyz.service..*)

代理实现AccountService接口的任何连接点

1
this(com.xyz.service.AccountService)

目标对象实现AccountService接口的任何连接点

1
target(com.xyz.service.AccountService)

运行方法参数列表是(java.io.Serializable)的连接点

1
args(java.io.Serializable)

目标对象具有该注解的连接点

1
@target(org.springframework.transaction.annotation.Transactional)

目标对象具有该注解的连接点

1
@within(org.springframework.transaction.annotation.Transactional)

目标方法具有该注解的连接点

1
@annotation(org.springframework.transaction.annotation.Transactional)

切面顺序

当多个通知需要在同一个连接点(执行方法)执行时,切面之间的优先级是通过Orde注解或者让bean实现Ordered接口来确定的。

引介

可以让程序员对现有的类添加新的接口,并确定该接口方法的实现

1
2
3
4
5
6
7
@Aspect
@Component
public class Aspectj {
//给TestService类添加接口Intorduction,Introduction接口的方法默认实现是IntroductionImpl
@DeclareParents(value = "com.qworldr.service.TestService",defaultImpl = IntroductionImpl.class)
public Introduction introduction;
}

测试
TestService可以强转成为Introduction对象运行。

1
2
3
4
5
6
7
8
@Autowired
private TestService testService;
@Test
public void test(){
testService.test();
Introduction testService = (Introduction) this.testService;
System.out.println(testService.introduction());
}