完全征服 Spring AOP 之 理论篇

基本知识

其实, 接触了这么久的 AOP, 我感受, AOP 给人难以理解的一个关键点是它的概念比较多, 并且坑爹的是, 这些概念通过了中文翻译后, 变得面目全非, 相同的一个术语, 在不一样的翻译下, 含义总有着各类莫名其妙的差异. 鉴于此, 我在本章的开头, 着重为为你们介绍一个 Spring AOP 的各项术语的基本含义. 为了术语传达的准确性, 我在接下来的叙述中, 能使用英文术语的地方, 尽可能使用英文.java

什么是 AOP

AOP(Aspect-Oriented Programming), 即 面向切面编程, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不一样的抽象软件结构的视角.
在 OOP 中, 咱们以类(class)做为咱们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)express

术语

Aspect(切面)

aspectpointcountadvice 组成, 它既包含了横切逻辑的定义, 也包括了链接点的定义. Spring AOP就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的链接点中.
AOP的工做重心在于如何将加强织入目标对象的链接点上, 这里包含两个工做:编程

  1. 如何经过 pointcut 和 advice 定位到特定的 joinpoint 上segmentfault

  2. 如何在 advice 中编写切面代码.数组

能够简单地认为, 使用 @Aspect 注解的类就是切面.app

advice(加强)

由 aspect 添加到特定的 join point(即知足 point cut 规则的 join point) 的一段代码.
许多 AOP框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 而且在 join point 上维护多个 advice, 进行层层拦截.
例如 HTTP 鉴权的实现, 咱们能够为每一个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里咱们能够分析这个 HTTP 请求是否有相应的权限, 若是有, 则执行 Controller, 若是没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了.框架

链接点(join point)

a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.函数

程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.
在 Spring AOP 中, join point 老是方法的执行点, 即只有方法链接点.this

切点(point cut)

匹配 join point 的谓词(a predicate that matches join points).
Advice 是和特定的 point cut 关联的, 而且在 point cut 相匹配的 join point 中执行.
在 Spring 中, 全部的方法均可以认为是 joinpoint, 可是咱们并不但愿在全部的方法上都添加 Advice, 而 pointcut 的做用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给知足规则的 joinpoint 添加 Advice.编码

关于join point 和 point cut 的区别

在 Spring AOP 中, 全部的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 经过 point cut, 咱们就能够肯定哪些 join point 能够被织入 Advice. 所以 join point 和 point cut 本质上就是两个不一样纬度上的东西.
advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 能够执行哪些 advice

introduction

为一个类型添加额外的方法或字段. Spring AOP 容许咱们为 目标对象 引入新的接口(和对应的实现). 例如咱们可使用 introduction 来为一个 bean 实现 IsModified 接口, 并以此来简化 caching 的实现.

目标对象(Target)

织入 advice 的目标对象. 目标对象也被称为 advised object.
由于 Spring AOP 使用运行时代理的方式来实现 aspect, 所以 adviced object 老是一个代理对象(proxied object)
注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.

AOP proxy

一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和加强逻辑的代理类.
在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象.

织入(Weaving)

将 aspect 和其余对象链接起来, 并建立 adviced object 的过程.
根据不一样的实现技术, AOP织入有三种方式:

  • 编译器织入, 这要求有特殊的Java编译器.

  • 类装载期织入, 这须要有特殊的类装载器.

  • 动态代理织入, 在运行期为目标类添加加强(Advice)生成子类的方式.
    Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.

advice 的类型

  • before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 可是它并不可以阻止 join point 的执行, 除非发生了异常(即咱们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)

  • after return advice, 在一个 join point 正常返回后执行的 advice

  • after throwing advice, 当一个 join point 抛出异常后执行的 advice

  • after(final) advice, 不管一个 join point 是正常退出仍是发生了异常, 都会被执行的 advice.

  • around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最经常使用的 advice.

关于 AOP Proxy

Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 经过它, 咱们能够为任意的接口实现代理.
若是须要为一个类实现代理, 那么可使用 CGLIB 代理. 当一个业务逻辑对象没有实现接口时, 那么Spring AOP 就默认使用 CGLIB 来做为 AOP 代理了. 即若是咱们须要为一个方法织入 advice, 可是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类.

完全理解 aspect, join point, point cut, advice

看完了上面的理论部分知识, 我相信仍是会有很多朋友感受到 AOP 的概念仍是很模糊, 对 AOP 中的各类概念理解的还不是很透彻. 其实这很正常, 由于 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的.
下面我以一个简单的例子来比喻一下 AOP 中 aspect, jointpoint, pointcut 与 advice 之间的关系.

让咱们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 做案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王刚好在这时候无心中发现了凶手行凶的过程, 可是因为天色已晚, 加上凶手蒙着面, 老王并无看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵固然不敢违背县令的命令, 只好把进出城的全部符合条件的人都抓了起来.

来让咱们看一下上面的一个小故事和 AOP 到底有什么对应关系.
首先咱们知道, 在 Spring AOP 中 join point 指代的是全部方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 join point, 经过 point cut, 咱们就能够肯定哪些 join point 能够被织入 Advice. 对应到咱们在上面举的例子, 咱们能够作一个简单的类比, join point 就至关于 爪哇的小县城里的百姓, point cut 就至关于 老王所作的指控, 即凶手是个男性, 身高约七尺五寸, 而 advice 则是施加在符合老王所描述的嫌疑人的动做: 抓过来审问.
为何能够这样类比呢?

  • join point --> 爪哇的小县城里的百姓: 由于根据定义, join point 是全部可能被织入 advice 的候选的点, 在 Spring AOP中, 则能够认为全部方法执行点都是 join point. 而在咱们上面的例子中, 命案发生在小县城中, 按理说在此县城中的全部人都有多是嫌疑人.

  • point cut --> 男性, 身高约七尺五寸: 咱们知道, 全部的方法(joint point) 均可以织入 advice, 可是咱们并不但愿在全部方法上都织入 advice, 而 pointcut 的做用就是提供一组规则来匹配joinpoint, 给知足规则的 joinpoint 添加 advice. 同理, 对于县令来讲, 他再昏庸, 也知道不能把县城中的全部百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 知足此修饰规则的百姓都是嫌疑人, 都须要抓起来审问.

  • advice --> 抓过来审问, advice 是一个动做, 即一段 Java 代码, 这段 Java 代码是做用于 point cut 所限定的那些 join point 上的. 同理, 对比到咱们的例子中, 抓过来审问 这个动做就是对做用于那些知足 男性, 身高约七尺五寸爪哇的小县城里的百姓.

  • aspect: aspect 是 point cut 与 advice 的组合, 所以在这里咱们就能够类比: "根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问" 这一整个动做能够被认为是一个 aspect.


或则咱们也能够从语法的角度来简单类比一下. 咱们在学英语时, 常常会接触什么 定语, 被动句 之类的概念, 那么能够作一个不严谨的类比, 即 joinpoint 能够认为是一个 宾语, 而 pointcut 则能够类比为修饰 joinpoint 的定语, 那么整个 aspect 就能够描述为: 知足 pointcut 规则的 joinpoint 会被添加相应的 advice 操做.

@AspectJ 支持

@AspectJ 是一种使用 Java 注解来实现 AOP 的编码风格.
@AspectJ 风格的 AOP 是 AspectJ Project 在 AspectJ 5 中引入的, 而且 Spring 也支持@AspectJ 的 AOP 风格.

使能 @AspectJ 支持

@AspectJ 能够以 XML 的方式或以注解的方式来使能, 而且不论以哪一种方式使能@ASpectJ, 咱们都必须保证 aspectjweaver.jar 在 classpath 中.

使用 Java Configuration 方式使能@AspectJ

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

使用 XML 方式使能@AspectJ

<aop:aspectj-autoproxy/>

定义 aspect(切面)

当使用注解 @Aspect 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如:

@Component
@Aspect
public class MyTest {
}

注意, 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Bean, 所以咱们还须要使用相似 @Component 之类的注解.
注意, 若是一个 类被@Aspect 标注, 则这个类就不能是其余 aspect 的 **advised object** 了, 由于使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制以外.

声明 pointcut

一个 pointcut 的声明由两部分组成:

  • 一个方法签名, 包括方法名和相关参数

  • 一个 pointcut 表达式, 用来指定哪些方法执行是咱们感兴趣的(即所以能够织入 advice).

在@AspectJ 风格的 AOP 中, 咱们使用一个方法来描述 pointcut, 即:

@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切点表达式
private void dataAccessOperation() {} // 切点前面

这个方法必须无返回值.
这个方法自己就是 pointcut signature, pointcut 表达式使用@Pointcut 注解指定.
上面咱们简单地定义了一个 pointcut, 这个 pointcut 所描述的是: 匹配全部在包 com.xys.service.UserService 下的全部方法的执行.

切点标志符(designator)

AspectJ5 的切点表达式由标志符(designator)和操做参数组成. 如 "execution( greetTo(..))" 的切点表达式, execution 就是 标志符, 而圆括号里的 greetTo(..) 就是操做参数

execution

匹配 join point 的执行, 例如 "execution(* hello(..))" 表示匹配全部目标类中的 hello() 方法. 这个是最基本的 pointcut 标志符.

within

匹配特定包下的全部 join point, 例如 within(com.xys.*) 表示 com.xys 包中的全部链接点, 即包中的全部类的全部方法. 而 within(com.xys.service.*Service) 表示在 com.xys.service 包中全部以 Service 结尾的类的全部的链接点.

this 与 target

this 的做用是匹配一个 bean, 这个 bean(Spring AOP proxy) 是一个给定类型的实例(instance of). 而 target 匹配的是一个目标对象(target object, 即须要织入 advice 的原始的类), 此对象是一个给定类型的实例(instance of).

bean

匹配 bean 名字为指定值的 bean 下的全部方法, 例如:

bean(*Service) // 匹配名字后缀为 Service 的 bean 下的全部方法
bean(myService) // 匹配名字为 myService 的 bean 下的全部方法
args

匹配参数知足要求的的方法.
例如:

@Pointcut("within(com.xys.demo2.*)")
public void pointcut2() {
}

@Before(value = "pointcut2()  &&  args(name)")
public void doSomething(String name) {
    logger.info("---page: {}---", name);
}
@Service
public class NormalService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    public void someMethod() {
        logger.info("---NormalService: someMethod invoked---");
    }


    public String test(String name) {
        logger.info("---NormalService: test invoked---");
        return "服务一切正常";
    }
}

当 NormalService.test 执行时, 则 advice doSomething 就会执行, test 方法的参数 name 就会传递到 doSomething 中.

经常使用例子:

// 匹配只有一个参数 name 的方法
@Before(value = "aspectMethod()  &&  args(name)")
public void doSomething(String name) {
}

// 匹配第一个参数为 name 的方法
@Before(value = "aspectMethod()  &&  args(name, ..)")
public void doSomething(String name) {
}

// 匹配第二个参数为 name 的方法
Before(value = "aspectMethod()  &&  args(*, name, ..)")
public void doSomething(String name) {
}
@annotation

匹配由指定注解所标注的方法, 例如:

@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}

则匹配由注解 AuthChecker 所标注的方法.

常见的切点表达式

匹配方法签名
// 匹配指定包中的全部的方法
execution(* com.xys.service.*(..))

// 匹配当前包中的指定类的全部方法
execution(* UserService.*(..))

// 匹配指定包中的全部 public 方法
execution(public * com.xys.service.*(..))

// 匹配指定包中的全部 public 方法, 而且返回值是 int 类型的方法
execution(public int com.xys.service.*(..))

// 匹配指定包中的全部 public 方法, 而且第一个参数是 String, 返回值是 int 类型的方法
execution(public int com.xys.service.*(String name, ..))
匹配类型签名
// 匹配指定包中的全部的方法, 但不包括子包
within(com.xys.service.*)

// 匹配指定包中的全部的方法, 包括子包
within(com.xys.service..*)

// 匹配当前包中的指定类中的方法
within(UserService)


// 匹配一个接口的全部实现类中的实现的方法
within(UserDao+)
匹配 Bean 名字
// 匹配以指定名字结尾的 Bean 中的全部方法
bean(*Service)
切点表达式组合
// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)

// 匹配名字以 Service 结尾, 而且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)

声明 advice

advice 是和一个 pointcut 表达式关联在一块儿的, 而且会在匹配的 join point 的方法执行的前/后/周围 运行. pointcut 表达式能够是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式.
下面咱们以几个简单的 advice 为例子, 来看一下一个 advice 是如何声明的.

Before advice

/**
 * @author xiongyongshun
 * @version 1.0
 * @created 16/9/9 13:13
 */
@Component
@Aspect
public class BeforeAspectTest {
    // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
    @Pointcut("execution(* com.xys.service.UserService.*(..))")
    public void dataAccessOperation() {
    }
}
@Component
@Aspect
public class AdviseDefine {
    // 定义 advise
    @Before("com.xys.aspect.PointcutDefine.dataAccessOperation()")
    public void doBeforeAccessCheck(JoinPoint joinPoint) {
        System.out.println("*****Before advise, method: " + joinPoint.getSignature().toShortString() + " *****");
    }
}

这里, @Before 引用了一个 pointcut, 即 "com.xys.aspect.PointcutDefine.dataAccessOperation()" 是一个 pointcut 的名字.
若是咱们在 advice 在内置 pointcut, 则能够:

@Component
@Aspect
public class AdviseDefine {
    // 将 pointcut 和 advice 同时定义
    @Before("within(com.xys.service..*)")
    public void doAccessCheck(JoinPoint joinPoint) {
        System.out.println("*****doAccessCheck, Before advise, method: " + joinPoint.getSignature().toShortString() + " *****");
    }
}

around advice

around advice 比较特别, 它能够在一个方法的以前以前和以后添加不一样的操做, 而且甚至能够决定什么时候, 如何, 是否调用匹配到的方法.

@Component
@Aspect
public class AdviseDefine {
    // 定义 advise
    @Around("com.xys.aspect.PointcutDefine.dataAccessOperation()")
    public Object doAroundAccessCheck(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 开始
        Object retVal = pjp.proceed();
        stopWatch.stop();
        // 结束
        System.out.println("invoke method: " + pjp.getSignature().getName() + ", elapsed time: " + stopWatch.getTotalTimeMillis());
        return retVal;
    }
}

around advice 和前面的 before advice 差很少, 只是咱们把注解 @Before 改成了 @Around 了.

下一小节完全征服 Spring AOP 之 实战篇

本文由 yongshun 发表于我的博客, 采用 署名-相同方式共享 3.0 中国大陆许可协议.
Email: yongshun1228@gmail .com
本文标题为: 完全征服 Spring AOP 之 实战篇
本文连接为: http://www.javashuo.com/article/p-mcrbztoz-bn.html