Log4Net配置详解及输出自定义消息类示例代码
1.简单使用实例
1.1 添加log4net.dll的引用。
在NuGet程序包中搜索log4net并添加,此次我所用版本为2.0.17。如下图:
1.2 添加配置文件
右键项目,添加新建项,搜索选择应用程序配置文件,命名为log4net.config,步骤如下图:
1.2.1 log4net.config简单配置示例
下面是一个简单的配置示例,详细节点及相应的说明详见 2. 配置文件节点详解 。
log4net.config配置
<?xml version="1.0" encoding="utf-8" ?> <configuration> <log4net> <logger name="DefaultLog"> <!--control log level: ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF--> <!--如果没有定义LEVEL的值,则缺省为DEBUG--> <level value="ALL" /> <appender-ref ref="FileAppenderDefault"></appender-ref> </logger> <!-- appender 定义日志输出方式 将日志以回滚文件的形式写到文件中。--> <appender name="FileAppenderDefault" type="log4net.Appender.RollingFileAppender"> <!--绝对路径--> <!--<file value="D:\KangarooLog.txt"></file>--> <!--日志输出到exe程序这个相对目录下--> <file value="../../Log/DefalutLog" /> <!--相对路径,在项目的根目录下--> <!--以最后一个路径为准,所以上面的绝对路径下不会写日志--> <!--<file value="./Log/Kangaroo.txt"></file>--> <!--防止多线程时不能写Log,官方说线程非安全--> <!--实际使用时,本地测试正常,部署后没有不能写日志的情况--> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <!--追加日志内容,true后续输出的日志会追加到之前的日志文件--> <appendToFile value="true" /> <!--可以为:Once|Size|Date|Composite--> <!--Composite为Size和Date的组合--> <rollingStyle value="Composite" /> <!--日志最大个数,都是最新的--> <!--rollingStyle节点为Date时,该节点不起作用--> <!--rollingStyle节点为Size时,只能有value个日志--> <!--rollingStyle节点为Composite时,每天有value个日志--> <maxSizeRollBackups value="10" /> <!--当备份文件时,为文件名加的后缀--> <!--后缀为*.txt时,例:AX.txt_2008-07-24.PxP 应该是程序上的一个bug--> <!--后缀为*.TXT时,例:AX.txt_2008-07-25.TXT--> <datePattern value="_yyyy-MM-dd'.log'" /> <!--每个文件的大小。只在混合方式与文件大小方式下使用。超出大小后在所有文件名后自动增加正整数重新命名,数字最大的最早写入。可用的单位:KB|MB|GB。不要使用小数,否则会一直写入当前日志--> <maximumFileSize value="10MB" /> <!--置为true,当前最新日志文件名永远为file节中的名字--> <staticLogFileName value="false" /> <!--输出级别在INFO和ERROR之间的日志--> <!--<filter type="log4net.Filter.LevelRangeFilter"> <param name="LevelMin" value="INFO" /> <param name="LevelMax" value="ERROR" /> </filter>--> <!--必须结合起来用,第一个只过滤出WARN,第二个拒绝其它其它日志输出--> <filter type="log4net.Filter.LevelMatchFilter"> <param name="LevelToMatch" value="WARN" /> </filter> <filter type="log4net.Filter.DenyAllFilter" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%n========== %n【日志级别】%-5level %n【记录时间】%date %n【执行时间】[%r]毫秒 %n【执行Log分类的名称】%logger %n【传入信息内容】%message %n=========="/> </layout> </appender> </log4net> </configuration> <!--==================================layout节点的配置说明==================================--> <!-- Made By YSL --> <!-- %m(message):输出的日志消息,如ILog.Debug(…)输出的一条消息 --> <!-- %n(new line):换行 --> <!-- %d(datetime):输出当前语句运行的时刻 --> <!-- %r(run time):输出程序从运行到执行到当前语句时消耗的毫秒数 --> <!-- %t(thread id):当前语句所在的线程ID --> <!-- %p(priority): 日志的当前优先级别,即DEBUG、INFO、WARN…等 --> <!-- %c(class):当前日志对象的名称,例如: --> <!-- 模式字符串为:%-10c -%m%n --> <!-- 代码为: --> <!-- ILog log=LogManager.GetLogger(“Exam.Log”); --> <!-- log.Debug(“Hello”); --> <!-- 则输出为下面的形式: --> <!-- Exam.Log - Hello --> <!-- %L:输出语句所在的行号 --> <!-- %F:输出语句所在的文件名 --> <!-- %-数字:表示该项的最小长度,如果不够,则用空格填充 -->
1.2.2 设置log4net.config配置文件属性
点击log4net.config,将其文件属性设为始终复制,如下图:
1.3 在项目中引入该配置文件
这里有两种方式引入配置文件。
1.3.1 在项目的 AssemblyInfo.cs 中引入配置文件
首先在项目中新建一个Config文件夹,将之前创建的log4net.config文件放入其中,随后在 AssemblyInfo.cs 中添加如下语句:
1.3.2 在项目运行时动态引入配置文件
使用固定语句引入配置文件,如下所示,其中,configFilePath 为配置文件的绝对路径。
log4net.Config.XmlConfigurator.ConfigureAndWatch(new System.IO.FileInfo(configFilePath));
1.4 创建帮助类使用日志进行记录
我们首先创建名为 Log4Helper 的类,并使用固定的log4net.LogManager.GetLogger()
语句实例化对应的Log对象,然后调用其对应的方法即可写入日志。示例代码如下:
Log4Helper代码
public class Log4Helper { private static readonly log4net.ILog logDefault = log4net.LogManager.GetLogger("DefaultLog"); /// <summary> /// 测试默认配置信息输出(输出范围做了限制) /// </summary> public static void TestDefaultLog() { logDefault.Debug("这是条调试信息"); logDefault.Info("这是条提示信息"); logDefault.Warn("这是条警告信息"); logDefault.Error("这是条错误信息"); logDefault.Fatal("这是条致命错误信息"); } /// <summary> /// Log4Net初始化(可读取自定义配置) /// </summary> /// <param name="configFilePath"></param> public static void Log4NetInit(string configFilePath) { log4net.Config.XmlConfigurator.ConfigureAndWatch(new System.IO.FileInfo(configFilePath)); } /// <summary> /// 返回指定名称的日志对象 /// </summary> public static log4net.ILog Log(string appenderName) { return log4net.LogManager.GetLogger(appenderName); } }
1.5 在主程序中引用
最后一步就是在主程序中引用 Log4Helper 中的日志记录方法,如下所示:
private void Application_Startup(object sender, StartupEventArgs e) { Log4Helper.TestDefaultLog(); }
根据上面 log4net.config 配置中 file 节点中的地址,在项目中会生成 Log 文件夹,该文件夹下会生成类似 DefalutLog_2024-07-01.log 的文件。该文件名由 file 节点和 datePattern 节点两部分组合而成,其中,staticLogFileName
节点需要被设置为 false ,若为 true ,则当前最新日志文件名永远为 file 节点中的名字,其余日志会根据 datePattern 节点自动添加后缀。
打开日志文件,会发现里面只有 WARN 警告信息,例如:
========== 【日志级别】WARN 【记录时间】2024-07-01 00:18:28,828 【执行时间】[53]毫秒 【执行Log分类的名称】DefaultLog 【传入信息内容】这是条警告信息 ==========
这是因为配置中的 filter 过滤节点,详见注释,将其注释再次运行,则会正常显示全部日志信息。
2. 配置文件节点详解
log4net的主要组成有四部分,分别是 Logger、Appender、Layout、Filter等,详见下方。
2.1 Logger 节点配置详解
以上文的配置为例解释:
<logger name="DefaultLog"> <!--control log level: ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF--> <!--如果没有定义LEVEL的值,则缺省为DEBUG--> <level value="ALL" /> <appender-ref ref="FileAppenderDefault"></appender-ref> </logger>
level 定义记录的日志级别,就是说,你要记录哪个级别以上的日志,级别由低到高依次是:
ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF
如果你 level 定义 INFO,那么低于 INFO 级别以下的信息,将不会记入日志,啥意思呢?
就是说,就算你在程序里,用 log.Debug() 来写入一个日志信息,可是你在配置中指定 level 为 INFO,由于 DEBUG 级别低于 INFO,所以,不会被记入日志。这样的处理非常灵活。
在具体写日志时,一般可以这样理解日志等级:
FATAL(致命错误):记录系统中出现的能使用系统完全失去功能,服务停止,系统崩溃等使系统无法继续运行下去的错误。例如,数据库无法连接,系统出现死循环。
ERROR(一般错误):记录系统中出现的导致系统不稳定,部分功能出现混乱或部分功能失效一类的错误。例如,数据字段为空,数据操作不可完成,操作出现异常等。
WARN(警告):记录系统中不影响系统继续运行,但不符合系统运行正常条件,有可能引起系统错误的信息。例如,记录内容为空,数据内容不正确等。
INFO(一般信息):记录系统运行中应该让用户知道的基本信息。例如,服务开始运行,功能已经开户等。
DEBUG (调试信息):记录系统用于调试的一切信息,内容或者是一些关键数据内容的输出。
appender-ref,要引用的 appender 的名字,由 Layout 控制输出格式。
最后还要说一个LogManager类,它用来管理所有的Logger。它的GetLogger静态方法,可以获得配置文件中相应的Logger:
log4net.ILog log = log4net.LogManager.GetLogger("logger-name");
2.2 Appender 节点配置详解
以上文的 FileAppenderDefault 节点为例:
FileAppenderDefault 节点配置
<!-- appender 定义日志输出方式 将日志以回滚文件的形式写到文件中。--> <appender name="FileAppenderDefault" type="log4net.Appender.RollingFileAppender"> <!--绝对路径--> <!--<file value="D:\KangarooLog.txt"></file>--> <!--日志输出到exe程序这个相对目录下--> <file value="../../Log/DefalutLog" /> <!--相对路径,在项目的根目录下--> <!--以最后一个路径为准,所以上面的绝对路径下不会写日志--> <!--<file value="./Log/Kangaroo.txt"></file>--> <!--防止多线程时不能写Log,官方说线程非安全--> <!--实际使用时,本地测试正常,部署后没有不能写日志的情况--> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <!--追加日志内容,true后续输出的日志会追加到之前的日志文件--> <appendToFile value="true" /> <!--可以为:Once|Size|Date|Composite--> <!--Composite为Size和Date的组合--> <rollingStyle value="Composite" /> <!--日志最大个数,都是最新的--> <!--rollingStyle节点为Date时,该节点不起作用--> <!--rollingStyle节点为Size时,只能有value个日志--> <!--rollingStyle节点为Composite时,每天有value个日志--> <maxSizeRollBackups value="10" /> <!--当备份文件时,为文件名加的后缀--> <!--后缀为*.txt时,例:AX.txt_2008-07-24.PxP 应该是程序上的一个bug--> <!--后缀为*.TXT时,例:AX.txt_2008-07-25.TXT--> <datePattern value="_yyyy-MM-dd'.log'" /> <!--每个文件的大小。只在混合方式与文件大小方式下使用。超出大小后在所有文件名后自动增加正整数重新命名,数字最大的最早写入。可用的单位:KB|MB|GB。不要使用小数,否则会一直写入当前日志--> <maximumFileSize value="10MB" /> <!--置为true,当前最新日志文件名永远为file节中的名字--> <staticLogFileName value="false" /> <!--输出级别在INFO和ERROR之间的日志--> <!--<filter type="log4net.Filter.LevelRangeFilter"> <param name="LevelMin" value="INFO" /> <param name="LevelMax" value="ERROR" /> </filter>--> <!--必须结合起来用,第一个只过滤出WARN,第二个拒绝其它其它日志输出--> <filter type="log4net.Filter.LevelMatchFilter"> <param name="LevelToMatch" value="WARN" /> </filter> <filter type="log4net.Filter.DenyAllFilter" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%n========== %n【日志级别】%-5level %n【记录时间】%date %n【执行时间】[%r]毫秒 %n【执行Log分类的名称】%logger %n【传入信息内容】%message %n=========="/> </layout> </appender>
每个节点均写有注释,这里额外介绍下 appender 的输出方式( type 属性),如下所示:
appender 输出方式
AdoNetAppender 将日志记录到数据库中。可以采用SQL和存储过程两种方式。 AnsiColorTerminalAppender 将日志高亮输出到ANSI终端。 AspNetTraceAppender 能用asp.net中Trace的方式查看记录的日志。 BufferingForwardingAppender 在输出到子Appenders之前先缓存日志事件。 ConsoleAppender 将日志输出到应用程序控制台。 EventLogAppender 将日志写到Windows Event Log。 FileAppender 将日志输出到文件。 ForwardingAppender 发送日志事件到子Appenders。 LocalSyslogAppender 将日志写到local syslog service (仅用于UNIX环境下)。 MemoryAppender 将日志存到内存缓冲区。 NetSendAppender 将日志输出到Windows Messenger service.这些日志信息将在用户终端的对话框中显示。 OutputDebugStringAppender 将日志输出到Debuger,如果程序没有Debuger,就输出到系统Debuger。如果系统Debuger也不可用,将忽略消息。 RemoteSyslogAppender 通过UDP网络协议将日志写到Remote syslog service。 RemotingAppender 通过.NET Remoting将日志写到远程接收端。 RollingFileAppender 将日志以回滚文件的形式写到文件中。 SmtpAppender 将日志写到邮件中。 SmtpPickupDirAppender 将消息以文件的方式放入一个目录中,像IIS SMTP agent这样的SMTP代理就可以阅读或发送它们。 TelnetAppender 客户端通过Telnet来接受日志事件。 TraceAppender 将日志写到.NET trace 系统。 UdpAppender 将日志以无连接UDP数据报的形式送到远程宿主或用UdpClient的形式广播。
2.3 Filter 节点配置详解
filter只能作为 appender 的子元素,type 属性表示 Filter 的类型。常用子元素 param 数量0个或多个,作用设置一些参数。具体例子详见上文 2.2 Appender 节点配置详解 。
额外补充下 filter 的类型说明:
DenyAllFilter 阻止所有的日志事件被记录 LevelMatchFilter 只有指定等级的日志事件才被记录 LevelRangeFilter 日志等级在指定范围内的事件才被记录 LoggerMatchFilter Logger名称匹配,才记录 PropertyFilter 消息匹配指定的属性值时才被记录 StringMathFilter 消息匹配指定的字符串才被记录
2.4 Layout 节点配置详解
layout 节点只能作为 appender 的子元素。type 属性表示 Layout 的类型。具体例子详见上文 2.2 Appender 节点配置详解 。
额外补充 layout 节点的 type 属性取值:
ExceptionLayout 只呈现日志事件中异常的文本信息 PatternLayout 可以通过类型字符串来配置的布局 RawPropertyLayout 从日志事件中提取属性值 RawTimeStampLayout 从日志事件中提取日期 RawUtcTimeStampLayout 从日志事件中提取UTC日期 SimpleLayout 很简单的布局 XmlLayout 把日志事件格式化为XML元素的布局
这其中我们主要使用的还是PatternLayout 类型,而在 ConversionPattern 节点中,我们可以进一步的配置日志输出格式,以 PatterLayout 的格式化字符串输出为例:
PatterLayout 的格式化字符串
%m、%message 输出的日志消息 %d、%datetime 输出当前语句运行的时刻,格式%date{yyyy-MM-dd HH:mm:ss,fff} %r、%timestamp 输出程序从运行到执行到当前语句时消耗的毫秒数 %p、%level 日志的当前优先级别 %c、%logger 当前日志对象的名称 %L、%line 输出语句所在的行号 %F、%file 输出语句所在的文件名,警告:只在调试的时候有效,调用本地信息会影响性能 %a、%appdomain 引发日志事件的应用程序域的名称。 %C、%class、%type 引发日志请求的类的全名,警告:会影响性能 %exception 异常信息 %u、%identity 当前活动用户的名字,我测试的时候%identity返回都是空的。警告:会影响性能 %l、%location 引发日志事件的名空间、类名、方法、行号。警告:会影响性能,依赖pdb文件 %M、%method 发生日志请求的方法名,警告:会影响性能 %n、%newline 换行符 %x、%ndc NDC(nested diagnostic context) %X、%mdc、%P、%properties 等介于 %property %property 输出{log4net:Identity=, log4net:UserName=, log4net:HostName=} %t、%thread 引发日志事件的线程,如果没有线程名就使用线程号。 %w、%username 当前用户的WindowsIdentity,类似:HostName/Username。警告:会影响性能 %utcdate 发生日志事件的UTC时间。例如:%utcdate{HH:mm:ss,fff} %% 输出一个百分号
额外补充下 PatterLayout 格式修饰符:
格式修饰符 | 对齐 | 最小宽 | 最大宽 | 说明 |
---|---|---|---|---|
%20logger | 右对齐 | 20 | 无 | 如果logger名不足20个字符,就在左边补空格 |
%-20logger | 左对齐 | 20 | 无 | 如果logger名不足20个字符,就在右边补空格 |
%.30logger | 左对齐 | 无 | 30 | 超过30个字符将截断 |
%20.30logger | 右对齐 | 20 | 30 | logger名要在20到30之间,少了在左边补空格,多了截断 |
%-20.30logger | 左对齐 | 20 | 30 | logger名要在20到30之间,少了在右边补空格,多了截断 |
3. 如何输出自定义类
自定义扩展输出,通过继承 log4net.Layout.PatternLayout 和 log4net.Layout.Pattern.PatternLayoutConverter 类,使用 log4net.Core.LoggingEvent 类的方法得到了要输出的 LogEntity 类的名称。
然后通过反射得到各个属性的值,使用 PatternLayout 类 AddConverter 方法传入得到的值,在 PatternLayoutConverter 中对其进行处理。注意配置文件 Appender 中的 Layout type 用到的类的命名空间以及类名,要同步更改为自定义类的。详见示例:
自定义拓展类的代码
namespace WPFPractice { public class CustomLayout : log4net.Layout.PatternLayout { public CustomLayout() { this.AddConverter("Custom", typeof(CustomConvert)); } } public class CustomConvert : log4net.Layout.Pattern.PatternLayoutConverter { protected override void Convert(System.IO.TextWriter writer, log4net.Core.LoggingEvent loggingEvent) { if (!string.IsNullOrEmpty(Option)) { object obj = loggingEvent.MessageObject; if (obj != null) { PropertyInfo info = obj.GetType().GetProperty(Option); if (info != null) { object cusMsg = info.GetValue(obj, null); writer.Write(cusMsg); } } } } } }
与之对应的,我们的配置文件也添加以下代码:
配置文件新增代码
<logger name="CustomLog"> <!--control log level: ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF--> <!--如果没有定义LEVEL的值,则缺省为DEBUG--> <level value="ALL" /> <appender-ref ref="FileAppenderCustom"></appender-ref> </logger> <appender name="FileAppenderCustom" type="log4net.Appender.RollingFileAppender"> <file value="../../Log/CustomLog" /> <appendToFile value="true" /> <lockingModel type="log4net.Appender.FileAppender+MinimalLock" /> <rollingStyle value="Composite" /> <maxSizeRollBackups value="5" /> <datePattern value="_yyyy-MM-dd'.log'" /> <maximumFileSize value="10MB" /> <staticLogFileName value="false" /> <layout type="WPFPractice.CustomLayout"> <conversionPattern value="%n========== %n【日志级别】%-5level %n【记录时间】%date %n【执行时间】[%r]毫秒 %n【执行线程ID】[%thread] %n【执行Log分类的名称】%logger %n【耗材类型名】%Custom{LabTypeName} %n【耗材名】%Custom{LabName} %n【耗材编号】%Custom{LabNumber} %n【是否吸头】%Custom{IsTip} %n【传入信息内容/类型】%message %n==========" /> </layout> </appender>
接下来我们新建一个类 LabwareModel,并在帮助类 Log4Helper 中添加调用 CustomLog 的方法,来测试能否输出自定义类 LabwareModel 中的内容。
自定义类 LabwareModel
public class LabwareModel { public string LabTypeName { get; set; } = "采样管"; public string LabName { get; set; } = "Custom_2000ul"; public double LabNumber { get; set; } = 200; public int IsTip { get; set; } = 1; }
帮助类 Log4Helper 如下:
查看帮助类 Log4Helper
private static readonly log4net.ILog logDefault = log4net.LogManager.GetLogger("DefaultLog"); /// <summary> /// 生成默认实例 /// </summary> /// <returns></returns> public static LabwareModel GetLabwareModel() { LabwareModel labwareModel = new LabwareModel(); return labwareModel; } /// <summary> /// 测试自定义配置信息输出 /// </summary> public static void TestCustomLog() { var labware = GetLabwareModel(); logCustom.Debug("这是条调试信息"); logCustom.Info("这是条提示信息"); logCustom.Warn("这是条警告信息"); logCustom.Error(labware); logCustom.Fatal("这是条致命错误信息"); }
在主程序中调用进行测试:
private void Application_Startup(object sender, StartupEventArgs e) { Log4Helper.TestCustomLog(); Log4Helper.TestDefaultLog(); }
根据上面 FileAppenderCustom 配置中 file 节点中的地址,在项目中会生成 Log 文件夹,该文件夹下会生成类似 CustomLog_2024-07-01.log 的文件。打开文件会看到输出内容如下:
日志输出内容
========== 【日志级别】DEBUG 【记录时间】2024-07-01 00:18:28,812 【执行时间】[37]毫秒 【执行线程ID】[1] 【执行Log分类的名称】CustomLog 【耗材类型名】 【耗材名】 【耗材编号】 【是否吸头】 【传入信息内容/类型】这是条调试信息 ========== ========== 【日志级别】INFO 【记录时间】2024-07-01 00:18:28,823 【执行时间】[48]毫秒 【执行线程ID】[1] 【执行Log分类的名称】CustomLog 【耗材类型名】 【耗材名】 【耗材编号】 【是否吸头】 【传入信息内容/类型】这是条提示信息 ========== ========== 【日志级别】WARN 【记录时间】2024-07-01 00:18:28,825 【执行时间】[50]毫秒 【执行线程ID】[1] 【执行Log分类的名称】CustomLog 【耗材类型名】 【耗材名】 【耗材编号】 【是否吸头】 【传入信息内容/类型】这是条警告信息 ========== ========== 【日志级别】ERROR 【记录时间】2024-07-01 00:18:28,826 【执行时间】[51]毫秒 【执行线程ID】[1] 【执行Log分类的名称】CustomLog 【耗材类型名】采样管 【耗材名】Custom_2000ul 【耗材编号】200 【是否吸头】1 【传入信息内容/类型】WPFPractice.LabwareModel ========== ========== 【日志级别】FATAL 【记录时间】2024-07-01 00:18:28,828 【执行时间】[53]毫秒 【执行线程ID】[1] 【执行Log分类的名称】CustomLog 【耗材类型名】 【耗材名】 【耗材编号】 【是否吸头】 【传入信息内容/类型】这是条致命错误信息 ==========
以上就是关于 Log4Net 配置详解。
到此这篇关于Log4Net配置详解及输出自定义消息类示例 的文章就介绍到这了,更多相关Log4Net配置输出自定义消息类内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
.net core如何在网络高并发下提高JSON的处理效率详解
这篇文章主要给大家介绍了关于.net core如何在网络高并发下提高JSON的处理效率的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用.net core具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧2019-04-04Visual Studio 2010崩溃重启问题(源文件编译崩溃)
在使用Visual Studio 2010突然出现崩溃现象:源文件修改只要一编译,马上就崩溃.搜索了很多的方法也不见效果,经过近1天的折腾,已经决定重装Windows 7了,遇到此问题的朋友们可以看看哦,或许对你有所帮助2013-01-01深入解析.NET 许可证编译器 (Lc.exe) 的原理与源代码剖析
许可证编译器 (Lc.exe) 的作用是读取包含授权信息的文本文件,并产生一个可作为资源嵌入到公用语言运行库可执行文件中的 .licenses 文件2013-07-07asp.net MVC使用PagedList.MVC实现分页效果
这篇文章主要为大家详细介绍了asp.net MVC使用PagedList.MVC实现分页效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2017-07-07
最新评论