场景:日志需要跨方法甚至跨类打印,又想让一个线程的打印可以按顺序成块打印
例:如HttpUtil工具类发起工具类:
public String postForm(String url,HttpForm param){ log.info("请求开始--"); log.info("url:[{}]",url); log.info("param:[{}]",param); post(url,param.toString()); } public String postJson(String url,String param){ log.info("请求开始--"); log.info("url:[{}]",url); log.info("param:[{}]",param); post(url,param.toString()); } /** * param 比如用HttpClient请求, 不管是表单提交还是form又或者文件最后都是要转成HttpEntity,所以可以 * 实际请求发出和返回封装一个方法,表单请求和json 另外用一个方法构造 */ public String post(String url,String param){ String result = httpclient.post(url,param.toString()); log.info("接口返回结果:[{}]", result); log.info("请求结束--"); }
如果按照以上代码,多线程里每次请求的日志不会按顺序打印出来,会和其他请求的日志夹杂在一起,效果如下
请求开始-- url:[http:123.com] 请求开始-- param:[name=张三] url:[http:321.com] 接口返回结果:[{"message":"你"}] param:[{"task":"回家"}] 请求结束-- ........
这样看着不太友好.
优化:(LogBuilder工具类末尾提供)
private LogBuilder logBuilder = new LogBuilder(); public String postForm(String url,HttpForm param){ logBuilder.appendLog("请求开始--"); logBuilder.appendLog("url:[{}]",url); logBuilder.appendLog("param:[{}]",param); post(url,param.toString()); } public String postJson(String url,String param){ logBuilder.appendLog("请求开始--"); logBuilder.appendLog("url:[{}]",url); logBuilder.appendLog("param:[{}]",param); post(url,param.toString()); } /** * param 比如用HttpClient请求, 不管是表单提交还是form又或者文件最后都是要转成HttpEntity,所以可以 * 实际请求发出和返回封装一个方法,表单请求和json 另外用一个方法构造 */ public String post(String url,String param){ String result = httpclient.post(url,param.toString()); logBuilder.appendLog("接口返回结果:[{}]", result); logBuilder.appendLog("请求结束--"); //这种方法需要自己传入log类 ,我用的是slf4j 的log, 且需要实现设置日志等级 logBuilder..setLogLevel(Level.INFO); logBuilder.log(log); //这种方法也需要自己传入log类 ,我用的是slf4j 的log, 不需要实现设置日志等级 logBuilder.logInfo(log); //这种方法通过回调的方式,不需要传入log类,不会限制log类的框架 logBuilder.log(logStr -> { log.info(logStr); });; }
如果按以上方法效果如下:
请求开始-- url:[http:123.com] param:[name=张三] 接口返回结果:[{"message":"失败"}] 请求结束-- 请求开始-- url:[http:321.com] param:[{"task":"回家"}] 接口返回结果:[{"message":"成功"}] 请求结束--
每次请求成块打印,看着友好多了。跨类的话可以定义成static,或者放入static map中。
工具类代码:
/** * 日志构建工具 * 当前线程内不同地方的日志合并打印 * */ public class LogBuilder { private ThreadLocal<StringBuilder> logBuilderThreadLocal = new ThreadLocal<>(); private Level logLevel; /** * 添加日志内容 * * @param format 添加内容 可以包含 {} 占位符 * @param params 替换 {} 占位符的参数 */ public LogBuilder appendLog(String format, String... params) { StringBuilder builder = logBuilderThreadLocal.get(); if (builder == null) { builder = new StringBuilder(); logBuilderThreadLocal.set(builder); } builder.append("\r\n"); String str = format(format, params); builder.append(str); return this; } /** * 把当前添加的日志内容全打印出来 * * @param log sl4j logger */ public void logTrace(Logger log) { log(logStr -> { log.trace(logStr); }); } /** * 把当前添加的日志内容全打印出来 * * @param log sl4j logger */ public void logDebug(Logger log) { log(logStr -> { log.debug(logStr); }); } /** * 把当前添加的日志内容全打印出来 * * @param log sl4j logger */ public void logInfo(Logger log) { log(logStr -> { log.info(logStr); }); } /** * 把当前添加的日志内容全打印出来 * * @param log sl4j logger */ public void logWarn(Logger log) { log(logStr -> { log.warn(logStr); }); } /** * 把当前添加的日志内容全打印出来 * * @param log sl4j logger */ public void logError(Logger log) { log(logStr -> { log.error(logStr); }); } /** * 把当前添加的日志内容全打印出来 * 调用此方法得先调用setLogLevel 设置日志打印级别 * * @param log sl4j logger */ public void log(Logger log) { if (logLevel == null) { throw new CustomException("请先调用setLogLevel方法设置日志打印级别"); } switch (logLevel) { case TRACE: log(logStr -> { log.trace(logStr); }); break; case DEBUG: log(logStr -> { log.debug(logStr); }); break; case INFO: log(logStr -> { log.info(logStr); }); break; case WARN: log(logStr -> { log.warn(logStr); }); break; case ERROR: log(logStr -> { log.error(logStr); }); break; default: throw new CustomException("找不到对应的日志打印级别"); } logLevel = null; } /** * 设置日志打印等级 * * @param logLevel sl4j的 level */ public void setLogLevel(Level logLevel) { this.logLevel = logLevel; } /** * 当前添加的日志内容全打印出来 * * @consumer 自己实现打印代码的回调 日志内容作为参数传入consumer方法 */ public void log(Consumer<String> consumer) { StringBuilder builder = logBuilderThreadLocal.get(); if (builder != null) { consumer.accept(builder.toString()); clear(); } } /** * 日志打印完需要 清除当前日志内容 */ private void clear() { logBuilderThreadLocal.remove(); } /* ----------------- 下面代码是对字段串中的{}占位符 拼接的代码,可以提取到工具类,也可以自己用其他方法实现 ----------*/ public static final String EMPTY_JSON = "{}"; public static final char C_BACKSLASH = '\\'; public static final char C_DELIM_START = '{'; public static final char C_DELIM_END = '}'; public String format(final String strPattern, final Object... argArray) { if (StringUtils.isEmpty(strPattern) || StringUtils.isEmpty(argArray)) { return strPattern; } final int strPatternLength = strPattern.length(); // 初始化定义好的长度以获得更好的性能 StringBuilder sbuf = new StringBuilder(strPatternLength + 50); int handledPosition = 0; int delimIndex;// 占位符所在位置 for (int argIndex = 0; argIndex < argArray.length; argIndex++) { delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); if (delimIndex == -1) { if (handledPosition == 0) { return strPattern; } else { // 字符串模板剩余部分不再包含占位符,加入剩余部分后返回结果 sbuf.append(strPattern, handledPosition, strPatternLength); return sbuf.toString(); } } else { if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) { if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) { // 转义符之前还有一个转义符,占位符依旧有效 sbuf.append(strPattern, handledPosition, delimIndex - 1); sbuf.append(Convert.utf8Str(argArray[argIndex])); handledPosition = delimIndex + 2; } else { // 占位符被转义 argIndex--; sbuf.append(strPattern, handledPosition, delimIndex - 1); sbuf.append(C_DELIM_START); handledPosition = delimIndex + 1; } } else { // 正常占位符 sbuf.append(strPattern, handledPosition, delimIndex); sbuf.append(Convert.utf8Str(argArray[argIndex])); handledPosition = delimIndex + 2; } } } // 加入最后一个占位符后所有的字符 sbuf.append(strPattern, handledPosition, strPattern.length()); return sbuf.toString(); } }