关于java:9000-字彻底征服-Spring-AOP

37次阅读

共计 9439 个字符,预计需要花费 24 分钟才能阅读完成。

基本知识

其实,接触了这么久的 AOP,我感觉,AOP 给人难以了解的一个关键点是它的概念比拟多,而且坑爹的是,这些概念通过了中文翻译后,变得面目全非,雷同的一个术语,在不同的翻译下,含意总有着各种莫名其妙的差异. 鉴于此,我在本章的结尾,着重为为大家介绍一个 Spring AOP 的各项术语的根本含意。

为了术语传播的准确性,我在接下来的叙述中,能应用英文术语的中央,尽量应用英文。

什么是 AOP

AOP(Aspect-Oriented Programming),即 面向切面编程,它与 OOP(Object-Oriented Programming,面向对象编程) 相辅相成,提供了与 OOP 不同的形象软件结构的视角。

在 OOP 中,咱们以类 (class) 作为咱们的根本单元,而 AOP 中的根本单元是 Aspect(切面)

术语

Aspect(切面)

aspectpointcountadvice 组成,它既蕴含了横切逻辑的定义,也包含了连接点的定义. Spring AOP 就是负责施行切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。

AOP 的工作重心在于如何将加强织入指标对象的连接点上,这里蕴含两个工作:

  1. 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
  2. 如何在 advice 中编写切面代码.

能够简略地认为,应用 @Aspect 注解的类就是切面。

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 总是办法的执行点,即只有办法连接点。

切点(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 操作。

另外,关注公众号 Java 技术栈,在后盾回复:面试,能够获取我整顿的 Spring 系列面试题和答案,十分齐全。

@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 了。

最初,关注公众号 Java 技术栈,在后盾回复:面试,能够获取我整顿的 Spring 系列面试题和答案,十分齐全。

作者:永顺 \
起源:segmentfault.com/a/1190000007469968

近期热文举荐:

1.600+ 道 Java 面试题及答案整顿(2021 最新版)

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!

4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0