AOP
AOP 的全称为 Aspect Oriented Programming,译为面向切面编程。实际上 AOP 就是通过预编译和运行期动态代理实现程序功能的统一维护的一种技术。在不同的技术栈中 AOP 有着不同的实现,但是其作用都相差不远,我们通过 AOP 为既有的程序定义一个切入点,然后在切入点前后插入不同的执行内容,以达到在不修改原有代码业务逻辑的前提下统一处理一些内容(比如日志处理、分布式锁)的目的。
为什么要使用 AOP
在实际的开发过程中,我们的应用程序会被分为很多层。通常来讲一个 Java 的 Web 程序会拥有以下几个层次:
- Web 层:主要是暴露一些 Restful API 供前端调用。
- 业务层:主要是处理具体的业务逻辑。
- 数据持久层:主要负责数据库的相关操作(增删改查)。
虽然看起来每一层都做着全然不同的事情,但是实际上总会有一些类似的代码,比如日志打印和安全验证等等相关的代码。如果我们选择在每一层都独立编写这部分代码,那么久而久之代码将变的很难维护。所以我们提供了另外的一种解决方案: AOP。这样可以保证这些通用的代码被聚合在一起维护,而且我们可以灵活的选择何处需要使用这些代码。
AOP 的核心概念
- 切面(Aspect) :通常是一个类,在里面可以定义切入点和通知。
- 连接点(Joint Point) :被拦截到的点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点指的就是被拦截的到的方法,实际上连接点还可以是字段或者构造器。
- 切入点(Pointcut) :对连接点进行拦截的定义。
- 通知(Advice) :拦截到连接点之后所要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。
- AOP 代理 :AOP 框架创建的对象,代理就是目标对象的加强。Spring 中的 AOP 代理可以使 JDK 动态代理,也可以是 CGLIB 代理,前者基于接口,后者基于子类。
Spring AOP
Spring 中的 AOP 代理还是离不开 Spring 的 IOC 容器,代理的生成,管理及其依赖关系都是由 IOC 容器负责,Spring 默认使用 JDK 动态代理,在需要代理类而不是代理接口的时候,Spring 会自动切换为使用 CGLIB 代理,不过现在的项目都是面向接口编程,所以 JDK 动态代理相对来说用的还是多一些。在本文中,我们将以注解结合 AOP 的方式来分别实现 Web 日志处理和分布式锁。
Spring AOP 相关注解
@Aspect@Pointcutpackage@Before@After@AfterReturning@Around@AfterThrowing@Before@After@AfterReturning@Around@AfterThrowingAOP 顺序问题
@Order(i)iWebLogAspect@Order(100)DistributeLockAspect@Order(99)DistributeLockAspect@Before@Order(99)@Order(100)@After@AfterReturning@Order(100)@Order(99)基于注解的 AOP 配置
使用注解一方面可以减少我们的配置,另一方面注解在编译期间就可以验证正确性,查错相对比较容易,而且配置起来也相当方便。相信大家也都有所了解,我们现在的 Spring 项目里面使用了非常多的注解替代了之前的 xml 配置。而将注解与 AOP 配合使用也是我们最常用的方式,在本文中我们将以这种模式实现 Web 日志统一处理和分布式锁两个注解。下面就让我们从准备工作开始吧。
准备工作
准备一个 Spring Boot 的 Web 项目
你可以通过 Spring Initializr 页面 生成一个空的 Spring Boot 项目,当然也可以下载 springboot-pom.xml 文件 ,然后使用 maven 构建一个 Spring Boot 项目。项目创建完成后,为了方便后面代码的编写你可以将其导入到你喜欢的 IDE 中,我这里选择了 Intelli IDEA 打开。
添加依赖
我们需要添加 Web 依赖和 AOP 相关依赖,只需要在 pom.xml 中添加如下内容即可:
清单 1. 添加 web 依赖
显示更多
清单 2. 添加 AOP 相关依赖
显示更多
其他准备工作
为了方便测试我还在项目中集成了 Swagger 文档,具体的集成方法可以参照 在 Spring Boot 项目中使用 Swagger 文档 。另外编写了两个接口以供测试使用,具体可以参考 本文源码 。由于本教程所实现的分布式锁是基于 Redis 缓存的,所以需要安装 Redis 或者准备一台 Redis 服务器。
利用 AOP 实现 Web 日志处理
为什么要实现 Web 日志统一处理
在实际的开发过程中,我们会需要将接口的出请求参数、返回数据甚至接口的消耗时间都以日志的形式打印出来以便排查问题,有些比较重要的接口甚至还需要将这些信息写入到数据库。而这部分代码相对来讲比较相似,为了提高代码的复用率,我们可以以 AOP 的方式将这种类似的代码封装起来。
Web 日志注解
清单 3. Web 日志注解代码
显示更多
nameintoDb实现 WebLogAspect 切面
第 1 步,我们定义了一个切面类 WebLogAspect 如清单 4 所示。其中@Aspect 注解是告诉 Spring 将该类作为一个切面管理,@Component 注解是说明该类作为一个 Spring 组件。
清单 4. WebLogAspect
显示更多
第 2 步,接下来我们需要定义一个切点。
清单 5. Web 日志 AOP 切点
显示较少
对于 execution 表达式, 官网 的介绍为(翻译后):
清单 6. 官网对 execution 表达式的介绍
显示更多
WebLogAspectexecution* cn.itweknow.sbaop.controller..*.*(..)execution()execution()**cn.itweknow.sbaop.controller..**.*(..)...@BeforeThreadLocal清单 7. @Before 代码
显示更多
@AfterReturningThreadLocal清单 8. @AfterReturning 代码
显示更多
第 5 步,当程序发生异常时,我们也需要将异常日志打印出来:
清单 9. @AfterThrowing 代码
显示更多
ControllerWebLog清单 10. 测试接口代码
显示更多
第 7 步,最后,启动项目,然后打开 Swagger 文档进行测试,调用接口后在控制台就会看到类似图 1 这样的日志。
图 1. 基于 Redis 的分布式锁测试效果
利用 AOP 实现分布式锁
为什么要使用分布式锁
我们程序中多多少少会有一些共享的资源或者数据,在某些时候我们需要保证同一时间只能有一个线程访问或者操作它们。在传统的单机部署的情况下,我们简单的使用 Java 提供的并发相关的 API 处理即可。但是现在大多数服务都采用分布式的部署方式,我们就需要提供一个跨进程的互斥机制来控制共享资源的访问,这种互斥机制就是我们所说的分布式锁。
注意
- 互斥性。在任时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。这个其实只要我们给锁加上超时时间即可。
- 具有容错性。只要大部分的 Redis 节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
分布式锁注解
清单 11. 分布式锁注解
显示更多
keykeytimeouttimeUnit注解参数解析器
keyAnnotationResolver获取锁方法
清单 12. 获取锁
显示更多
RedisStringCommands.SetOption.SET_IF_ABSENTsetNXkeykeykeyvalue释放锁方法
清单 13. 释放锁
显示更多
切面
@Around清单 14. 环绕通知
显示更多
测试
清单 15. 分布式锁测试代码
显示更多
101010channelkey图 2. 基于 Redis 的分布式锁测试效果
这就说明我们的分布式锁已经生效。
结束语
在本教程中,我们主要了解了 AOP 编程以及为什么要使用 AOP。也介绍了如何在 Spring Boot 项目中利用 AOP 实现 Web 日志统一处理和基于 Redis 的分布式锁。