On this page
第 6 章:布局
** TCP 实现将遵循鲁棒性的一般原则:对您所做的事情要保守一些,对您从他人那里接受的一些内容要开放一些。
-JON POSTEL,RFC 793 **
什么是布局?
如果您想知道,布局与佛罗里达 State 的大型庄园无关。布局是 logback 组件,负责将传入事件转换为 String。 Layout介面中的format()
方法取得代表事件(任何类型)的物件,并传回 String。 Layout
接口的摘要如下所示。
public interface Layout<E> extends ContextAware, LifeCycle {
String doLayout(E event);
String getFileHeader();
String getPresentationHeader();
String getFileFooter();
String getPresentationFooter();
String getContentType();
}
该界面非常简单,但是足以满足许多格式需求。您可能从约瑟夫·海勒(Joseph Heller)的* Catch-22 *中认识到来自得克萨斯 State 的德克萨斯 State 开发人员,可能会惊呼:它只需要五种方法即可实现布局!?
Logback-classic
Logback-classic 连接到仅处理ch.qos.logback.classic.spi.ILoggingEvent类型的事件。这一事实在本节中将显而易见。
编写自己的自定义布局
让我们为 logback-classic 模块实现一个简单而又实用的布局,该布局显示自应用程序启动以来经过的时间,日志记录事件的级别,括号之间的调用者线程,其 Logger 名称,破折号和事件消息和一条新线。
输出示例如下:
10489 DEBUG [main] com.marsupial.Pouch-世界您好。
这是由 Texan 开发人员编写的可能的实现:
*示例:布局(logback-examples/src/main/java/chapters/layouts/MySampleLayout.java)的示例实现
package chapters.layouts;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.LayoutBase;
public class MySampleLayout extends LayoutBase<ILoggingEvent> {
public String doLayout(ILoggingEvent event) {
StringBuffer sbuf = new StringBuffer(128);
sbuf.append(event.getTimeStamp() - event.getLoggingContextVO.getBirthTime());
sbuf.append(" ");
sbuf.append(event.getLevel());
sbuf.append(" [");
sbuf.append(event.getThreadName());
sbuf.append("] ");
sbuf.append(event.getLoggerName();
sbuf.append(" - ");
sbuf.append(event.getFormattedMessage());
sbuf.append(CoreConstants.LINE_SEP);
return sbuf.toString();
}
}
请注意,MySampleLayout
扩展了LayoutBase。此类 Management 所有布局实例的公共状态,例如布局是开始还是停止,页眉,页脚和 Content Type 数据。它使开发人员可以专注于Layout
期望的格式。注意LayoutBase
类是通用的。在其类声明中,MySampleLayout
扩展了LayoutBase<ILoggingEvent>
。
doLayout(ILoggingEvent event)
方法(即MySampleLayout
中的唯一方法)从实例化StringBuffer
开始。它通过添加事件参数的各个字段来进行。来自得克萨斯 State 的德 State 人小心地打印了邮件的格式化形式。如果将一个或多个参数与日志记录请求一起传递,则这很重要。
将这些各种字符添加到字符串缓冲区后,doLayout()
方法将缓冲区转换为String
并返回结果值。
在上面的示例中,doLayout
方法将忽略事件中包含的所有最终异常。在实际的布局实现中,您很可能也希望打印异常的内容。
配置您的自定义布局
自定义布局被配置为任何其他组件。如前所述,FileAppender
及其子类需要一个编码器。为了满足此要求,我们将LayoutWrappingEncoder
的实例传递给FileAppender
,该实例包装了MySampleLayout
。这是配置文件:
*示例:MySampleLayout 的配置(logback-examples/src/main/resources/chapters/layouts/sampleLayoutConfig.xml)*查看为.groovy
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="chapters.layouts.MySampleLayout" />
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
示例应用程序chapters.layouts.SampleLogging使用作为其第一个参数传递的配置脚本来配置logback,然后记录调试消息和错误消息。
要运行此示例,请从* logback-examples *目录中发出以下命令。
Java Chapters.layouts.SampleLogging src/main/java/chapters/layouts/sampleLayoutConfig.xml
这将产生:
0 DEBUG [main] chapters.layouts.SampleLogging - Everything's going well
0 ERROR [main] chapters.layouts.SampleLogging - maybe not quite...
那很简单。怀疑论者埃利亚(Elea)坚持认为,除了不确定性本身(不确定性)之外,其他都不确定,他可能会问:带有选项的布局怎么样?Reader 可以在MySampleLayout2.java中找到我们的自定义布局的略微修改版本。如本手册中所提到的,向布局或任何其他 logback 组件添加属性就像声明该属性的 setter 方法一样简单。
MySampleLayout2类包含两个属性。第一个是可以添加到输出中的前缀。第二个属性用于选择是否显示从中发送日志记录请求的线程的名称。
这是MySampleLayout2类的副本:
package chapters.layouts;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.LayoutBase;
public class MySampleLayout2 extends LayoutBase<ILoggingEvent> {
String prefix = null;
boolean printThreadName = true;
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public void setPrintThreadName(boolean printThreadName) {
this.printThreadName = printThreadName;
}
public String doLayout(ILoggingEvent event) {
StringBuffer sbuf = new StringBuffer(128);
if (prefix != null) {
sbuf.append(prefix + ": ");
}
sbuf.append(event.getTimeStamp() - event.getLoggerContextVO().getBirthTime());
sbuf.append(" ");
sbuf.append(event.getLevel());
if (printThreadName) {
sbuf.append(" [");
sbuf.append(event.getThreadName());
sbuf.append("] ");
} else {
sbuf.append(" ");
}
sbuf.append(event.getLoggerName());
sbuf.append(" - ");
sbuf.append(event.getFormattedMessage());
sbuf.append(LINE_SEP);
return sbuf.toString();
}
}
只需添加相应的 setter 方法即可启用属性配置。请注意,PrintThreadName
属性是布尔值,而不是String
。 关于配置的章节详细介绍了logback组件的配置。 关于乔兰的章节提供了更多详细信息。这是为MySampleLayout2
量身定制的配置文件。
观看为.groovy
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="chapters.layouts.MySampleLayout2">
<prefix>MyPrefix</prefix>
<printThreadName>false</printThreadName>
</layout>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
PatternLayout
经典的 Logback 具有灵活的布局,称为PatternLayout。在所有布局中,PatternLayout
记录一个日志事件并返回String
。但是,可以通过调整PatternLayout
的转换模式来自定义String
。
PatternLayout
的转换模式与 C 编程语言中printf()
函数的转换模式密切相关。转换模式由 Literals 文本和称为* conversion specifiers 的格式控制表达式组成。您可以随意在转换模式中插入任何 Literals 文本。每个转换说明符均以百分号'%'开头,后跟可选的 format 修饰符*,转换字和括号之间的可选参数。转换字控制数据字段进行转换,例如 Logger 名称,级别,日期或线程名称。格式修饰符控制字段的宽度,填充以及左对齐或右对齐。
正如在某些场合已经提到的,FileAppender
和子类期望使用编码器。因此,当与FileAppender
或其子类结合使用时,必须将PatternLayout
包装在编码器中。鉴于FileAppender
/PatternLayout
组合是如此普遍,logback 附带了一个名为PatternLayoutEncoder
的编码器,该编码器专门用于包装PatternLayout
实例,以便可以将其视为编码器。以下是以编程方式将ConsoleAppender
与PatternLayoutEncoder
配置在一起的示例:
*示例:PatternLayout (logback-examples/src/main/java/chapters/layouts/PatternSample.java)的用法示例
package chapters.layouts;
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 {
static public void main(String[] args) throws Exception {
Logger rootLogger = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
LoggerContext loggerContext = rootLogger.getLoggerContext();
// we are not interested in auto-configuration
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("Message 1");
rootLogger.warn("Message 2");
}
}
在上面的示例中,转换模式设置为 “%-5level [%thread]:%message%n” 。稍后将给出 logback 中包含的转换字的提要。以以下方式运行PatternSample
应用程序:
Java Java Chapters.layouts.PatternSample
将在控制台上产生以下输出。
调试[main]:消息 1 警告[main]:消息 2
请注意,在转换模式 “%-5level [%thread]:%message%n” 中,Literals 文本和转换说明符之间没有明确的分隔符。解析转换模式时,PatternLayout
能够区分 Literals 文本(空格字符,方括号,冒号字符)和转换说明符。在上面的示例中,转换说明符%-5level 表示应将日志记录事件的级别调整为 5 个字符的宽度。格式说明符将在下面说明。
在PatternLayout
中,括号可用于对转换模式进行分组。 因此,'('和')'具有特殊含义,如果要用作 Literals,则需要转义. 括号的特殊性质还为explained below。
如前所述,某些转换说明符可能包括在括号之间传递的可选参数。带有选项的 samples 转换说明符可能是%logger{10}
。这里的“ logger”是转换词,“ 10”是选项。选项是下面进一步讨论。
下表描述了可识别的转换词及其选项。当在同一表单元格中列出多个转换词时,它们被视为别名。
Conversion Word | Effect | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
c { length } | |||||||||||||||||||||||||||||||
lo { length } logger { length } |
在记录事件的起点输出 Logger 的名称。 此转换字将整数作为其唯一的选择。转换器的缩写算法将缩短 Logger 名称,通常不会造成重大意义损失。将 length 选项的值设置为零是一个 exception。它将使转换字将子字符串返回到 Logger 名称中最右边的点字符。下表提供了实际使用的缩写算法的示例。 |
转换说明符 | Logger 名称 | 结果 |
%logger | mainPackage.sub.sample.Bar | mainPackage.sub.sample.Bar | %logger{0} | mainPackage.sub.sample.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 | 请注意,即使长度超过* length *选项,也不会缩写 Logger 名称中最右边的段。其他段可能会缩短到最多一个字符,但永远不会删除。 |
|||||
C { length } class { length } |
输出发出日志记录请求的调用方的全限定类名。 就像上面的*%logger *转换词一样,此转换将整数作为缩短类名的选项。零具有特殊含义,将导致在没有包名前缀的情况下打印简单类名。默认情况下,类名是完整打印的。 生成呼叫者类别信息的速度不是特别快。因此,除非执行速度不成问题,否则应避免使用它。 |
||||||||||||||||||||||||||||||
contextName cn |
输出事件起源处的 Logger 所附加到的 Logger 上下文的名称。 | ||||||||||||||||||||||||||||||
d { pattern } date { pattern } d { pattern , timezone } date { pattern , timezone } |
用于输出记录事件的日期。日期转换词允许使用模式字符串作为参数。模式语法与java.text.SimpleDateFormat接受的格式兼容。 您可以为 ISO8601 日期格式指定字符串*“ ISO8601” *。请注意,在没有模式参数的情况下,%date 转换字默认为ISO 8601 日期格式。 这是一些示例参数值。他们假定实际日期为 2006 年 10 月 20 日星期五,并且作者在午餐后返回工作来处理此文档。 |
转换模式 | 结果 |
%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} | 10 月 20 日 2006; 14:06:49.812 | 第二个参数指定时区。例如,“%date{HH:mm:ss.SSS, Australia/Perth}”将显示世界上最孤立的城市澳大利亚珀斯所在时区的时间。请注意,在缺少 timezone 参数的情况下,将使用主机 Java 平台的默认时区。如果指定的时区标识符未知或拼写错误,则认为格林尼治标准时间(GMT)时区是由TimeZone.getTimeZone(String)方法规范规定的。 常见错误假设逗号','字符被解释为参数分隔符,则模式 HH:mm:ss,SSS 将被解释为模式HM:mm:ss 和时区SSS 。如果您希望在日期模式中包含逗号,则只需将模式用引号引起来。例如,%date{ " HH:mm:ss,SSS " }。 |
|||||||||||||||||
F/file | 输出发出记录请求的 Java 源文件的文件名。 生成文件信息并不是特别快。因此,除非执行速度不成问题,否则应避免使用它。 |
||||||||||||||||||||||||||||||
caller{} caller{} caller{depth, evaluator-1, ... evaluator-n} caller{depthStart..depthEnd, evaluator-1, ... evaluator-n} | 输出生成日志事件的呼叫者的位置信息。 位置信息取决于 JVM 的实现,但通常包括调用方法的标准名称,后跟调用者的源代码,文件名和括号之间的行号。 可以将一个整数添加到* caller 转换说明符的选项中,以配置要显示的信息的深度。 例如, %caller{2} 将显示以下摘录: 0 [93]调试-记录语句 mainPackage.sub.sample.Bar.sampleMethodName(Bar.java:22)处的呼叫者 0 在 mainPackage.sub.sample.Bar.createLoggingRequest(Bar.java:17)上的调用方 1 而 %caller{3} 会显示其他摘录: 16 [94]调试-记录语句 mainPackage.sub.sample.Bar.sampleMethodName(Bar.java:22)处的呼叫者 0 在 mainPackage.sub.sample.Bar.createLoggingRequest(Bar.java:17)上的调用方 1 调用方 2 位于 mainPackage.ConfigTester.main(ConfigTester.java:38) 可以将一个范围说明符添加到 caller 转换说明符的选项中,以配置要显示的信息的深度范围。 例如, %caller{1..2} 将显示以下摘录: 0 [95]调试-记录语句 mainPackage.sub.sample.Bar.createLoggingRequest(Bar.java:17)上的呼叫者 0 此转换字还可以使用评估程序在计算呼叫者数据之前根据给定的标准测试日志记录事件。例如,仅当名为 CALLER_DISPLAY_EVAL *的评估程序返回“肯定”答案时,使用 %caller{3, CALLER_DISPLAY_EVAL} 将显示三行堆栈跟踪。 评估器如下所述。 |
||||||||||||||||||||||||||||||
L /行 | 输出发出记录请求的行号。 生成行号信息并不是特别快。因此,除非执行速度不成问题,否则应避免使用它。 |
||||||||||||||||||||||||||||||
m/msg/message | 输出与日志记录事件关联的应用程序提供的消息。 | ||||||||||||||||||||||||||||||
M/method | 输出发出记录请求的方法名称。 生成方法名称并不是特别快。因此,除非执行速度不成问题,否则应避免使用它。 |
||||||||||||||||||||||||||||||
n | 输出平台相关的行分隔符或多个字符。 该转换字提供的性能几乎与使用不可移植的行分隔符字符串(例如“n”或“rn”)相同。因此,这是指定行分隔符的首选方式。 |
||||||||||||||||||||||||||||||
p/le/level | 输出记录事件的级别。 | ||||||||||||||||||||||||||||||
r/relative | 输出从应用程序启动到创建日志记录事件为止经过的毫秒数。 | ||||||||||||||||||||||||||||||
t/thread | 输出生成日志事件的线程的名称。 | ||||||||||||||||||||||||||||||
X { key:-defaultVal } mdc { key:-defaultVal } |
输出与生成日志事件的线程关联的 MDC(Map 的诊断上下文)。 如果 mdc 转换字后跟两个括号之间的键,如 %mdc{} ,则将输出与键“ userid”相对应的 MDC 值。如果该值为 null,则输出在 :- 运算符之后指定的default value。如果未指定默认值,则输出空字符串。 如果没有给出密钥,则 MDC 的全部内容将以“ key1 = val1,key2 = val2”格式输出。 有关此主题的更多详细信息,请参见关于 MDC 的章节。 |
||||||||||||||||||||||||||||||
前 { depth } exception { depth } throwable { depth } ex {depth, evaluator-1, ..., evaluator-n} exception {depth, evaluator-1, ..., evaluator-n} throwable {depth, evaluator-1, ..., evaluator-n} |
输出与日志事件关联的异常的堆栈跟踪(如果有)。默认情况下,将输出完整的堆栈跟踪。 * throwable *转换词后面可以带有以下选项之一: |
* short :打印堆栈跟踪的第一行
full :打印完整的堆栈跟踪
任何整数:打印给定行数的堆栈跟踪
这里有些例子:
|转换模式|结果
| %ex | mainPackage.foo.bar.TestException:休斯顿我们有一个问题
at mainPackage.foo.bar.TestThrower.fire(TestThrower.java:22)
at mainPackage.foo.bar.TestThrower.readyToLaunch(TestThrower.java:17)
在 mainPackage.ExceptionLauncher.main(ExceptionLauncher.java:38)|
| %ex{short} | mainPackage.foo.bar.TestException:休斯顿我们有一个问题
在 mainPackage.foo.bar.TestThrower.fire(TestThrower.java:22)|
| %ex{full} | mainPackage.foo.bar.TestException:休斯顿我们有一个问题
at mainPackage.foo.bar.TestThrower.fire(TestThrower.java:22)
at mainPackage.foo.bar.TestThrower.readyToLaunch(TestThrower.java:17)
在 mainPackage.ExceptionLauncher.main(ExceptionLauncher.java:38)|
| %ex{2} | mainPackage.foo.bar.TestException:休斯顿我们有一个问题
at mainPackage.foo.bar.TestThrower.fire(TestThrower.java:22)
在 mainPackage.foo.bar.TestThrower.readyToLaunch(TestThrower.java:17)中|
此转换字还可以使用评估程序在创建输出之前根据给定的标准测试日志记录事件。例如,仅当名为 EX_DISPLAY_EVAL 的评估程序返回“否定”答案时,使用 %ex{full, EX_DISPLAY_EVAL} 才会显示异常的完整堆栈跟踪。评估器在本文档的后面进行了描述。
如果在转换模式中未指定%throwable 或其他与 throwable 相关的转换字,则由于堆栈跟踪信息的重要性,PatternLayout
将自动将其添加为最后一个转换字。如果您不希望显示堆栈跟踪信息,则可以用$ nopex 转换词代替%throwable。另请参见%nopex 转换词。
| xEx { depth }
xException { depth }
xThrowable { depth }
xEx {depth, evaluator-1, ..., evaluator-n}
xException {depth, evaluator-1, ..., evaluator-n}
xThrowable {depth, evaluator-1, ..., evaluator-n} |与上面的%throwable 转换词相同,并附加了类包装信息。
在异常的每个堆栈帧的末尾,将添加一个字符串,该字符串由包含相关类的 jar 文件组成,后跟该 jar 清单中的“ Implementation-Version”。这项创新技术是最初由 James Strachan 建议。如果信息不确定,则类包装数据之前将使用代字号,即“~”字符。
这是一个例子:
com.xyz.Wombat(Wombat.java:57)的 java.lang.NullPointerException ~[wombat-1.3.jar:1.3] com.xyz.Wombat(Wombat.java:76)~[wombat-1.3 .jar:1.3]在 sun.reflect.NativeMethodAccessorImpl.invoke0(本机方法)~[na:1.5.0_06]在 sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)~[na:1.5.0_06]在星期日.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)~[na:1.5.0_06]在 java.lang.reflect.Method.invoke(Method.java:585)~[na:1.5.0_06]在 org.junit .internal.runners.TestMethod.invoke(TestMethod.java:59)[junit-4.4.jar:na]位于 org.junit.internal.runners.MethodRoadie.runTestMethod(MethodRoadie.java:98)[junit-4.4.jar: na] ...等
即使在任意复杂的类加载器层次结构中,Logback 也要竭尽全力以确保其显示的类包装信息是正确的。但是,当无法保证信息的绝对正确性时,它将在数据前加上波浪号(即“~”字符)。因此,从理论上讲,印刷的类包装信息可能与真实的类包装信息不同。因此,在上面的示例中,假定 Wombat 类的包装数据前面有波浪号,则实际上可能有正确的包装数据[wombat.jar:1.7]。
请注意,鉴于其潜在成本,计算结果为包装数据默认为禁用。启用打包数据计算后,PatternLayout
将自动在模式字符串的末尾假设%xThrowable 后缀而不是%throwable 后缀。
用户反馈表示 Netbeans 对包装信息感到窒息。|
| nopex
nopexception |尽管它Feign处理堆栈跟踪数据,但是此转换字不会输出任何数据,因此可以有效地忽略异常。
%nopex 转换词允许用户重写PatternLayout
的内部安全机制,该机制在没有其他转换词处理异常的情况下静默添加%xThrowable 转换关键字。
| marker |输出与 Logger 请求关联的标记。
如果标记包含子标记,则转换器将根据以下所示格式显示父名称和子名称。
parentName [ child1, child2 ]|
| property{} |输出与名为 key 的属性关联的值。有关如何定义离子define variables和variable scopes的相关文档。如果 key 不是 Logger 上下文的属性,则将在 System 属性中查找 key 。
key *没有默认值。如果省略,则返回值将是“ Property_HAS_NO_KEY”,表明错误情况。
| replace( p ) {r, t} |用子模式'p'替换字符串中的't'替换正则表达式'r'的出现。例如,“%replace(%msg)\ {'s', ''}”将删除事件消息中包含的所有空格。
模式“ p”可以任意复杂,特别是可以包含多个转换关键字。例如,“%replace(%logger%msg)\ {'.', '/'}“将用正斜杠替换 Logger 中的所有点或事件消息。
| rEx { depth }
rootException { depth }
rEx {depth, evaluator-1, ..., evaluator-n}
rootException {depth, evaluator-1, ..., evaluator-n} |输出与日志记录事件关联的异常的堆栈跟踪(如果有)。将首先输出根本原因,而不是标准的“根本原因最后”。这是一个示例输出(针对空间进行编辑):
java.lang.NullPointerException
在 com.xyz.Wombat(Wombat.java:57)~[113]
在 com.xyz.Wombat(Wombat.java:76)~[114]
包装:org.springframework.BeanCreationException:创建名称为'wombat'的 bean 时出错:
at org.springframework.AbstractBeanFactory.getBean(AbstractBeanFactory.java:248) [spring-2.0.jar:2.0]
at org.springframework.AbstractBeanFactory.getBean(AbstractBeanFactory.java:170) [spring-2.0.jar:2.0]
at org.apache.catalina.StandardContext.listenerStart(StandardContext.java:3934) [tomcat-6.0.26.jar:6.0.26]
%rootException 转换器接受与上述%xException 转换器相同的可选参数,包括深度和评估器。它还输出包装信息。简而言之,%rootException 与%xException 非常相似,只是异常输出的 Sequences 相反。
%rootException 转换器的作者 Tomasz Nurkiewicz 在博客条目“首先记录异常的根本原因”中记录了他的贡献。
%字符具有特殊含义
假设在转换模式的上下文中,百分号带有特殊含义,为了将其作为 Literals 包含在内,需要用反斜杠将其转义,例如“%d%p % %m%n”。
对紧随转换字词的 Literals 的限制
在大多数情况下,Literals 自然包含空格或其他定界字符,以使它们不会与转换词混淆。例如,模式“%level [%thread]-%message%n”包含字符串 Literals" ["
和"] - "
。但是,如果一个字符(可能是 java 标识符的一部分)紧随转换字之后,则将把 logback 的模式解析器当作愚弄,以为 Literals 是转换字的一部分。例如,模式“%date %nHello ”将被解释为两个转换词%date 和%nHello,并且由于%nHello 不是已知的转换词,因此 logback 将为%nHello 输出%PARSER_ERROR [nHello]。如果希望字符串 Literals“ Hello”立即将%n 和 Hello 分开,请将空的参数列表传递给%n。例如,“%date %n{} Hello”将被解释为%date,后跟%n,然后是 Literals“ Hello”。
Format modifiers
默认情况下,相关信息按原样输出。但是,借助格式修饰符,可以更改每个数据字段的最小和最大宽度以及对齐方式。
可选的格式修饰符位于百分号与转换字符或单词之间。
第一个可选的格式修饰符是左对齐标志,它只是减号(-)。然后是可选的“最小字段宽度”修饰符。这是一个十进制常数,代表要输出的最少字符数。如果数据项包含的字符较少,则在左侧或右侧填充它,直到达到最小宽度。默认为在左侧填充(右对齐),但您可以使用左侧对齐标志指定右填充。填充字符为空格。如果数据项大于最小字段宽度,则字段将扩展以容纳数据。该值永远不会被截断。
可以使用最大字段宽度修饰符更改此行为,该修饰符由句点和十进制常数指定。如果数据项比最大字段长,那么多余的字符将从数据项的“开始”处删除。例如,如果最大字段宽度为 8,数据项的长度为 10 个字符,则将删除数据项的前两个字符。此行为与 C 中的 printf 函数不同,后者从头开始进行截断。
通过在句点之后添加减号,可以从结尾截断。在这种情况下,如果最大字段宽度为 8,数据项的长度为 10 个字符,则将删除数据项的最后两个字符。
以下是 Logger 转换说明符的各种格式修改器示例。
Format modifier | Left justify | Minimum width | Maximum width | Comment |
---|---|---|---|---|
%20logger | false | 20 | none | 如果 Logger 名称的长度少于 20 个字符,请在左侧填充空格。 |
%-20logger | true | 20 | none | 如果 Logger 名称的长度少于 20 个字符,请在右边加空格。 |
%.30logger | NA | none | 30 | 如果 Logger 名称超过 30 个字符,请从头开始截断。 |
%20.30logger | false | 20 | 30 | 如果 Logger 名称少于 20 个字符,请在左空格处留空格。但是,如果 Logger 名称超过 30 个字符,则从头开始截断。 |
%-20.30logger | true | 20 | 30 | 如果 Logger 名称少于 20 个字符,请在右空格处加空格。但是,如果 Logger 名称超过 30 个字符,则从* beginning *处截断。 |
%.-30logger | NA | none | 30 | 如果 Logger 名称超过 30 个字符,请从* end *截断。 |
下表列出了格式修饰符截断的示例。请注意,方括号(即成对的[[]]字符)不是输出的一部分。它们用于界定输出的宽度。
Format modifier | Logger name | Result |
---|---|---|
[%20.20logger] | main.Name | [ main.Name] |
[%-20.20logger] | main.Name | [main.Name ] |
[%10.10logger] | main.foo.foo.bar.Name | [o.bar.Name] |
[%10.-10logger] | main.foo.foo.bar.Name | [main.foo.f] |
只输出一个字母
您可能只想打印 T,D,W,I 和 E,而不是为该级别打印 TRACE,DEBUG,WARN,INFO 或 ERROR。您可以为此编写一个custom converter,或者只是使用格式修饰符(仅讨论)以将级别值缩短为单个字符。适当的转换说明符应为“ %.-1level
”。
转换字选项
转换说明符后可以跟选项。总是在花括号之间声明。我们已经看到了选项提供的一些可能性,例如与 MDC 转换说明符一起使用,如:*%mdc{someKey} *。
转换说明符可能有多个选择。例如,使用评估程序的转换说明符(很快将介绍)可以将评估程序名称添加到选项列表中,如下所示:
<pattern>%-4relative [%thread] %-5level - %msg%n \
%caller{2, DISP_CALLER_EVAL, OTHER_EVAL_NAME, THIRD_EVAL_NAME}</pattern>
如果选项包括大括号,空格或逗号之类的特殊字符,则可以将其括在单引号或双引号之间。例如,考虑下一个模式。
<pattern>%-5level - %replace(%msg){'\d{14,16}', 'XXXX'}%n</pattern>
我们将选项\d{16}
和XXXX
传递给replace
转换字。它用 XXXX 替换消息中包含的任何 14、15 或 16 位数字序列,从而有效地混淆了信用卡号。请注意,“\d”是正则表达式中一位数字的简写。 “{14,16}”被解释为“{14, 16}”,即,重复上一个项目至少 14 次,但最多重复 16 次。
括号很特殊
在 logback 中,模式字符串中的括号被视为分组标记。因此,可以对子模式进行分组并在该子模式上应用格式指令。从 0.9.27 版本开始,logback 支持可转换子模式的复合转换字,例如%replace。
例如图案
%-30( %d{} [%thread] ) %-5level%logger{32}-%msg%n
会将子模式“%d{HH:mm:ss.SSS} [%thread]”生成的输出分组,以便在少于 30 个字符的情况下使用右填充。
如果不进行分组,则输出为
13:09:30 [main] DEBUG cqlogback.demo.ContextListener-类负载哈希码为 13995234 13:09:30 [main] DEBUG cqlogback.demo.ContextListener-初始化 ServletContext 13:09:30 [main] DEBUG cqlogback .demo.ContextListener-试用平台 Mbean 服务器 13:09:30 [pool-1-thread-1]信息 ch.qos.logback.demo.LoggingTask-Howdydy-diddly-ho-0 13:09:38 [btpool0-7]信息 cqldemo.lottery.LotteryAction-尝试了数量:50. 13:09:40 [btpool0-7]信息 c.q.l.d.prime.NumberCruncherImpl-开始分解。 13:09:40 [btpool0-7]调试 c.q.l.d.prime.NumberCruncherImpl-尝试 2 作为一个因子。 13:09:40 [btpool0-7]信息 c.q.l.d.prime.NumberCruncherImpl-找到的因子 2
与“%-30()”分组将是
13:09:30 [main] DEBUG cqlogback.demo.ContextListener-类负载哈希码为 13995234 13:09:30 [main] DEBUG cqlogback.demo.ContextListener-初始化 ServletContext 13:09:30 [main] DEBUG cqlogback .demo.ContextListener-试用平台 Mbean 服务器 13:09:30 [pool-1-thread-1]信息 ch.qos.logback.demo.LoggingTask-Howdydy-diddly-ho-0 13:09:38 [btpool0-7]信息 cqldemo.lottery.LotteryAction-尝试了数量:50. 13:09:40 [btpool0-7]信息 c.q.l.d.prime.NumberCruncherImpl-开始分解。 13:09:40 [btpool0-7]调试 c.q.l.d.prime.NumberCruncherImpl-尝试 2 作为一个因子。 13:09:40 [btpool0-7]信息 c.q.l.d.prime.NumberCruncherImpl-找到的因子 2
后一种形式更易于阅读。
如果需要将括号字符视为 Literals,则需要在每个括号前面加上反斜杠来对其进行转义。就像 ( %d{} [%thread] ) 一样。
Coloring
如上所述,按parentheses分组可以给子图案着色。从 1.0.5 版开始,PatternLayout
识别“%black”,“%red”,“%green”,“%yellow”,“%blue”,“%magenta”,“%cyan”,“%white”,“将%gray”,“%boldRed”,“%boldGreen”,“%boldYellow”,“%boldBlue”,“%boldMagenta”,“%boldCyan”,“%boldWhite”和“%highlight”作为转换词。这些转换字旨在包含一个子模式。着色词包围的任何子图案将以指定的颜色输出。
以下是说明着色的配置文件。请注意,包含“%logger{15}”的%cyan 转换说明符。这将输出 Logger 名称,缩写为青色的 15 个字符。 %highlight 转换说明符针对级别为 ERROR 的事件以红色显示其子模式,对于警告为红色,对于警告为红色,对于信息为蓝色,对于其他级别以默认颜色显示。
*示例:突出显示级别(logback-examples/src/main/resources/chapters/layouts/highlighted.xml)*查看为.groovy
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
将withJansi
设置为 true 将启用 Jansi 库对 ANSI 颜色代码的解释,如果基础终端不兼容,它将透明过滤掉 ANSI 转义序列。对于跨平台部署,这是最安全的选择,但在 Classpath 上需要 org.fusesource.jansi:jansi:1.17 或更高版本。请注意,基于 Unix 的 os(例如 Linux 和 Mac OS X)本机支持 ANSI 颜色代码,通常不需要启用 Jansi 库,但是这样做是无害的。但是,在 Windows 上,建议启用 Jansi 以受益于 DOS 命令提示符上的颜色代码解释,否则可能会被发送无法解释的 ANSI 转义序列。
这是相应的输出:
[main] WARN c.l.TrivialMain - a warning message 0
[main] DEBUG c.l.TrivialMain - hello world number1
[main] DEBUG c.l.TrivialMain - hello world number2
[main] INFO c.l.TrivialMain - hello world number3
[main] DEBUG c.l.TrivialMain - hello world number4
[main] WARN c.l.TrivialMain - a warning message 5
[main] ERROR c.l.TrivialMain - Finish off with fireworks
创建着色转换词只需很少的代码行。标题为创建自定义转换说明符的部分讨论了在配置文件中注册转换字所需的步骤。
Evaluators
如上所述,当需要一个转换说明符基于一个或多个EventEvaluator对象动态运行时,选项列表会派上用场。 EventEvaluator
对象负责确定给定的日志记录事件是否符合评估者的条件。
让我们回顾一个涉及EventEvaluator
的示例。下一个配置文件将日志记录事件输出到控制台,显示日期,线程,级别,消息和呼叫者数据。鉴于提取日志事件的调用方数据是很昂贵的,因此,仅当日志请求来自特定的日志程序并且消息包含特定字符串时,我们才会这样做。因此,我们确保仅特定的日志记录请求将生成并显示其呼叫者信息。在其他情况下,如果调用方数据多余,我们将不会损害应用程序性能。
评估器,尤其是“评估表达式”以过滤器一章的专用部分表示,如果您想以任何有意义的方式使用评估器,则必须阅读它们。另请注意,以下示例隐式基于JaninoEventEvaluator
,而JaninoEventEvaluator
需要Janino library。请参阅安装文档的corresponding section。
*示例:EventEvaluators 的示例用法(logback-examples/src/main/resources/chapters/layouts/callerEvaluatorConfig.xml)*查看为.groovy
<configuration>
<evaluator name="DISP_CALLER_EVAL">
<expression>logger.contains("chapters.layouts") && \
message.contains("who calls thee")</expression>
</evaluator>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%-4relative [%thread] %-5level - %msg%n%caller{2, DISP_CALLER_EVAL}
</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
上面的评估表达式匹配从 Logger 发出的事件,该事件的名称包含字符串“ chapters.layouts”,消息包含字符串“ who call thee”。由于 XML 编码规则,&字符不能按原样编写,而需要转为¡。
下列类利用了上述配置文件中提到的某些 Feature。
*示例:EventEvaluators (logback-examples/src/main/java/chapters/layouts/CallerEvaluatorExample.java)的用法示例
上面的应用程序没有做任何特别的花哨。发出了五个日志记录请求,第三个发出消息“谁叫你?”。
The command
Java Chapters.layouts.CallerEvaluatorExample src/main/java/chapters/layouts/callerEvaluatorConfig.xml
will yield
发出日志记录请求时,将评估相应的日志记录事件。只有第三个记录事件符合评估标准,才会显示其呼叫者数据。对于其他日志记录事件,评估标准不匹配,并且不会打印任何呼叫者数据。
可以更改表达式以符合实际情况。例如,可以将 Logger 名称和请求级别结合在一起。因此,记录级别为* WARN *或更高级别的请求,该请求源自应用程序的敏感部分,例如:金融 Transaction 模块,将显示其呼叫者数据。
重要: 使用* caller 转换词,当表达式的值为 true 时,输出呼叫者数据。
让我们考虑另一种情况。当异常包含在日志记录请求中时,还将输出其堆栈跟踪。但是,对于某些特定的异常,可能要取消堆栈跟踪。
下面显示的 Java 代码创建三个日志请求,每个日志请求都有一个异常。第二个 exception 与其他 exception 不同:它包含字符串“不显示此”,并且类型为chapters.layouts.TestException
。作为其消息命令,现在让我们阻止第二个异常的打印。
*示例:EventEvaluators (logback-examples/src/main/java/chapters/layouts/ExceptionEvaluatorExample.java)的用法示例
在下一个配置文件中,评估表达式匹配包含类型为chapters.layouts.TextException
的 throwable 的事件,正是我们希望抑制的异常类型。
示例:EventEvaluators 的示例用法(logback-examples/src/main/resources/chapters/layouts/exceptionEvaluatorConfig.xml)
使用此配置,每次在日志记录请求中包含* chapters.layouts.TestException *的实例时,都会抑制堆栈跟踪。
启动命令
Java Chapters.layouts.ExceptionEvaluatorExample src/main/java/chapters/layouts/exceptionEvaluatorConfig.xml
will yield
日志记录语句 0 java.lang.Exception:显示在 Chapters.layouts.ExceptionEvaluatorExample.main(ExceptionEvaluatorExample.java:43)[logback-examples-0.9.19.jar:na]日志记录语句 1 日志记录语句 2 java.lang.Exception:显示在 chapters.layouts.ExceptionEvaluatorExample.main(ExceptionEvaluatorExample.java:43)[logback-examples-0.9.19.jar:na]
注意第二条日志语句没有堆栈跟踪。我们有效地抑制了TextException
的堆栈跟踪。每条堆栈跟踪行末尾方括号之间的文本为packaging information,前面已讨论过。
注意使用 *%ex 转换说明符,当表达式的计算结果为 false 时,将显示堆栈跟踪。
创建自定义转化说明
到目前为止,我们已经在PatternLayout
中显示了内置的转换词。但是也可以添加您自己创建的转换词。
构建自定义转换说明符包括两个步骤。
Step 1
首先,您必须扩展ClassicConverter
类。 ClassicConverter个对象负责从ILoggingEvent
个实例中提取信息并生成一个 String。例如,%logger 转换词下面的转换器LoggerConverter从ILoggingEvent
中提取 Logger 的名称,并将其作为字符串返回。它可能在此过程中缩写 Logger 名称。
这是一个 Client 转换器,它返回自创建以来经过的时间(以纳秒为单位):
*示例:示例转换器示例(src/main/java/chapters/layouts/MySampleConverter.java) *
此实现非常简单。 MySampleConverter
类扩展了ClassicConverter
,并实现convert
方法,该方法返回自创建以来经过的纳秒数。
Step 2
第二步,我们必须让 Logback 知道新的Converter
。为此,我们需要在配置文件中声明新的转换词,如下所示:
*示例:示例转换器示例(src/main/java/chapters/layouts/mySampleConverterConfig.xml)*查看为.groovy
在配置文件中声明了新的转换词后,我们就可以在PatternLayout
模式中对其进行引用,就像其他转换词一样。
The command:
Java Chapters.layouts.SampleLogging src/main/java/chapters/layouts/mySampleConverterConfig.xml
应该产生类似于以下的输出:
Reader 可能想看一下其他Converter
的实现,例如MDCConverter,以了解更复杂的行为,例如选项处理。要创建自己的着色方案,请查看HighlightingCompositeConverter。
HTMLLayout
HTMLLayout(包含在经典的 logback 中)以 HTML 格式生成日志。 HTMLLayout
在 HTML 表格中输出记录事件,该表的每一行都对应一个记录事件。
这是HTMLLayout
使用其默认 CSS 样式表生成的示例输出:
表列的内容是在转换模式的帮助下指定的。有关转换模式的文档,请参见PatternLayout。因此,您可以完全控制表的内容和格式。您可以选择并显示PatternLayout
知道的转换器的任意组合。
关于将PatternLayout
与HTMLLayout
结合使用的一个显着 exception 是,转换说明符不应由空格字符或更一般地由 Literals 分隔。模式中找到的每个说明符将导致一个单独的列。同样,将为模式中找到的每个 Literals 文本块生成一个单独的列,这可能会浪费屏幕上的宝贵房地产。
这是简单但功能齐全的配置文件,说明了HTMLLayout
的用法。
*示例:HTMLLayout 示例(src/main/java/chapters/layouts/htmlLayoutConfig1.xml)*以.groovy 格式查看
TrivialMain应用程序记录了一些异常结束的消息。命令:
Java chapters.layouts.TrivialMain src/main/java/chapters/layouts/htmlLayoutConfig1.xml
将在当前文件夹中创建文件* test.html *。 * test.html *的内容应类似于:
Stack traces
如果使用*%em 转换字显示堆栈跟踪,将创建一个表列以显示堆栈跟踪。在大多数情况下,该列将为空,浪费屏幕空间。此外,在单独的列上打印堆栈跟踪不会产生非常可读的结果。幸运的是,%ex *转换字不是显示堆栈跟踪的唯一方法。
通过实现IThrowableRenderer
接口可以获得更好的解决方案。可以将此类实现分配给HTMLLayout
,以 Management 与异常有关的显示数据。默认情况下,为每个HTMLLayout
实例分配一个DefaultThrowableRenderer。如上图所示,它以易于读取的方式在* new table row *及其堆栈跟踪上写入异常。
如果出于某种原因,您仍希望使用*%ex *模式,则可以在配置文件中指定NOPThrowableRenderer,以禁止显示堆栈跟踪的单独行。我们不知道为什么要这么做,但如果您愿意,可以这样做。
CSS
HTMLLayout
创建的 HTML 的显示是通过级联样式表(CSS)进行控制的。在没有具体说明的情况下,HTMLLayout
将默认为其内部 CSS。但是,您可以指示HTMLLayout
使用外部 CSS 文件。为此,可以将cssBuilder
元素嵌套在<layout>
元素内,如下所示。
HTMLLayout
通常与SMTPAppender
结合使用,以便以 HTML 格式愉快地格式化外发电子邮件。
Log4j XMLLayout
XMLLayout(经典 logback 的一部分)以 log4j.dtd 兼容格式生成输出,以与能够处理log4j's XMLLayout生成的文件的Chainsaw和Vigilog之类的工具进行互操作。
作为 log4j 1.2.15 版中的原始 XMLLayout,logback-classic 中的 XMLLayout 具有两个布尔属性,即 locationInfo 和 properties。将 locationInfo 设置为 true 可以在每个事件中包含位置信息(呼叫者数据)。将属性设置为 true 可以包含 MDC 信息。默认情况下,两个选项都设置为 false。
这是一个示例配置
*示例:Log4jXMLLayout 示例(src/main/java/chapters/layouts/log4jXMLLayout.xml)*以.groovy 格式查看
Logback access
大多数 logback-access 布局仅仅是 logback-classic 布局的改编。传统的 Logback 和 Logback 访问模块可满足不同的需求,但通常提供可比较的功能。
编写自己的布局
编写自定义Layout
进行 logback 访问与其在 logback-classic 中的同级Layout
几乎相同。
PatternLayout
log_access 中的PatternLayout的配置方式与经典版本相同。但是,它具有附加的转换说明符,适用于记录仅在 HTTP Servlet 请求和 HTTP Servlet 响应中可用的特定信息位。
以下是 logback-access 中PatternLayout
的转换说明符的列表。
Conversion Word | Effect |
---|---|
/远程 IP | 远端 IP 地址。 |
A /本地 IP | 本地 IP 地址。 |
b/B /字节发送 | 响应的内容长度。 |
h/clientHost | Remote host. |
H /协议 | Request protocol. |
l | 远程日志名称。在 logback-access 中,此转换器始终返回值“-”。 |
reqParameter{paramName} | 响应参数。 |
此转换词使用大括号的第一个选项,并在请求中查找相应的参数。
%reqParameter{} 显示相应的参数。
| i{} /Headers {} |请求 Headers。
该转换词使用大括号中的第一个选项,并在请求中查找相应的 Headers。
%header{} 显示请求的引荐来源。
如果未指定任何选项,它将显示每个可用的标题。
| m/requestMethod | |请求方法。|
| r/requestURL |请求的 URL。
| s/statusCode |请求的状态码。
| D/elapsedTime |服务请求所花费的时间,以毫秒为单位。
| T/elapsedSeconds |服务请求所花费的时间,以秒为单位。
| t/date |输出记录事件的日期。日期转换说明符后可以跟一组花括号,其中包含java.text.SimpleDateFormat
使用的日期和时间模式字符串。 * ISO8601 *也是有效值。
例如, %t{HH:mm:ss,SSS} 或 %t{dd MMM yyyy ;HH:mm:ss,SSS} 。如果没有给出日期格式说明符,则假定使用通用日志格式日期格式,即: %t{dd/MMM/yyyy:HH:mm:ss Z} |
| u /用户 |远程用户。|
| q/queryString |请求查询字符串,以“?”开头。
| U/requestURI |请求的 URI。
| S/sessionID |会话 ID。|
| v /服务器 |服务器名称。|
| I/threadName |处理请求的线程的名称。
| localPort |本地端口。
| reqAttribute{} |请求的属性。
此转换词使用大括号的第一个选项,并在请求中查找相应的属性。
%reqAttribute{} 显示相应的属性。
| reqCookie{} |请求 Cookie。
该转换词使用大括号的第一个选项,并在请求中查找相应的 cookie。
%cookie{} 显示相应的 cookie。
| responseHeader{} |响应的标题。
该转换词采用大括号形式的第一个选项,并在响应中查找相应的 Headers。
%header{} 显示响应的引荐来源。
| requestContent |此转换字显示请求的内容,即请求的InputStream
。它与TeeFilter,javax.servlet.Filter
一起使用,后者用TeeHttpServletRequest代替了原始的HttpServletRequest
。后一个对象允许多次访问请求的InputStream
而不会丢失任何数据。
| fullRequest |此转换器输出与请求关联的数据,包括所有 Headers 和请求内容。
| responseContent |此转换字显示响应的内容,即响应的InputStream
。它与TeeFilter,javax.servlet.Filter
一起使用,后者用TeeHttpServletResponse代替了原始的HttpServletResponse
。后一个对象允许多次访问请求的InputStream
而不会丢失任何数据。
| fullResponse |此转换字将获取与响应关联的所有可用数据,包括响应的所有 Headers 和响应内容。
Logback access'PatternLayout
还可以识别三个关键字,它们的作用类似于快捷方式。
keyword | 等效转换模式 |
---|---|
普通或* CLF * | *%h%l%u [%t]“%r”%s%b * |
combined | *%h%l%u [%t]“%r”%s%b“%i{Referer}”“%i{User-Agent}” * |
common 关键字对应于模式'%h%l%u [%t]“%r”%s%b'*,该模式显示 Client 端主机,远程日志名称,用户,日期,请求的 URL,状态代码和回应的内容长度
combined 关键字是'%h%l%u [%t]“%r”%s%b“%i{}”“%i{}”'的快捷方式。此模式的开始与 common *模式非常相似,但也显示两个请求 Headers,即 Referer 和 User-agent。
HTMLLayout
在 logback-access 中找到的HTMLLayout类与 logback-classic 中的HTMLLayout类相似。
默认情况下,它将创建一个包含以下数据的表:
Remote IP
Date
Request URL
Status code
Content Length
这是HTMLLayout
在 logback-access 中产生的示例输出:
有什么比真实的例子更好的呢?我们自己的用于 log_1 的 log4j 属性使用 logback-access 来演示RollingFileAppender
和HTMLLayout
的实时输出。
在对我们的translator网络应用程序的每个新用户请求中,新条目都会添加到访问日志中,您可以通过跟随此链接查看。