关于java:手把手教你从零设计一个日志框架

4次阅读

共计 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

正文完
 0