1. Spring 的 AOP
1.1 介绍
面向切面编程(Aspect Oriented Programming) ,是对面向对象编程(OOP)的补充。 OOP 是 面向 类 , AOP 是面向 切面。
AOP 在 Spring 中的作用 :
- 提供 声明式的企业服务 ,尤其是 替代 EJB 的声明式服务 。最重要的比如 声明式事物
- 用户可以实现自定义的 切面 ,使用 AOP 补充 OOP
1.1.1 AOP 概念
★ Aspect : 切面 ,横切多个类的一个模块化关注点 。 通常通过有规律的类或者类的注解 @Aspect 来实现 切面
★ Join Point : 程序执行过程中的一个连接点 ,例如方法的执行 , 异常的处理 。
★ Advice : 通知 。 切面在指定的一个连接点采取的动作。 通常包括 around(环绕),before(前置),after(后置)通知 ,作为一个拦截器提供通知 。
★ Pointcut : 切点 。 匹配 Join Point 的 断言 。通知 是和 切点表达式相关联的 并且在通过切点 匹配的任意连接点执行。
★ Introduction :
★ Target object :被一个或者多个切面通知的对象 。因为 Spring AOP 是通过使用运行时代理来实现的 ,所以这个对象永远是 一个被代理的对象。
★ AOP Proxy : AOP 代理 , 为了实现 切面规定而被 AOP 框架创建的对象 。 在 Spring 框架中, AOP 是一个 JDK 动态代理 或者 CGLIB 代理 ,可手动配置。
★ Weaing : 编织 ,将 切面和其他应用类型或者对象 连接起来创建 一个被通知的对象 。这个过程在编译时期(例如使用 AspectJ 编译器实现) ,加载时期或者运行时期可以完成。 Spring AOP 像其他纯 Java 的 AOP 框架 一样,在运行时期进行编织 。
通知类型 :
★ Before advice : 在 Join Point 之前执行的通知 ,但是没有能力阻止 Join Point 的执行(除非抛出异常)
★ After returning advice : 在 连接点正常完成之后执行的通知 :例如 ,如果方法没有抛出异常正常返回后执行该通知。
★ After throwing advice : 如果一个方法因为抛出异常 退出,执行 通知
★ After (finally) advice : 不管连接点 退出与否都会执行的通知 (正常或者异常返回)
★ Around advice : 环绕 一个方法调用的连接点的通知 。能够在方法调用之前和方法调用之后自定义行为 ,而且还负责是否继续执行 join point ,还是返回自定义的值或者是抛出异常 。都可以自定义。
1.1.2 Spring AOP 的 功能和目标
Spring AOP 是 用纯 Java 实现的,适合在 Servlet 容器 / Application server 使用 。
Spring AOP 的 目标不仅仅是 实现 AOP ,而是为 AOP 实现和 Spring Ioc 提供紧密集成 ,从而解决企业级应用中的一些常见问题。
Spring AOP 需要集成 AspectJ
1.1.3 AOP 代理
Spring AOP 默认使用 标准的 JDK 动态代理 作为 AOP 的代理 ,使 任意接口(或者一组接口)被代理 。
Spring AOP 同时也可以使用 CGLIB 代理 ,相对于 JDK 代理的接口,代理类更有必要 。当 业务对象 没有实现接口 时 会默认使用 CGLIB 代理 。因为编程到接口而不是类是一个很好的变成习惯,因此 类通常 会实现一个或者多个接口 。 当然也可以强制使用 CGLIB 代理 。
1.2 @AspectJ 支持
1.2.1 开启 @AspectJ 支持
需要 aspectj 和 aspectjweaver 两个 jar 包 ,开启可通过以下两种方式 开启
使用 java 配置开启 使用 @EnableAspectJAutoProxy:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
使用 XML 配置开启:
<aop:aspectj-autoproxy/>
需要加入 schema
1.2.2 声明一个 切面
定义类并在类上加上注解@Aspect
package org.xyz;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
}
配置到 Spring 的配置文件(或者扫描相关包):
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of aspect here as normal -->
</bean>
1.2.3 声明 一个切点
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
支持的 切入点指示符 (PCD)
★ execution : 匹配方法执行连接点
★ within :
@Pointcut("within(com.peptalk.controller.*)")
public void controllerPoint(){
}
★ this:
★ target:
★ args:
★ @target:
★ @args:
★ @within:
★ @annotation:
@Pointcut("@annotation(com.vastio.aop.OperationLog)") // 匹配标注该注解的连接点
public void methodCachePointcut() {
}
切点表达式的组合 (&& 、 || 、 !): (XML 中使用 and 、 or 、 not 组合 多个切点表达式)
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
一些表达式的栗子 :
execution(public * *(..)) // 匹配任意 public 类型方法的执行
execution(* set*(..)) // 匹配 任意以 set开头的方法的执行
execution(* com.xyz.service.AccountService.*(..)) // 匹配 AccountService 类中的任意方法的执行
execution(* com.xyz.service.*.*(..)) // 匹配 service 包中任意方法的执行
execution(* com.xyz.service..*.*(..)) // 匹配 service 包及其子包中 的任意方法的执行
within(com.xyz.service.*) // service包内执行的任意连接点(spring aop 中的方法的执行)
within(com.xyz.service..*) // service及其子包中的连接点
this(com.xyz.service.AccountService) //实现 AccountService 接口的 代理的任意连接点
target(com.xyz.service.AccountService) //实现 AccountService接口的目标对象的任意连接点
args(java.io.Serializable) //一个参数并且在运行传递的序列化的参数的任意连接点
@target(org.springframework.transaction.annotation.Transactional) //目标对象有 @Transactional注解的任意连接点
@within(org.springframework.transaction.annotation.Transactional)
@annotation(org.springframework.transaction.annotation.Transactional)
@args(com.xyz.security.Classified) 一个参数并且运行时传递的标有 @Classified的标注
bean(tradeService)
bean(*Service) spring bean 的名称 以 Service 结尾的任意连接点
1.2.4 声明式的通知
★ Before advce : @Before
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") //在连接点之前执行
public void doAccessCheck() {
// ...
}
}
XML 对应
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
★ After returning advice : @AfterReturning 返回值可以作为参数
returning 属性的名称必须要和 通知方法里面的参数名称对应,才能传递
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal") //在连接点之后执行,可以将返回值作为参数
public void doAccessCheck(Object retVal) {
// ...
}
}
XML 中对应
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
★ After throwing advice : @AfterThrowing 异常可以作为参数
throwing 属性的 名称 必须和通知方法里面的参数的 名称对应 ,才能传递
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex") // 抛出异常后执行
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
XML 中对应
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
★ After (finally) advice : @After
匹配的方法是否顺利执行 通知都会执行。 所以 该通知 必须 处理 正常和 异常 两种情况 ,通常用作资源的释放 。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
XML 中对应
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
★ Around advice :@Around ProceedingJoinPoint 可以作为参数 ,JoinPoint 的 子类
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch 连接点之前执行的代码
Object retVal = pjp.proceed();
// stop stopwatch 连接点之后执行的代码
return retVal;
}
}
XML中对应
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
...
</aop:aspect>
取得当前的连接点 JoinPoint
每个通知方法都 可以声明 JoinPoint 类型的参数作为 第一个参数 , 环绕 通知 声明的是 JoinPoint 的 子类 ProceedingJoinPoint
JoinPoint 提供的方法 :
Object [] getArgs(); //返回方法参数
Object getThis(); // 返回代理对象
Signature getSignature(); // 返回被通知的方法的描述
Object getTarget(); // 返回目标对象
传递参数给 advice
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
或者
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
Advice parameters and generics
generic type
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
可以这样使用
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
不可以这样使用
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
确定参数的名称
参数名称通过 java 反射 不能获取,所以使用如下的策略 :通过可选的 argNames 属性可以指定参数名称和注解名称
如果第一个参数为 JoinPoint 可以不用加入
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
1.3 基于 schema 的 aop 支持
1.3.1 定义切面
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
1.3.2 声明切点
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
整体
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
1.6 代理机制
JDK 动态代理 或者 CGLIB 代理 :如果目标对象实现了至少一个 接口 那么默认使用 JDK 的动态代理 ,被目标对象实现的所有接口都会被代理 ;如果目标对象没有实现任何接口 ,那么则会默认使用 CGLIB 代理 。
以下几个需要考虑的问题:
★ 被 final 修饰的方法 不能被通知 ,因为他们不能被重写 。
★ Spring 3.2 之后 , CGLIB jar 包已经整合到 spring 的 核心包中 。意味着 基于 CGLIB 代理 可以像 JDK 动态代理那样 一直存在 。
★ Spring 4.0 之后,被代理的对象的构造函数不会再被调用两次 。
强制使用 CGLIB 的 XML 配置 :
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
或者
<aop:aspectj-autoproxy proxy-target-class="true"/>
基于注解:
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}
<一下内容暂略------->
<补充 : Spring AOP 拦截 controller
正常我们使用 Spring AOP 横切 service 层很容易实现 ,而在横切 controller 层时却不起作用 。因为 service 层 是由 Spring 负责 扫描管理的 ,而 controller 层是由 Spring mvc 负责扫描管理的 ,如果要保证在横切 controller 层 时 也能够实现 aop 功能 ,就必须保证 Spring AOP 的 注解由 Spring mvc 负责扫描 ,即 开启 Spring aop 的配置要配置在 Spring mvc 的配置文件中 。