之前老四写了关于 Java 反射的一些浅析知识文章,详情可戳《浅析Java反射系列相关基础知识(上)之类的加载以及反射的基本应用》、《 浅析Java反射系列相关进阶知识(下)之JDK动态代理及反射泛型》查看,在后一篇文章中就提及到 JDK(Java Development Kit,Java 语言的软件开发工具包) 动态代理相关的知识,这也是在 Spring AOP(Aspect Oriented Programming,面向切面编程) 中比较重要的知识点,今天我们就一起回顾一下 Spring AOP 的基本使用方法,最后简单的分析 Spring AOP 的源码实现,让我们对Spring AOP的底层实现有一个更清晰的了解。
先来了解一下 Spring AOP 中的一些概念,或者说面向切面编程的一些概念,所谓你得先会用才能再去了解人家是怎么实现的,如果会都不会用,底层实现自然而然看起来也就更吃力了。Spring AOP 其实提供了四种类型的 AOP 支持:
- 基于代理的经典 Spring AOP
- 纯 POJO(Plain Ordinary Java Object,简单的 Java 对象) 切面
- @AspectJ 注解驱动的切面
- 注入式 AspectJ 切面
鉴于现在 Spring Boot 越来越被广泛的使用,而 Spring Boot 是潜在的要求注解的形式进行编程,所以老四也主要使用 @AspectJ 注解来进行 AOP 基础知识浅析,基本放弃以前经常使用的 xml(eXtensible Markup Language,可扩展标记语言) 配置的方式的讲解,后面可能会基本给一个示例。
AOP 的概念是什么?为什么要使用面向切面编程?
这里老四就不那么多的废话了,AOP 其实就是按照一定的规则,将代码织入实现约定的流程当中。目的当然就是解耦、去重、服务增强。
AOP 中一些重要的概念及术语
- 连接点(join point):在 Spring AOP 中通俗点指的就是具体被拦截,你要切入的方法。因为在 Spring AOP 中只支持方法的切入,是一种基于方法的 AOP,通过动态代理技术把它织入对应的流程中。
- 通知(advice):所谓「通知」就是按照规定将业务代码织入到连接点的前后了。通知的几种类型如下:
前置通知(before advice):在目标方法被调用之前调用通知功能
后置通知(after advice):在目标方法完成之后调用通知,是不管方法是否遇到异常都会执行的。
环绕通知(around advice):慎用!跟之前老四浅析的动态代理十分相似,环绕通知就是拿到目标方法,然后在目标方法可以自定义一些行为,然后在 Spring AOP 中通过叫 proceed() 的方法对目标方法进行主调实现环绕形式的通知。
事后返回通知(after-returning advice):在目标方法执行成功之后调用通知,这里就是要求方法必须正常执行成功才会执行,遇到异常通知失效。
异常通知(after-throwing advice):顾名思义,抛出异常后调用通知。 - 切点(point cut):说白了就是正则表达式描述的某个方法,将这个方法抽象描述一下,避免代码重复。例如要在某个类的 HelloGlorze() 进行切入前置、后置等通知,在每个通知上面写同样的方法全限定路径是糟糕的行为。当然,我们的切面很多时候不单单应用于单个方法,它可能是多个类的不同方法,所以切点就是提供一种通过正则表达式以及指示器来定义和适配连接点的功能。
列举一些常用的指示器:
arg()/@args():限制连接点匹配参数为(指定类型、指定注解标注)的执行方法。
execution():用于匹配连接点的执行方法
this():限制连接点匹配 AOP 代理 Bean 引用为指定的类型
target/@target():目标对象/限制目标对象配置指定的注解
within/@within:限制连接点匹配 指定/注解 类型
@annotation:限定带有指定注解的连接点 - 目标对象(target):就是被代理的对象,就是目标方法所属类的实例。
- 引入(introduction):说白一点就是引入新的类和方法,来增强对目标方法的补充。
- 织入(weaving):这个其实就是所谓的「动态代理」技术,为原有的对象生成代理对象,然后根据切点拦截连接点,然后按照约定将引入的方法织入到流程当中。
我们知道 Spring AOP 是在运行期进行切面织入的,即在应用运行的某个时刻,AOP 容器为目标对象动态的创建一个代理对象,从而进行流程的织入,因此也解释了只能切入方法的原因。除此之外「切面编程」在目标对象的生命周期里还有两个时期进行织入:
编译器时期切入:顾名思义,切面会在目标类编译时期被织入,独立的 AspectJ 切面框架就支持这种织入方式。
类加载器时期切入:此时期切面会在目标类加载到 JVM(Java Virtual Machine,Java虚拟机)时被织入,当然这种方式是需要特定的类加载器才可以的,同样独立的 AspectJ 框架也支持这种方式织入切面 - 切面(Aspect):可以理解为上述切点、各类通知以及引入的定义集,他们集体定义了面向切面编程。在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern),以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。所以通知和切点是切面的最基本元秦。
老四这里使用给出一个简单的切面示例,供大家简单的复习巩固一下基于注解创建切面的使用方式。
定义一个简单的接口,声明一个 helloGlorze 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package com.glorze.aspect; /** * 切面测试目标接口 * @ClassName Glorze * @author: 高老四 * @since: 2019年1月25日 下午2:28:46 */ public interface GlorzeService { /** * 目标方法 * @Title: helloGlorze * @param name 名字 * @return String * @author: glorze.com * @since: 2019年1月25日 下午2:53:36 */ public String helloGlorze(String name); } |
实现 Glorze 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package com.glorze.aspect; import org.springframework.stereotype.Service; /** * 目标实现类 * @ClassName GlorzeImpl * @author: glorze.com * @since: 2019年1月25日 下午2:54:27 */ @Service public class GlorzeServiceImpl implements GlorzeService { @Override public String helloGlorze(String name) { System.out.println("您好: " + name); return "Glorze"; } } |
我们就以 helloGlorze 方法为连接点,接下来开发切面。
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 |
package com.glorze.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; /** * 切面类 * @ClassName GlorzeAspect * @author: glorze.com * @since: 2019年1月28日 下午10:14:17 */ @Aspect public class GlorzeAspect { @Pointcut("execution(* com.glorze.aspect.GlorzeServiceImpl.helloGlorze(..))") public void pointcut() {} @Before("pointcut()") public void before() { System.out.println("====helloGlorze方法之前输出===="); } @After("pointcut()") public void after() { System.out.println("====helloGlorze方法完成之后输出===="); } @AfterReturning("pointcut()") public void afterReturning() { System.out.println("====helloGlorze方法执行成功之后输出===="); } @AfterThrowing("pointcut()") public void afterThrowing() { System.out.println("====helloGlorze方法抛出异常之后输出===="); } @Around("pointcut()") public void around(ProceedingJoinPoint jp) throws Throwable { System.out.println("主调 helloGlorze 方法之前...."); // 回调目标对象的原有方法,即 helloGlorze 方法 jp.proceed(); System.out.println("回调之后输出这句话...."); } } |
测试一下:
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 |
package com.glorze.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import com.glorze.aspect.GlorzeService; /** * 切面控制器 * @ClassName GlorzeController * @author: glorze.com * @since: 2019年1月28日 下午10:41:48 */ @Controller public class GlorzeController { @Autowired private GlorzeService glorzeService; @RequestMapping("/index") public String index() { glorzeService.helloGlorze("高老四"); return null; } } |
引入
重点说一下 AOP 中的这个「引入」,前面提及到引入就是「引入新的类和方法,来增强对目标方法的补充」。假设,上面的 GlorzeService 接口是不开源的类,但是此时我们需要一个「请坐」方法,这种情景在开发中也很常见,所以引入就为我们解决这样的问题。通过引入,我们可以增强接口的功能,通过自定义的服务或者第三方服务来实现对主服务的业务进行补充和增强。
我们再定义一个 GlorzeEnhance 接口,里面声明 sitDown 抽象方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package com.glorze.aspect; /** * Spring AOP之引入 * @ClassName GlorzeEnhance * @author: glorze.com * @since: 2019年1月28日 下午10:54:03 */ public interface GlorzeEnhance { /** * 待客之道之请坐方法 * @Title: sitDown * @param name * @return void * @author: 高老四博客 * @since: 2019年1月28日 下午10:54:39 */ public void sitDown(String name); } |
接着在切面中引入新的 GlorzeEnhanceImpl 类来增强原有的 GlorzeService 服务。
1 2 |
@DeclareParents(value = "com.glorze.aspect.GlorzeServiceImpl", defaultImpl = GlorzeEnhanceImpl.class) public GlorzeEnhance glorzeEnhance; |
测试如下:
1 2 3 4 5 6 7 |
@RequestMapping("/index") public String index() { glorzeService.helloGlorze("高老四"); GlorzeEnhance GlorzeEnhance = (GlorzeEnhance)glorzeService; GlorzeEnhance.sitDown("四哥"); return null; } |
先于篇幅原因,Spring AOP源码浅析下一篇文章在发布吧。
更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请捐赠盒烟钱,点我去赞助。或者扫描文章下面的微信/支付宝二维码打赏任意金额(点击「给你买杜蕾斯」),也可扫描小站放的支付宝领红包二维码,线下支付享受优惠的同时老四也可以获得对应赏金,老四这里抱拳谢谢诸位了。捐赠时请备注姓名或者昵称,因为您的署名会出现在赞赏列表页面,您的捐赠钱财也会被用于小站的服务器运维上面,再次抱拳感谢。