为什么要做日志?
日志都有什么级别?
日志打印通常有四种级别,从高到低分别是:ERROR、WARN、INFO、DEBUG。
ERROR是错误的意思,但不代表出现异常的地方就该打ERROR。
ERROR是相对程序正确运行来说的,如果出现了ERROR那就代表出问题了,开发人员必须要查一下原因,或许是程序问题,或许是环境问题,或许是理论上不该出错的地方出错了。总之,如果你觉得某个地方出问题时需要解决,就打ERROR,如果不需要解决就不要打ERROR。
举例而言:
如果有一个接口。调用者传过来的参数不在你的接受范围内,在这种情况下你不能打ERROR,因为传什么值是用户决定的,并不影响程序正确运行。想象一下,如果你的服务器上有监控程序的话,检测到ERROR或WARN就报警,参数错误你也打ERROR,那运维人员会疯掉的。
这种调用者传入的接口参数错误问题,就应该打个INFO了,调用者说你的接口总是返回错误代码,你可以告诉他,是他的哪个参数传错了。
WARN是指出现了不影响程序正确运行的问题,WARN也是问题但不影响程序正常运行,如果WARN出现的过于频繁或次数太多,那就代表你要检查一下程序或环境或依赖程序是否真的出问题了。
假如你访问一个接口,设置了一个超时,超时之后会抛异常,你在try块里不该打ERROR也不该打INFO来无视它,这时你应该打WARN,紧紧是警告一下,如果超时过多那就该检查一下了,是不是对方接口有问题了或者是网络环境出问题了。
INFO和DEBUG就是指一般的信息了。在程序出问题时,如果这条log可以帮助你分析问题或查看程序的运行情况,那就应该打个INFO。如果仅仅是为了在调试阶段查看程序是否运行正确那就要打DEBUG。
日志该打印什么信息?
时间 进程|线程 级别 模块 Filter 操作者 入参 内容
以下内容也在打印考虑范围内:
- 会话标识。能知道是哪个客户端或者是哪个用户触发、登陆账号、seesion信息等。
- 其他信息:
场景信息(谁,什么功能等);
状态信息(开始,中断,结束)以及重要参数;
版本号、线程号等。
日志格式:日志格式统一使用主语+谓语+宾语+状语
的格式,日志打印应该遵从人类的自然语言,任何没有开发经验或是外行人都能从你的日志中捕获到有用的信息。
可以加入分隔符,使用分隔符使每个域都能够清晰识别,分隔符可以提高日志可分析性。
使用[]进行参数变量隔离,这样的格式写法,可读性更好,对于排查问题更有帮助
注意: 在生产环境中不能打印DEBUG级别的日志,DEBUG级别的日志只能用于开发调试或测试环节,同时在输出DEBUG级别的日志的时候,也应该依据项目组的开发需求打印日志,尽量做到:
Java是面向对象编程。
反例:UserServiceImpl interface = new UserServiceImpl();
应该面向接口的对象编程,而不是面向实现,这也是软件设计模式的原则,
正确的做法应该是:
UserService interface = new UserServiceImpl();
日志框架里面也是如此,日志有门面接口,有具体实现的实现框架,所以大家不要面向实现编程。
考虑日志对性能的影响:日志的频繁打印会对模块的性能产生极大的影响。当日志产生的速度大于日志文件写磁盘的速度,会导致日志内容积压在内存中,导致内存泄漏。
不打印重复的日志,避免重复打印日志,浪费磁盘空间,务必在log4j.xml中设置 additivity = false【阿里开发手册规约中有强调】
正例:
不打无用的、无意义、不完全的日志。
无用、无意义例子:(不要这样做)
if(message instanceof TextMessage){
//do sth
}else{
log.warn("Unknown message type");
}
上面打印的warn信息,看日志根本无法清楚识别消息类型是什么,上述例子只是充当了提示信息,对解决问题毫无帮助。
正确的打印内容应该是: 消息类型;消息ID;上下文信息;出问题的原因。
无用、无意义例子:(不要这样做)
log.info("调用客户系统开始...");
...
log.info("调用客户系统结束...");
...
log.info("用户签约失败...");
当我们看到这样的日志的时候,没有包含任何有意义的信息。调用开始,我想知道谁调用的,入参是啥;调用结束,我想知道调用结果,出参是啥,调用成功还是失败;同样,用户签约失败,我想知道的是哪个用户失败,为什么失败;显然都没展示,所以这样的日志是没有任何意义的。
不完全的日志例子:(不要这样做)
log.error('XX 发生异常', e.getMessage());
e.getMessage()只会记录记录错误基本描述信息,不会记录详细的堆栈异常信息。
正确的打印方式应该是:
log.error('XX 发生异常', e);//这样就会打印堆栈信息
在异常捕获代码中务必打印堆栈信息
示例:
try {
int i=1/0;
} catch (Exception ex) {
logger.info("--- getMessage ---");
logger.error(ex.getMessage());
System.out.println();
logger.info("--- print Stack Trace ---");
logger.error("打印异常堆栈信息",ex);
}
控制台输出为:
2018-12-27 11:02:16,737 INFO main com.Test [//] — getMessage —
2018-12-27 11:02:16,739 ERROR main com.Test [//] / by zero
2018-12-27 11:02:16,739 INFO main com.Test [//] — print Stack Trace —
2018-12-27 11:02:16,740 ERROR main com.Test [//] 打印异常堆栈信息
java.lang.ArithmeticException: / by zero
at com.Test.main(Test.java:17)
打印了异常堆栈信息才能快速定位问题。
4.预期会被正常处理的异常,仅需要打印基本信息留作记录,不需要去打印异常堆栈信息,使用堆栈的跟踪是一个巨大的开销,要谨慎使用。
5.不打印混淆信息的日志:所打印出来的日志都应该是清楚准确的表达,当你不能确定具体原因的时候,不能在日志中只是展示一种可能的情况,这样会影响对问题的判断,这样的日志千万不能打。
反例:
Connection connection = ConnectionFactory.getConnection();
if (connection == null) {
log.warn("System initialized unsuccessfully");
}
一般不在循环里打印日志
日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点【阿里开发手册规约中有强调】
应用中的扩展日志【阿里开发手册规约中有强调】
这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
正例: mppserver 应用中单独监控时区转换异常,如:mppserver _ monitor _ timeZoneConvert . log
谨慎地选择记录日志的级别,生产环境原则上禁止输出 debug 日志;有选择地输出info日志。
特殊情况:
有可能有些问题只会在线上出现,那么此时就需要用到debug来进行问题定位和分析。
如果有上述特殊情况,在生产情况下需要开启DEBUG,需要使用开关进行管理,不能一直开启。
日志打印的时候不能因为打印日志引入新的异常,例如空指针异常
反例:(不要这么做)
log.debug("Processing request with id: {}", request.getId());
//request对象如果是null,那么就会出现NPE。
日志打印的时候,假如需要处理列表、数组类的数据,最好是只输出对象的大小,或者某些关键字段,例如:编号(Commons Beanutils),如果将全部内容打印出来可能会占用大量的资源
写完功能,进行测试时,尽量不用编辑器的调试,而是通过日志来查看功能实现和进行异常定位、分析。
你需要进行功能逻辑判断的要点也可能是你打日志的要点,会出现的异常正是日志需要去记录的。没有编辑器的debug调试,你也可以清楚的知道功能实现的逻辑,异常的原因和快速定位问题。
需要明确对象的toString方法是否输出的东西就是你想要的东西,是否需要重写对象的toString方法
另外,建议打印对象信息时,可以采用json格式打印,这样查看起来更清晰。
log.info("用户绑卡,实名校验,用户信息:{}", JSONObject.toJSONString(userInfo));
使用参数化信息的方式:不要进行字符串拼接,那样会产生很多String对象,占用空间,影响性能。
反例:(不要这么做)
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
正例:
logger.debug("UserName is:[{}] and age is : [{}] ", name, age);
//加一个"[]"可读性会更强一些。
记录非预期执行情况
为程序在“有可能”执行到的地方打印日志。如果我想删除一个文件,结果返回成功。但事实上,那个文件在你想删除之前就不存在了。最终结果是一致的,但程序得让我们知道这种情况,要查清为什么文件在删除之前就已经不存在。
如果要抛出异常,就把异常留给最终处理的方法进行打印,不要在抛出异常的地方进行打印。
反例:(不要这么做)
try {
//do sth
} catch (Exception e) {
logger.error('XX 发生异常', e);
throw new IOException();
}
如果你在方法的开始和结束都记录了日志,那么你可以人工找出效率不高的代码。
因为打印的日志有时间戳,你可以根据开始和结束时间获得执行时间,如果执行时间较长,可能就是代码效率有问题。
如果你的方法名的含义很清晰,看日志将是一件愉快的事情。同样的,分析异常也更得更简单了,因为你知道每一步都在干些什么。所以:方法的命名很重要
作为一个webserver,建议将请求者的ip和请求时间,打印到日志中
在和资源打交道的时候,也要记录关键的信息,比如说磁盘访问,比如数据库访问,比如请求网络服务器,这些都算是与小系统的交互,必须要将输入和输出写入日志。而且这些内容都会伴有异常,遇到异常更是要以error写入logger。
异常捕获后除了抛出异常、打印日志外,还可以做哪些操作?
比如说碰到了一个主机访问异常,你可以在异常体中尝试重启主机。
比如连接不成功,可以尝试重新连接。
实现进入方法前打印参数,退出方法时打印输出的方式
/**
* 定义一个注解,一般为方法级别,在需要打印日志的方法上面使用
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogPrint {
}
注意:
/**
* 拦截器方法具体要实现的功能,日志打印就在该部分实现
*/
@Service
public class LogPrintInterceptor implements HandlerInterceptor {
/**
* 请求处理之前进行调用(Controller方法调用之前)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 在方法执行之前需要打印的日志,可以从servlet和handler中获取请求的参数信息
*/
// 千万千万要返回true,否则将不进入方法执行
return true;
}
/**
* 请求处理之后进行调用(Controller方法调用之后)
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
/**
* 在方法执行完之后需要打印的日志
*/
}
/**
* 渲染了对应的视图之后执行
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
/**
* 在方法执行完之后需要打印的日志
*/
}
}
/**
* 配置拦截器方法
*/
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Autowired
private LogPrintInterceptor logPrintInterceptor;
/**
* 配置注解拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logPrintInterceptor)
.addPathPatterns("/**");
super.addInterceptors(registry);
}
}
@Service
public class LogPrintFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
/**
* 方法执行前日志打印
*/
// 实际业务
chain.doFilter(request, response);
/**
* 方法执行后日志打印
*/
}
@Override
public void destroy() {
}
}
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Bean
public FilterRegistrationBean logPrintFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
// 这个位置只能采用创建新对象的方式来设置拦截器,采用注入的方式,实验失败
registration.setFilter(new LogPrintFilter());
registration.addUrlPatterns("/**");
registration.addInitParameter("paramName", "paramValue");
registration.setName("logPrint");
registration.setOrder(1);
return registration;
}
}
@Aspect
@Component
public class LogPrintAspect {
/**
* 方法切点
*/
@Pointcut(value = "execution(* com.cmft.logplat.base.controller.*.*(..))")
private void pointcut(){
}
/**
* 方法执行之前
*/
@Before(value = "pointcut()")
public void before(JoinPoint joinPoint){
//
}
/**
* 方法执行之后
*/
@After(value = "pointcut()")
public void after(JoinPoint joinPoint){
//
}
}
注意:
本人程序媛一枚,因为离港澳较近,周末兼职港澳人肉代购。
欢迎各位大佬添加本人微信,还会经常有点赞活动送价值不菲的小礼品哦。
即使现在不需要代购,等以后有了女(男)朋友、有了宝宝就肯定会需要的喽。