共计 9363 个字符,预计需要花费 24 分钟才能阅读完成。
Java 里的各种日志框架,置信大家都不生疏。Log4j/Log4j2/Logback/jboss logging等等,其实这些日志框架外围构造没什么区别,只是细节实现上和其性能上有所不同。本文带你从零开始,一步一步的设计一个日志框架
输入组件 – Appender
提到日志框架,最容易想到的外围性能,那就是输入日志了。输入的形式能够有很多:规范输入 / 控制台(Standard Output/Console)、文件(File)、邮件(Email)、甚至是音讯队列(MQ)和数据库。
当初将输入性能形象成一个组件“输入器”– Appender,这个 Appender 组件的外围性能就是输入,上面是 Appender 的实现代码:
public interface Appender {void append(String body);
}
不同的输入形式,只须要实现 Appender 接口做不同的实现即可,比方 ConsoleAppender – 输入至控制台
public class ConsoleAppender implements Appender {
private OutputStream out = System.out;
private OutputStream out_err = System.err;
@Override
public void append(LoggingEvent event) {
try {out.write(event.toString().getBytes(encoding));
} catch (IOException e) {e.printStackTrace();
}
}
}
输入内容 – LoggingEvent
有了输入形式的组件之后,当初须要思考输入内容的问题。一行日志的核心内容至多应该蕴含以下几个信息:
- 日志工夫戳
- 线程信息
- 日志名称(个别是全类名)
- 日志级别
- 日志主体(须要输入的内容,比方 info(日志主题))
为了不便的治理输入内容,当初须要创立一个输入内容的类来封装这些信息:
public class LoggingEvent {
public long timestamp;// 日志工夫戳
private int level;// 日志级别
private Object message;// 日志主题
private String threadName;// 线程名称
private long threadId;// 线程 id
private String loggerName;// 日志名称
//getter and setters...
@Override
public String toString() {
return "LoggingEvent{" +
"timestamp=" + timestamp +
", level=" + level +
", message=" + message +
", threadName='" + threadName + '\'' +
", threadId=" + threadId +
", loggerName='" + loggerName + '\'' +
'}';
}
}
对于每一次日志打印,应该属于一次输入的“事件 -Event”,所以这里命名为LoggingEvent
那当初有了 LoggingEvent,那么对于 Appender 来说,接管的输入内容参数须要为 LoggingEvent 了。当初再回去把 Appender.append 办法的入参批改成LoggingEvent:
public interface Appender {void append(LoggingEvent event);
}
日志级别设计 – Level
一个日志框架,应该提供日志级别的性能,程序在应用时能够打印不同级别的日志,还能够依据日志级别来调整那些日志能够显示,个别日志级别会定义为以下几种,级别从左到右排序,只有大于等于某级别的 LoggingEvent 才会进行输入
ERROR > WARN > INFO > DEBUG > TRACE
当初来创立一个日志级别的枚举,只有两个属性,一个级别名称,一个级别数值(不便做比拟)
public enum Level {ERROR(40000, "ERROR"), WARN(30000, "WARN"), INFO(20000, "INFO"), DEBUG(10000, "DEBUG"), TRACE(5000, "TRACE");
private int levelInt;
private String levelStr;
Level(int i, String s) {
levelInt = i;
levelStr = s;
}
public static Level parse(String level) {return valueOf(level.toUpperCase());
}
public int toInt() {return levelInt;}
public String toString() {return levelStr;}
public boolean isGreaterOrEqual(Level level) {return levelInt>=level.toInt();
}
}
日志级别定义实现之后,再将 LoggingEvent 中的日志级别替换为这个 Level 枚举
public class LoggingEvent {
public long timestamp;// 日志工夫戳
private Level level;// 替换后的日志级别
private Object message;// 日志主题
private String threadName;// 线程名称
private long threadId;// 线程 id
private String loggerName;// 日志名称
//getter and setters...
}
当初根本的输入形式和输入内容都曾经根本实现,下一步须要设计日志打印的入口,毕竟有入口能力打印嘛
日志打印入口 – Logger
当初来思考日志打印入口如何设计,作为一个日志打印的入口,须要蕴含以下外围性能:
- 提供 error/warn/info/debug/trace 几个打印的办法
- 领有一个 name 属性,用于辨别不同的 logger
- 调用 appender 输入日志
- 领有本人的专属级别(比方本身级别为 INFO,那么只有INFO/WARN/ERROR 才能够输入)
先来简略创立一个 Logger 接口,不便扩大
public interface Logger{void trace(String msg);
void info(String msg);
void debug(String msg);
void warn(String msg);
void error(String msg);
String getName();}
再创立一个默认的 Logger 实现类:
public class LogcLogger implements Logger{
private String name;
private Appender appender;
private Level level = Level.TRACE;// 以后 Logger 的级别,默认最低
private int effectiveLevelInt;// 冗余级别字段,方便使用
@Override
public void trace(String msg) {filterAndLog(Level.TRACE,msg);
}
@Override
public void info(String msg) {filterAndLog(Level.INFO,msg);
}
@Override
public void debug(String msg) {filterAndLog(Level.DEBUG,msg);
}
@Override
public void warn(String msg) {filterAndLog(Level.WARN,msg);
}
@Override
public void error(String msg) {filterAndLog(Level.ERROR,msg);
}
/**
* 过滤并输入,所有的输入办法都会调用此办法
* @param level 日志级别
* @param msg 输入内容
*/
private void filterAndLog(Level level,String msg){LoggingEvent e = new LoggingEvent(level, msg,getName());
// 指标的日志级别大于以后级别才能够输入
if(level.toInt() >= effectiveLevelInt){appender.append(e);
}
}
@Override
public String getName() {return name;}
//getters and setters...
}
好了,到当初为止,当初曾经实现了一个 最最最根本的 日志模型,能够创立 Logger,输入不同级别的日志。不过显然还不太够,还是短少一些外围性能
日志层级 – Hierarchy
个别在应用日志框架时,有一个很根本的需要:不同包名的日志应用不同的输入形式,或者不同包名下类的日志应用不同的日志级别,比方我想让框架相干的 DEBUG 日志输入,便于调试,其余默认用 INFO 级别。
而且在应用时并不心愿每次创立 Logger 都援用一个 Appender,这样也太不敌对了;最好是间接应用一个全局的Logger 配置,同时还反对非凡配置的 Logger,且这个配置须要让程序中创立 Logger 时无感(比方 LoggerFactory.getLogger(XXX.class))
可下面现有的设计可无奈满足这个需要,须要稍加革新
当初设计一个层级构造,每一个 Logger 领有一个 Parent Logger, 在filterAndLog时优先应用本人的 Appender,如果本人没有 Appender,那么就向上调用父类的appnder,有点反向“双亲委派(parents delegate)”的意思
上图中的 Root Logger, 就是全局默认的 Logger,默认状况下它是所有Logger(新创建的)的Parent Logger。 所以在 filterAndLog 时,默认都会应用 Root Logger 的 appender 和 level 来进行输入
当初将 filterAndLog 办法调整一下,减少向上调用的逻辑:
private LogcLogger parent;// 先给减少一个 parent 属性
private void filterAndLog(Level level,String msg){LoggingEvent e = new LoggingEvent(level, msg,getName());
// 循环向上查找可用的 logger 进行输入
for (LogcLogger l = this;l != null;l = l.parent){if(l.appender == null){continue;}
if(level.toInt()>effectiveLevelInt){l.append(e);
}
break;
}
}
好了,当初这个日志层级的设计曾经实现了,不过下面提到不同包名应用不同的 logger 配置,还没有做到,包名和 logger 如何实现对应呢?
其实很简略,只须要为每个包名的配置独自定义一个全局 Logger,在解析包名配置时间接为不同的包名
日志上下文 – LoggerContext
思考到有一些全局的 Logger,和 Root Logger 须要被各种 Logger 援用,所以得设计一个 Logger 容器,用来存储这些 Logger
/**
* 一个全局的上下文对象
*/
public class LoggerContext {
/**
* 根 logger
*/
private Logger root;
/**
* logger 缓存,寄存解析配置文件后生成的 logger 对象,以及通过程序手动创立的 logger 对象
*/
private Map<String,Logger> loggerCache = new HashMap<>();
public void addLogger(String name,Logger logger){loggerCache.put(name,logger);
}
public void addLogger(Logger logger){loggerCache.put(logger.getName(),logger);
}
//getters and setters...
}
有了寄存 Logger 对象们的容器,下一步能够思考创立 Logger 了
日志创立 – LoggerFactory
为了不便的构建 Logger 的层级构造,每次 new 可不太敌对,当初创立一个 LoggerFactory 接口
public interface ILoggerFactory {
// 通过 class 获取 / 创立 logger
Logger getLogger(Class<?> clazz);
// 通过 name 获取 / 创立 logger
Logger getLogger(String name);
// 通过 name 创立 logger
Logger newLogger(String name);
}
再来一个默认的实现类
public class StaticLoggerFactory implements ILoggerFactory {
private LoggerContext loggerContext;// 援用 LoggerContext
@Override
public Logger getLogger(Class<?> clazz) {return getLogger(clazz.getName());
}
@Override
public Logger getLogger(String name) {Logger logger = loggerContext.getLoggerCache().get(name);
if(logger == null){logger = newLogger(name);
}
return logger;
}
/**
* 创立 Logger 对象
* 匹配 logger name,拆分类名后和已创立(包含配置的)的 Logger 进行匹配
* 比方以后 name 为 com.aaa.bbb.ccc.XXService,那么 name 为 com/com.aaa/com.aaa.bbb/com.aaa.bbb.ccc
* 的 logger 都能够作为 parent logger,不过这里须要程序拆分,优先匹配“最近的”* 在这个例子里就会优先匹配 com.aaa.bbb.ccc 这个 logger,作为本人的 parent
*
* 如果没有任何一个 logger 匹配,那么就应用 root logger 作为本人的 parent
*
* @param name Logger name
*/
@Override
public Logger newLogger(String name) {LogcLogger logger = new LogcLogger();
logger.setName(name);
Logger parent = null;
// 拆分包名,向上查找 parent logger
for (int i = name.lastIndexOf("."); i >= 0; i = name.lastIndexOf(".",i-1)) {String parentName = name.substring(0,i);
parent = loggerContext.getLoggerCache().get(parentName);
if(parent != null){break;}
}
if(parent == null){parent = loggerContext.getRoot();
}
logger.setParent(parent);
logger.setLoggerContext(loggerContext);
return logger;
}
}
再来一个动态工厂类,方便使用:
public class LoggerFactory {private static ILoggerFactory loggerFactory = new StaticLoggerFactory();
public static ILoggerFactory getLoggerFactory(){return loggerFactory;}
public static Logger getLogger(Class<?> clazz){return getLoggerFactory().getLogger(clazz);
}
public static Logger getLogger(String name){return getLoggerFactory().getLogger(name);
}
}
至此,所有根本组件曾经实现,剩下的就是拆卸了
配置文件设计
配置文件需至多须要有以下几个配置性能:
- 配置 Appender
- 配置 Logger
- 配置 Root Logger
上面是一份最小配置的示例
<configuration>
<appender name="std_plain" class="cc.leevi.common.logc.appender.ConsoleAppender">
</appender>
<logger name="cc.leevi.common.logc">
<appender-ref ref="std_plain"/>
</logger>
<root level="trace">
<appender-ref ref="std_pattern"/>
</root>
</configuration>
除了 XML 配置,还能够思考减少 YAML/Properties 等模式的配置文件,所以这里须要将解析配置文件的性能形象一下,设计一个 Configurator 接口,用于解析配置文件:
public interface Configurator {void doConfigure();
}
再创立一个默认的 XML 模式的配置解析器:
public class XMLConfigurator implements Configurator{
private final LoggerContext loggerContext;
public XMLConfigurator(URL url, LoggerContext loggerContext) {
this.url = url;// 文件 url
this.loggerContext = loggerContext;
}
@Override
public void doConfigure() {
try{DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
Document document = documentBuilder.parse(url.openStream());
parse(document.getDocumentElement());
...
}catch (Exception e){...}
}
private void parse(Element document) throws IllegalAccessException, ClassNotFoundException, InstantiationException {//do parse...}
}
解析时,拆卸 LoggerContext,将配置中的 Logger/Root Logger/Appender 等信息构建实现,填充至传入的 LoggerContext
当初还须要一个初始化的入口,用于加载 / 解析配置文件,提供加载 / 解析后的全局 LoggerContext
public class ContextInitializer {
final public static String AUTOCONFIG_FILE = "logc.xml";// 默认应用 xml 配置文件
final public static String YAML_FILE = "logc.yml";
private static final LoggerContext DEFAULT_LOGGER_CONTEXT = new LoggerContext();
/**
* 初始化上下文
*/
public static void autoconfig() {URL url = getConfigURL();
if(url == null){System.err.println("config[logc.xml or logc.yml] file not found!");
return ;
}
String urlString = url.toString();
Configurator configurator = null;
if(urlString.endsWith("xml")){configurator = new XMLConfigurator(url,DEFAULT_LOGGER_CONTEXT);
}
if(urlString.endsWith("yml")){configurator = new YAMLConfigurator(url,DEFAULT_LOGGER_CONTEXT);
}
configurator.doConfigure();}
private static URL getConfigURL(){
URL url = null;
ClassLoader classLoader = ContextInitializer.class.getClassLoader();
url = classLoader.getResource(AUTOCONFIG_FILE);
if(url != null){return url;}
url = classLoader.getResource(YAML_FILE);
if(url != null){return url;}
return null;
}
/**
* 获取全局默认的 LoggerContext
*/
public static LoggerContext getDefautLoggerContext(){return DEFAULT_LOGGER_CONTEXT;}
}
当初还差一步,将加载配置文件的办法嵌入LoggerFactory,让 LoggerFactory.getLogger 的时候主动初始化,来革新一下StaticLoggerFactory:
public class StaticLoggerFactory implements ILoggerFactory {
private LoggerContext loggerContext;
public StaticLoggerFactory() {
// 结构 StaticLoggerFactory 时,间接调用配置解析的办法,并获取 loggerContext
ContextInitializer.autoconfig();
loggerContext = ContextInitializer.getDefautLoggerContext();}
}
当初,一个日志框架就曾经根本实现了。尽管还有很多细节没有欠缺,但主体性能都曾经蕴含,麻雀虽小五脏俱全
残缺代码
本文中为了便于浏览,有些代码并没有贴上来,具体残缺的代码能够参考:
https://github.com/kongwu-/logc