lable  

从启动配置项我们可以看到tomcat自己实现了一个日志类来代替jdk默认提供的日志组件。
那么tomcat为什么要自己实现一个日志组件呢?我们通过这套日志组件我们又可以做什么?
通过源码我们可以发现tomcat实现了一个日志工厂(LogFactory),除了支持自己实现的一套基于JUL封装的DirectJDKLog类外还支持了自定义Log类

private LogFactory() {
    /*
     * Work-around known a JRE bug.
     * https://bugs.openjdk.java.net/browse/JDK-8194653
     *
     * Pre-load the default file system. No performance impact as we need to
     * load the default file system anyway. Just do it earlier to avoid the
     * potential deadlock.
     *
     * This can be removed once the oldest JRE supported by Tomcat includes
     * a fix.
     */
    FileSystems.getDefault();

    // Look via a ServiceLoader for a Log implementation that has a
    // constructor taking the String name.
    // 通过ServiceLoader获取自定义的Log类构造函器并保存到discoveredLogConstructor成员变量中
    ServiceLoader<Log> logLoader = ServiceLoader.load(Log.class);
    Constructor<? extends Log> m=null;
    for (Log log: logLoader) {
        Class<? extends Log> c=log.getClass();
        try {
            m=c.getConstructor(String.class);
            break;
        }
        catch (NoSuchMethodException | SecurityException e) {
            throw new Error(e);
        }
    }
    discoveredLogConstructor=m;
}
public Log getInstance(String name) throws LogConfigurationException {
    // 如果用户没有使用自定义的Log类则走默认的DirectJDKLog
    if (discoveredLogConstructor == null) {
        return DirectJDKLog.getInstance(name);
    }
    try {
        return discoveredLogConstructor.newInstance(name);
    } catch (ReflectiveOperationException | IllegalArgumentException e) {
        throw new LogConfigurationException(e);
    }
}

可以看出只要符合JULI就可以被服务发现并使用,这里配置一下自定义的Log简单体验下tomcat为我们提供的日志服务发现功能

// ServiceLoader.java
private static final String PREFIX = "META-INF/services/";  // 这里读的是META-INF/services/源码中需要自己编译后创建该文件
// CustomJDKLog.java (org.apache.juli.logging包下创建的一个新类)
package org.apache.juli.logging;
public class CustomJDKLog extends DirectJDKLog {
    public CustomJDKLog() {
        super(CustomJDKLog.class.getName());
        System.out.println(CustomJDKLog.class.getName());
    }
    public CustomJDKLog(String name) {
        super(name);
    }
    static Log getInstance(String name) {
        System.out.println("getInstance:" + CustomJDKLog.class.getName());
        return new CustomJDKLog( name );
    }
}

替换日志类

# 项目根目录下执行
mkdir -p target/classes/META-INF/services
cd ./target/classes/META-INF/services
touch org.apache.juli.logging.Log

# 只展示target目录下的主要目录文件
├── classes
│   ├── META-INF
│   │   └── services
│   │       └── org.apache.juli.logging.Log # 这里配置上需要被服务发现的类
│   ├── javax
...

# org.apache.juli.logging.Log文件上配置一行
echo "org.apache.juli.logging.CustomJDKLog" > ./target/classes/META-INF/services/org.apache.juli.logging.Log

启动tomcat可以发现我们的日志类已经替换默认的DirectJDKLog,也就是说只要我们符合juli规范即可利用tomcat的服务发现提供jar包的形式替换掉系统默认的DirectJDKLog

接下来再看看tomcat默认配置是怎么为我们划分日志的

# 声明所有使用的handler,与后面的配置相对应,前缀是十进制的话会被代码切割出类名
handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3manager.org.apache.juli.AsyncFileHandler, 4host-manager.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
# 指定rootLogger处理日志的Handler
.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler

# 和上面handlers关联的配置项
1catalina.org.apache.juli.AsyncFileHandler.level = FINE
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8

# 日志等级
2localhost.org.apache.juli.AsyncFileHandler.level = FINE
# 目标文件目录
2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
# 目标文件前缀名称
2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost.
# 编码格式
2localhost.org.apache.juli.AsyncFileHandler.encoding = UTF-8

3manager.org.apache.juli.AsyncFileHandler.level = FINE
3manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
3manager.org.apache.juli.AsyncFileHandler.prefix = manager.
3manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8

4host-manager.org.apache.juli.AsyncFileHandler.level = FINE
4host-manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
4host-manager.org.apache.juli.AsyncFileHandler.prefix = host-manager.
4host-manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8

java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter
java.util.logging.ConsoleHandler.encoding = UTF-8

# 自定义logger名称的可以通过以下配置关联handler
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler

org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = 3manager.org.apache.juli.AsyncFileHandler

org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].handlers = 4host-manager.org.apache.juli.AsyncFileHandler

日志文件:

.
├── catalina.2022-02-04.log # 服务启动日志
├── host-manager.2022-02-04.log # 
├── localhost.2022-02-04.log # 系统错误日志
├── localhost_access_log.2022-02-04.txt # 
└── manager.2022-02-04.log # 

JUL框架:

从源码上可以看到tomcat会根据配置的handlers和.handlers实例化Handler对象放在一个WeakHashMap和classloader关联(Handler对象会找到logging.properties内的关联handlers的配置,比如1catalina.org.apache.juli.AsyncFileHandler.xxx),接着在调用方调用getLogger时调用addLogger实例化Logger并关联Handler,日志是先写入AsyncFileHandler队列再通过一个线程不断从队列中取出LogRecord写入文件。

// ClassLoaderLogManager.java
protected synchronized void readConfiguration(InputStream is, ClassLoader classLoader)
    ...
    // 最上层rootLogger使用.handlers配置的handler
    String rootHandlers = info.props.getProperty(".handlers");
    String handlers = info.props.getProperty("handlers");
    Logger localRootLogger = info.rootNode.logger;
    if (handlers != null) {
        StringTokenizer tok = new StringTokenizer(handlers, ",");
        while (tok.hasMoreTokens()) {
            String handlerName = (tok.nextToken().trim());
            String handlerClassName = handlerName;
            String prefix = "";
            if (handlerClassName.length() <= 0) {
                continue;
            }
            // 10进制的前缀才会切割出prefix
            if (Character.isDigit(handlerClassName.charAt(0))) {
                int pos = handlerClassName.indexOf('.');
                if (pos >= 0) {
                    prefix = handlerClassName.substring(0, pos + 1);
        	   // 切割出handlers配置的类名
                    handlerClassName = handlerClassName.substring(pos + 1);
                }
            }
            try {
                // 设置了prefix进ThreadLocal这样实例化出来的AsyncFileHandler通过configure方法就能拿到正确的配置项
                this.prefix.set(prefix);
                // 根据类名实例化Handler(AsyncFileHandler、ConsoleHandler)
                Handler handler = (Handler) classLoader.loadClass(
                        handlerClassName).getConstructor().newInstance();
                // The specification strongly implies all configuration should be done
                // during the creation of the handler object.
                // This includes setting level, filter, formatter and encoding.
                this.prefix.set(null);
                // 将实例化出来的handler和classloader关联
                info.handlers.put(handlerName, handler);
                if (rootHandlers == null) {
                    localRootLogger.addHandler(handler);
                }
            } catch (Exception e) {
                // Report error
                System.err.println("Handler error");
                e.printStackTrace();
            }
        }
    }
}
public synchronized boolean addLogger(final Logger logger) {
    ...
    // 设置日志等级
    final String levelString = getProperty(loggerName + ".level");
    if (levelString != null) {
        try {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override
                public Void run() {
                    logger.setLevel(Level.parse(levelString.trim()));
                    return null;
                }
            });
        } catch (IllegalArgumentException e) {
            // Leave level set to null
        }
    }

    // Always instantiate parent loggers so that
    // we can control log categories even during runtime
    int dotIndex = loggerName.lastIndexOf('.');
    if (dotIndex >= 0) {
        // getLogger内会调用addLogger递归添加logger
        final String parentName = loggerName.substring(0, dotIndex);
        Logger.getLogger(parentName);
    }
    ...
    // 将Logger实例和Handler实例关联起来
    String handlers = getProperty(loggerName + ".handlers");
    if (handlers != null) {
        logger.setUseParentHandlers(false);
        StringTokenizer tok = new StringTokenizer(handlers, ",");
        while (tok.hasMoreTokens()) {
            String handlerName = (tok.nextToken().trim());
            Handler handler = null;
            ClassLoader current = classLoader;
            while (current != null) {
                info = classLoaderLoggers.get(current);
                if (info != null) {
                    handler = info.handlers.get(handlerName);
                    if (handler != null) {
                        break;
                    }
                }
                current = current.getParent();
            }
            if (handler != null) {
                logger.addHandler(handler);
            }
        }
    }
    ...
}

// FileHandler.java
// 在ClassLoaderLogManager的 readConfiguration 反射调用了空参构造
public FileHandler() {
    this(null, null, null, DEFAULT_MAX_DAYS);
}
public FileHandler(String directory, String prefix, String suffix, int maxDays) {
    this.directory = directory;
    this.prefix = prefix;
    this.suffix = suffix;
    this.maxDays = maxDays;
    // 空参构造函通过configure会调用ClassLoaderLogManager的getProperty方法获取配置,这里会产生日志的全路径和文件名
    configure();
    openWriter();
    clean();
}

// AsyncFileHandler.java
public static void deregisterHandler() {
    int newCount = handlerCount.decrementAndGet();
    if (newCount == 0) {
        try {
            Thread dummyHook = new Thread();
            // 添加钩子函数在程序关闭时把队列中的日志记录落地
            Runtime.getRuntime().addShutdownHook(dummyHook);
            Runtime.getRuntime().removeShutdownHook(dummyHook);
        } catch (IllegalStateException ise) {
            // JVM is shutting down.
            // Allow up to 10s for for the queue to be emptied
            // 队列为空,尝试休眠,防止空转
            int sleepCount = 0;
            while (!AsyncFileHandler.queue.isEmpty() && sleepCount < 10000) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    // Ignore
                }
                sleepCount++;
            }
        }
    }
}
public void run() {
    while (true) {
        try {
            LogEntry entry = queue.take();
            // 将日志刷盘
            entry.flush();
        } catch (InterruptedException x) {
            // Ignore the attempt to interrupt the thread.
        } catch (Exception x) {
            x.printStackTrace();
        }
    }
}
posted on 2022-02-04 22:56  lable  阅读(109)  评论(0编辑  收藏  举报