在前面的一篇文章《使用ThreadLocal和AOP做线程缓存提高性能,缩短API网关响应时间》中介绍了使用自定义注解和spring aop实现本地线程缓存。今天介绍一下springboot项目使用自定义注解和aop记录类名方法名参数耗时信息,实现日志打印。
针对方法做日志打印,主要是一些对耗时敏感的操作,如数据库查询,dubbo方法实现等,基于自定义注解和aop实现方法日志打印,一方面把日志和核心业务逻辑分开,另一方面代码也显得更加优雅可读,更重要的是,我们可以针对这些日志做一些性能监控,便于系统的重构优化。
自定义一个注解PrintLog,作用在方法上,只要在方法上使用了该注解的都会进行日志打印。可以选择需要打印到的logger,便于不同的使用场景适用不同的日志文件等,同时还可以选择是否打印参数,有时候参数可能很庞大,我们可以选择不打印参数,当然默认是需要打印的,便于我们追踪。注解类如下:
package cn.lovecto.promotion.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 打印日志注解
*
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface PrintLog {
/**
* 选择打印日志的logger,不指定则默认是拦截器的logger
* @return
*/
String value() default "";
/**
* 是否打印参数,默认打印参数
* @return
*/
boolean printParams() default true;
}
有了注解类,我们来实现这个注解的拦截器PrintLogAnnotationInterceptor,这里面负责处理核心的日志打印逻辑,主要处理获取类名、方法名、参数列表、耗时信息等。PrintLogAnnotationInterceptor类如下:
package cn.lovecto.promotion.interceptor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import cn.lovecto.promotion.annotation.PrintLog;
/**
* 打印日志注解拦截器
*
*
*/
@Aspect
@Component
public class PrintLogAnnotationInterceptor {
private static final Logger logger = LoggerFactory
.getLogger(PrintLogAnnotationInterceptor.class);
/**
* 针对方法执行打印方法及参数和耗时
*
* @param joinPoint
* @param printlog
* @return
* @throws Throwable
*/
@Around(value = "@annotation(printlog)")
public Object printLog(ProceedingJoinPoint joinPoint, PrintLog printlog)
throws Throwable {
long start = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
throw e;
} finally {
long end = System.currentTimeMillis();
print(start, end, joinPoint, printlog);
}
return result;
}
/**
* 打印逻辑
*
* @param start
* @param end
* @param joinPoint
* @param printlog
*/
private void print(long start, long end, ProceedingJoinPoint joinPoint,
PrintLog printlog) {
try {
Logger curentLogger = logger;
if (!StringUtils.isEmpty(printlog.value())) {
curentLogger = LoggerFactory.getLogger(printlog.value());
}
String msg = getTypeAndMethodAndParams(joinPoint,
printlog.printParams());
long cost = end - start;
if (cost > 100) {// 大于100毫秒
curentLogger.warn("{} cost {} ms", msg, cost);
} else {
curentLogger.info("{} cost {} ms", msg, cost);
}
} catch (Exception e) {
logger.error("print exception", e);
}
}
/**
* 获取执行方法的类名方面以及参数详情
*
* @param joinPoint
* @param printParams
* 是否打印参数
* @return
*/
private String getTypeAndMethodAndParams(ProceedingJoinPoint joinPoint,
Boolean printParams) {
StringBuilder strBuilder = new StringBuilder();
strBuilder.append(joinPoint.getTarget().getClass().getTypeName())
.append(".").append(joinPoint.getSignature().getName())
.append("(");
if (printParams && joinPoint.getArgs().length > 0) {
for (int i = 0; i < joinPoint.getArgs().length; i++) {
strBuilder.append(joinPoint.getArgs()[i]);
if (i != (joinPoint.getArgs().length - 1)) {
strBuilder.append(",");
} else {
strBuilder.append(")");
}
}
} else {
strBuilder.append(")");
}
return strBuilder.toString();
}
}
打印日志在finally代码块中,这样做主要是一种编程思想,永远不要相信自己的代码是完美的,不管目标方法是否执行,我们都会记录到日志;同时finally中的print方法里面也要使用try catch,避免因为这个日志功能运行时出错而影响正常业务。getTypeAndMethodAndParams这个方法主要是获取目标方法的类名、方法名、参数列表等,根据是否需要打印参数来决定是否遍历参数列表进行打印。此处简单使用StringBuilder,不管参数是否是普通来下还是对象,我们都建议定义的类实现toString方法(针对自己写的类实现toString方法是种良好的编程习惯),这样日志会更详尽。
接下来是使用的地方,使用将是非常的简单:
package cn.lovecto.promotion.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import cn.lovecto.promotion.annotation.PrintLog;
import cn.lovecto.promotion.dao.mapper.PromotionMapper;
import cn.lovecto.promotion.model.Promotion;
/**
* 促销查询
*
*/
@Service
public class PromotionDao {
@Autowired
private PromotionMapper promotionMapper;
@PrintLog(value = "db")
public int selectCount(Promotion promotion){
return promotionMapper.selectCount(promotion);
}
}
只需要在需要日志记录的方法上加上注解就可以啦!!!