概述
平时我们在做项目时经常需要对一些重要功能操作记录日志,方便以后跟踪是谁在操作此功能;我们在操作某些功能时也有可能会发生异常,但是每次发生异常要定位原因我们都要到服务器去查询日志才能找到,而且也不能对发生的异常进行统计,从而改进我们的项目,要是能做个功能专门来记录操作日志和异常日志那就好了,今天我们就来用springBoot Aop 来做日志记录。主要讲两个内容,一个是如何在 SpringBoot 中引入 Aop 功能,二是如何使用Aop做切面去统一处理Web请求的日志,用于封装需要记录的日志信息,包括操作的描述、时间、消耗时间、url、请求参数和返回结果等信息。
引入依赖
因为需要对web请求做切面来记录日志,所以先引入web模块,并创建一个简单的hello请求的处理。
<!-- 大多数的web应用都使用spring-boot-starter-web模块进行快速搭建和运行。 -->
<!-- spring-boot-starter-web -->
<!-- 对全栈web开发的支持, 包括tomCat和 spring-webMVC -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
<scope>provided</scope>
</dependency>
在Spring Boot中引入AOP就跟引入其他模块一样,非常简单,只需要在pom.xml中加入如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--MySQL依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 引入 MyBatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
在完成了引入AOP 依赖包后,一般来说并不需要做其他的配置。也许在Spring 中使用过注解配置方式的会问是否需要在程序主类中增加 @EnableAspectJAutoProxy 来启用,实际上并不需要。那是因为 spring.aop.auto 属性默认是开启的,那也就是说只要引用AOP依赖后,默认就已经增加了 @EnableAspectJAutoProxy。
添加日志信息封装类WebLog
import lombok.Data;
/**
* 把今天最好的表现当作明天最新的起点..~
*
* Today the best performance as tomorrow newest starter!
*
* @类描述: Controller层的日志封装类
*
* @author: <a href="mailto:duleilewuhen@sina.com">独泪了无痕</a>
* @创建时间: 2020-08-20 00:49:41
* @版本: V 1.0.2
* @since: JDK 1.8
*/
@Data
@NoArgsConstructor
public class WebLog implements Serializable {
private static final long serialVersionUID = 5132663133670672913L;
/** 主键 */
private Integer id;
/** 操作版本号 */
private String operateVer;
/** 操作模块 */
private String operateModel;
/** 操作类型 */
private String operateType;
/** 操作说明 */
private String operateDesc;
/** 操作者ID */
private String operateUserId;
/** 操作者名称 */
private String operateUserName;
/** 目标类名 */
private String operateClass;
/** 目标方法 */
private String operateMethod;
/** 操作时间 */
private Long startTime;
/** 消耗时间(单位:毫秒) */
private Long spendTime;
/** IP地址 */
private String ipAddress;
/** 请求方式(请求类型) */
private String requestType;
/** 根路径 */
private String basePath;
/** URI */
private String uri;
/** URL */
private String url;
/** 请求参数 */
private Object parameter;
/** 请求返回的结果 */
private String result;
/** 日志类型(info:操作日志,error:错误) */
private String logType = "info";
/** 异常详情 */
private String excepDetail = "";
/** 异常名称 */
private String excepName = "";
}
添加切面类WebLogAspect
重要说明
实现AOP 的切面主要有以下几个要素:
- 使用 @Aspect 注解 将一个类定义为切面类
- 使用 @Pointcut 定义一个切入点,这个可以是一个规则表达式,也可以是一个注解等
- execution: 用于匹配子表达式
@Pointcut("execution(public * org.dllwh.aop.controller..*.*(..))") public void webLog() { }
- within: 用于匹配连接点所在的Java类或者包
// 匹配AopController类中的所有方法 @Pointcut("within(org.dllwh.aop.controller.AopController)") // 匹配org.dllwh.aop.controller包及其子包中所有类中的所有方法 @Pointcut("within(org.dllwh.aop.controller..*)")
- this: 指定AOP代理类的类型
@Before("before()") public void beforeAdvide(JoinPoint point, Object proxy){ //处理逻辑 }
- target:指定目标对象的类型
@Before("before()") public void beforeAdvide(JoinPoint point, Object proxy){ //处理逻辑 }
- args: 指定参数的类型
@Before("before() && args(age,username)") public void beforeAdvide(JoinPoint point, int age, String username){ //处理逻辑 }
- bean:指定特定的bean名称,可以使用通配符(Spring自带的)
@Pointcut("bean(person)") public void before(){}
- @within: 匹配使用指定注解的类,其所有方法都将被匹配
@Pointcut("@within(org.dllwh.annotation.AdviceAnnotation)") // 所有被@AdviceAnnotation标注的类都将匹配
- @target: 带有指定注解的类型,和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME
@Pointcut("@target(org.dllwh.annotation.AdviceAnnotation)") // 所有被@AdviceAnnotation标注的类都将匹配
- @args: 指定运行时传的参数带有指定的注解
@Before("@args(org.dllwh.annotation.AdviceAnnotation)") public void beforeAdvide(JoinPoint point){ //处理逻辑 }
- @annotation:指定方法所应用的注解。也就是说,所有被指定注解标注的方法都将匹配。
@Pointcut("@annotation(org.dllwh.annotation.AdviceAnnotation)") public void before(){}
- 根据需要在切入点不同位置的切入内容
- 使用 @Before 在切入点开始处切入内容
- 使用 @After 在切入点结尾处切入内容
- 使用 @AfterReturing 在切入点return 内容之后切入(用来处理返回值做一些加工处理)
- 使用 @Around 在切入点前后切入内容,并在自己控制何时执行切入点自身的内容
- 使用 @AfterThrowing 用来处理当切入内容部分抛出异常之后的处理逻辑
实例
定义了一个日志切面,在环绕通知中获取日志需要的信息,并应用到controller层中所有的public方法中去。
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
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;
import org.aspectj.lang.reflect.MethodSignature;
import org.dllwh.aop.annotation.OperLog;
import org.dllwh.aop.model.WebLog;
import org.dllwh.aop.service.LogService;
import org.dllwh.aop.util.IpUtilHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson.JSON;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.json.JSONUtil;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
/**
* 把今天最好的表现当作明天最新的起点..~
*
* Today the best performance as tomorrow newest starter!
*
* @类描述: 统一日志处理切面
*
* @author: <a href="mailto:duleilewuhen@sina.com">独泪了无痕</a>
* @创建时间: 2020-08-20 01:00:35
* @版本: V 2.1
* @since: JDK 1.8
*/
@Aspect
@Component
@Order(1)
@Slf4j
public class WebLogAspect {
/** 操作版本号 */
// @Value("${version}")
// private String operVer;
/** 时间线程 */
private static final ThreadLocal<Long> beginTimeThreadLocal = new NamedThreadLocal<Long>("ThreadLocal beginTime");
/** 日志线程 */
private static final ThreadLocal<WebLog> logThreadLocal = new NamedThreadLocal<WebLog>("ThreadLocal log");
/** 异步操作记录日志的线程池 */
private ScheduledThreadPoolExecutor threadPoolTaskExecutor = new ScheduledThreadPoolExecutor(10);
@Autowired
LogService logService;
/**
* 设置操作日志切入点 记录操作日志 在注解的位置切入代码
*
* @within: 匹配使用指定注解的类,其所有方法都将被匹配
* @annotation:指定方法所应用的注解。也就是说,所有被指定注解标注的方法都将匹配。
*/
@Pointcut("@annotation(org.dllwh.aop.annotation.OperLog)")
// @Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
// @Pointcut("@annotation(org.springframework.web.bind.annotation.RestController)")
public void webLog() {
}
/**
* 定义切入点,设置操作异常切入点记录异常日志 扫描所有controller包下操作
*/
@Pointcut("execution(public * org.dllwh.aop.controller..*.*(..))")
public void operExceptionLog() {
}
/**
* 前置通知,用于拦截记录用户的操作。
*
* 在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
*
* @param joinPoint 切点
*/
@Before(value = "webLog() || operExceptionLog()")
public void doBefore(JoinPoint joinPoint) {
log.info("=========执行前置通知==================");
// 线程绑定变量(该数据只有当前请求的线程可见)
beginTimeThreadLocal.set(System.currentTimeMillis());
}
/**
* 环绕通知,决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值
*
* 必须有这个返回值。可以这样理解,Around方法之后,不再是被织入的函数返回值,而是Around函数返回值
*
* @param joinPoint 切点
* @return
* @throws Throwable
*/
@Around(value = "webLog() || operExceptionLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
logThreadLocal.set(new WebLog());
log.info("=========执行环绕通知==================");
// 执行方法
Object result = joinPoint.proceed();
return result;
}
/**
* 最终(后置)通知。
*
* 当目标方法执行成功后执行该方法体,不论是正常返回还是异常退出
*
* @param joinPoint 切点
*/
@After(value = "webLog() || operExceptionLog()")
public void doAfter(JoinPoint joinPoint) {
log.info("=========执行后置通知==================");
}
/**
* 后置返回通知,当目标方法执行成功后执行该方法体。
*
* 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
*
* @param joinPoint 切入点
* @param keys 返回结果
* @param returnObject 返回值的信息
*/
@AfterReturning(value = "webLog() || operExceptionLog()", returning = "keys")
public void doAfterReturning(JoinPoint joinPoint, Object keys) {
log.info("=========执行后置返回通知===============");
WebLog webLog = getWebLog(joinPoint);
webLog.setLogType("info");
if (keys != null) {
webLog.setResult(JSON.toJSONString(keys));
}
// 方法结束时间
Long endTime = System.currentTimeMillis();
// 得到线程绑定的局部变量(开始时间)
long startTime = beginTimeThreadLocal.get();
// 执行时长(毫秒)
webLog.setSpendTime(endTime - startTime);
webLog.setStartTime(startTime);
logThreadLocal.set(webLog);
// 通过线程池来执行日志保存
threadPoolTaskExecutor.execute(new SaveLogThread(webLog, logService));
log.info("{}", JSONUtil.parse(webLog));
}
/**
* 异常通知
*
* 当目标方法抛出异常返回后,执行该方法体,用于拦截记录异常日志
*
* @param joinPoint
* @param exception 为Throwable类型将匹配任何异常
*/
@AfterThrowing(value = "webLog() || operExceptionLog()", throwing = "exception")
public void doAfterThrowing(JoinPoint joinPoint, Throwable exception) {
log.info("=========执行异常通知===============");
WebLog webLog = getWebLog(joinPoint);
webLog.setExcepName(exception.getClass().getName());
// 异常信息
webLog.setExcepDetail(stackTraceToString(exception));
webLog.setLogType("error");
logThreadLocal.set(webLog);
// 通过线程池来执行日志保存
threadPoolTaskExecutor.execute(new SaveLogThread(webLog, logService));
log.info("{}", JSONUtil.parse(webLog));
}
/**
* 获取日志信息
*
* @param joinPoint
* @param exception
* @return
*/
private WebLog getWebLog(JoinPoint joinPoint) {
WebLog webLog = logThreadLocal.get();
// 获取当前请求对象
RequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 获取目标方法的参数
Object[] getArgs = joinPoint.getArgs();
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = methodSignature.getMethod();
// 当前类有 OperLog 注解的方法
OperLog opLog = method.getAnnotation(OperLog.class);
if (opLog != null) {
// 通过注解OperLog管理请求日志
webLog.setOperateModel(opLog.operModel());
webLog.setOperateType(opLog.operType());
webLog.setOperateDesc(opLog.operDesc());
} else if (method.isAnnotationPresent(ApiOperation.class)) { // 当前类有 swagger 注解的方法
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
webLog.setOperateDesc(apiOperation.value());
}
String urlStr = request.getRequestURL().toString();
webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
webLog.setIpAddress(IpUtilHelper.getClientIP(request));
// 请求方式
webLog.setRequestType(request.getMethod());
// 请求参数
webLog.setParameter(getParameter(method, getArgs));
webLog.setUri(request.getRequestURI());
// 请求Url
webLog.setUrl(request.getRequestURL().toString());
// 获取执行方法的类的名称(包名加类名)
webLog.setOperateClass(joinPoint.getTarget().getClass().getName());
// 获取方法名
webLog.setOperateMethod(method.getName());
return webLog;
}
/**
* 根据方法和传入的参数获取请求参数
*
* @param method
* @param args
*/
private Object getParameter(Method method, Object[] args) {
List<Object> argList = new ArrayList<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
// 将RequestBody注解修饰的参数作为请求参数
RequestBody requestBody = parameters[i].getAnnotation(RequestBody.class);
if (requestBody != null) {
argList.add(args[i]);
}
// 将RequestParam注解修饰的参数作为请求参数
RequestParam requestParam = parameters[i].getAnnotation(RequestParam.class);
if (requestParam != null) {
Map<String, Object> map = new HashMap<>();
String key = parameters[i].getName();
if (!StringUtils.isEmpty(requestParam.value())) {
key = requestParam.value();
}
map.put(key, args[i]);
argList.add(map);
}
}
if (argList.size() == 0) {
return null;
} else if (argList.size() == 1) {
return argList.get(0);
} else {
return argList;
}
}
/**
* 转换异常信息为字符串
*
* @param exception 异常
*/
private String stackTraceToString(Throwable exception) {
// 异常名称
String exceptionName = exception.getClass().getName();
// 异常信息
String exceptionMessage = exception.getMessage();
// 堆栈信息
StackTraceElement[] elements = exception.getStackTrace();
StringBuffer strbuff = new StringBuffer();
for (StackTraceElement stet : elements) {
strbuff.append(stet + "\n");
}
String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
return message;
}
/**
* 保存日志线程
*/
private class SaveLogThread implements Runnable {
private WebLog webLog;
private LogService logService;
public SaveLogThread(WebLog webLog, LogService logService) {
this.webLog = webLog;
this.logService = logService;
}
@Override
public void run() {
log.info("通过线程池来执行日志保存");
logService.insert(webLog);
}
}
}
进行接口测试
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("aopController")
public class AopController {
@RequestMapping(value = "exception")
public void exception() {
int i = 1/0;
}
@RequestMapping(value = "void")
public void nOHaveReturnResult() {
}
@RequestMapping(value = "return")
public String haveReturnResult() {
String result = "{\"userName\":\"dllwh\",\"realName\":\"独泪了无痕\"}";
return result;
}
}