okong

前言

前面两章节我们介绍了一些日志框架的常见配置及使用实践。一般上,在开发过程中,像log4j2logback日志框架都提供了很多Appender,基本上可以满足大部分的业务需求了。但在一些特殊需求或者需要将日志进行集中管理(集群部署时,日志是分拆到不同服务器上的,不可能去每一台服务器上去下载文件的,也不便于日志检索)时,就需要自定义Appender,将日志集中输出或者其他一些特殊需求。所以本章节就来简单介绍下关于log4j2logback的自定义Appender知识。

一点知识

编写自定义Appender时,我们先来看看log4j2logback自带了哪些Appender,了解下是否可以满足我们的个性化需求,避免重复制造*。

log4j2自带Appender

先看一张官网提供的Appender说明:

官方Appender

名称 描述
AsyncAppender 使用一个单独线程记录日志,实现异步处理日志事件。
CassandraAppender 将日志信息输出到一个Apache的Cassandra数据库
ConsoleAppender 将日志信息输出到控制台
FailoverAppender 包含其他appenders,按顺序尝试,直至成功或结尾
FileAppender 一个OutputStreamAppender,将日志输出到文件
FlumeAppender 将日志输出到Apache Flume系统
JDBCAppender 将日志通过JDBC输出到关系型数据库
JMS Appender 将日志输出到JMS(Java Message Service)
JPAAppender 将日志输出到JPA框架
HttpAppender 通过HTTP输出日志
KafkaAppender 将日志输出到Apache Kafka
MemoryMappedFileAppender 将日志输出到一块文件关联的内存
OutputStreamAppender 将日志输出到一个OutputStream
RandomAccessFileAppender 性能比FileAppender高20%~200%的文件输出Appender
RewriteAppender 允许对日志信息进行加工
RollingFileAppender 按log文件最大长度限度生成新文件
RollingRandomAccessFA 添加了缓存的RollingFileAppender
RoutingAppender 将日志事件分类,按条件分配给子appender
SMTPAppender 将日志输出到邮件
SocketAppender 将日志输出到一个Socket
SyslogAppender 是一个SocketAppender,将日志输出到远程系统日志
ZeroMQ/JeroMQ Appender 使用JeroMQ库将日志输出到ZeroMQ终端

基本上已经覆盖了百分之九十的业务场景了。相关的详细说明或者配置大家自行搜索或者查看官网说明。
官网地址:http://logging.apache.org/log4j/2.x/manual/appenders.html

logback自带Appender

log4j2一样,自带的都差不多了。

名称 描述
ConsoleAppender 将日志输出到控制台
FileAppender 将日志输出到文件
RollingFileAppender 滚动文件生成,按条件生成不同文件,配合TriggeringPolicy使用
SocketAppender 输出日志到远程实例中,明文传输
SSLSocketAppender 输出日志到远程实例中,密文传输
SMTPAppender 将日志输出到邮件
DBAppender 日志事件插入数据库中,需要提前创建表
SyslogAppender 是一个SocketAppender,将日志输出到远程系统日志
SiftingAppender 可基于任何给定的实时属性分开(或者筛选)日志,如基于用户会话分开日志事件
AmqpAppender 将日志输出到MQ服务中

具体可查看:https://blog.csdn.net/tianyaleixiaowu/article/details/73327752 很详细!

或者查看官网:https://logback.qos.ch/manual/appenders.html

自定义Appender

自定义Appender时,可以按实现的功能,适当的继承(log4j2appender类基本上被设置成了final无法继承)或者参考一些已有的功能,当然了也可以直接继承其基类接口的。以下就简单的示例下,没有实现特定的功能,⊙﹏⊙‖∣

log4j2自定义Appender

按官网的扩展说明,我们来简单实现一个appender。

extend appender

官网地址:http://logging.apache.org/log4j/2.x/manual/extending.html#Appenders

0.编写自定义appender类,继承AbstractAppender抽象实现类:

MyLog4j2Appender.java

/**
 * 自定义log4j2输出源,简单的输出到控制台
 * @author oKong
 *
 */
//这里的 MyLog4j2 对应就是 xml中,
/**
 * 
 *  <appenders>
 *     <MyLog4j2 name="customAppender" printString="一枚趔趄的猿">
 *     </MyLog4j2>
 *  </appenders>
 *
 */
@Plugin(name = "MyLog4j2", category = "Core", elementType = "appender", printObject = true)
public class MyLog4j2Appender extends AbstractAppender {

    String printString;
   /**  
     *构造函数 可自定义参数 这里直接传入一个常量并输出
     * 
    */ 
    protected MyLog4j2Appender(String name, Filter filter, Layout<? extends Serializable> layout,String printString) {
        super(name, filter, layout);
        this.printString = printString;
    }

    @Override
    public void append(LogEvent event) {
         if (event != null && event.getMessage() != null) {
             // 此处自定义实现输出             
             // 获取输出值:event.getMessage().toString()
             // System.out.print(event.getMessage().toString());
             // 格式化输出
             System.out.print(printString + ":" + getLayout().toSerializable(event));
          }
        
    }
    
    /**  接收配置文件中的参数 
     * 
     * @PluginAttribute 字面意思都知道,是xml节点的attribute值,如<oKong name="oKong"></oKong> 这里的name 就是 attribute
     * @PluginElement:表示xml子节点的元素,
     * 如
     *     <oKong name="oKong">
     *         <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
     *     </oKong>
     *   其中,PatternLayout就是 的 Layout,其实就是{@link Layout}的实现类。
     */ 
    @PluginFactory
    public static MyLog4j2Appender createAppender(
            @PluginAttribute("name") String name,
            @PluginElement("Filter") final Filter filter, 
            @PluginElement("Layout") Layout<? extends Serializable> layout,
            @PluginAttribute("printString") String printString) {
        
        if (name == null) {
            LOGGER.error("no name defined in conf."); 
            return null; 
        } 
        //默认使用 PatternLayout
        if (layout == null) { 
            layout = PatternLayout.createDefaultLayout(); 
        } 
        
        return new MyLog4j2Appender(name, filter, layout, printString);
    }
    
    @Override
    public void start() {
        System.out.println("log4j2-start方法被调用");
        super.start();
    }
    
    @Override
    public void stop() {
        System.out.println("log4j2-stop方法被调用");
        super.stop();
    }
}

简单说明下,相关注意点:

  • @Plugin注解:这个注解,是为了在之后配置log4j2-spring.xml时,指定的Appender Tag。
  • 构造函数:除了使用父类的以外,也可以增加一些自己的配置。
  • 重写append()方法:这里面需要实现具体的逻辑,日志的去向。
  • createAppender()方法:主要是接收log4j2-spring.xml中的配置项。

1.使用自定义的appender。

log4j2-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
 <configuration status="WARN" monitorInterval="30" packages="cn.lqdev.learning">
     <!--定义appenders-->
     <appenders>
         <MyLog4j2 name="oKong" printString="一枚趔趄的猿(log4j2)">
            <!--输出日志的格式-->
             <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
         </MyLog4j2>
     </appenders>
     <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
     <loggers>
         <!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
         <logger name="org.springframework" level="INFO"></logger>
         <logger name="org.mybatis" level="INFO"></logger>
         <!-- 自定义包下设置为INFO,则可以看见输出的日志不包含debug输出了 -->
         <logger name="cn.lqdev.learning" level="INFO"/>
         <root level="all">
             <appender-ref ref="oKong"/>
         </root>
     </loggers>
 </configuration>

这里需要注意,需要在configuration中,加入属性packages为自定类所在包名cn.lqdev.learning才会被扫描生效,不知道是否还有其他方法。

2.启动后,就可以看见相关输出了。

...部分省略...
一枚趔趄的猿(log4j2):[14:47:43:751] [INFO] - org.apache.juli.logging.DirectJDKLog.log(DirectJDKLog.java:180) - Using a shared selector for servlet write/read
一枚趔趄的猿(log4j2):[14:47:43:761] [INFO] - org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer.start(TomcatEmbeddedServletContainer.java:216) - Tomcat started on port(s): 8080 (http)
一枚趔趄的猿(log4j2):[14:47:43:764] [INFO] - org.springframework.boot.StartupInfoLogger.logStarted(StartupInfoLogger.java:57) - Started Chapter25Application in 2.03 seconds (JVM running for 3.164)
一枚趔趄的猿(log4j2):[14:47:43:764] [INFO] - cn.lqdev.learning.springboot.chapter25.Chapter25Application.main(Chapter25Application.java:14) - Chapter25启动!

不知道如何整合log4j2的,可以查看:《第二十三章:日志管理之整合篇》

logback自定义Appender

logback的自定义,也是类似的,都是基于一个基类appender来实现。本身logback提供了AppenderBaseUnsynchronizedAppenderBase两个抽象类(同步和非同步),所以我们自定义时,只需要看实际业务继承其中的一个即可。先看下其类继承结构:

0.编写自定义appender类。

MyLogbackAppender.java

@Getter
@Setter
public class MyLogbackAppender extends UnsynchronizedAppenderBase<ILoggingEvent>{

    Layout<ILoggingEvent> layout;
    
    //自定义配置 
    String printString;

    
    
    @Override
    public void start(){
        //这里可以做些初始化判断 比如layout不能为null ,
        if(layout == null) {
            addWarn("Layout was not defined");
        }
        //或者写入数据库 或者redis时 初始化连接等等
         super.start();
    }
    

    @Override
    public void stop()
    {
       //释放相关资源,如数据库连接,redis线程池等等
        System.out.println("logback-stop方法被调用");
        if(!isStarted()) {
            return;
        }
        super.stop();
    }
    
    @Override
    public void append(ILoggingEvent event) {
        if (event == null || !isStarted()){
            return;
        }
             // 此处自定义实现输出             
             // 获取输出值:event.getFormattedMessage()
             // System.out.print(event.getFormattedMessage());
             // 格式化输出        
        System.out.print(printString + ":" + layout.doLayout(event));
          
    }
}

也简单说明下,相关注意点:

  • start方法:初始时调用。故在编写如数据库入库,连接缓存或者mq时,可以在这个方法里面进行初始化操作。
  • stop:当停止时,调用。可做些资源释放操作。

1.使用自定义appender:

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 -->
    <property name="LOG_HOME" value="/home" />
    <!-- 控制台输出 -->
    <appender name="MyLogback"
        class="cn.lqdev.learning.springboot.chapter25.config.MyLogbackAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- 日志收集最低日志级别 -->
            <level>INFO</level>
        </filter>
        <layout
            class="ch.qos.logback.classic.PatternLayout">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </layout>
        <!-- 自定义参数 -->
        <printString>一枚趔趄的猿(logback)</printString>
    </appender>

    <!-- 自定义包下设置为INFO,则可以看见输出的日志不包含debug输出了 -->
    <logger name="cn.lqdev.learning" level="INFO" />

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="MyLogback" />
    </root>
    
</configuration>

2.应用启动,查看控制台输出,效果是一样的:

...部分省略...
一枚趔趄的猿(logback):2018-08-25 15:01:57.486 [main] INFO  org.apache.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
一枚趔趄的猿(logback):2018-08-25 15:01:57.497 [main] INFO  org.apache.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
一枚趔趄的猿(logback):2018-08-25 15:01:57.520 [main] INFO  o.s.b.c.e.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
一枚趔趄的猿(logback):2018-08-25 15:01:57.523 [main] INFO  c.l.l.springboot.chapter25.Chapter25Application - Started Chapter25Application in 54.349 seconds (JVM running for 55.377)
一枚趔趄的猿(logback):2018-08-25 15:01:57.524 [main] INFO  c.l.l.springboot.chapter25.Chapter25Application - Chapter25启动!

关于ShutdownHook

当你运行了以上的自定义appender后,停止应用时,你会发现定义的stop方法并没有被执行。还需要配置一个ShutdownHook系统钩子,使得在jvm在退出时之前会调用。

一点知识

我们知道,在java中,注册一个关闭钩子是很简单的,使用Runtime类即可,具体用法如下:

        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {

            @Override
            public void run() {
                // 执行资源释放操作

            }
        }));

而在SpringBoot中,只需要配置logging.register-shutdown-hooktrue即可。

logging.register-shutdown-hook=true

对于logback而言,也可以在logback-spring.xml中配置:

<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>

也是可以的。再或者在启动类手动注册这个DelayingShutdownHook也是可以的

这里有个坑,log4j2而言,配置失效了。谷歌了一圈也没有发现解决方法,网上的方案试了一遍都是不行。。很尴尬。要是使用log4j2的话,可以取巧下,在start()方法里面,注册钩子之后调用stop方法。希望有知道的大神分享下如何解决!

参考资料

  1. https://blog.csdn.net/zhoucheng05_13/article/details/78494458
  2. http://logging.apache.org/log4j/2.x/manual/appenders.html
  3. http://logging.apache.org/log4j/2.x/manual/extending.html#Appenders
  4. https://logback.qos.ch/manual/appenders.html
  5. https://blog.csdn.net/hupoling/article/details/75353854

总结

本文主要是简单介绍了log4j2logback自定义appender相关知识。实现起来是相对简单的,需要注意当涉及需要关闭释放相关资源时,需要确认下关闭前是否有被调用,不然可能造成连接未关闭等行为,避免不必要的问题。关于最后使用log4j2关闭钩子未生效问题,由于现在都默认使用logback了,这个问题就不深究了,还望有知道的同学分享下解决方案!谢谢!同时由于没有对两个框架有过多的深入了解,只能点到为止了,若文中有误,还望指出!

最后

目前互联网上很多大佬都有SpringBoot系列教程,如有雷同,请多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢。

老生常谈

  • 个人QQ:499452441
  • 公众号:lqdevOps

公众号

个人博客:http://blog.lqdev.cn

完整示例:https://github.com/xie19900123/spring-boot-learning/tree/master/chapter-25

原文地址:http://blog.lqdev.cn/2018/08/25/springboot/chapter-twenty-five/

相关文章: