在开发springboot项目时,我们可能需要自定义日志实现。需要对slf4j的日志实现进行一次外层包装 这个很简单,按照org.slf4j.Logger方式定义一个类Logger类MyLogger。
让后实现MyLoggerImpl:
public class MyLoggerImpl implements CoreLogger {
private static final String FQCN = CoreLoggerImpl.class.getName();
private Logger logger = null;
private Class<?> clazz;
private String className;
public CoreLoggerImpl(Logger logger, Class<?> clazz){
this.logger = logger;
this.clazz = clazz;
}
public CoreLoggerImpl(Logger logger, String clazz){
this.logger = logger;
this.className = clazz;
}
public CoreLoggerImpl(String clazz){
this.logger = LoggerFactory.getLogger(clazz);
this.className = clazz;
}
public CoreLoggerImpl(Class<?> clazz){
this.logger = LoggerFactory.getLogger(clazz);
this.clazz = clazz;
}
@Override
public String getName() {
return logger.getName();
}
@Override
public boolean isTraceEnabled() {
return logger.isTraceEnabled();
}
@Override
public void trace(String msg) {
logger.trace(msg);
}
/*
省略其他的
*/
@Override
public void error(Marker marker, String format, Object arg) {
logger.error(marker, format, arg);
}
@Override
public void error(Marker marker, String format, Object arg1, Object arg2) {
logger.error(marker, format, arg1, arg2);
}
@Override
public void error(Marker marker, String format, Object... arguments) {
logger.error(marker, format, arguments);
}
@Override
public void error(Marker marker, String msg, Throwable t) {
logger.error(marker, msg, t);
}
}
然后就可以直接在外部实现获取我么自定义的日志打印了,其实这只是对slf4j进行了一次包装。
在使用过程中,发现这样的包装方式会导致日志打印出来的行号是错误的,行号打印的是MyLoggerImpl的调用行。
在使用slf4j的时候,默认引用的是slf4j的org.slf4j.Logger接口这个Logger接口还有一个子接口org.slf4j.spi.LocationAwareLogger,logback的ch.qos.logback.classic.Logger实现类也实现了这个接口:
public interface LocationAwareLogger extends Logger {
// these constants should be in EventContants. However, in order to preserve binary backward compatibility
// we keep these constants here
final public int TRACE_INT = 00;
final public int DEBUG_INT = 10;
final public int INFO_INT = 20;
final public int WARN_INT = 30;
final public int ERROR_INT = 40;
/**
* Printing method with support for location information.
*
* @param marker The marker to be used for this event, may be null.
* @param fqcn The fully qualified class name of the <b>logger instance</b>,
* typically the logger class, logger bridge or a logger wrapper.
* @param level One of the level integers defined in this interface
* @param message The message for the log event
* @param t Throwable associated with the log event, may be null.
*/
public void log(Marker marker, String fqcn, int level, String message, Object[] argArray, Throwable t);
}
ch.qos.logback.classic.Logger实现部分:
public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {
public static final String FQCN = ch.qos.logback.classic.Logger.class.getName();
private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
final Throwable t) {
final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);
if (decision == FilterReply.NEUTRAL) {
if (effectiveLevelInt > level.levelInt) {
return;
}
} else if (decision == FilterReply.DENY) {
return;
}
buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
}
public void log(Marker marker, String fqcn, int levelInt, String message, Object[] argArray, Throwable t) {
Level level = Level.fromLocationAwareLoggerInteger(levelInt);
filterAndLog_0_Or3Plus(fqcn, marker, level, message, argArray, t);
}
public void debug(String msg) {
filterAndLog_0_Or3Plus(FQCN, null, Level.DEBUG, msg, null, null);
}
}
通过更深的追踪,发现,最终决定行号的是以下的代码:LineOfCallerConverter
public class LineOfCallerConverter extends ClassicConverter {
public String convert(ILoggingEvent le) {
StackTraceElement[] cda = le.getCallerData();
if (cda != null && cda.length > 0) {
return Integer.toString(cda[0].getLineNumber());
} else {
return CallerData.NA;
}
}
}
ILoggingEvent .getCallerData是获取调用栈信息的。然后去第一个节点(也就是我自定义的类)
public class LoggingEvent implements ILoggingEvent {
public LoggingEvent(String fqcn, Logger logger, Level level, String message, Throwable throwable, Object[] argArray) {
this.fqnOfLoggerClass = fqcn;
this.loggerName = logger.getName();
this.loggerContext = logger.getLoggerContext();
this.loggerContextVO = loggerContext.getLoggerContextRemoteView();
this.level = level;
this.message = message;
this.argumentArray = argArray;
if (throwable == null) {
throwable = extractThrowableAnRearrangeArguments(argArray);
}
if (throwable != null) {
this.throwableProxy = new ThrowableProxy(throwable);
LoggerContext lc = logger.getLoggerContext();
if (lc.isPackagingDataEnabled()) {
this.throwableProxy.calculatePackagingData();
}
}
timeStamp = System.currentTimeMillis();
}
public StackTraceElement[] getCallerData() {
if (callerDataArray == null) {
callerDataArray = CallerData
.extract(new Throwable(), fqnOfLoggerClass, loggerContext.getMaxCallerDataDepth(), loggerContext.getFrameworkPackages());
}
return callerDataArray;
}
}
CallerData.extract()方法就是获取slf4j的Logger调用之前的调用栈信息的。
public static StackTraceElement[] extract(Throwable t, String fqnOfInvokingClass, final int maxDepth, List<String> frameworkPackageList) {
if (t == null) {
return null;
}
StackTraceElement[] steArray = t.getStackTrace();
StackTraceElement[] callerDataArray;
int found = LINE_NA;
for (int i = 0; i < steArray.length; i++) {
if (isInFrameworkSpace(steArray[i].getClassName(), fqnOfInvokingClass, frameworkPackageList)) {
// the caller is assumed to be the next stack frame, hence the +1.
found = i + 1;
} else {
if (found != LINE_NA) {
break;
}
}
}
// we failed to extract caller data
if (found == LINE_NA) {
return EMPTY_CALLER_DATA_ARRAY;
}
int availableDepth = steArray.length - found;
int desiredDepth = maxDepth < (availableDepth) ? maxDepth : availableDepth;
callerDataArray = new StackTraceElement[desiredDepth];
for (int i = 0; i < desiredDepth; i++) {
callerDataArray[i] = steArray[found + i];
}
return callerDataArray;
}
主要在于 fqnOfLoggerClass, 和 loggerContext.getMaxCallerDataDepth()参数,一个日志的调用连的开始处,一个是获取的栈深度。fqnOfLoggerClass就是 Logger.log()方法参数中的fqcn 参数。而fqcn就是日志Logger的类名。
所以只要我们控制了fqcn 参数,就可以控制,日志查找的栈位置,进而控制获取的文件以及行号。所以我们需要在我们自定义的日志类中直接调用LocationAwareLogger的log方法,并传入我们自己的 fqcn(MyLoggerImpl.class.getName)。
public class MyLoggerImpl implements MyLogger {
private static final String FQCN = MyLoggerImpl.class.getName();
private Logger logger = null;
private Class<?> clazz;
private String className;
public CoreLoggerImpl(Logger logger, Class<?> clazz){
this.logger = logger;
this.clazz = clazz;
}
public CoreLoggerImpl(Logger logger, String clazz){
this.logger = logger;
this.className = clazz;
}
public CoreLoggerImpl(String clazz){
this.logger = LoggerFactory.getLogger(clazz);
this.className = clazz;
}
public CoreLoggerImpl(Class<?> clazz){
this.logger = LoggerFactory.getLogger(clazz);
this.clazz = clazz;
}
@Override
public String getName() {
return logger.getName();
}
/*
省略其他的
*/
@Override
public void error(Marker marker, String format, Object arg) {
// 做判断是以防更改了日志实现框架之后,logger未实现LocationAwareLogger接口。如果只是logback使用,不需要
//(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t)
if (this.logger instanceof LocationAwareLogger) {
((LocationAwareLogger) this.logger).log(null, FQCN, LocationAwareLogger.ERROR_INT, msg, null, null);
return;
}
logger.error(marker, format, arg);
}
}