**本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看 活动链接 **
前言
项目初期,为了快速迭代上线,很多功能都是项目搭建者从别的地方Copy过去,然后根据项目实际情况进行相应的修改。
后期伴随着业务的迭代,对于中心化组件的需求也渐渐产生。比如我的老东家,在K8S上差不多部署了200个项目,顺理成章的诞生了授权组件,日志组件,注解组件,长连接服务等。
当然啦,我们今天的主角是日志组件。
要素
格式
日志的格式一般包含几种类型:入参还是出参,类,方法,内容,开始时间,描述,还是耗时。有了固定的格式后,日志的采集会变得相对容易点。
如果是对安全有要求的,可能还会带上加密信息和签名。
可配置
不能说你用了日志组件,然后所有的方法中都打印日志,大大浪费了硬盘空间,暴殄天物。
- 有些需要按项目层面打日志,例如controller层面和service层面。
- 有些service的方法我不需要打印日志,那我需要加个注解用来表示不用打印日志。
- 有些方法不需要打印入参,只需要打印出参,这时候我需要支持出入参是否打印配置。
- 有些方法的入参或者出参数据量较大,但数据又不关键。同时方法的开始时间和耗时又必须要保留。
看到这里肯定有人要说了,哪有这么复杂。但相信我,在金融级别的系统里就是有这种需求。
侵入性
SpringBoot诞生初期为什么会快速流行,就是源于它配置简单。 如果一个组件的代码侵入性很强,用的人就会大打折扣甚至无人问津。
在老东家搞的Dubbo组件就因为需要在apollo加一行配置,推了好久才被同事渐渐接受。太难了~~
实现
格式
这是车辙自己在用的日志格式
[类型:'入参'] | [类:'TestLogService'] | [方法:'annotest'] | [入参:'[{"yuan":"123456.789"}]'] | [开始时间:'2021-05-13 17:49:46'] | [描述:'测试']。
[类型:'出参'] | [类:'TestLogService'] | [方法:'annotest'] | [出参:'null'] | [结束时间:'2021-05-13 17:49:46'] | [耗时:31] | [描述:'测试']
可配置
- 定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import(EnableLogRegister.class)
public @interface EnableApplicationLog {
/**
* 是否开启controller注解
* @return
*/
boolean enableController() default true;
/**
* 是否开启service注解
* @return
*/
boolean enableService() default true;
}
- 通过实现
ImportBeanDefinitionRegistrar
,将自己需要的类交给Spring管理。这样就根据EnableApplicationLog
注解上的配置判断是否让切面类被Spring管理。因此组件没有采用SpringBoot Starter的形式,相对灵活。可以参考Mybatis的MapperScan是怎么做的。
class EnableLogRegister implements ImportBeanDefinitionRegistrar{
boolean enableController = annoAttrs.getBoolean("enableController");
if(enableController){
basePackages.add(BASE_HANDLER_CONTROLLER);
}
boolean enableService = annoAttrs.getBoolean("enableService");
if(enableService){
basePackages.add(BASE_HANDLER_SERVICE);
}
}
- 定义切点
@Aspect
@Slf4j
public class EnableLogControllerLogHandler extends BaseLogHandler {
@Pointcut("within(@(org.springframework.stereotype.Controller || org.springframework.web.bind.annotation.RestController) *)")
private void allMethod() {
}
@Around("allMethod()")
public Object doAround(ProceedingJoinPoint call) throws Throwable {
return handle(call);
}
}
// service层面切点与controller一致
- 定义可配置功能注解。
// 不需要打印日志注解
public @interface NoNeedLog {
}
// 需要打印日志的配置注解
public @interface LogParam {
String desc() default "";
boolean isPrintIn() default true;
boolean isPrintOut() default true;
}
- 顶层handler处理
// 判断是否需要打印日志
if(!needLog(call)){
Object result = call.proceed();
return result;
}
Boolean isPrintIn = true, isPrintOut = true;
String desc = null;
// 获取是否需要打印入参和出参
LogParam logParam = this.getLogParam(call);
if(logParam != null){
isPrintIn = logParam.isPrintIn();
isPrintOut = logParam.isPrintOut();
desc = logParam.desc();
}
// 入参参数打印
Long startStamp = System.currentTimeMillis();
if(isPrintIn){
doBeforeParam(call, desc);
}
// invoker调用
Object result = call.proceed();
// 返回参数打印
if(isPrintOut){
doAfterParam(result, startStamp, call, desc);
}
return result;
上面的代码基本上包含了主要的实现,相信小伙伴们应该对此有了一定的认识,可以在下方评论中留言哟!
使用
SpringBoot启动类上添加注解开启日志打印
@EnableApplicationLog(enableController = false)
public class TrackAdapterApplication {}
方法不需要日志
@NoNeedLog
public void annotest(AnnoBean annoBean)
日志配置不需要入参,添加描述
@LogParam(desc = "测试", isPrintIn = false)
public void annotest(AnnoBean annoBean)
结尾
其实日志组件里还有不少东西可以写,比如说抛出的异常我怎么处理。捕获打印异常并继续抛出还是直接做处理并返回,亦或是做一个异常处理组件用来记录并告警。这边也不能给出一个完美的答案,毕竟适合的才是最好的。
这是时隔多月的第一篇博文,接下来希望出一些更高质量的文章,咱们下期见啦!