继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

SpringBoot | 第二十五章:日志管理之自定义Appender

蝴蝶不菲
关注TA
已关注
手记 372
粉丝 81
获赞 381

前言

前面两章节我们介绍了一些日志框架的常见配置及使用实践。一般上,在开发过程中,像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两个抽象类(同步和非同步),所以我们自定义时,只需要看实际业务继承其中的一个即可。先看下其类继承结构:

5b82504c00016b0c09320827.jpg

0.编写自定义appender类。

MyLogbackAppender.java

@Getter@Setterpublic 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系列教程,如有雷同,请多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢。

原文出处:https://www.cnblogs.com/okong/p/springboot-twenty-five.html

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP