Spring Boot AOP 学习实践:请求日志记录和注解式分布式锁
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Spring Boot AOP 学习实践:请求日志记录和注解式分布式锁,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含9135字,纯文字阅读大概需要14分钟。
内容图文
AOP 概念
AOP 是一种可以通过预编译方式和运行期动态代理方式实现在不修改已有源代码的情况下给程序动态添加统一功能的技术,它的全称是 Aspect Oriented Programming,中文被译为面向切面编程。AOP 可以看作是面向对象编程的一种补充,也可以看作是对设计模式的更高级别抽象,它被广泛应用于处理一些具有横切性质的系统级服务,比如日志记录、性能统计、安全检查、异常处理、事务管理,等等。AOP 技术的关键在于为现有的类生成代理类,以达到在不修改源码的情况下增强原有类对象功能的目的,AOP 的代理技术可以分为两大类:静态代理和动态代理。
静态代理 vs 动态代理
其中,静态代理是指利用 AOP 框架提供的命令进行编译,从而在代码编译阶段就可以生成 AOP 代理类,这种方式就是所谓的预编译方式,也称为编译时增强,常见的这类框架如 AspectJ;而动态代理则是借助于 JDK 动态代理、CGLib 动态代理等技术在代码运行时在内存中临时生成 AOP 动态代理类,也称为运行时增强,Spring 框架实现 AOP 就是用了这种技术。
对比二者区别,一般来说,静态代理技术在性能上更有优势,因为代码运行之前已经编译生成了代理类,但是这也要求依托于特定的框架和编译方式来实现;而动态代理技术需要每次运行时都进行动态代理类的生成,代理类要么与目标类实现相同的接口,要么是目标类的子类,这样代理类的实例就可以作为目标类的实例来使用。在 Spring AOP 中,主要使用了 JDK 动态代理和 CGLib 动态代理。
JDK 动态代理
在 JDK 类库中,java.util.reflect.Proxy
类是用来实现动态代理的顶层类,可以通过 Proxy
类的静态方法 Proxy.newProxyInstance()
动态创建一个类的代理类并返回实例化对象,由此创建的代理类都是 Proxy
的子类。
JDK 动态代理实现步骤
- 创建目标类(被代理类)的接口。
public interface Target {
void test();
}
- 创建目标类(被代理类)的具体实现类。
public class TargetImpl implements Target {
@Override
public void test() {
System.out.println("----- 模拟 Web 请求执行,此处停歇 300 毫秒 -----");
try {
TimeUnit.MILLISECONDS.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 创建代理类实现
InvocationHandler
接口,并持有目标类对象引用,然后在invoke
方法中利用反射调用目标对象方法。
public class TargetProxy implements InvocationHandler {
private final Target target;
public TargetProxy(Target target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 模拟前置增强
System.out.println("开始记录请求日志(有没有点 AOP @Before 操作的感觉)");
long startTime = System.currentTimeMillis();
// 反射调用目标对象的方法
Object targetResult = method.invoke(target, args);
// 模拟后置增强
long time = System.currentTimeMillis() - startTime;
System.out.println("结束请求日志记录,本次请求共耗时 " + time + " ms(有没有点 AOP @After 操作的感觉)");
return targetResult;
}
}
- 利用
Proxy.newProxyInstance()
方法创建代理对象,利用代理对象实现对目标对象的方法调用。
public class JdkDynamicProxyTest {
public static void main(String[] args) {
// 新建目标类对象
Target target = new TargetImpl();
// 目标类的类加载器
ClassLoader loader = target.getClass().getClassLoader();
// 目标类实现的所有接口
Class<?>[] interfaces = target.getClass().getInterfaces();
// 新建代理类对象
TargetProxy targetProxy = new TargetProxy(target);
// 生成最终的代理对象
Target proxyInstance = (Target) Proxy.newProxyInstance(loader, interfaces, targetProxy);
// 用最终生成的代理对象调用目标对象的方法
proxyInstance.test();
}
}
- 最终输出
开始记录请求日志(有没有点 AOP @Before 操作的感觉)
----- 模拟 Web 请求执行,此处停歇 300 毫秒 -----
结束请求日志记录,本次请求共耗时 300 ms(有没有点 AOP @After 操作的感觉)
CGLib 动态代理
CGLib 是一个高性能代码生成类库,底层通过 ASM 字节码框架生成类的字节码,与 JDK 动态代理不同,CGLib 不会要求目标类的接口一定存在,因为它是通过继承父类的方式实现代理类的。这样的话,代理子类可以调用父类里面的方法,如果想要实现目标类增强,则可以创建一个方法拦截器并实现 CGLib 的 MethodInterceptor
接口,重写 intercept()
方法,从而达到 AOP 的效果。
关于 CGLib 动态代理的实例模拟,这里就不过多展开了。在 Spring 框架内部,框架会自动选择实现动态代理的方式,如果目标类不存在对应的接口,那么就使用 CGLib 实现动态代理,否则就使用 JDK 类库实现动态代理。
二者有何区别
通过总结,JDK 动态代理和 CGLib 主要有以下几点区别:
- 字节码创建方式:JDK 动态代理依托于 JVM 实现代理类字节码的创建,CGLib 通过 ASM 框架创建字节码。
- JDK 动态代理必须要求目标类实现了某个接口,CGLib 则要求目标类不能被
final
修饰,因为final
类无法被继承。 - CGLib 不能对目标类声明为
final
的方法进行代理,因为类继承时子类无法操作到父类的final
方法。
Spring AOP 核心概念
- 切面(Aspect):通常是一个类,在这个类里可以定义切入点和通知,Spring 对应的注解为
@Aspect
。 - 切入点(Pointcut):定义对什么样的连接点进行拦截,Spring 对应的注解为
@Pointcut
。 - 连接点(JoinPoint):被拦截到的点,Spring 只支持方法类型的连接点,所以在 Spring 中连接点就是指被拦截到的方法。
- 通知(Advice):拦截到连接点之后需要执行的增强代码,共有前置通知、后置通知、环绕通知、最终通知、异常通知五类。
Spring AOP 几大通知注解
- @Before 前置通知:在连接点方法执行之前需要执行的增强操作。
- @After 后置通知:在连接点方法执行之后需要执行的增强操作。
- @Around 环绕通知:在连接点前后执行增强操作,并且由开发者自己决定何时执行连接点内容。
- @AfterReturning 最终通知:在连接点
return
之后执行增强操作,可以用来对返回值做加工处理。 - @AfterThrowing 异常通知:当连接点内容执行过程中抛出异常之后的处理逻辑。
Spring AOP 结合注解案例
近几年来,Spring Boot 框架在 Spring MVC 框架的基础上做了很大的改进,使用 Java Config 实现基于代码的配置,结合 Spring 框架本身强大的注解支持,替代了过去繁琐的 XML 文件配置,极大地提高了开发效率。而将注解与 AOP 配合使用,也成了日常开发中我们常用的方式,下面就以两个实际案例来展示一下 Spring AOP 以及 AOP 结合注解的使用方式。
案例 1:请求日志记录
案例目的:在基于 Spring Boot 框架的 Web 项目中,对每一次后端接口的请求,程序都需要将请求的链接、参数、请求方式等信息记录到日志中,当接口请求完毕后,还需要将响应的结果、响应耗时等信息记录到日志中。
我们假设关于项目的其他准备工作已经就绪,那么继续往后的第一步,就是先添加 Spring Boot AOP 依赖,需要在项目的 pom.xml
文件中添加如下内容:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
接下来,我们需要创建日志记录的 AOP 切面类 RequestLogAspect
,并注解为 @Aspect
切面以及 Spring IOC 组件类。
@Aspect
@Component
public class RequestLogAspect {
}
接下来,我们需要定义一个切点。
@Aspect
@Component
public class RequestLogAspect {
@Pointcut("execution(public * com.gitee.springboot.practice.log..*Controller.*(..))")
public void traceRequestLog() {}
}
对于 execution
表达式,每一部分都有对应的语法要求,具体解释如下:
execution() - 表达式主体
public - 方法修饰符(AOP 只能作用于 public 方法)
第一个 * 号 - 表示方法的返回值类型,* 代表任意类型
com.gitee.springboot.practice.log - 切面所切的包名
包名后面的 .. - 表示当前包及其子包
第二个 * 号 - 表示类名,* 代表所有类(此处的 *Controller 代表所有以 Controller 结尾的类)
最后的 .*(..) - 最前面 .* 表示任意方法名,括号内表示参数类型,.. 代表所有类型
接下来,根据案例目的,我们需要配置进入切点之前的前置操作以及切点执行完 return
之后的后置操作,以此来追踪请求日志。
@Slf4j
@Aspect
@Component
public class RequestLogAspect {
private final ThreadLocal<Long> startTime = new ThreadLocal<>();
@Pointcut("execution(public * com.gitee.springboot.practice.log..*Controller.*(..))")
public void traceRequestLog() {}
@Before("traceRequestLog()")
public void handleBefore(JoinPoint point) {
startTime.set(System.currentTimeMillis());
StringBuilder logs = new StringBuilder("=============== New Web Request ===============\n");
RequestAttributes requestAttrs = RequestContextHolder.currentRequestAttributes();
ServletRequestAttributes servletRequestAttrs = (ServletRequestAttributes) requestAttrs;
HttpServletRequest request = servletRequestAttrs.getRequest();
String url = String.format("%s %s %s %s, with query string : %s\n", request.getRemoteAddr(),
request.getProtocol(), request.getMethod(), request.getRequestURL(), request.getQueryString()
);
String method = "api method = " + point.getSignature().toLongString();
logs.append(url).append(method);
log.info(logs.toString());
}
@AfterReturning(value = "traceRequestLog()", returning = "result")
public void handleAfterReturning(Object result) {
final long time = System.currentTimeMillis() - startTime.get();
log.info("Web Response Returned [ {} ] in {} ms", result, time);
startTime.remove();
}
}
尝试访问对应的请求接口,示例代码会输出包含以下内容的日志:
[INFO] =============== New Web Request ===============
[INFO] 192.168.1.2 HTTP/1.1 GET http://192.168.1.2:8080/aop/request/log/get, with query string : param=test&test=demo
[INFO] api method = public java.lang.String com.gitee.springboot.practice.log.RequestLogTestController.testAopRequestLog()
[INFO] Web Response Returned [ Hello Spring AOP ] in 2044 ms
上述案例的全部代码已经上传至我个人的代码仓库,欢迎 点击查阅全部代码
案例 2:注解式分布式锁
案例目的:对于想要实现分布式锁的接口或方法,只需要加上对应的注解就能带有分布式锁功能。
与上一个案例类似,我们需要先添加好 AOP 依赖,并创建好 AOP 切面类,本案例的切面类如下所示:
@Slf4j
@Aspect
@Component
public class RedisLockAspect {
}
接下来,需要定义一个切点。
@Slf4j
@Aspect
@Component
public class RedisLockAspect {
@Pointcut("execution(public * com.gitee.springboot.practice..*.*(..))")
public void redisLock() {}
}
然后,需要定义一个 AOP 增强通知,这里需要使用 @Around
通知,因为需要开发者灵活地控制何时执行连接点处的操作。
@Slf4j
@Aspect
@Component
public class RedisLockAspect {
@Autowired
private RedisLockService redisLock;
@Pointcut("execution(public * com.gitee.springboot.practice..*.*(..))")
public void redisLock() {}
/**
* 定义环绕通知,在切入点前后切入内容,并自己控制何时执行切入点自身的内容
*
* @param point 连接点
* @param lock RedisLock 注解 {@link RedisLock}
*/
@Around("redisLock() && @annotation(lock)")
public void handleAround(ProceedingJoinPoint point, RedisLock lock) {
if (redisLock.tryLock(lock.value(), lock.await(), lock.interval(), lock.timeout())) {
try {
// 执行连接点处的操作
point.proceed();
} catch (Throwable throwable) {
log.error("系统异常 {}", throwable.toString());
} finally {
log.info("{} 执行完毕,即将释放锁", Thread.currentThread().getName());
redisLock.releaseLock(lock.value());
}
}
}
}
温馨提示:代码中已经预先实现了
RedisLockService
分布式锁,限于篇幅,本篇文章只讲述 AOP 相关的技术点。如果你感兴趣,可以在我的个人代码仓库 查阅全部注解式分布式锁实现代码。
以上的切面类当中还有一个不同,就是在 handleAround
方法中绑定了一个 RedisLock
参数,这个参数就是需要新增实现的注解类,具体关于这个注解类的内容,可以在上面我给出的全部实现代码中查阅,同时请注意上述 @Around
需要一并绑定上注解参数。
有了上述的基础之后,就可以在接口或其它需要用到 Redis 分布式锁的地方直接加上对应的注解就可以了,以下是一个使用示例:
@GetMapping("/lock/annotation")
@RedisLock(value = "annotation-based", await = 5000)
public void testRedisLockAnnotation() {
// 模拟业务操作
try {
TimeUnit.MILLISECONDS.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
AOP、过滤器、拦截器
按照上面的总结和描述,也让我不禁想到开发中另外的两个常用手段:过滤器和拦截器。在实现某些公共业务逻辑的时候,过滤器、拦截器、AOP 都可以实现,且都可以做到不修改既有代码,那么它们三者的区别是什么,以及又分别适合什么样的开发场景呢?
过滤器
首先看过滤器,过滤器拦截的是请求的 URL,如果业务中实现了对某些请求 URL 的过滤,过滤器将是拦截它们的第一道关卡。过滤器并没有定义某个业务逻辑执行的前置操作和后置操作,它只能是以请求为单位,请求一到达就判断是否需要执行过滤操作。同时,过滤器方法带有 request、response
对象作为参数,因此可以对请求参数和响应数据做修改和处理,然后再对请求放行;过滤器方法的返回值是 void
,这意味着它不能直接返回某些内容。另外,过滤器是依赖于 Servlet 规范的,并不依赖于其它开发框架。
拦截器
其次是拦截器,拦截器拦截的也是请求的 URL,但是相对于过滤器,拦截器可以对请求做更加细致的处理。Spring 的拦截器提供了三个方法:preHandle、postHandle、afterCompletion
,分别用来处理:请求执行之前的操作,请求执行之后但是还未渲染 ModelAndView
的操作,请求执行结束并且 ModelAndView
渲染也已经结束后的操作。另外,拦截器是依赖于 Spring 框架的,并不是 Servlet 规范的内容。
AOP
AOP 拦截的是类的元数据(包、类、方法、参数等),相对于拦截器,其处理又可以更加细致,而且相当灵活,不完全局限于对请求接口的拦截,而是针对具体的代码,能够在此基础上实现更加复杂的业务逻辑。
结束语
第一次这么细致地写一篇技术文章,为了能加深自己的印象,也希望能够帮助到阅读到此处的你。文章主要从 AOP 技术出发,整理了实现 AOP 技术的底层原理,以及 Spring 框架如何使用 AOP 功能集成,最后以两个案例梳理了不带注解和带注解的 AOP 功能在 Spring Boot 框架中的实际使用,并对比了过滤器、拦截器、AOP 三者的简单区别。
原文:https://www.cnblogs.com/codeboyzhou/p/springboot-aop-study-and-practice.html
内容总结
以上是互联网集市为您收集整理的Spring Boot AOP 学习实践:请求日志记录和注解式分布式锁全部内容,希望文章能够帮你解决Spring Boot AOP 学习实践:请求日志记录和注解式分布式锁所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。