AOP
AOP的全称为Aspect Oriented Programming,译为面向切面编程。实际上AOP就是通过预编译和运行期动态代理实现程序功能的统一维护的一种技术。在不同的技术栈中AOP有着不同的实现,但是其作用都相差不远,我们通过AOP为既有的程序定义一个切入点,然后在切入点前后插入不同的执行内容,以达到在不修改原有代码业务逻辑的前提下统一处理一些内容(比如日志处理、分布式锁)的目的。
为什么要使用AOP
在实际的开发过程中,我们的应用程序会被分为很多层。通常来讲一个Java的Web程序会拥有以下几个层次:
- Web层:主要是暴露一些Restful API供前端调用。
- 业务层:主要是处理具体的业务逻辑。
- 数据持久层:主要负责数据库的相关操作(增删改查)。
虽然看起来每一层都做着全然不同的事情,但是实际上总会有一些类似的代码,比如日志打印和安全验证等等相关的代码。如果我们选择在每一层都独立编写这部分代码,那么久而久之代码将变的很难维护。所以我们提供了另外的一种解决方案: AOP。这样可以保证这些通用的代码被聚合在一起维护,而且我们可以灵活的选择何处需要使用这些代码。
AOP的核心概念
1.切面(Aspect):通常是一个类,在里面可以定义切入点和通知。
2.连接点(JointPoint):被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截的到的方法,实际上连接点还可以是字段或者构造器。
3.切入点(Pointcut):对连接点进行拦截的定义。
4.通知(Advice):拦截到连接点之后所要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。
5.AOP代理:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
Spring AOP
Spring中的AOP代理还是离不开Spring的IOC容器,代理的生成,管理及其依赖关系都是由IOC容器负责,Spring默认使用JDK动态代理,在需要代理类而不是代理接口的时候,Spring会自动切换为使用CGLIB代理,不过现在的项目都是面向接口编程,所以JDK动态代理相对来说用的还是多一些。在本文中,我们将以注解结合AOP的方式来分别实现Web日志处理和分布式锁。
Spring AOP相关注解
1.@Aspect
: 将一个java类定义为切面类。
2.@Pointcut
:定义一个切入点,可以是一个规则表达式,比如下例中某个package下的所有函数,也可以是一个注解等。
3.@Before
:在切入点开始处切入内容。
4.@After
:在切入点结尾处切入内容。
5.@AfterReturning
:在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)。
6.@Around
:在切入点前后切入内容,并自己控制何时执行切入点自身的内容。
7.@AfterThrowing
:用来处理当切入内容部分抛出异常之后的处理逻辑。
其中@Before
、@After
、@AfterReturning
、@Around
、@AfterThrowing
都属于通知。
AOP顺序问题
在实际情况下,我们对同一个接口做多个切面,比如日志打印、分布式锁、权限校验等等。这时候我们就会面临一个优先级的问题,这么多的切面该如何告知Spring执行顺序呢?这就需要我们定义每个切面的优先级,我们可以使用@Order(i)
注解来标识切面的优先级,i的值越小,优先级越高。假设现在我们一共有两个切面,一个WebLogAspect
,我们为其设置@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依赖
1 | <dependency> |
清单 2. 添加AOP相关依赖
1 | <dependency> |
其他准备工作
为了方便测试我还在项目中集成了Swagger文档,具体的集成方法可以参照在Spring Boot项目中使用Swagger文档。另外编写了两个接口以供测试使用,具体可以参考本文源码。由于本教程所实现的分布式锁是基于Redis缓存的,所以需要安装Redis或者准备一台Redis服务器。
利用AOP实现Web日志处理
为什么要实现Web日志统一处理
在实际的开发过程中,我们会需要将接口的出请求参数、返回数据甚至接口的消耗时间都以日志的形式打印出来以便排查问题,有些比较重要的接口甚至还需要将这些信息写入到数据库。而这部分代码相对来讲比较相似,为了提高代码的复用率,我们可以以AOP的方式将这种类似的代码封装起来。
Web日志注解
清单 3. Web日志注解代码
1 |
|
其中name
为所调用接口的名称,intoDb
则标识该条操作日志是否需要持久化存储,Spring Boot连接数据库的配置,可以参考小代学Spring Boot之数据源这篇文章,具体的数据库结构可以点击这里获取。现在注解有了,我们接下来需要编写与该注解配套的AOP切面。
实现WebLogAspect切面
1.首先我们定义了一个切面类WebLogAspect
如清单4所示。其中@Aspect
注解是告诉Spring将该类作为一个切面管理,@Component
注解是说明该类作为一个Spring组件。
清单 4. WebLogAspect
1 |
|
2.接下来我们需要定义一个切点。
清单 5. Web日志AOP切点
1 | "execution(* cn.itweknow.sbaop.controller..*.*(..))") ( |
对于execution表达式,官网的介绍为(翻译后),如清单6所示:
清单 6. 官网对execution表达式的介绍
1 | execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?) |
其中除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。这个解释可能有点难理解,下面我们通过一个具体的例子来了解一下。在WebLogAspect 中我们定义了一个切点,其execution表达式为* cn.itweknow.sbaop.controller..*.*(..)
,下表为该表达式比较通俗的解析:
标识符 | 含义 |
---|---|
execution() | 表达式的主体 |
第一个 * 符号 | 表示返回值的类型,* 代表所有返回类型 |
cn.itweknow.sbaop.controller | AOP所切的服务的包名,即需要进行横切的业务类 |
包名后面的 .. | 表示当前包及子包 |
第二个 * | 表示类名,* 表示所有类 |
最后的 .*(..) | 第一个 . 表示任何方法名,括号内为参数类型,..代表任何类型参数 |
3.@Before
修饰的方法中的内容会在进入切点之前执行,在这个部分我们需要打印一个开始执行的日志,并将请求参数和开始调用的时间存储在ThreadLocal
中,方便在后面结束调用时打印参数和计算接口耗时。
清单 7. @Before代码
1 | "webLog()&& @annotation(controllerWebLog)") (value = |
4.@AfterReturning
,当程序正常执行有正确的返回时执行,我们在这里打印结束日志,最后不能忘的是清除ThreadLocal
里的内容。
清单 8. @AfterReturning代码
1 | "webLog()&& @annotation(controllerWebLog)", returning = "res") (value = |
5.当程序发生异常时,我们也需要将异常日志打印出来
清单 9. @AfterThrowing代码
1 | "webLog()&& @annotation(controllerWebLog)", throwing = "throwable") (value = |
6.至此,我们的切面已经编写完成了。下面我们需要将ControllerWebLog注解使用在我们的测试接口上,接口内部的代码已省略,如有需要的话,请参照本文源码。
清单 10. 测试接口代码
1 | "/post-test") ( |
7.最后,启动项目,然后打开Swagger文档进行测试,调用接口后在控制台就会看到类似图1这样的日志。
图 1. Web日志AOP测试效果图
利用AOP实现分布式锁
为什么要使用分布式锁
我们程序中多多少少会有一些共享的资源或者数据,在某些时候我们需要保证同一时间只能有一个线程访问或者操作它们。在传统的单机部署的情况下,我们简单的使用Java提供的并发相关的API处理即可。但是现在大多数服务都采用分布式的部署方式,我们就需要提供一个跨进程的互斥机制来控制共享资源的访问,这种互斥机制就是我们所说的分布式锁。
注意
1.互斥性。在任时刻,只有一个客户端能持有锁。
2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。这个其实只要我们给锁加上超时时间即可。
3.具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
分布式锁注解
清单 11. 分布式锁注解
1 |
|
其中,key为分布式所的key值,timeout为锁的超时时间,默认为5,timeUnit为超时时间的单位,默认为秒。
注解参数解析器
由于注解属性在指定的时候只能为常量,我们无法直接使用方法的参数。而在绝大多数的情况下分布式锁的key值是需要包含方法的一个或者多个参数的,这就需要我们将这些参数的位置以某种特殊的字符串表示出来,然后通过参数解析器去动态的解析出来这些参数具体的值,然后拼接到key上。在本教程中我也编写了一个参数解析器AnnotationResolver
。篇幅原因,其源码就不直接粘在文中,需要的读者可以查看源码。
获取锁方法
清单 12. 获取锁
1 | private String getLock(String key, long timeout, TimeUnit timeUnit) { |
RedisStringCommands.SetOption.SET_IF_ABSENT
实际上是使用了setNX
命令,如果key已经存在的话则不进行任何操作返回失败,如果key不存在的话则保存key并返回成功,该命令在成功的时候返回1,结束的时候返回0。我们随机产生了一个value并且在获取锁成功的时候返回出去,是为了在释放锁的时候对该值进行比较,这样可以做到解铃还须系铃人,由谁创建的锁就由谁释放。同时还指定了超时时间,这样可以保证锁释放失败的情况下不会造成接口永远不能访问。
释放锁方法
清单 13. 释放锁
1 | private void unLock(String key, String value) { |
切面
切点和Web日志处理的切点一样,这里不再赘述。我们在切面中使用的通知类型为@Around,在切点之前我们先尝试获取锁,若获取锁失败则直接返回错误信息,若获取锁成功则执行方法体,当方法结束后(无论是正常结束还是异常终止)释放锁。
清单 14. 环绕通知
1 | "distribute()&& @annotation(distributeLock)") (value = |
测试
清单 15. 分布式锁测试代码
1 | "/post-test") ( |
在本次测试中我们将锁的超时时间设置为10秒钟,在接口中让当前线程睡眠10秒,这样可以保证10秒钟之内锁不会被释放掉,测试起来更加容易些。启动项目后,我们快速访问两次该接口,注意两次请求的channel传值需要一致(因为锁的key中包含该值),会发现第二次访问时返回如下结果:
图 2. 基于Redis的分布式锁测试效果
这就说明我们的分布式锁已经生效。
结束语
在本教程中,我们主要了解了AOP编程以及为什么要使用AOP。也介绍了如何在Spring Boot项目中利用AOP实现Web日志统一处理和基于Redis的分布式锁。你可以在Github上找到本教程的完整实现,如果你想对本教程做补充的话欢迎发邮件(gancy.programmer@gmail.com)给我或者直接在Github上提交Pull Reqeust。当然如果你觉得本篇文章还不错的话,顺手给个star,这是对我最好的鼓励。
转载说明:
“最初由 IBM developerWorks 中国网站发表,其网址是https://www.ibm.com/developerworks/cn/java/j-spring-boot-aop-web-log-processing-and-distributed-locking/index.html
。(注:IBM developerWorks 现已更名为 IBM Developer,> 其网址是 https://developer.ibm.com/cn 。)
ps:“学习不止,码不停蹄”,如果你喜欢我的文章,就关注我吧。
