AOP
Spring的两大核心之一的AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。是一种通过动态代理实现程序功能扩展和统一维护的一种技术。
本篇介绍通过使用AOP,在进行接口调用时根据其访问的控制层方法打印对应的请求日志,例如:调用接口,客户端浏览器型号,电脑系统型号, 方法执行时间,请求参数等等。
AOP的相关术语
- 横切关注点
从每个方法中抽取出来的同一类非核心业务。 - 切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。 - 通知(Advice)
切面必须要完成的各个具体工作 - 目标(Target)
被通知的对象 - 代理(Proxy)
向目标对象应用通知之后创建的代理对象 - 连接点(Joinpoint)
横切关注点在程序代码中的具体位置 - 切入点(pointcut)
定位连接点的方式
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Aop组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 分析客户端信息的工具类-->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.20</version>
</dependency>
<!--lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
AOP的实现
AspectJ是实现了AOP思想的最流行的AOP框架,本文将使用AspectJ注解的方式实现AOP。
- 将目标对象和切面类标识为IOC的组件,并在切面类上通过@Aspect注解将其标识为一个切面组件
- 在切面类中,将抽取的非核心业务代码(横切关注点)封装为一个通知方法,通过切入点表达式作用于抽取横切关注点的位置,即连接点
上代码
自定义切面类
package com.wk.aop;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* @description: 日志切面 打印请求日志
* @author: wk
* @create: 2022-03-17 20:14
*/
@Slf4j // lombok中日志注解
@Aspect // 表明是一个切面类
@Component
public class LogAspect {
/**
* 进入方法时间戳
*/
private Long startTime;
/**
* 方法结束时间戳(计时)
*/
private Long endTime;
public LogAspect() {
}
/**
* 定义切入点表达式
* 访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
* 权限修饰符可以使用默认 第一个*表示返回值类型 ..表示当前包以及其子包下 第二个*表示任意方法 (..)表示任意参数列表
*/
private final String POINTCUT = "execution(* com.wk.controller..*(..))";
/**
* 前置通知,方法之前执行
* @param joinPoint
*/
@Before(POINTCUT)
public void doBefore(JoinPoint joinPoint) {
// 获取当前的HttpServletRequest对象
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取请求头中的User-Agent
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
// 打印请求的内容
startTime = System.currentTimeMillis();
log.info("请求开始时间:{}", LocalDateTime.now());
log.info("请求Url : {}", request.getRequestURL().toString());
log.info("请求方式 : {}", request.getMethod());
log.info("请求ip : {}", request.getRemoteAddr());
log.info("请求内容类型 : {}", request.getContentType());
log.info("请求参数 : {}", Arrays.toString(joinPoint.getArgs()));
// 系统信息
log.info("浏览器 : {}", userAgent.getBrowser().toString());
log.info("浏览器版本 : {}", userAgent.getBrowserVersion());
log.info("操作系统: {}", userAgent.getOperatingSystem().toString());
}
/**
* 后置通知
* @param joinPoint
*/
@After(POINTCUT)
public void doAfter(JoinPoint joinPoint) {
System.out.println("Logger-->后置通知,方法名:"+joinPoint.getSignature().getName()+",方法执行完毕");
}
/**
* 返回通知 正常结束时进入此方法
*
* @param ret
*/
@AfterReturning(returning = "ret", pointcut = POINTCUT)
public void doAfterReturning(Object ret) {
endTime = System.currentTimeMillis();
log.info("请求结束时间 : {}", LocalDateTime.now());
log.info("请求耗时 : {}", (endTime - startTime));
// 处理完请求,返回内容
log.info("请求返回 : {}", ret);
}
/**
* 异常通知: 1. 在目标方法非正常结束,发生异常或者抛出异常时执行
*
* @param throwable
*/
@AfterThrowing(pointcut = POINTCUT, throwing = "throwable")
public void doAfterThrowing(Throwable throwable) {
// 保存异常日志记录
log.error("发生异常时间 : {}", LocalDateTime.now());
log.error("抛出异常 : {}", throwable.getMessage());
}
}
测试结果
直接调用controller下接口
2022-03-18 20:41:40.513 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 请求开始时间:2022-03-18T20:41:40.513
2022-03-18 20:41:40.513 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 请求Url : http://localhost:8888/user/pageList
2022-03-18 20:41:40.513 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 请求方式 : GET
2022-03-18 20:41:40.514 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 请求ip : 0:0:0:0:0:0:0:1
2022-03-18 20:41:40.514 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 请求内容类型 : null
2022-03-18 20:41:40.514 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 请求参数 : [3, 5]
2022-03-18 20:41:40.514 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 浏览器 : CHROME9
2022-03-18 20:41:40.514 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 浏览器版本 : 92.0.4515.131
2022-03-18 20:41:40.514 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 操作系统: WINDOWS_10
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3dfa974f] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@423460067 wrapping com.mysql.cj.jdbc.ConnectionImpl@478c29b8] will not be managed by Spring
==> Preparing: select * from sys_user limit ?,?
==> Parameters: 10(Integer), 5(Integer)
<== Columns: id, user_name, nick_name, pass_word, phone_number, email, address, create_time, create_user, update_time, update_user
<== Row: 1504379036472725511, 武杰, 万杰, mollit, 90, o.knynlcdi@qq.com, 天津重庆市商都县, 2022-03-17 19:02:51, proident ipsum consectetur, 2022-03-17 19:02:51, in occaecat ullamco
<== Row: 1504379036472725512, 易刚, 程军, commodo ut et, 20, e.lrfko@qq.com, 四川省南通市其它区, 2022-03-17 19:02:55, ad, 2022-03-17 19:02:55, ad consequat dolore
<== Row: 1504379036472725513, 罗秀兰, 叶杰, sunt ut incididunt, 76, c.qrzvq@qq.com, 云南省新余市涪陵区, 2022-03-17 19:02:59, consectetur magna dolor, 2022-03-17 19:02:59, consequat
<== Total: 3
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3dfa974f]
2022-03-18 20:41:40.520 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 请求结束时间 : 2022-03-18T20:41:40.520
2022-03-18 20:41:40.520 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 请求耗时 : 7
2022-03-18 20:41:40.520 INFO 23180 --- [nio-8888-exec-4] com.wk.aop.LogAspect : 请求返回 : BaseResult(code=20000, msg=处理成功, resultData=[User(id=1504379036472725511, userName=武杰, nickName=万杰, passWord=mollit, address=天津重庆市商都县, phoneNumber=90, email=o.knynlcdi@qq.com, createUser=proident ipsum consectetur, updateUser=in occaecat ullamco, createTime=Thu Mar 17 19:02:51 CST 2022, updateTime=Thu Mar 17 19:02:51 CST 2022), User(id=1504379036472725512, userName=易刚, nickName=程军, passWord=commodo ut et, address=四川省南通市其它区, phoneNumber=20, email=e.lrfko@qq.com, createUser=ad, updateUser=ad consequat dolore, createTime=Thu Mar 17 19:02:55 CST 2022, updateTime=Thu Mar 17 19:02:55 CST 2022), User(id=1504379036472725513, userName=罗秀兰, nickName=叶杰, passWord=sunt ut incididunt, address=云南省新余市涪陵区, phoneNumber=76, email=c.qrzvq@qq.com, createUser=consectetur magna dolor, updateUser=consequat, createTime=Thu Mar 17 19:02:59 CST 2022, updateTime=Thu Mar 17 19:02:59 CST 2022)])
Logger-->后置通知,方法名:pageList,方法执行完毕