017_layout排版

1. 什么是layout?

1.1. Layout负责把事件转换成字符串。Layout接口的doLayout()方法的参数是代表任何类型的事件, 返回字符串。Layout接口概要如下:

1.2. 接口很简单却足够完成很多格式化需求。

1.3. Logback-classic只处理ch.qos.logback.classic.spi.ILoggingEvent类型的事件。

1.4. LayoutBase类管理对所有layout实例通用的状态, 比如layout是启动还是停止、header、footer和content type数据。LayoutBase类允许开发者在自己的layout里实现具体的格式化方式。LayoutBase类是泛型的。

2. 自定义layout例子

2.1. 让我们实现一个简单却可工作的layout, 打印内容包括: 自程序启动以来逝去的时间、记录事件的级别、包含在方括号里的调用者线程的名字、logger名、连字符、事件消息和换行。

2.2. 新建一个名为OwnCustomLayout的Java项目, 同时添加相关jar包

2.3. MySampleLayout.java继承LayoutBase<ILoggingEvent>。MySampleLayout类里唯一的方法doLayout(ILoggingEvent event), 一开始先初始化一个StringBuffer, 接着添加event参数的各种字段, 然后把StringBuffer转换成Stirng, 最后返回这个String。

2.4. 如何为layout增加选项?为layout或任何logback的其他组件添加属性非常简单: 声明一个属性及setter方法接即可。MySampleLayout类包含一个为输出添加前缀的属性。

package com.fj.ocl;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.LayoutBase;

public class MySampleLayout extends LayoutBase<ILoggingEvent> {
	String prefix;
	
	public String getPrefix() {
		return prefix;
	}

	public void setPrefix(String prefix) {
		this.prefix = prefix;
	}

	@Override
	public String doLayout(ILoggingEvent event) {
		StringBuffer buf = new StringBuffer(128);
		if(prefix != null) {
			buf.append(prefix + ": ");
		}
	    buf.append(event.getTimeStamp() - event.getLoggerContextVO().getBirthTime());
	    buf.append(" ");
	    buf.append(event.getLevel());
	    buf.append(" [");
	    buf.append(event.getThreadName());
	    buf.append("] ");
	    buf.append(event.getLoggerName());
	    buf.append(" - ");
	    buf.append(event.getFormattedMessage());
	    buf.append(CoreConstants.LINE_SEPARATOR);
	    return buf.toString();
	}
}

2.5. 配置自定义layout与配置其他layout是一样的。ConsoleAppender类需要一个encoder, 为了满足这个需求, 我们把包裹了MySimpleLayout的LayoutWrappingEncoder实例传递给ConsoleAppender。

2.6. OwnCustomLayout.java

package com.fj.ocl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OwnCustomLayout {
	public static final Logger logger = LoggerFactory.getLogger(OwnCustomLayout.class);
	
	public static void main(String[] args) {
		logger.info("自定义一个简单的Layout。");
	}
}

2.7. 运行程序

3. PatternLayout

3.1. 与所有Layout一样, PatternLayout的参数是一个记录事件并返回一个字符串, 但是被返回的字符串可以通过 PatternLayout的转换模式进行任意定制。

3.2. PatternLayout的转换模式与C语言里的printf()的转换模式很接近。转换模式是由文本文字和格式控制表达式(称为格式转换符(conversion specifier))组成。你可以在格式转换符内插入任意文本文字。每个格式转换符以"%"开头, 接着是可选的格式修饰符(format modifier)、一个转换符(conversion word)和放在括号里的其他可选参数。转换符控制待转换的数据字段, 比如logger名、级别、日期或线程名。格式修饰符控制字段的宽度、填充和左右对齐方式。

3.3. 已经讲过几次了, FileAppender及其子类需要一个encoder。所以, 当用于FileAppender或其子类时, PatternLayout必须被包裹在一个encoder里。由于FileAppender与PatternLayout的组合用法太常用了, 所以logback提供了一个名为"PatternLayoutEncoder"的encoder, 它唯一的设计目标就是包裹一个PatternLayout实例, 以便PatternLayout能被看作一个encoder。

3.4. 用编程方式为ConsoleAppender配置了一个PatternLayoutEncoder。新建一个名为ProgrammingPatternLayout的Java项目, 同时添加相关jar包。

3.5. 编写PatternSample.java

package com.fj.ppl;

import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;

public class PatternSample {
	private static final Logger rootLogger = (Logger) LoggerFactory.getLogger("ROOT");
	
	public static void main(String[] args) {
		LoggerContext loggerContext = rootLogger.getLoggerContext();
		loggerContext.reset();
		
		PatternLayoutEncoder encoder = new PatternLayoutEncoder();
		encoder.setContext(loggerContext);
		encoder.setPattern("%-5level [%thread]: %message%n");
		encoder.start();
		
		ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<ILoggingEvent>();
		appender.setContext(loggerContext);
		appender.setEncoder(encoder);
		appender.start();
		
		rootLogger.addAppender(appender);
		
		rootLogger.debug("用编程方式为ConsoleAppender配置了一个PatternLayoutEncoder。");
		rootLogger.warn("用编程方式为ConsoleAppender配置了一个PatternLayoutEncoder。");
	}
}

3.6. 运行结果

4. 转换符说明

4.1. 下表列出了转换符及其选项。当单元格里同时列出多个转换符时, 它们就是同义词。

转换符

作用

c{length}

lo{length}

logger{length}

输出源记录事件的logger名。

可以有一个整数型的参数, 功能是缩短logger名。设为"0"表示只输出logger名里最右边的点号之后的字符串。下表是缩写算法例子。

格式转换符

logger名

结果

%logger

mainPackage.sub.sample.Bar

mainPackage.sub.sample.Bar

%logger{0}

mainPackage.sub.sample.Bar

Bar

%logger{5}

mainPackage.sub.sample.Bar

m.s.s.Bar

%logger{10}

mainPackage.sub.sample.Bar

m.s.s.Bar

%logger{15}

mainPackage.sub.sample.Bar

m.s.sample.Bar

%logger{16}

mainPackage.sub.sample.Bar

m.sub.sample.Bar

%logger{26}

mainPackage.sub.sample.Bar

mainPackage.sub.sample.Bar

注意最右边的logger名永远不被省略, 即使它的长度超过了"length"选项。logger名里的其他片段可以被缩短为至少1个字符, 但永远不会消失。

C{length}

class{length}

输出执行记录请求的调用者的全限定类名。

和上面的"%logger"一样, 也有"length"属性, 表示缩短类名。"length"为"0"表示不输出包名。默认输出类的全限定名。

输出调用者的类信息并不很快, 所以尽量避免使用, 除非执行速度不造成任何问题。

contextName

cn

输出事件源头关联的logger的logger上下文的名称。

d{pattern}

date{pattern}

输出记录事件的日期。该转换符有可选的模式字符串选项。模式语法与java.text.SimpleDateFormat的格式兼容。

可以为ISO8061日期格式指定字符串"ISO8601"。如果没有模式选项, 则默认为ISO 8601日期格式。

下面是一些选项值的例子。

格式转换符

结果

%d

2006-10-20 14:06:49,812

%date

2006-10-20 14:06:49,812

%date{ISO8601}

2006-10-20 14:06:49,812

%date{HH:mm:ss.SSS}

14:06:49.812

%date{dd MMM yyyy ;HH:mm:ss.SSS}

20 oct. 2006;14:06:49.812

该转换符还可以有第二个选项: 时区。因此, "date{HH:mm:ss.SSS,Australia/Perth}"会输出澳大利亚珀斯城所在时区的时间。

由于逗号","是选项分隔符, 所以"[HH:mm:ss,SSS]"会输出"SSS"时区的时间, 但是"SSS"时区并不存在, 因此会用默认的GMT时区输出时间。如果想在日期模式里使用逗号, 可以用引号包含之, 例

如%date{"HH:mm:ss,SSS"}。

F

file

输出执行记录请求的Java源文件的文件名。

输出文件信息并不很快, 所以尽量避免使用, 除非执行速度不造成任何问题。

caller{depth}

caller{depth, evaluator-1, ... evaluator-n}

输出生成记录事件的调用者的位置信息。

位置信息依赖JVM实现, 但通常由调用方法的全限定名、放在括号里的文件名和行号。

该转换符可以有一个整数选项, 表示显示信息的深度。

例如, %caller{2}会输出:

Caller+0  at com.fj.cw.CwDao.add(CwDao.java:17)

Caller+1  at com.fj.cw.CwService.add(CwService.java:7)

例如, %caller{3}会输出:

Caller+0  at com.fj.cw.CwDao.add(CwDao.java:17)

Caller+1  at com.fj.cw.CwService.add(CwService.java:7)

Caller+2  at com.fj.cw.CwControl.main(CwControl.java:18)

 

在创建输出之前, 该转换符可以用求值式来测试是否满足给定的条件。

例如, 只有求值式"CALLER_DISPLAY_EVAL"返回true时, "%caller{3, CALLER_DISPLAY_EVAL}"才会输出三行堆栈跟踪。

L

line

输出执行记录请求的行号。

输出行号并不很快, 所以尽量避免使用, 除非执行速度不造成任何问题。

m

msg

message

输出与记录事件相关联的应用程序提供的消息。

M

method

输出执行记录请求的方法名。

输出方法名并不很快, 所以尽量避免使用, 除非执行速度不造成任何问题。

n

输出与平台相关的行分隔符。

该转换符与不可移植的行分隔符如"\n"或"\r\n"的性能几乎一样。所以该换行符是指定行分隔符的首选方式。

p

le

level

输出记录事件的级别。

r

relative

输出从程序启动到创建记录事件的逝去时间, 单位毫秒。

t

thread

输出产生记录事件的线程名。

X{key}

mdc{key}

输出与产生记录事件的线程相关联的MDC。

如果该转换符后有放在花括号里的key, 比如: %mdc{clientNumber}, 则输出该key对应的值。

如果没有指定key, 则输出MDC的全部内容, 格式是"key1=val1, key2=val2"。

ex{length}

exception{length}

throwable{length}

 

ex{length, evaluator-1, ... evaluator-n}

exception{length, evaluator-1, ... evaluator-n}

throwable{length, evaluator-1, ... evaluator-n}

输出与记录事件相关联的堆栈跟踪, 如果有的话。默认输出全部堆栈跟踪。

 

"throw"转换符可跟下面选项之一:

  • short: 打印堆栈跟踪的第一行;
  • full: 打印全部堆栈跟踪;
  • 任意整数: 堆栈跟踪的行数。

 

示例:

转换模式

结果

%ex

java.lang.RuntimeException: 运行时异常。

at com.fj.cw.error.ErrorDao.error(ErrorDao.java:11)

at com.fj.cw.error.ErrorService.error(ErrorService.java:7)

at com.fj.cw.CwControl.main(CwControl.java:24)

%ex{short}

java.lang.RuntimeException: 运行时异常。

at com.fj.cw.error.ErrorDao.error(ErrorDao.java:11)

%ex{full}

java.lang.RuntimeException: 运行时异常。

at com.fj.cw.error.ErrorDao.error(ErrorDao.java:11)

at com.fj.cw.error.ErrorService.error(ErrorService.java:7)

at com.fj.cw.CwControl.main(CwControl.java:24)

%ex{2}

java.lang.RuntimeException: 运行时异常。

at com.fj.cw.error.ErrorDao.error(ErrorDao.java:11)

at com.fj.cw.error.ErrorService.error(ErrorService.java:7)

 

在创建输出之前, 该转换符可以用求值式来测试是否满足给定的条件。例如, 只有求值式"EX_DISPLAY_EVAL"返回false时, "%ex{full, EX_DISPLAY_EVAL}"才会输出全部堆栈跟踪。

nopex

nopexception

表示不输出任何堆栈跟踪, 因此可以高效地忽略异常。

如果没有指定"%xThrowable"或其他与throwable有关的转换符, 则PatternLayout会自动把"%xThrowable"作为最后面的转换符, 但本转换符可以覆盖这种默认行为, 从而不显示堆栈跟踪信息。

marker

输出与记录请求相关联的marker。

如果marker包含子marker, 则按照下面的格式输出父、子marker的名称: parentName[ child1, child2 ]。

property{key}

输出名为"key"上下文属性的值。如果"key"不是logger上下文的属性, 则从系统属性里查找。

"key"没有默认值, 如果忽略之, 则返回错误提示"Property_HAS_NO_KEY"。

4.2. 由于在转换模式上下文里, 百分号"%"有特殊含义, 所以如果想把"%"作为普通文本文字, 则必须用"\"对它进行转义。

5. 格式修饰符

5.1. 默认情况下, 相关信息会按原格式输出。但是, 在格式修饰符的帮助下, 就可以为每个字段指定最小、最大宽度, 以及对齐方式。

5.2. 可选的格式修饰符位于百分号与转换符之间。

5.3. 第一个可选的格式修饰符是左对齐标志, 符号是减号"-"。接着是可选的最小宽度修饰符, 符号是表示输出的最少字符的十进制数字。如果字符数小于最小宽度, 则左填充或右填充。默认是左填充(即右对齐)。填充符是空格。如果字符数大于最小宽度, 则扩张到字符的宽度。字符永远不会被截断。

5.4. 最大宽度修饰符能够改变上面的行为, 符号是点号"."后加数字。如果字符数大于最大宽度, 则从前面截断字符。例如, 如果最大宽度是"8", 字符有10个, 则前两个字符会被抛弃。C语言的printf函数是从字符尾部进行截断。

5.5. 在点号"."后加上减号"-"表示从尾部截断。例如, 最大宽度是"8", 字符有10个, 则最后两个字符会被抛弃。

5.6. 下面是格式修饰符的各种例子。

5.7. 下表是格式修饰符截断的例子。

6. 用一个字符输出级别

6.1. 可以不打印级别的全名, 而是用T、D、W、I和E, 分别对应TRACE、DEBUG、WARN、INFO和ERROR。你既可以写一个自定义转换器或简单地用上面的格式修饰符把级别缩短为一个字符, 如"%.-1level"。

7. 圆括号的特殊含义

7.1. 在logback里, 圆括号被视为编组标记。因此可以将一个子模式进行编组, 然后对这个编组应用格式化指令。

7.2. 如下将对子模式"%d{HH:mm:ss.SSS} [%thread]"产生的输出进行编组, 结果是如果少于30个字符就右填充。

7.3. 如果要把圆括号作为普通文本文字, 则用前置"\"进行转义, 比如"\(%d{HH:mm:ss.SSS} [%thread]\)"。

8. 选项

8.1. 格式修饰符后可以跟选项。选项总是在花括号里声明。我们已经见过选项的一些用法, 比如与MDC转换符联合使用的: %mdc{someKey}。

9. 求值式(Evaluator)

9.1. 当格式转换符需要根据一个或多个EventEvaluator对象而有动态的行为时, 选项列表就派得上用场了。EventEvaluator对象负责决定给定的记录事件是否匹配求值式的条件。

<evaluator name="DISPLAY_CALL"> 
	<expression>throwable != null &amp;&amp; throwable instanceof java.lang.RuntimeException</expression>
</evaluator>

<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
	<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
		<pattern>%caller{5, DISPLAY_CALL}</pattern>
	</encoder>
</appender>

10. 转换符格式修饰符例子

10.1. 新建一个名为ConversionWord的Java项目, 同时添加相关jar包

10.2. 在src目录下创建logback.xml

<!-- contextName输出事件源头关联的logger的logger上下文的名称。 -->
<!-- mdc{key}输出与产生记录事件的线程相关联的MDC。 -->
<!-- property{key}输出名为"key"上下文属性的值。如果"key"不是 logger上下文的属性, 则从系统属性里查找。 -->
<!-- relative输出从程序启动到创建记录事件的逝去时间, 单位毫秒。 -->
<!-- date{pattern}输出记录事件的日期。 -->
<!-- level输出记录事件的级别。 -->
<!-- thread输出产生记录事件的线程名。 -->
<!-- class{length}输出执行记录请求的调用者的全限定类名。 -->
<!-- logger{length}输出源记录事件的logger名。 -->
<!-- file输出执行记录请求的Java源文件的文件名。 -->
<!-- line输出执行记录请求的行号。 -->
<!-- marker输出与记录请求相关联的marker。 -->
<!-- message输出与记录事件相关联的应用程序提供的消息。 -->
<!-- method输出执行记录请求的方法名。 -->
<!-- caller{depth, evaluator-1, ... evaluator-n}输出生成记录事件的调用者的位置信息。 -->
<!-- exception{length, evaluator-1, ... evaluator-n}输出与记录事件相关联的堆栈跟踪, 如果有的话。默认输出全部堆栈跟踪。 -->
<!-- nopexception表示不输出任何堆栈跟踪, 因此可以高效地忽略异常。 -->
<!-- n输出与平台相关的行分隔符。 -->
<configuration>
	<contextName>转换符</contextName>
	
	<evaluator name="DISPLAY_CALL"> 
		<expression>throwable != null &amp;&amp; throwable instanceof java.lang.RuntimeException</expression>
	</evaluator>
	
	<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>%-32([%.-1level] [%-8.3thread] [%8.3logger{64}]:) %marker [%10.-5message] %nopexception%n%caller{5, DISPLAY_CALL}%n</pattern>
		</encoder>
	</appender>

	<appender name="errorFile" class="ch.qos.logback.core.FileAppender">
		<file>errorFile.log</file>
		<append>false</append>
		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>%contextName %mdc{mdcKey} %property{ptyKey} %relative %class{64}: %message%n%exception</pattern>
		</encoder>
	</appender>
	
	<appender name="defaultErrorFile" class="ch.qos.logback.core.FileAppender">
		<file>defaultErrorFile.log</file>
		<append>false</append>
		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
			<pattern>%date{yyy-MM-dd HH:mm:ss}: %file %method %line %message%n</pattern>
		</encoder>
	</appender>
	
	<logger name="com.fj.cw.error.ErrorDao" level="error" additivity="false">
		<appender-ref ref="errorFile" />
	</logger>
	
	<logger name="com.fj.cw.noerror.DefaultErrorDao" level="error" additivity="false">
		<appender-ref ref="defaultErrorFile" />
	</logger>

	<root level="all">
		<appender-ref ref="stdout"></appender-ref>
	</root>
</configuration>

10.3. DefaultErrorDao.java

package com.fj.cw.noerror;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultErrorDao {
	private static final Logger logger = LoggerFactory.getLogger(DefaultErrorDao.class);
	
	public void error() {
		try {
			throw new RuntimeException("运行时异常。");
		} catch (Exception e) {
			logger.error("我是一条错误信息。", e);
		}
	}
}

10.4. DefaultErrorService.java

package com.fj.cw.noerror;

public class DefaultErrorService {
	private DefaultErrorDao dao = new DefaultErrorDao();
	
	public void error() {
		dao.error();
	}
}

10.5. ErrorDao.java

package com.fj.cw.error;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ErrorDao {
	private static final Logger logger = LoggerFactory.getLogger(ErrorDao.class);
	
	public void error() {
		try {
			throw new RuntimeException("运行时异常。");
		} catch (Exception e) {
			logger.error("我是一条错误信息。", e);
		}
	}
}

10.6. ErrorService.java

package com.fj.cw.error;

public class ErrorService {
	private ErrorDao dao = new ErrorDao();
	
	public void error() {
		dao.error();
	}
}

10.7. CwDao.java

package com.fj.cw;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

public class CwDao {
	private static final Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
	
	public void add() {
		try {
			throw new RuntimeException("运行时异常。");
		} catch (Exception e) {
			Marker marker = MarkerFactory.getMarker("异常标记");
			marker.add(MarkerFactory.getMarker("子标记"));
			logger.error(marker, "添加一条数据", e);
		}
	}
	
	public void delete() {
		logger.warn("删除一条数据");
	}
	
	public void modify() {
		logger.info("修改一条数据");
	}
	
	public void select() {
		logger.debug("查询数据");
	}
}

10.8. CwService.java

package com.fj.cw;

public class CwService {
	private CwDao dao = new CwDao();
	
	public void add() {
		dao.add();
	}
	
	public void delete() {
		dao.delete();
	}
	
	public void modify() {
		dao.modify();
	}
	
	public void select() {
		dao.select();
	}
}

10.9. CwControl.java

package com.fj.cw;

import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import com.fj.cw.error.ErrorService;
import com.fj.cw.noerror.DefaultErrorService;
import ch.qos.logback.classic.LoggerContext;

public class CwControl {
	public static void main(String[] args) {
		MDC.put("mdcKey", "我是和记录事件的线程相关联的MDC值。");
		LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
		lc.putProperty("ptyKey", "我是上下文属性值。");
		
		CwService service = new CwService();
		service.add();
		service.delete();
		service.modify();
		service.select();
		
		ErrorService errorService = new ErrorService();
		errorService.error();
		
		DefaultErrorService noErrorService = new DefaultErrorService();
		noErrorService.error();
	}
}

10.10. 运行项目控制台输出

10.11. defaultErrorFile.log

10.12. errorFile.log

11. 创建自定义格式转换符

11.1. 创建自定义格式转换符有两步。第一步创建一个继承自ClassicConverter的类; 第二步在配置文件里配置这个类。

11.2. 新建一个名为OwnConversionWord的Java项目, 同时添加相关jar包。

11.3. 首先, 必须继承ClassicConverter类。ClassicConverter对象负责从ILoggingEvent提取信息, 并产生一个字符串。

package com.fj.ocw;

import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;

public class MyConversion extends ClassicConverter {
	@Override
	public String convert(ILoggingEvent event) {
		return event.getMessage().replaceAll("\\.", "。");
	}
}

11.4. 第二步在logback.xml里配置我们的Converter。

11.5. 测试类

package com.fj.ocw;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 替换日志信息中的英文符号为中文符号。
 */
public class TestMyConverter {
	private static final Logger logger = LoggerFactory.getLogger(TestMyConverter.class);
	
	public static void main(String[] args) {
		logger.error("我是一条错误日志.");
		logger.warn("我是一条警告日志.");
		logger.debug("我是一条测试日志.");
	}
}

11.6. 运行项目

12. HTMLLayout

12.1. HTMLLayout以HTML表格的形式输出记录, 表格的每行对应于一个记录事件。

12.2. 表格的列是由格式转换符指定的, 因此你可以完全控制表格的内容和格式。你可以选择和显示任何被PatternLayout所知的转换器的组合。

12.3. PatternLayout与HTMLLayout一起使用的一个例外是, 格式转换符不能用空格分隔, 或更一般地说, 不能被文本文字分隔。格式转换符里的每个转换符都会产生一个单独的列。同样地, 转换符里的每块文本文字也会导致生成一个单独的列。

12.4. 新建一个名为HTMLLayout的Java项目, 同时添加相关jar包。

12.5. 在src目录下添加logback.xml

12.6. 测试类TestHTMLLayout.java

package com.fj.htmllayout;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestHTMLLayout {
	private static final Logger logger = LoggerFactory.getLogger(TestHTMLLayout.class);
	
	public static void main(String[] args) {
		logger.error("我是一条错误日志.");
		logger.warn("我是一条警告日志.");
		logger.debug("我是一条测试日志.");
	}
}

12.7. 运行结果

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值