在控制器头上打上注解,设置两个参数作者和功能描述
运行后自动打印出必要的日志,排查错误时能清晰的定位。
主要包括sql语句,出入参数,时间与耗时,描述,路径与文件信息,线程号。
首先写个注解,设置俩参数
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoLog {
String description() default "";
String author() default "";
}
然后利用切面给注解标注的方法加功能
这一个类需要导入相关的包,这里用的时Hutool的Log工具,用的SpringAOP。@RequiredArgsConstructor这个注解生成构造。
写这个需要先了解一下Spring的切面编程,对业务代码进行增强。
首先搭好这个架子,doBefore中获取开始时间,其他两个是在结束时执行打印日志的功能
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SystemLogAspect {
private final static Log log = LogFactory.get();
private final static String INFO = "info", ERROR = "error";
private final HttpServletRequest request;
private long time;
/**
* controller层切点,注解方式
*/
@Pointcut(value = "@annotation(zs.util.anno.AutoLog)")
public void controllerAspect() {
}
/**
* 前置通知 用于拦截Controller层记录用户的操作的开始时间
*/
@Before(value = "controllerAspect()")
public void doBefore(JoinPoint joinPoint) {
time = System.currentTimeMillis();
}
/**
* 后置正常通知 用于拦截Controller层操作
*
* @param joinPoint 切点对象
* @param result 参数返回数据
*/
@AfterReturning(value = "controllerAspect()", returning = "result")
public void afterReturning(JoinPoint joinPoint, String result) throws Exception {
build(INFO, joinPoint, result, null);
}
/**
* 后置异常通知(在方法执行之后返回) 用于拦截Controller层操作
*
* @param joinPoint 切点对象
* @param ex 异常
*/
@AfterThrowing(value = "controllerAspect()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) throws Exception {
build(ERROR, joinPoint, null, ex);
}
/**
* 获取注解中对方法的描述信息,也就是切点方法头上注解的参数
*/
private AnnotationInfo getControllerMethodDescription(JoinPoint joinPoint) throws Exception {
String methodName = joinPoint.getSignature().getName();
Class> classTarget = joinPoint.getTarget().getClass();
Class>[] par = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
Method objMethod = classTarget.getMethod(methodName, par);
String description = objMethod.getAnnotation(AutoLog.class).description();
String author = objMethod.getAnnotation(AutoLog.class).author();
return new AnnotationInfo().description(description).author(author);
}
@Data
@Accessors(fluent = true)
private class AnnotationInfo {
private String description;
private String author;
}
}
具体构建方法build,从JoinPoint中拿切点方法信息,从request里拿路径,再获取结束时间,再拼接上注解中获取的参数。
private void build(String logType, JoinPoint joinPoint, String result, Exception ex) throws Exception {
StringBuilder builder = new StringBuilder("\n");
/* 日志请求url */
String logUrl = request.getRequestURI();
if (StrUtil.isNotBlank(logUrl)) {
builder.append("请求路径: ").append(logUrl).append("\t");
}
/* 项目路径 */
String classPath = joinPoint.getTarget().getClass().getPackage().getName() + "." + joinPoint.getSignature().toShortString();
if (StrUtil.isNotBlank(classPath)) {
builder.append("调用方法 ").append(classPath).append("\n");
}
/* 获取注解 */
AnnotationInfo annotationInfo = getControllerMethodDescription(joinPoint);
/* 方法描述 */
String description = annotationInfo.description();
if (StrUtil.isNotBlank(description)) {
builder.append("描述: ").append(description);
}
/* 方法作者 */
String author = annotationInfo.author();
if (StrUtil.isNotBlank(author)) {
builder.append("——").append(author).append("\n");
}
/* 请求参数 */
String logParam = ServletUtil.getParamMap(request).isEmpty()
? HttpUtil.decode(ArrayUtil.toString(joinPoint.getArgs()), CharsetUtil.UTF_8)
: ServletUtil.getParamMap(request).toString();
logParam = StrUtil.replaceChars(logParam, "\r\n\t ", "");
if (StrUtil.isNotBlank(logParam)) {
builder.append("请求参数: ").append(logParam).append("\n");
}
/* info 日志返回数据 */
if (INFO.equals(logType)) {
String logResult = StrUtil.isBlank(result) ? "" : result;
if (StrUtil.isNotBlank(logResult)) {
builder.append("返回结果: ").append(logResult).append("\n");
}
builder.append("耗时: ").append(System.currentTimeMillis() - time + "ms").append("\n");
log.info(builder.toString());
}
/* error 日志异常信息 */
if (ERROR.equals(logType)) {
String logResult = getErrorOf(ex).orElse("");
if (StrUtil.isNotBlank(logResult)) {
builder.append("错误信息: ").append(logResult);
}
log.error(builder.toString());
}
}
错误日志方法
/**
* 提取error信息中关键信息
*/
private Optional getErrorOf(Exception ex) {
if (StrUtil.isBlank(ex.toString())) {
return Optional.empty();
}
StringBuilder builder = new StringBuilder(ExceptionUtil.getMessage(ex) + "\n");
StackTraceElement[] stackTrace = ex.getStackTrace();
for (StackTraceElement traceElement : stackTrace) {
String checkErrMsg = traceElement.toString();
if (checkErrMsg.contains("zs.") && !checkErrMsg.contains("$$")) {
builder.append(traceElement.toString()).append("\n");
}
}
return Optional.of(builder.toString());
}
把sql语句写错一点
以上日志并不包括打印sql语句,我这用的mybatis,经过很多标签拼装sql语句和#{}拼接值,最便捷的办法就是用mybatis带的工具打印,找到yml文件发现需要配置configuration.log_impl,默认实现org.apache.ibatis.logging.stdout.StdOutImpl,原来的效果不好,看着乱,点进去发现是实现org.apache.ibatis.logging.Log接口,于是我自己写一个。
public class MybatisLog implements Log {
public MybatisLog(String clazz) {
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public boolean isTraceEnabled() {
return false;
}
@Override
public void error(String s, Throwable e) {
System.err.println(s);
e.printStackTrace(System.err);
}
@Override
public void error(String s) {
System.err.println(s);
}
@Override
public void debug(String s) {
if(s.indexOf("Preparing")==5){
System.out.println("SQL语句: "+s.split("Preparing: ")[1]);
}
}
@Override
public void trace(String s) {
System.out.println(s);
}
@Override
public void warn(String s) {
System.out.println(s);
}
}
这样就打印出纯净的sql语句,带#{}的地方拼接'?',我分页用的pagehelper,内部直接拼的Limit字符串,所以参数没有'?'