Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

上一次主要是给了一个解题方案,没有具体讲解,这次又做到了就来看下几种方案,链表转置一直是我比较疑惑的问题,特别是边界处理,而这个问题要把难度加大了
我先讲一下我一开始的思路和解题方法,首先就是写一个转置方法,就处理 k 个一组的内部转置,然后外部循环处理分组以及前后连接等问题,但是这里就涉及到一个问题,就是前后连接的话对于整个链表头,也就是第一个 k 元素组来说,头是个空的,就需要额外处理,一开始就是用判空做额外处理,然后在前后连接的时候也有一些困扰,看了自己的代码库发现其实之前也做过,而且发现这个思路跟以前做的还是一样的,只不过在处理 k 个元素内部的转置的时候有了点差异

一开始的思路是这样的,想了下其实还是比较有问题,从处理难度上来说,在 k 组内处理的时候一开始 A 节点会是悬空的,然后得处理到组内最后一个元素的时候再接上,逻辑会更复杂,而另一种思路就是直接把 A 先挪到 C 后面,这样每次只需要移动一个,可能思路上会更清晰一点

也就是这样的,这种方案就是之前发过的题解代码,上一篇

而最后这种则代码会简单一些,但是需要一定的理解成本,比较重要的一点是我们加了个虚拟的头结点,第一步会先获取下链表的总长度,然后在内循环里处理转置,重点就是分四步,
也就是图里的,第一步先把 cur 的下一个节点设置成 next 的 next 节点,这里就是图里 A 连接到 C,第二步是把 next 的 next 节点设置成虚拟头的 next节点,第三步是把 pre 的next 节点设置成 next,第四步是 next 往后移动

1
2
3
4
cur.next = next.next;
next.next = pre.next;
pre.next = next;
next = cur.next;


在备注下代码

上周因为一些事情没有更新在这里,是因为新电脑还没到,手头没有把移动硬盘里的 time machine 恢复出来的机器,所以单独更了一篇在新建的一个 cloudflare page 服务上,总体体验还可以,就是有个小点后面可以讲一下,继续完善下 Java 的这个日志,或者说主要讲的是 logback
前面讲了logback 的初始化逻辑,后面就是我最开始来看这个的逻辑,日志的滚动策略的触发逻辑
我们常规的记录日志的方式比如

1
logger.info("log some thing and biz info: {}",biz);

这里就调用了
ch.qos.logback.classic.Logger#info(java.lang.String, java.lang.Object)

1
2
3
public void info(String format, Object arg) {
filterAndLog_1(FQCN, null, Level.INFO, format, arg, null);
}

这里的 FQCN 就是这个类的全限定名

1
public static final String FQCN = ch.qos.logback.classic.Logger.class.getName();

这个具体的 filterAndLog_1 方法还是在同一个 Logger 类里的
ch.qos.logback.classic.Logger#filterAndLog_1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void filterAndLog_1(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t) {

final FilterReply decision = loggerContext.getTurboFilterChainDecision_1(marker, this, level, msg, param, t);

if (decision == FilterReply.NEUTRAL) {
if (effectiveLevelInt > level.levelInt) {
return;
}
} else if (decision == FilterReply.DENY) {
return;
}

buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param }, t);
}

这里面先是走过滤器逻辑,如果是拒绝的,那就不往下执行,也就不执行下面的写日志逻辑了,如果是中性的,那就判断日志级别是否符合,如果直接是拒绝就 return 了,如果是 ACCEPT 就往下执行了

1
2
3
public enum FilterReply {
DENY, NEUTRAL, ACCEPT;
}

然后继续调用同样的类的方法
ch.qos.logback.classic.Logger#buildLoggingEventAndAppend

1
2
3
4
5
6
private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
final Throwable t) {
LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
le.setMarker(marker);
callAppenders(le);
}

先把日志包装成 LoggingEvent ,然后调用
ch.qos.logback.classic.Logger#callAppenders

1
2
3
4
5
6
7
8
9
10
11
12
13
public void callAppenders(ILoggingEvent event) {
int writes = 0;
for (Logger l = this; l != null; l = l.parent) {
writes += l.appendLoopOnAppenders(event);
if (!l.additive) {
break;
}
}
// No appenders in hierarchy
if (writes == 0) {
loggerContext.noAppenderDefinedWarning(this);
}
}

这里会按 Logger 调用 appendLoopOnAppenders

1
2
3
4
5
6
private int appendLoopOnAppenders(ILoggingEvent event) {
if (aai != null) {
return aai.appendLoopOnAppenders(event);
} else {
return 0;
}

继续调用事件的
ch.qos.logback.core.spi.AppenderAttachableImpl#appendLoopOnAppenders

1
2
3
4
5
6
7
8
9
10
public int appendLoopOnAppenders(E e) {
int size = 0;
final Appender<E>[] appenderArray = appenderList.asTypedArray();
final int len = appenderArray.length;
for (int i = 0; i < len; i++) {
appenderArray[i].doAppend(e);
size++;
}
return size;
}

因为 Logger 里有关联 appender,就具体调用关联的 appender 来添加日志
然后就接着调用
ch.qos.logback.core.UnsynchronizedAppenderBase#doAppend 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void doAppend(E eventObject) {
// WARNING: The guard check MUST be the first statement in the
// doAppend() method.

// prevent re-entry.
if (Boolean.TRUE.equals(guard.get())) {
return;
}

try {
guard.set(Boolean.TRUE);

if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
}
return;
}

if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}

// ok, we now invoke derived class' implementation of append
this.append(eventObject);

} catch (Exception e) {
if (exceptionCount++ < ALLOWED_REPEATS) {
addError("Appender [" + name + "] failed to append.", e);
}
} finally {
guard.set(Boolean.FALSE);
}
}

这里先判断了 guard 的状态,guard 是个 ThreadLocal,是为了拦截异常的无限循环调用,这里有些疑惑的,看了一圈可能相关的是前面的 Logger 会往父级查找,然后如果不同层级的 Logger 关联了相同的 appender 的话,或者这个 doAppend 又被其他 appender 调用了,就可能会出现循环调用,然后会在开始实际的 append的之前先设置 guard 状态,
然后再执行 append,这里会走到
ch.qos.logback.core.OutputStreamAppender#append

1
2
3
4
5
6
7
8
@Override
protected void append(E eventObject) {
if (!isStarted()) {
return;
}

subAppend(eventObject);
}

忘了补充下,我们这里使用的是
ch.qos.logback.core.rolling.RollingFileAppender
接下去就是调用的 RollingFileAppendersubAppend 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void subAppend(E event) {
// The roll-over check must precede actual writing. This is the
// only correct behavior for time driven triggers.

// We need to synchronize on triggeringPolicy so that only one rollover
// occurs at a time
synchronized (triggeringPolicy) {
if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
rollover();
}
}

super.subAppend(event);
}

这里就会先判断是否触发事件,
而这里就是主要的逻辑,看是否需要走 rollover,也就是具体的滚动逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public boolean isTriggeringEvent(File activeFile, final E event) {

long time = getCurrentTime();

// first check for roll-over based on time
if (time >= nextCheck) {
Date dateInElapsedPeriod = dateInCurrentPeriod;
elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convertMultipleArguments(dateInElapsedPeriod, currentPeriodsCounter);
currentPeriodsCounter = 0;
setDateInCurrentPeriod(time);
computeNextCheck();
return true;
}

// next check for roll-over based on size
if (invocationGate.isTooSoon(time)) {
return false;
}

if (activeFile == null) {
addWarn("activeFile == null");
return false;
}
if (maxFileSize == null) {
addWarn("maxFileSize = null");
return false;
}
if (activeFile.length() >= maxFileSize.getSize()) {

elapsedPeriodsFileName = tbrp.fileNamePatternWithoutCompSuffix.convertMultipleArguments(dateInCurrentPeriod, currentPeriodsCounter);
currentPeriodsCounter++;
return true;
}

return false;
}

这里主要就用到了 nextCheck 判断,如果通过判断就可以执行滚动了,然后就是后面的判断,是否太快了,也就是 isTooSoon 然后再看当前的活跃日志以及最大文件大小是否设置,然后如果当前的活跃日志的大小超过了配置也会触发滚动

log 初始化过程,首先是在启动类里会获取 logger

1
private static final Log logger = LogFactory.getLog(SpringApplication.class);

然后是

1
2
3
public static Log getLog(Class clazz) throws LogConfigurationException {
return getFactory().getInstance(clazz);
}

继续下去是
org.apache.commons.logging.impl.SLF4JLogFactory#getInstance(java.lang.Class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Log getInstance(Class clazz) throws LogConfigurationException {
return this.getInstance(clazz.getName());
}
public Log getInstance(String name) throws LogConfigurationException {
Log instance = (Log)this.loggerMap.get(name);
if (instance != null) {
return instance;
} else {
Logger slf4jLogger = LoggerFactory.getLogger(name);
Object newInstance;
if (slf4jLogger instanceof LocationAwareLogger) {
newInstance = new SLF4JLocationAwareLog((LocationAwareLogger)slf4jLogger);
} else {
newInstance = new SLF4JLog(slf4jLogger);
}

Log oldInstance = (Log)this.loggerMap.putIfAbsent(name, newInstance);
return (Log)(oldInstance == null ? newInstance : oldInstance);
}
}

然后再往下处理就是 LoggerFactory.getLogger(name); 了,跟普通的获取 logger 一样

1
2
3
4
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}

继续下去会先判断是否已初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}

第一次调用的话就会走到 org.slf4j.LoggerFactory#performInitialization 里面是先调用绑定,这也是 slf4j 门面模式的特点

1
2
3
4
5
6
private final static void performInitialization() {
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
} else {
failedBinding(ncde);
throw ncde;
}
} catch (java.lang.NoSuchMethodError nsme) {
String msg = nsme.getMessage();
if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
INITIALIZATION_STATE = FAILED_INITIALIZATION;
Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
Util.report("Your binding is version 1.5.5 or earlier.");
Util.report("Upgrade your binding to version 1.6.x.");
}
throw nsme;
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}

非安卓环境会进入第一个 if 逻辑,先要去找static_logger_binder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
// use Set instead of list in order to deal with bug #138
// LinkedHashSet appropriate here because it preserves insertion order
// during iteration
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}

上面的 STATIC_LOGGER_BINDER_PATH
就是 org/slf4j/impl/StaticLoggerBinder.class 如果找不到在外层方法就会抛出 NoSuchMethodError 错误,然后就是通过 StaticLoggerBinder.getSingleton(); 获取 StaticLoggerBinder 实例了

1
2
3
4
public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}
private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

比较重要的是这里的 static 代码块还有一个逻辑,就是下面这个初始化逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static {
SINGLETON.init();
}
void init() {
try {
try {
new ContextInitializer(defaultLoggerContext).autoConfig();
} catch (JoranException je) {
Util.report("Failed to auto configure default logger context", je);
}
// logback-292
if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
}
contextSelectorBinder.init(defaultLoggerContext, KEY);
initialized = true;
} catch (Exception t) { // see LOGBACK-1159
Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
}
}

上面的 defaultLoggerContext 也是通过静态代码块处理的

1
private LoggerContext defaultLoggerContext = new LoggerContext();

然后是调用 ch.qos.logback.classic.util.ContextInitializer#autoConfig 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void autoConfig() throws JoranException {
StatusListenerConfigHelper.installIfAsked(loggerContext);
URL url = findURLOfDefaultConfigurationFile(true);
if (url != null) {
configureByResource(url);
} else {
Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
if (c != null) {
try {
c.setContext(loggerContext);
c.configure(loggerContext);
} catch (Exception e) {
throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
.getCanonicalName() : "null"), e);
}
} else {
BasicConfigurator basicConfigurator = new BasicConfigurator();
basicConfigurator.setContext(loggerContext);
basicConfigurator.configure(loggerContext);
}
}
}

前面会先查找配置文件路径
URL url = findURLOfDefaultConfigurationFile(true); 还是调用这个类内的 ch.qos.logback.classic.util.ContextInitializer#findURLOfDefaultConfigurationFile 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public URL findURLOfDefaultConfigurationFile(boolean updateStatus) {
ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this);
URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus);
if (url != null) {
return url;
}

url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus);
if (url != null) {
return url;
}

url = getResource(GROOVY_AUTOCONFIG_FILE, myClassLoader, updateStatus);
if (url != null) {
return url;
}

return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus);
}

才发现原来这里也是硬编码了文件名

1
2
3
4
5
6
7
public class ContextInitializer {

final public static String GROOVY_AUTOCONFIG_FILE = "logback.groovy";
final public static String AUTOCONFIG_FILE = "logback.xml";
final public static String TEST_AUTOCONFIG_FILE = "logback-test.xml";
final public static String CONFIG_FILE_PROPERTY = "logback.configurationFile";

如果不为空就调用了
ch.qos.logback.classic.util.ContextInitializer#configureByResource 这边是根据后缀判断处理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void configureByResource(URL url) throws JoranException {
if (url == null) {
throw new IllegalArgumentException("URL argument cannot be null");
}
final String urlString = url.toString();
if (urlString.endsWith("groovy")) {
if (EnvUtil.isGroovyAvailable()) {
// avoid directly referring to GafferConfigurator so as to avoid
// loading groovy.lang.GroovyObject . See also http://jira.qos.ch/browse/LBCLASSIC-214
GafferUtil.runGafferConfiguratorOn(loggerContext, this, url);
} else {
StatusManager sm = loggerContext.getStatusManager();
sm.add(new ErrorStatus("Groovy classes are not available on the class path. ABORTING INITIALIZATION.", loggerContext));
}
} else if (urlString.endsWith("xml")) {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(loggerContext);
configurator.doConfigure(url);
} else {
throw new LogbackException("Unexpected filename extension of file [" + url.toString() + "]. Should be either .groovy or .xml");
}
}

因为我们找到的是 logback.xml,所以走的是
ch.qos.logback.classic.joran.JoranConfigurator
然后调用了
ch.qos.logback.core.joran.GenericConfigurator#doConfigure(java.net.URL)
这个 GenericConfiguratorJoranConfigurator 的父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final void doConfigure(URL url) throws JoranException {
InputStream in = null;
try {
informContextOfURLUsedForConfiguration(getContext(), url);
URLConnection urlConnection = url.openConnection();
// per http://jira.qos.ch/browse/LBCORE-105
// per http://jira.qos.ch/browse/LBCORE-127
urlConnection.setUseCaches(false);

in = urlConnection.getInputStream();
doConfigure(in, url.toExternalForm());
} catch (IOException ioe) {
String errMsg = "Could not open URL [" + url + "].";
addError(errMsg, ioe);
throw new JoranException(errMsg, ioe);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ioe) {
String errMsg = "Could not close input stream";
addError(errMsg, ioe);
throw new JoranException(errMsg, ioe);
}
}
}
}

然后继续调用
ch.qos.logback.core.joran.GenericConfigurator#doConfigure(java.io.InputStream, java.lang.String) 处理具体的内容,前面处理了内容获取,因为如果是走的 url 文件就需要从网络获取文件流,

1
2
3
4
5
public final void doConfigure(InputStream inputStream, String systemId) throws JoranException {
InputSource inputSource = new InputSource(inputStream);
inputSource.setSystemId(systemId);
doConfigure(inputSource);
}

而实际的处理 xml 内容则是在
ch.qos.logback.core.joran.GenericConfigurator#doConfigure(org.xml.sax.InputSource)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final void doConfigure(final InputSource inputSource) throws JoranException {

long threshold = System.currentTimeMillis();
// if (!ConfigurationWatchListUtil.wasConfigurationWatchListReset(context)) {
// informContextOfURLUsedForConfiguration(getContext(), null);
// }
SaxEventRecorder recorder = new SaxEventRecorder(context);
recorder.recordEvents(inputSource);
doConfigure(recorder.saxEventList);
// no exceptions a this level
StatusUtil statusUtil = new StatusUtil(context);
if (statusUtil.noXMLParsingErrorsOccurred(threshold)) {
addInfo("Registering current configuration as safe fallback point");
registerSafeConfiguration(recorder.saxEventList);
}
}

需要解析 xml 了,先是调用 buildInterpreter 构建了 Interpreter

1
2
3
4
5
6
7
public void doConfigure(final List<SaxEvent> eventList) throws JoranException {
buildInterpreter();
// disallow simultaneous configurations of the same context
synchronized (context.getConfigurationLock()) {
interpreter.getEventPlayer().play(eventList);
}
}

build 里还有一些逻辑,就是匹配后面要处理的规则集,就是要确认那些标签要处理

1
2
3
4
5
6
7
8
9
protected void buildInterpreter() {
RuleStore rs = new SimpleRuleStore(context);
addInstanceRules(rs);
this.interpreter = new Interpreter(context, rs, initialElementPath());
InterpretationContext interpretationContext = interpreter.getInterpretationContext();
interpretationContext.setContext(context);
addImplicitRules(interpreter);
addDefaultNestedComponentRegistryRules(interpretationContext.getDefaultNestedComponentRegistry());
}

第一步就是先 new 一个 ch.qos.logback.core.joran.spi.SimpleRuleStore 然后调用了 ch.qos.logback.classic.joran.JoranConfigurator#addInstanceRules 来添加规则,就是下面的,可以看到对于 logger,appender 这些的配置,其中前面点的父类也又一些规则 ,ch.qos.logback.core.joran.JoranConfiguratorBase 逻辑类似就不展开了,这里的规则就对应了后面的各种 Action,就是对应的处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public void addInstanceRules(RuleStore rs) {
// parent rules already added
super.addInstanceRules(rs);

rs.addRule(new ElementSelector("configuration"), new ConfigurationAction());

rs.addRule(new ElementSelector("configuration/contextName"), new ContextNameAction());
rs.addRule(new ElementSelector("configuration/contextListener"), new LoggerContextListenerAction());
rs.addRule(new ElementSelector("configuration/insertFromJNDI"), new InsertFromJNDIAction());
rs.addRule(new ElementSelector("configuration/evaluator"), new EvaluatorAction());

rs.addRule(new ElementSelector("configuration/appender/sift"), new SiftAction());
rs.addRule(new ElementSelector("configuration/appender/sift/*"), new NOPAction());

rs.addRule(new ElementSelector("configuration/logger"), new LoggerAction());
rs.addRule(new ElementSelector("configuration/logger/level"), new LevelAction());

rs.addRule(new ElementSelector("configuration/root"), new RootLoggerAction());
rs.addRule(new ElementSelector("configuration/root/level"), new LevelAction());
rs.addRule(new ElementSelector("configuration/logger/appender-ref"), new AppenderRefAction<ILoggingEvent>());
rs.addRule(new ElementSelector("configuration/root/appender-ref"), new AppenderRefAction<ILoggingEvent>());

// add if-then-else support
rs.addRule(new ElementSelector("*/if"), new IfAction());
rs.addRule(new ElementSelector("*/if/then"), new ThenAction());
rs.addRule(new ElementSelector("*/if/then/*"), new NOPAction());
rs.addRule(new ElementSelector("*/if/else"), new ElseAction());
rs.addRule(new ElementSelector("*/if/else/*"), new NOPAction());

// add jmxConfigurator only if we have JMX available.
// If running under JDK 1.4 (retrotranslateed logback) then we
// might not have JMX.
if (PlatformInfo.hasJMXObjectName()) {
rs.addRule(new ElementSelector("configuration/jmxConfigurator"), new JMXConfiguratorAction());
}
rs.addRule(new ElementSelector("configuration/include"), new IncludeAction());

rs.addRule(new ElementSelector("configuration/consolePlugin"), new ConsolePluginAction());

rs.addRule(new ElementSelector("configuration/receiver"), new ReceiverAction());

}

然后调用了 interpreter 的 eventPlayer 处理 xml 的各个标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void play(List<SaxEvent> aSaxEventList) {
eventList = aSaxEventList;
SaxEvent se;
for (currentIndex = 0; currentIndex < eventList.size(); currentIndex++) {
se = eventList.get(currentIndex);

if (se instanceof StartEvent) {
interpreter.startElement((StartEvent) se);
// invoke fireInPlay after startElement processing
interpreter.getInterpretationContext().fireInPlay(se);
}
if (se instanceof BodyEvent) {
// invoke fireInPlay before characters processing
interpreter.getInterpretationContext().fireInPlay(se);
interpreter.characters((BodyEvent) se);
}
if (se instanceof EndEvent) {
// invoke fireInPlay before endElement processing
interpreter.getInterpretationContext().fireInPlay(se);
interpreter.endElement((EndEvent) se);
}

}
}

接下去就在 startElement 中调用会先获取 getApplicableActionList 适配的 Action, callBeginAction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void startElement(String namespaceURI, String localName, String qName, Attributes atts) {

String tagName = getTagName(localName, qName);
elementPath.push(tagName);

if (skip != null) {
// every startElement pushes an action list
pushEmptyActionList();
return;
}

List<Action> applicableActionList = getApplicableActionList(elementPath, atts);
if (applicableActionList != null) {
actionListStack.add(applicableActionList);
callBeginAction(applicableActionList, tagName, atts);
} else {
// every startElement pushes an action list
pushEmptyActionList();
String errMsg = "no applicable action for [" + tagName + "], current ElementPath is [" + elementPath + "]";
cai.addError(errMsg);
}
}

里面就是用了 ruleStore 来判断是否适配,并且取出对应的 Action

1
2
3
4
5
6
7
8
9
10
List<Action> getApplicableActionList(ElementPath elementPath, Attributes attributes) {
List<Action> applicableActionList = ruleStore.matchActions(elementPath);

// logger.debug("set of applicable patterns: " + applicableActionList);
if (applicableActionList == null) {
applicableActionList = lookupImplicitAction(elementPath, attributes, interpretationContext);
}

return applicableActionList;
}

比如我这边往下处理,通过规则匹配拿到了 LoggerAction, 然后调用 Action.begin 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void callBeginAction(List<Action> applicableActionList, String tagName, Attributes atts) {
if (applicableActionList == null) {
return;
}

Iterator<Action> i = applicableActionList.iterator();
while (i.hasNext()) {
Action action = (Action) i.next();
// now let us invoke the action. We catch and report any eventual
// exceptions
try {
action.begin(interpretationContext, tagName, atts);
} catch (ActionException e) {
skip = elementPath.duplicate();
cai.addError("ActionException in Action for tag [" + tagName + "]", e);
} catch (RuntimeException e) {
skip = elementPath.duplicate();
cai.addError("RuntimeException in Action for tag [" + tagName + "]", e);
}
}
}

就是下面这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void begin(InterpretationContext ec, String name, Attributes attributes) {
// Let us forget about previous errors (in this object)
inError = false;
logger = null;

LoggerContext loggerContext = (LoggerContext) this.context;

String loggerName = ec.subst(attributes.getValue(NAME_ATTRIBUTE));

if (OptionHelper.isEmpty(loggerName)) {
inError = true;
String aroundLine = getLineColStr(ec);
String errorMsg = "No 'name' attribute in element " + name + ", around " + aroundLine;
addError(errorMsg);
return;
}

logger = loggerContext.getLogger(loggerName);

String levelStr = ec.subst(attributes.getValue(LEVEL_ATTRIBUTE));

if (!OptionHelper.isEmpty(levelStr)) {
if (ActionConst.INHERITED.equalsIgnoreCase(levelStr) || ActionConst.NULL.equalsIgnoreCase(levelStr)) {
addInfo("Setting level of logger [" + loggerName + "] to null, i.e. INHERITED");
logger.setLevel(null);
} else {
Level level = Level.toLevel(levelStr);
addInfo("Setting level of logger [" + loggerName + "] to " + level);
logger.setLevel(level);
}
}

String additivityStr = ec.subst(attributes.getValue(ActionConst.ADDITIVITY_ATTRIBUTE));
if (!OptionHelper.isEmpty(additivityStr)) {
boolean additive = OptionHelper.toBoolean(additivityStr, true);
addInfo("Setting additivity of logger [" + loggerName + "] to " + additive);
logger.setAdditive(additive);
}
ec.pushObject(logger);
}

这里就会处理 logger 获取逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public final Logger getLogger(final String name) {

if (name == null) {
throw new IllegalArgumentException("name argument cannot be null");
}

// if we are asking for the root logger, then let us return it without
// wasting time
if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
return root;
}

int i = 0;
Logger logger = root;

// check if the desired logger exists, if it does, return it
// without further ado.
Logger childLogger = (Logger) loggerCache.get(name);
// if we have the child, then let us return it without wasting time
if (childLogger != null) {
return childLogger;
}

// if the desired logger does not exist, them create all the loggers
// in between as well (if they don't already exist)
String childName;
while (true) {
int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
if (h == -1) {
childName = name;
} else {
childName = name.substring(0, h);
}
// move i left of the last point
i = h + 1;
synchronized (logger) {
childLogger = logger.getChildByName(childName);
if (childLogger == null) {
childLogger = logger.createChildByName(childName);
loggerCache.put(childName, childLogger);
incSize();
}
}
logger = childLogger;
if (h == -1) {
return childLogger;
}
}
}

如果从 loggerCache 中能取到就直接获取,如果不能就会去创建

我们在使用 Java 的日志库的时候,比如我们现在项目在用的 logback,可以配置滚动策略,简单介绍下启动逻辑,这里我们定义的是
ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy 它是继承了 ch.qos.logback.core.rolling.TimeBasedRollingPolicy
它的启动方法就是下面这个 start 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void start() {
SizeAndTimeBasedFNATP<E> sizeAndTimeBasedFNATP = new SizeAndTimeBasedFNATP<E>(Usage.EMBEDDED);
if(maxFileSize == null) {
addError("maxFileSize property is mandatory.");
return;
} else {
addInfo("Archive files will be limited to ["+maxFileSize+"] each.");
}

sizeAndTimeBasedFNATP.setMaxFileSize(maxFileSize);
timeBasedFileNamingAndTriggeringPolicy = sizeAndTimeBasedFNATP;

if(!isUnboundedTotalSizeCap() && totalSizeCap.getSize() < maxFileSize.getSize()) {
addError("totalSizeCap of ["+totalSizeCap+"] is smaller than maxFileSize ["+maxFileSize+"] which is non-sensical");
return;
}

// most work is done by the parent
super.start();
}

配置了 timeBasedFileNamingAndTriggeringPolicy 策略

然后调用了父类的启动方法,主要看下父类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public void start() {
// set the LR for our utility object
renameUtil.setContext(this.context);

// find out period from the filename pattern
if (fileNamePatternStr != null) {
fileNamePattern = new FileNamePattern(fileNamePatternStr, this.context);
determineCompressionMode();
} else {
addWarn(FNP_NOT_SET);
addWarn(CoreConstants.SEE_FNP_NOT_SET);
throw new IllegalStateException(FNP_NOT_SET + CoreConstants.SEE_FNP_NOT_SET);
}

compressor = new Compressor(compressionMode);
compressor.setContext(context);

// wcs : without compression suffix
fileNamePatternWithoutCompSuffix = new FileNamePattern(Compressor.computeFileNameStrWithoutCompSuffix(fileNamePatternStr, compressionMode), this.context);

addInfo("Will use the pattern " + fileNamePatternWithoutCompSuffix + " for the active file");

if (compressionMode == CompressionMode.ZIP) {
String zipEntryFileNamePatternStr = transformFileNamePattern2ZipEntry(fileNamePatternStr);
zipEntryFileNamePattern = new FileNamePattern(zipEntryFileNamePatternStr, context);
}

if (timeBasedFileNamingAndTriggeringPolicy == null) {
timeBasedFileNamingAndTriggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<E>();
}
timeBasedFileNamingAndTriggeringPolicy.setContext(context);
timeBasedFileNamingAndTriggeringPolicy.setTimeBasedRollingPolicy(this);
timeBasedFileNamingAndTriggeringPolicy.start();

if (!timeBasedFileNamingAndTriggeringPolicy.isStarted()) {
addWarn("Subcomponent did not start. TimeBasedRollingPolicy will not start.");
return;
}

// the maxHistory property is given to TimeBasedRollingPolicy instead of to
// the TimeBasedFileNamingAndTriggeringPolicy. This makes it more convenient
// for the user at the cost of inconsistency here.
if (maxHistory != UNBOUND_HISTORY) {
archiveRemover = timeBasedFileNamingAndTriggeringPolicy.getArchiveRemover();
archiveRemover.setMaxHistory(maxHistory);
archiveRemover.setTotalSizeCap(totalSizeCap.getSize());
if (cleanHistoryOnStart) {
addInfo("Cleaning on start up");
Date now = new Date(timeBasedFileNamingAndTriggeringPolicy.getCurrentTime());
cleanUpFuture = archiveRemover.cleanAsynchronously(now);
}
} else if (!isUnboundedTotalSizeCap()) {
addWarn("'maxHistory' is not set, ignoring 'totalSizeCap' option with value ["+totalSizeCap+"]");
}

super.start();
}
  • 第一步是给 renameUtil 设置 context

  • 第二步是判断 fileNamePatternStr是否已配置,没配置会报错,如果配置了就会去判断压缩格式,是否要压缩以及压缩的是 gz 还是 zip

  • 第三步就是设置压缩器了

  • 第四步是判断文件后缀格式,注意是不带压缩格式的

  • 第五步是判断 timeBasedFileNamingAndTriggeringPolicy 是否已设置,这里是在子类里已经设置了 ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP 否则就是 ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy 这个默认的

  • 第六步比较重要就是启动 timeBasedFileNamingAndTriggeringPolicy

timeBasedFileNamingAndTriggeringPolicy 的启动逻辑里首先是调用了 SizeAndTimeBasedFNATP 的 start 方法,然后里面最开始调用了父类的 start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public void start() {
// we depend on certain fields having been initialized in super class
super.start();

if(usage == Usage.DIRECT) {
addWarn(CoreConstants.SIZE_AND_TIME_BASED_FNATP_IS_DEPRECATED);
addWarn("For more information see "+MANUAL_URL_PREFIX+"appenders.html#SizeAndTimeBasedRollingPolicy");
}

if (!super.isErrorFree())
return;


if (maxFileSize == null) {
addError("maxFileSize property is mandatory.");
withErrors();
}

if (!validateDateAndIntegerTokens()) {
withErrors();
return;
}

archiveRemover = createArchiveRemover();
archiveRemover.setContext(context);

// we need to get the correct value of currentPeriodsCounter.
// usually the value is 0, unless the appender or the application
// is stopped and restarted within the same period
String regex = tbrp.fileNamePattern.toRegexForFixedDate(dateInCurrentPeriod);
String stemRegex = FileFilterUtil.afterLastSlash(regex);

computeCurrentPeriodsHighestCounterValue(stemRegex);

if (isErrorFree()) {
started = true;
}
}

也就是下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void start() {
DateTokenConverter<Object> dtc = tbrp.fileNamePattern.getPrimaryDateTokenConverter();
if (dtc == null) {
throw new IllegalStateException("FileNamePattern [" + tbrp.fileNamePattern.getPattern() + "] does not contain a valid DateToken");
}

if (dtc.getTimeZone() != null) {
rc = new RollingCalendar(dtc.getDatePattern(), dtc.getTimeZone(), Locale.getDefault());
} else {
rc = new RollingCalendar(dtc.getDatePattern());
}
addInfo("The date pattern is '" + dtc.getDatePattern() + "' from file name pattern '" + tbrp.fileNamePattern.getPattern() + "'.");
rc.printPeriodicity(this);

if (!rc.isCollisionFree()) {
addError("The date format in FileNamePattern will result in collisions in the names of archived log files.");
addError(CoreConstants.MORE_INFO_PREFIX + COLLIDING_DATE_FORMAT_URL);
withErrors();
return;
}

setDateInCurrentPeriod(new Date(getCurrentTime()));
if (tbrp.getParentsRawFileProperty() != null) {
File currentFile = new File(tbrp.getParentsRawFileProperty());
if (currentFile.exists() && currentFile.canRead()) {
setDateInCurrentPeriod(new Date(currentFile.lastModified()));
}
}
addInfo("Setting initial period to " + dateInCurrentPeriod);
computeNextCheck();
}

前面是日期时区等配置处理,然后是判断按日期生成的文件会不会冲突,接下去是设置当前时间段,如果配置了日志文件的话就会把当前时间段设置成已有的日志文件的最后更改时间,最后的 computeNextCheck 比较重要

主要是下面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
protected void computeNextCheck() {
nextCheck = rc.getNextTriggeringDate(dateInCurrentPeriod).getTime();
}
public Date getNextTriggeringDate(Date now) {
return getEndOfNextNthPeriod(now, 1);
}
public Date getEndOfNextNthPeriod(Date now, int periods) {
return innerGetEndOfNextNthPeriod(this, this.periodicityType, now, periods);
}
static private Date innerGetEndOfNextNthPeriod(Calendar cal, PeriodicityType periodicityType, Date now, int numPeriods) {
cal.setTime(now);
switch (periodicityType) {
case TOP_OF_MILLISECOND:
cal.add(Calendar.MILLISECOND, numPeriods);
break;

case TOP_OF_SECOND:
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.SECOND, numPeriods);
break;

case TOP_OF_MINUTE:
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.MINUTE, numPeriods);
break;

case TOP_OF_HOUR:
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.HOUR_OF_DAY, numPeriods);
break;

case TOP_OF_DAY:
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.DATE, numPeriods);
break;

case TOP_OF_WEEK:
cal.set(Calendar.DAY_OF_WEEK, cal.getFirstDayOfWeek());
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.WEEK_OF_YEAR, numPeriods);
break;

case TOP_OF_MONTH:
cal.set(Calendar.DATE, 1);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.add(Calendar.MONTH, numPeriods);
break;

default:
throw new IllegalStateException("Unknown periodicity type.");
}

return cal.getTime();

 会按照我们设置的 FileNamePattern 中的 datePattern 来推断 periodicityType,比如我们是小时滚动的,那就是 TOP_OF_HOUR ,算出来下一个时间检查点,后面会按这个判断是否作为触发事件触发日志滚动更新

父类的 start 逻辑讲完以后,子类的其实比较简单,先判断使用方式,我们这是嵌入式的,然后是父类有没有产生错误,继续是最大文件大小是否设置了,再判断日期格式是否正常,然后是归档移除类的创建,并设置到上下文中,然后计算当前时间的最大日志文件计数器,最后判断是否报错,没有的话就启动成功了

php 代码调试里的神器就是 echo 111;exit; 但是对于使用了接口和继承比较多的话,有时候比较难找,可能定位到了一段代码但是不知道怎么调用过来的,这时候就可以用这个方法

总结下来有三种,

第一种是最巧妙的

1
2
3
function a() {
echo 111;exit;
}

比如本来是上面这样子,那么其实我们可以主动new 个异常

1
2
3
4
5
function a() {
$e = new Exception();
print_r($e->getTraceAsString());
echo 111;exit;
}

这样我的 trace 调用链路就出来了

第二种

这个就是比较简单的,调用 php 自身提供的方法

1
debug_backtrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): array

第一个参数是个掩码

debug_backtrace()Populates both indexes
debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT)
debug_backtrace(1)
debug_backtrace(0)Omits index "object" and populates index "args".
debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)Omits index "object" and index "args".
debug_backtrace(2)
debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECTDEBUG_BACKTRACE_IGNORE_ARGS)
debug_backtrace(3)

第二个参数是限制栈深度

第三种

这个也是用自身的方法

1
debug_print_backtrace(int $options = 0, int $limit = 0): void

这里的第一个参数只有一个可以传的

DEBUG_BACKTRACE_IGNORE_ARGSWhether or not to omit the “args” index, and thus all the function/method arguments, to save memory.

就是隐藏参数,不然如果对于一些框架代码,这个打印会非常大,需要注意下

0%