Browse Source

feat(java):合并日志文章

liuchuanwei 6 months ago
parent
commit
8f7b6fc430
2 changed files with 546 additions and 5 deletions
  1. 546 0
      java/怎么打印日志?.md
  2. 0 5
      java/日志/slf4j logger 占位符.md

+ 546 - 0
java/怎么打印日志?.md

@@ -0,0 +1,546 @@
+参考:
+* [工作3年啦,你竟然连Java日志体系都没闹懂?说不过去~ ](https://mp.weixin.qq.com/s/8gTsdstUqHdyjLvRkv7nqw)
+* https://www.cnblogs.com/cb0327/p/5759441.html
+
+## logger 使用步骤
+
+1. 引入依赖
+```xml
+<project>
+    <properties>
+        <slf4j.version>1.7.25</slf4j.version>
+        <logback.version>1.1.11</logback.version>
+	</properties>
+    <dependencies>
+        <!-- slf4j -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+    
+        <!-- logback -->
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+            <version>${logback.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>${logback.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-access</artifactId>
+            <version>${logback.version}</version>
+        </dependency>
+    </dependencies>
+</project>
+```
+
+2. 配置 logback.xml
+> 实际项目中,可修改下面模板使用
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+    <!-- region 自定义属性 -->
+    
+	<!-- 定义日志文件的存储地址 -->
+	<property name="LOG_DIR" value="/data/logs/logGuide/"/>
+	<!-- 定义日志最大的历史 30天 -->
+	<property name="maxHistory" value="30"/>
+	<!-- 定义日志文件大小 -->
+	<property name="maxFileSize" value="20MB"/>
+
+	<!--
+		%p:输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
+		%r:输出自应用启动到输出该日志讯息所耗费的毫秒数
+		%t:输出产生该日志事件的线程名
+		%f:输出日志讯息所属的类别的类别名
+		%c:输出日志讯息所属的类的全名
+		%d:输出日志时间点的日期或时间,指定格式的方式: %d{yyyy-MM-dd HH:mm:ss}
+		%l:输出日志事件的发生位置,即输出日志讯息的语句在他所在类别的第几行。
+		%m:输出代码中指定的讯息,如log(message)中的message
+		%n:输出一个换行符号
+	-->
+	<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
+	<property name="pattern" value="%d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level  %msg%n"/>
+    
+    <!--endregion-->
+
+	<!--
+		Appender: 设置日志信息的去向,常用的有以下几个
+            ch.qos.logback.core.ConsoleAppender (控制台)
+            ch.qos.logback.core.rolling.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新文件)
+            ch.qos.logback.core.FileAppender (文件)
+	-->
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<!-- 字符串System.out(默认)或者System.err -->
+		<target>System.out</target>
+		<!-- 对记录事件进行格式化 -->
+		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+			<pattern>${pattern}</pattern>
+		</encoder>
+	</appender>
+
+	<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<!-- 被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建 -->
+		<file>${LOG_DIR}/info.log</file>
+		<!-- 当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名。属性class定义具体的滚动策略类 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<!-- 必要节点,包含文件名及"%d"转换符,"%d"可以包含一个java.text.SimpleDateFormat指定的时间格式,默认格式是 yyyy-MM-dd -->
+			<fileNamePattern>${LOG_DIR}/info_%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
+			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>${maxFileSize}</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,如果是6,则只保存最近6个月的文件,删除之前的旧文件 -->
+			<maxHistory>${maxHistory}</maxHistory>
+		</rollingPolicy>
+		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+			<pattern>${pattern}</pattern>
+		</encoder>
+		<!-- LevelFilter: 级别过滤器,根据日志级别进行过滤 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>INFO</level>
+			<!-- 用于配置符合过滤条件的操作 ACCEPT:日志会被立即处理,不再经过剩余过滤器 -->
+			<onMatch>ACCEPT</onMatch>
+			<!-- 用于配置不符合过滤条件的操作 DENY:日志将立即被抛弃不再经过其他过滤器 -->
+			<onMismatch>DENY</onMismatch>
+		</filter>
+	</appender>
+
+	<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${LOG_DIR}/error.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<fileNamePattern>${LOG_DIR}/error_%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
+			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+				<maxFileSize>${maxFileSize}</maxFileSize>
+			</timeBasedFileNamingAndTriggeringPolicy>
+			<maxHistory>${maxHistory}</maxHistory>
+		</rollingPolicy>
+		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+			<pattern>${pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>ERROR</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+	</appender>
+
+	<appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${LOG_DIR}/debug.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+			<FileNamePattern>${LOG_DIR}/debug.%d{yyyy-MM-dd}.log
+			</FileNamePattern>
+		</rollingPolicy>
+		<encoder>
+			<Pattern>${pattern}</Pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>DEBUG</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+	</appender>
+
+	<!--
+		用来设置某一个包或者具体的某一个类的日志打印级别并指定appender
+		<logger>仅有一个name属性,一个可选的level和一个可选的addtivity属性
+		name:
+			用来指定受此logger约束的某一个包或者具体的某一个类。
+		level:
+			用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
+			如果未设置此属性,那么当前logger将会继承上级的级别。
+		additivity:
+			是否向上级logger传递打印信息。默认是true。
+		<logger>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger
+	-->
+
+	<!--
+	<logger name="liu.log.guide.util" additivity="false">
+		<level value="info"/>
+		<appender-ref ref="STDOUT"/>
+		<appender-ref ref="DEBUG"/>
+		<appender-ref ref="INFO"/>
+		<appender-ref ref="ERROR"/>
+	</logger>
+
+	<logger name="liu.log.guide.util.file" additivity="false">
+		<level value="info"/>
+		<appender-ref ref="STDOUT"/>
+	</logger>
+	-->
+	<logger name="liu.log.guide">
+		<appender-ref ref="ERROR"/>
+	</logger>
+
+	<!--
+		也是<logger>元素,但是它是根logger。默认debug
+		level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
+		<root>可以包含零个或多个<appender-ref>元素,标识这个appender将会添加到这个logger。
+	-->
+	<root level="INFO">
+		<appender-ref ref="STDOUT"/>
+		<appender-ref ref="DEBUG"/>
+		<appender-ref ref="INFO"/>
+		<appender-ref ref="ERROR"/>
+	</root>
+
+
+</configuration>
+
+```
+
+3. 打印日志
+```java
+public class LogGuideApplication {
+
+    private static Logger logger = LoggerFactory.getLogger(LogGuideApplication.class);
+
+    public static void main(String[] args) {
+        logger.trace("trace ...");
+        logger.debug("debug ...");
+        logger.info("info ...");
+        logger.warn("warn ...");
+        logger.error("error ...");
+    }
+}
+```
+
+
+
+## logger 配置
+logback.xml 的结构:
+```
+configuration
+    |- property
+    |- appender
+    |- logger
+    |- root
+```
+
+**第1种情况:只配置root**
+```xml
+<configuration>
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>%d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level %msg%n</pattern>
+		</encoder>
+	</appender>
+	<!-- 将 >= INFO 级别的日志信息,交给 STDOUT 处理-->
+	<root level="INFO">
+		<appender-ref ref="STDOUT"/>
+	</root>
+</configuration>
+```
+结果:控制台只会打印大于等于 INFO 级别的日志信息
+
+**第2种情况:配置一个logger**
+
+```xml
+<configuration>
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>%d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level %msg%n</pattern>
+		</encoder>
+	</appender>
+    
+    <logger name="liu.log.guide">
+		<appender-ref ref="STDOUT"/>
+	</logger>
+    
+	<!-- 将 >= INFO 级别的日志信息,交给 STDOUT 处理-->
+	<root level="INFO">
+		<appender-ref ref="STDOUT"/>
+	</root>
+</configuration>
+```
+
+logger 有3个属性:
+* name 日志范围,包名或者类名。root没有name属性,默认根目录。日志范围会优先匹配最精确的包名。
+比如说上面这个例子,在没有name="liu.log.guide"的logger之前,是由root记录所有类包日志的。有了logger之后,包"liu.log.guide"里的日志会交给最精确匹配的logger来记录。
+* level 日志级别,默认继承上级的日志级别。用来筛选不同日志级别的日志信息。
+* additivity 是否将日志信息传递给上级,默认为true
+
+**上面的情况会发生如下过程:**
+1. logger 默认继承了上级root的level,所以会记录所有日志级别level大于等于INFO的日志信息。
+2. logger 将筛选后的日志信息交给appender处理。除此之外,additivity属性默认true,所以还会将日志信息传递给上级logger,即root
+3. root 收到下级logger传递来的日志信息后,将其全部交给自己的appender处理
+4. appender 收到日志信息后,如果设置了levelFilter日志级别过滤器,就会只将过滤后的日志信息输出到目的地。
+> logger上下级关系使有name表示的包或类决定的。比如 name="liu.log" 就是 name="liu.log.guide" 的上级
+
+所以最明显的结果就是:日志级别大于等于INFO的日志都会被记录2次,这2次分别来自于logger和 root
+
+**扩展**
+如果将上面的logback.xml改为如下这样,会发生什么呢?
+```xml
+<configuration>
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>%d{yyyy-MM-dd:HH:mm:ss.SSS} [%thread] %-5level %msg%n</pattern>
+		</encoder>
+	</appender>
+    
+    <logger name="liu.log.guide" level="INFO">
+		<appender-ref ref="STDOUT"/>
+	</logger>
+	<root level="WARN">
+		<appender-ref ref="STDOUT"/>
+	</root>
+</configuration>
+```
+
+## appender 
+参考 https://www.cnblogs.com/cb0327/archive/2004/01/13/5770794.html
+
+### 自定义 appender
+
+> 本身logback提供了AppenderBase和UnsynchronizedAppenderBase两个抽象类(同步和异步),所以我们自定义时,只需要看实际业务继承其中的一个即可。
+
+1. 自定义appender类
+```java
+public class MyDbAppender 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));
+
+    }
+    // 省略 setter 和 getter 
+}
+```
+2. logback配置
+```xml
+<configuration>
+	<!-- 自定义appender -->
+	<appender name="MyDbAppender" class="liu.log.guide.appender.MyLogAppender">
+		<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+			<!-- 日志收集最低日志级别 -->
+			<level>INFO</level>
+		</filter>
+		<layout class="ch.qos.logback.classic.PatternLayout">
+			<pattern>${pattern}</pattern>
+		</layout>
+		<!-- 自定义参数 -->
+		<printString>刘传伟(logback)</printString>
+	</appender>
+
+    <logger name="liu.log.guide">
+		<appender-ref ref="MyDbAppender"/>
+	</logger>
+    <root>
+        <appender-ref ref="MyDbAppender"/>
+    </root>
+</configuration>
+```
+
+> 在 springboot-log-guide 中会用spring 继承logback将日志写入到数据库中
+
+参考:https://www.cnblogs.com/zimublog/p/12923786.html
+
+```java
+@Component
+public class MyLogDbAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
+
+    @Autowired
+    private BusinessLogService businessLogService;
+
+    @PostConstruct
+    public void init() {
+        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
+        ThresholdFilter filter = new ThresholdFilter();
+        filter.setLevel("ERROR");
+        filter.setContext(context);
+        filter.start();
+        this.addFilter(filter);
+        this.setContext(context);
+
+        // 定义 logger (和在 logback.xml 中的配置是对应的)
+        Logger logger = context.getLogger("liu.log.guide");
+        logger.setLevel(Level.DEBUG);
+        logger.setAdditive(false);
+        logger.addAppender(MyLogDbAppender.this);
+        // 定义要有下面这句
+        super.start();
+    }
+
+    @Override
+    public void append(ILoggingEvent event) {
+        if (event == null || !isStarted()) {
+            return;
+        }
+        // TODO 根据实际情况替换
+        BusinessLog businessLog = new BusinessLog();
+        businessLog.setLevel(event.getLevel().levelStr);
+        businessLog.setClassPath(event.getLoggerName());
+        businessLog.setMessage(event.getMessage());
+        businessLog.setCreateBy(1L);
+        businessLog.setCreateDate(new Date());
+        try {
+            businessLogService.add(businessLog);
+        } catch (Exception e) {
+            this.addError("上报错误日志失败:" + e.getMessage());
+        }
+    }
+}
+```
+## 对比 log4j.properties
+参考:https://blog.csdn.net/drift_away/article/details/7403658
+
+```properties
+log4j.rootLogger=DEBUG,CONSOLE,A1,im 
+#DEBUG,CONSOLE,FILE,ROLLING_FILE,MAIL,DATABASE
+################### 
+# Console Appender 
+################### 
+log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender 
+log4j.appender.Threshold=DEBUG 
+log4j.appender.CONSOLE.Target=System.out 
+log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout 
+log4j.appender.CONSOLE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n 
+#log4j.appender.CONSOLE.layout.ConversionPattern=[start]%d{DATE}[DATE]%n%p[PRIORITY]%n%x[NDC]%n%t[THREAD] n%c[CATEGORY]%n%m[MESSAGE]%n%n
+##################### 
+# File Appender 
+##################### 
+log4j.appender.FILE=org.apache.log4j.FileAppender 
+log4j.appender.FILE.File=file.log 
+log4j.appender.FILE.Append=false 
+log4j.appender.FILE.layout=org.apache.log4j.PatternLayout 
+log4j.appender.FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n 
+# Use this layout for LogFactor 5 analysis
+
+
+```
+
+
+## 打印日志的最佳实践
+
+参考:[别再乱打日志了,这份 Java 日志规范,应有尽有,建议收藏!!](https://mp.weixin.qq.com/s/UqvMC6t39YTPDa_FqrkbaA)
+
+## 占位符
+slf4j logger 占位符非常简单,并没有类型的区别
+```java
+String name = "中国";
+logger.info("我的名字叫{}", name);
+```
+
+## isDebugEnabled
+
+参考:[Java日志框架中需要判断log.isDebugEnabled()吗?](https://www.cnblogs.com/leiwei/p/14674143.html)
+
+很多代码中都用到了 isDebugEnabled ,如下:
+```java
+if (logger.isTraceEnabled()) {  
+   logger.trace("Detected " + this.multipartResolver);  
+}  
+else if (logger.isDebugEnabled()) {  
+   logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());  
+}
+```
+
+看源码可知,isDebugEnabled() 用来判断当前日志级别是否低于debug,比如info级别高于debug,则不会打印日志。
+```java
+public boolean isDebugEnabled(Marker marker) {
+	final FilterReply decision = callTurboFilters(marker, Level.DEBUG);
+	if (decision == FilterReply.NEUTRAL) {
+		return effectiveLevelInt <= Level.DEBUG_INT;
+	} else {
+		throw new IllegalStateException("Unknown FilterReply value: " + decision);
+	}
+}
+```
+
+**为什么要加if判断?**
+如果不加if判断,即使最终不需要debug日志输出,编译时也会自动执行字符串拼接,多少会消耗一部分性能。
+而加上if判断之后,就会跳过if条件内的代码
+```java
+//代码1
+logger.debug("这是一条debug日志:" + msg);
+//代码2
+if(logger.isDebugEnabled()) {
+	logger.debug("这是一条debug日志:" + msg);
+}
+```
+
+网上有部分人说,不需要加if判断,使用占位符即可。
+```java
+logger.debug("这是一条debug日志:{}", msg);
+```
+
+但在某些情况下,占位符依然不能避免性能消耗。
+```java
+logger.debug("这是一条debug日志:{}", obj.toString());
+```
+如上面这段代码,最终不会打印debug日志,但是会执行 obj.toString()
+
+### 最佳实践
+
+
+==**原则一:如果打印字符串常量,不需要`isDebugEnabled`**==
+
+比较下面两段代码:
+
+```java
+//代码1
+logger.debug("hello, world");
+//代码2
+if(logger.isDebugEnabled()){
+    logger.debug("hello, world");
+}
+```
+
+因为打印的日志是字面常量,没有计算逻辑。两段代码的性能是几乎一样的。添加`isDebugEnabled`反而会导致额外的代码。
+
+==**原则二:如果有参数,且参数只是字符串常量或计算简单,使用占位符**==
+
+考虑如下代码,`debug`方法包含了参数`user.getName()`。虽然执行`debug`方法时,会计算`user.getName()`,但只是一个简单的**get**方法,没有复杂计算,这时候,也可以不添加`isDebugEnabled`。
+
+```java
+logger.debug("hello, {}", user.getName());
+```
+
+==**原则三:如果有参数,且参数计算复杂,添加`isDebugEnabled`**==
+
+```java
+logger.debug("order price: {}", calculatePrice());
+```
+
+假设`calculatePrice`方法需要经过复杂计算。那么就应该添加`isDebugEnabled`判断,使用如下的代码:
+```java
+if(logger.isDebugEnabled()){
+    logger.debug("order price: {}", calculatePrice());
+}
+```

+ 0 - 5
java/日志/slf4j logger 占位符.md

@@ -1,5 +0,0 @@
-slf4j logger 占位符非常简单,并没有类型的区别
-```java
-String name = "中国";
-logger.info("我的名字叫{}", name);
-```