Java日志——JUL(java.util.logging)的使用及原理

日志原理简述

日志功能的实现基本靠一下几个组件来完成:

  • Loggers:Logger负责捕捉事件并将其发送给合适的Appender
  • Appenders:也称为Handlers,负责从Logger中取出日志消息,并使用Layout来格式化消息,然后将消息发送出去,比如发送到控制台、文件或其他日志收集系统。
  • Layouts:也称为Formatters,负责对日志事件进中的数据进行转换和格式化。
  • Filters:过滤器,根据需要定制哪些信息会被记录,哪些信息会被放过。

总结一下就是:用户使用Logger来进行日志记录,Logger持有若干个Handler,日志的输出操作是由Handler完成的。在Handler在输出日志前,会经过Filter的过滤,判断代码是否可以继续执行,Filter返回false,日志方法return,Handler不会处理;Filter返回true,则继续向下执行,Handler会将日志内容输出到指定位置(日志文件、控制台等)。Handler在输出日志时会使用Layout,将输出内容进行排版。

JUL

JUL是JDK自带的一个日志实现,java.util.logging包下的各个类提供了原生的日志记录和输出支持。

JUL定义的日志级别(Level类)

要了解JUL的日志级别就要看一下java.util.logging.Level这个类,具体的日志级别都定义在了代码里。

// 用于关闭日志功能
public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);
// 用于表明程序严重的失败或错误
public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);
// 用于表明潜在的问题
public static final Level WARNING = new Level("WARNING", 900, defaultBundle);
// 信息级别,通过消息会被输入到控制台,所以信息级别一般记录对最终用户或系统管理员有用的信息
public static final Level INFO = new Level("INFO", 800, defaultBundle);
// 配置级别,用于表明系统静态配置的信息
public static final Level CONFIG = new Level("CONFIG", 700, defaultBundle);
// 用于提供跟踪信息的消息级别
public static final Level FINE = new Level("FINE", 500, defaultBundle);
// 用于提供跟踪信息的消息级别
public static final Level FINER = new Level("FINER", 400, defaultBundle);
// 用于提供跟踪信息的消息级别
public static final Level FINEST = new Level("FINEST", 300, defaultBundle);
// 记录所有级别的日志
public static final Level ALL = new Level("ALL", Integer.MIN_VALUE, defaultBundle);

JUL的Level提供了三个跟踪信息的级别,FINE、FINER、FINEST的确切含义在不同子系统之间变化,但通常用FINEST记录大量最详细的日志信息,用FINER记录记录相比于FINEST精简的日志信息,FINEST记录最精简,也是最重要的日志信息。

关于构造函数中defaultBundle的解释

// 默认资源包位置
private static final String defaultBundle = "sun.util.logging.resources.logging";
// 用于本地化级别名称的资源包名称
private final String resourceBundleName;

resourceBundleName是用来指定外部资源包的,如果不指定,会默认用defaultBundle指定的资源包,资源包是干嘛的呢,我在rt.jar包下找到了这个资源。

package sun.util.logging.resources;

import java.util.ListResourceBundle;

public final class logging extends ListResourceBundle {
    public logging() {
    }

    protected final Object[][] getContents() {
        return new Object[][]{{"ALL", "All"}, {"CONFIG", "Config"}, {"FINE", "Fine"}, {"FINER", "Finer"}, {"FINEST", "Finest"}, {"INFO", "Info"}, {"OFF", "Off"}, {"SEVERE", "Severe"}, {"WARNING", "Warning"}};
    }
}

原来就是控制台输出日志时,定义本地化后的级别名称。或许你会有疑问,明明在控制台上看到的是警告、信息等中文的Level,这实际上是SimpleFormatter进行的处理。

public synchronized String format(LogRecord record) {
	// 省略方法前半部分的代码
	return String.format(format,
                             dat,
                             source,
                             record.getLoggerName(),
                             record.getLevel().getLocalizedLevelName(),
                             message,
                             throwable);
}

在进行格式化的时候,传入了record.getLevel().getLocalizedLevelName(),我们再看Level类的该方法,打断点可以看到具体本地化的过程。

final synchronized String getLocalizedLevelName() {

        // 先拿缓存的locale值
        final String cachedLocalizedName = getCachedLocalizedLevelName();
        if (cachedLocalizedName != null) {
            return cachedLocalizedName;
        }

        // 如果没有缓存的locale取电脑默认的Loacle
        final Locale newLocale = Locale.getDefault();
        try {
            // 用locale去处理defaultBundle = "sun.util.logging.resources.logging"中的英文级别
            localizedLevelName = computeLocalizedLevelName(newLocale);
        } catch (Exception ex) {
            localizedLevelName = name;
        }
        cachedLocale = newLocale;
        return localizedLevelName;
    }

除了上面这些内容,Level类还包含了一个静态内部类KnownLevel和两个transient修饰的属性。

    // 用transient修饰,在序列化时会忽略这两个属性
    // 用来缓存通过本地locale翻译的Level级别名称
    private transient String localizedLevelName;
    // 用来缓存本地Locale对象
    private transient Locale cachedLocale;

Level中的静态内部类KnownLevel的功能

knownlevel类维护所有已知级别的全局列表。api允许同一名称/值的多个自定义级别实例创建(根据nameToLevels、intToLevels两个Map对象也可以看出同一个key对应多个value)。这个类提供了方便的方法来查找级别,例如:通过给定的名称、给定的值或给定的本地化名称。

static final class KnownLevel {
        private static Map<String, List<KnownLevel>> nameToLevels = new HashMap<>();
        private static Map<Integer, List<KnownLevel>> intToLevels = new HashMap<>();
        // 用于存放标准的Level对象或客户自定义Level的对象
        final Level levelObject;
        // 表示日志配置中指定级别的级别对象。在match方法中用到
        final Level mirroredLevel;
        KnownLevel(Level l) {
            this.levelObject = l;
            if (l.getClass() == Level.class) {
                this.mirroredLevel = l;
            } else {
                // this mirrored level object is hidden
                this.mirroredLevel = new Level(l.name, l.value, l.resourceBundleName, false);
            }
        }
		// 将新建的Level加到nameToLevels、intToLevels Map中进行缓存
        static synchronized void add(Level l) {
            // the mirroredLevel object is always added to the list
            // before the custom Level instance
            KnownLevel o = new KnownLevel(l);
            List<KnownLevel> list = nameToLevels.get(l.name);
            if (list == null) {
                list = new ArrayList<>();
                nameToLevels.put(l.name, list);
            }
            list.add(o);

            list = intToLevels.get(l.value);
            if (list == null) {
                list = new ArrayList<>();
                intToLevels.put(l.value, list);
            }
            list.add(o);
        }
        .....
        // 省略了通过给定的名称、给定的值或给定的本地化名称三个方法的代码
        ......
        
        // 判断一个给定的Level是否是被KnownLevel管理的Level
        // 这个方法用在Level类的readResolve方法中,防止序列化反序列化得到没有被KnownLevel管理的Level
        static synchronized KnownLevel matches(Level l) {
            List<KnownLevel> list = nameToLevels.get(l.name);
            if (list != null) {
                for (KnownLevel level : list) {
                    Level other = level.mirroredLevel;
                    Class<? extends Level> type = level.levelObject.getClass();
                    if (l.value == other.value &&
                           (l.resourceBundleName == other.resourceBundleName ||
                               (l.resourceBundleName != null &&
                                l.resourceBundleName.equals(other.resourceBundleName)))) {
                        if (type == l.getClass()) {
                            return level;
                        }
                    }
                }
            }
            return null;
        }
    }

LogManager类的用法

LogManager的作用是读取配置文件和管理Logger实例。LogManager类初始化时默认使用JVM系统属性java.util.logging.manager定义的类路径来加载配置类,配置LogManager,但如果没有找到,就new一个空对象。

LogManager对象的初始化

// The global LogManager object
private static final LogManager manager;
// 类加载时的逻辑,对全局的manager初始化
static {
        manager = AccessController.doPrivileged(new PrivilegedAction<LogManager>() {
            @Override
            public LogManager run() {
                LogManager mgr = null;
                String cname = null;
                try {
                    // DEBUG时这个值是空的,说明没有设置该属性时,该属性为空
                    cname = System.getProperty("java.util.logging.manager");
                    if (cname != null) {
                        try {
                            Class<?> clz = ClassLoader.getSystemClassLoader()
                                    .loadClass(cname);
                            mgr = (LogManager) clz.newInstance();
                        } catch (ClassNotFoundException ex) {
                            Class<?> clz = Thread.currentThread()
                                    .getContextClassLoader().loadClass(cname);
                            mgr = (LogManager) clz.newInstance();
                        }
                    }
                } catch (Exception ex) {
                    System.err.println("Could not load Logmanager \"" + cname + "\"");
                    ex.printStackTrace();
                }
                if (mgr == null) {
                    mgr = new LogManager();
                }
                // 最终返回的是一个没有设置任何内容的LogManager对象
                return mgr;
            }
        });
    }

现在类初始化完成,全局的LogManager对象manager不为null了,但是这个什么属性都没设置的manager并没有什么用啊,所以LogManager类提供了一个ensureLogManagerInitialized方法,确保LogManager对象被初始化,在getLogManager方法返回LogManager对象之前被调用。

    // 确保返回的LogManager是被初始化过的
    public static LogManager getLogManager() {
        if (manager != null) {
            manager.ensureLogManagerInitialized();
        }
        return manager;
    }
   // ensureLogManagerInitialized方法主要做了两件事儿
   // 1.读取基本的配置  2.创建rootLogger
   final void ensureLogManagerInitialized() {
   		......
   		// 省略部分代码
   		initializedCalled = true;
            try {
                AccessController.doPrivileged(new PrivilegedAction<Object>() {
                    @Override
                    public Object run() {
                        assert rootLogger == null;
                        assert initializedCalled && !initializationDone;

                        // 读取基本配置
                        owner.readPrimordialConfiguration();

                        // 创建一个RootLogger
                        owner.rootLogger = owner.new RootLogger();
                        owner.addLogger(owner.rootLogger);
                        if (!owner.rootLogger.isLevelInitialized()) {
                            owner.rootLogger.setLevel(defaultLevel);
                        }

                        // 添加Logger.global
                        // 并且也特意说明了Logger.global不要通过Logger.getGlobal方法来获取
                        // 因为getGlobal方法内调用的是LogManager.getLogManager 这样就循环调用了
                        @SuppressWarnings("deprecation")
                        final Logger global = Logger.global;

                        // 确保global被注册到了全局LogManager对象中
                        // owner就是全局LogManager对象
                        owner.addLogger(global);
                        return null;
                    }
                });
            } finally {
                initializationDone = true;
            }
   }

LogManager对象配置的装载

在初始化时,有一行readPrimordialConfiguration方法的调用,点进去后,发现最终读取配置文件是LogManager.readConfiguration方法实现的。

public void readConfiguration() throws IOException, SecurityException {
        checkPermission();
        // 如果指定配配置类,就拿配置类的属性
        String cname = System.getProperty("java.util.logging.config.class");
        if (cname != null) {
            try {
                // 实例化这个配置类,配置类的构造方法中,应该调用LogManager.readConfiguration来为	 LogManager全局对象添加配置
                try {
                    Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(cname);
                    clz.newInstance();
                    return;
                } catch (ClassNotFoundException ex) {
                    Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass(cname);
                    clz.newInstance();
                    return;
                }
            } catch (Exception ex) {
                System.err.println("Logging configuration class \"" + cname + "\" failed");
                System.err.println("" + ex);
                // keep going and useful config file.
            }
        }
        // 如果没有指定配置类,就用默认配置文件
        String fname = System.getProperty("java.util.logging.config.file");
        if (fname == null) {
            // JRE安装路径
            fname = System.getProperty("java.home");
            if (fname == null) {
                throw new Error("Can't find java.home ??");
            }
            File f = new File(fname, "lib");
            // 可以看出最终默认使用的是logging.propertie文件来配置全局LogManager对象
            f = new File(f, "logging.properties");
            fname = f.getCanonicalPath();
        }
        try (final InputStream in = new FileInputStream(fname)) {
            final BufferedInputStream bin = new BufferedInputStream(in);
            readConfiguration(bin);
        }
    }

LogManager中的LoggerContext

LoggerContext为每个context的Logger提供命名空间。默认的LogManager对象有一个系统上下文SystemLoggerContext和一个用户上下文LoggerContext。系统上下文用于维护所有系统Logger的命名空间,并由系统代码查询。如果系统Logger不存在于用户上下文中,它也将被添加到用户上下文中。用户代码查询用户上下文,并在用户上下文中添加所有Logger。

	// LoggerContext for system loggers and user loggers
    private final LoggerContext systemContext = new SystemLoggerContext();
    private final LoggerContext userContext = new LoggerContext();

我们发现getSystemContext这个方法在demandSystemLogger方法中被引用到了,而后者正是Logger.demandLogger方法所调用的。

private static Logger demandLogger(String name, String resourceBundleName, Class<?> caller) {
        LogManager manager = LogManager.getLogManager();
        SecurityManager sm = System.getSecurityManager();
        if (sm != null && !SystemLoggerHelper.disableCallerCheck) {
            if (caller.getClassLoader() == null) {
                // 获取系统Logger
                return manager.demandSystemLogger(name, resourceBundleName);
            }
        }
        // 获取用户Logger
        return manager.demandLogger(name, resourceBundleName, caller);
    }

不管是获取用户Logger还是系统Logger,都会调用LogManage的addLogger方法。这个方法在注释上就明确说明了,如果同名的Logger存在,则返回false,否则将注册一个新的Logger,并返回true。

	public boolean addLogger(Logger logger) {
        final String name = logger.getName();
        if (name == null) {
            throw new NullPointerException();
        }
        drainLoggerRefQueueBounded();
        LoggerContext cx = getUserContext();
        // 从这行可以看出,系统Logger也一定会被加入到用户LoggerContext中去
        if (cx.addLocalLogger(logger)) {
            // Do we have a per logger handler too?
            // Note: this will add a 200ms penalty
            loadLoggerHandlers(logger, name, name + ".handlers");
            return true;
        } else {
            return false;
        }
    }

不管是获取系统Logger还是用户Logger,他们的方法中都有这一段代码,这也让我们明白了LoggerContext的作用,不就是提供了一个键值对,缓存了现有的所有Logger对象嘛。

	if (addLogger(sysLogger)) {
		// successfully added the new system logger
        logger = sysLogger;
    } else {
        logger = getLogger(name);
    }
    public Logger getLogger(String name) {
        return getUserContext().findLogger(name);
    }

LogManager中的LogNode

LogNode维护了Logger对象之间的父子关系,在创建一个Logger对象时,LogManager的addLocalLogger方法会为新的Logger分配节点,并维护上父Logger。

LogManager中的RootLogger

RootLogger类的注释说:用一个Logger的子类作为rootLogger,让我们只在第一次需要全局handler时初始化。
LogManager提供了一个私有的方法对全局的Handlers进行初始化

	private synchronized void initializeGlobalHandlers() {
        if (initializedGlobalHandlers) {
            return;
        }
        initializedGlobalHandlers = true;
        if (deathImminent) {
            // Aaargh...
            // The VM is shutting down and our exit hook has been called.
            // Avoid allocating global handlers.
            return;
        }
        loadLoggerHandlers(rootLogger, null, "handlers");
    }

说了半天全局Handlers,那么Handlers到底是什么,我们再看loadLoggerHandlers方法的实现,以及调用的parseClassNames方法,最终发现就是从Logmanager.props属性中获取"handlers"属性。也就是说,全局Handlers就是我们在配置文件中配置的handlers属性,去翻看配置文件,一定会发现有配置这个属性。

Logger类的用法

Logger类将需要记录的信息封装成一个对象,并且还封装了信息的级别,日志输出的实现,实际上是靠Handler实现的。

LoggerBundle

Logger提供了一个LoggerBundle类,这个类只有两个属性,一个是资源的名字resourceBundleName,另一个是资源对象userBundle。LoggerBundle用在日志记录时,会将这两个属性设置给LogRecord对象。LoggerBundle的构造器时private,也就是说不能被外部代码new出来,只能通过其提供的get()方法得到该对象。Logger类默认提供了两个LoggerBundle对象。

	static final String SYSTEM_LOGGER_RB_NAME = "sun.util.logging.resources.logging";
	// This instance will be shared by all loggers created by the system code
    private static final LoggerBundle SYSTEM_BUNDLE =
            new LoggerBundle(SYSTEM_LOGGER_RB_NAME, null);

    // This instance indicates that no resource bundle has been specified yet,
    // and it will be shared by all loggers which have no resource bundle.
    private static final LoggerBundle NO_RESOURCE_BUNDLE =
            new LoggerBundle(null, null);

根据SYSTEM_LOGGER_RB_NAME的值,我找到了指向的类,之前在介绍日志输出时也提到过这个类。

public final class logging extends ListResourceBundle {
    public logging() {
    }
    protected final Object[][] getContents() {
        return new Object[][]{{"ALL", "All"}, {"CONFIG", "Config"}, {"FINE", "Fine"}, {"FINER", "Finer"}, {"FINEST", "Finest"}, {"INFO", "Info"}, {"OFF", "Off"}, {"SEVERE", "Severe"}, {"WARNING", "Warning"}};
    }
}

Logger记录日志

Logger提供了log(Level level, String msg)方法供用户调用来记录日志,先通过isLoggable方法,判断当前Logger对象的日志功能是否关闭或当前Logger对象的设定级别是否小于要记录的级别。比如说:Logger设定为INFO级别,当要记录WARNING和SERVER级别时,就要会返回false,不再向下执行。

	public void log(Level level, String msg) {
        if (!isLoggable(level)) {
            return;
        }
        LogRecord lr = new LogRecord(level, msg);
        doLog(lr);
    }
    public boolean isLoggable(Level level) {
        if (level.intValue() < levelValue || levelValue == offValue) {
            return false;
        }
        return true;
    }

Filter过滤日志

JDK提供了一个filter接口来定制日志的过滤功能,只要实现isLoggable接口,在进行日志输出前,会判断是否进行输出。

	public interface Filter {
    	public boolean isLoggable(LogRecord record);
	}
	public void log(LogRecord record) {
        if (!isLoggable(record.getLevel())) {
            return;
        }
        Filter theFilter = filter;
        // 如果Logger对象有通过setFilter设置过滤器,在这里会进行判断是否输出日志
        if (theFilter != null && !theFilter.isLoggable(record)) {
            return;
        }
        // 省略后面的代码
		......
		// 从这里可以看出真正实现日志输出到控制台或文件是靠handler来实现的
		for (Handler handler : loggerHandlers) {
                handler.publish(record);
            }
        // 省略后面的代码
        ......
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值