日志的重要性在项目中不言而喻, 但是对controller层的出入参一个一个的添加, 将是一个比较枯燥的事情, 可以考虑使用aop来记录.
spring项目中的全局异常处理有两种模式,其一是实现HandlerExceptionResolver接口的模式, 其二是使用@RestControllerAdvice的模式
之前使用springmvc时写过一个aop记录日志的功能(详见: https://github.com/zonaChang/spring-aop-log.git), 最近项目使用的是springboot+@RestControllerAdvice的模式, 之前的方式不适合当前环境, 虽有该文章.
简述思路:
1. 使用aop将controller层的出入参记录打印出来
2. 为了便于观察, 使用环绕通知将出入参打印在一条记录里面(也可以使用前置,后置,异常通知+ThreadLocal将出入参添加到一条记录)
3. 不仅要记录可以正常执行的请求, 对于异常请求,也应该将其记录下来(该方式暂有一个问题未解决, 如果是controller的参数类型解析错误(如: controller需要一个integer类型的, 但入参为一个字符串), 则不能记录具体的入参. 有能解决的烦请指教, 可以考虑下dispatchServlet调度方面涉及到的组件 )
aop代码:
import com.carl.study.comm.exception.ExceptionControllerAdvice;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/**
* @author changez
* @desc 记录日志
* @datetime 2019/7/20 9:40
*/
@Slf4j
@Component
@Aspect
public class LogAop {
/**
* 全局异常处理类全限定名
*/
private String exceptionAdviceName = ExceptionControllerAdvice.class.getName();
private static ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();
// @ControllerAdvice 环绕通知,出异常后获取到的参数信息为异常处理的参数信息, 所以暂时不使用环绕通知
// || execution(* com.bz.wes.org.comm.exception.ExceptionControllerAdvice.*(..))
@Pointcut("execution(* com.carl.study.*.controller..*.*(..)) || execution(* com.carl.study.exception.ExceptionControllerAdvice.*(..))")
public void logAroundPointCut(){}
/**
* controller层入参日志记录
*/
@Around("logAroundPointCut()")
public Object intoControllerLog(ProceedingJoinPoint point) throws Throwable {
// 目标方法所在类全限定名
String targetName = point.getSignature().getDeclaringType().getName();
// 目标方法签名
StringBuilder sb = null ;
if (targetName.equals(exceptionAdviceName)) {
// 异常处理则从threadLocal中取出入参
sb = threadLocal.get();
// controller参数解析异常不会进入目标方法, 而直接请求异常处理器
sb = sb == null ? new StringBuilder(300).append("请求异常, 未进入controller ") : sb;
} else {
// 调用目标方法, 将参数存入到threadLocal, 便于发生异常时取到请求参数
sb = new StringBuilder(300);
// 类名+方法名
String target = point.getSignature().getDeclaringTypeName() + "." + point.getSignature().getName();
Object[] args = point.getArgs();
String[] paramsName = ((MethodSignature) point.getSignature()).getParameterNames();
sb.append(target).append(" 入参:【");
if(args != null && paramsName != null && args.length > 0 && paramsName.length > 0) {
for (int i=0; i< paramsName.length; i++) {
sb.append(" ").append(paramsName[i]).append(" = ").append(args[i]).append(",");
}
sb.deleteCharAt(sb.length()-1);
}
sb.append("】");
threadLocal.set(sb);
}
// 调用目标方法
Object result = point.proceed();
sb.append(" 出参:【").append(result).append("】");
// 记录日志
log.info(sb.toString());
// 调用结果返回
return result;
}
}
全局异常处理:
这种模式下, 请求出现异常后, 类似于产生了一个新的请求来调用相应的异常处理器, 此时在aop中将获取的是该异常处理器的入参, 而非原始的请求入参(在aop中使用threadLocal解决该问题)
import com.carl.study.comm.response.JsonResponse;
import com.carl.study.comm.response.ResponseCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class ExceptionControllerAdvice {
/**
* 拦截参数异常
*/
@ExceptionHandler(value = IllegalArgumentException.class)
public MetaRestResponse myErrorHandler(IllegalArgumentException e) {
log.error(">>>拦截参数异常>>" + e.getMessage());
JsonResponse result = JsonResponse.error(ResponseCode.BAD_REQUEST, e.getMessage());
return result;
}
}