当前位置: 首页 > news >正文

Java日志系列——logback,log4j2使用

Java日志系列——logback,log4j2使用

  • Logback
    • 官方网站
    • QuickStart
      • 1.添加依赖
      • 2.代码
    • spi机制
      • 自定义Config配置
        • logback
        • 自定义
        • ch.qos.logback.classic.spi.Configurator文件
    • 主要组件
    • 配置文件式配置
      • 官网配置
      • 1.在resource下新建logback.xml
      • 2.写入logback.xml文件
      • 3.测试
      • 配置文件方式将日志写入文件中
        • 1.在logback.xml中编写FIle类型Appender
        • 2.引用appender配置
        • 3.测试
      • 自定义日志输出
      • 滚动日志(rollingFileAppender)
      • 过滤器Filter
        • 简单过滤器
        • 级别过滤器
      • 属性
      • 异步日志
  • log4j2
    • 官网地址
    • QuickStart
      • 1. 依赖
      • 2.添加log4j2.xml
      • 3.测试
    • 使用Slf4j作为日志门面(推荐)
      • 1.添加依赖
      • 2.使用slf4j的测试
    • 配置相关
      • 重加载配置文件
      • 配置文件
      • 配置输出格式
      • 配置Logger
      • 官网建议以严格的格式进行配置
    • JSON格式配置
    • YAML配置方式(推荐,个人最喜欢)
    • 异步日志
      • 依赖
      • 全局异步
      • 混合异步(局部异步,最常用)
        • 关闭行号信息
      • 注意点
    • 无垃圾模式
      • 文档地址

Logback

Logback是由log4j创始人设计的另一个开源日志组件,性能比log4j要好。
Logback主要分为三个模块:

  1. logback-core:其它两个模块的基础模块
  2. logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API
  3. logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能后续的日志代码都是通过SLF4]日志门面搭建日志系统,所以在代码是没有区别,主要是通过修改配置文件和pom.xml依赖

官方网站

https://logback.qos.ch/

QuickStart

1.添加依赖

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>

2.代码

    @Test
    void test(){
        final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
        logger.error("error");
        logger.warn("warn");
        logger.info("info");
        logger.debug("debug");
        logger.trace("trace");
    }

在这里插入图片描述

spi机制

SPI全称Service Provider Interface,是java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件
他是一种服务
发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

自定义Config配置

通过模仿logback我们自己就可以修改config
我们可以找到BasicConfigurator的源码,这是默认的logback的配置,我们将其模仿自己也可以写一个

public class DefineConfigurator extends ContextAwareBase implements Configurator {
    public DefineConfigurator() {
    }

    @Override
    public void configure(LoggerContext lc) {
        this.addInfo("Setting up default configuration.");
        ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender();
        ca.setContext(lc);
        ca.setName("console");
        LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder();
        encoder.setContext(lc);
        //格式化处理
        final PatternLayout layout = new PatternLayout();
        layout.setPattern("%d{HH :mm :ss.sSS} [%thread] %-5level %logger{36} - %msg%n");
//        TTLLLayout layout = new TTLLLayout();
        layout.setContext(lc);
        layout.start();
        encoder.setLayout(layout);
        ca.setEncoder(encoder);
        ca.start();
        Logger rootLogger = lc.getLogger("ROOT");
        rootLogger.addAppender(ca);
    }
}

logback

在这里插入图片描述

自定义

在这里插入图片描述

ch.qos.logback.classic.spi.Configurator文件

com.example.jul.DefineConfigurator

主要组件

  1. appender,输出源,一个日志可以后好几个输出源
  2. encoder,一个appender有一个encoder,负责将一个event事件转换成一组byte数组,并将转换后的字节数据输出到文件中。Encoder负责把事件转换为字节数组,并把字节数组写到合适的输出流。因此,encoder可以控制在什么时候、把什么样的字节数组写入到其拥有者维护的输出流中。Encoder接口有两个实现类,LayoutWrappingEncoder与PatternLayoutEncoder。注意:在logback 0.9.19版之前没有encoder。在之前的版本里,多数appender依靠layout来把事件转换成字符串并用java.io.Writer把字符串输出。在之前的版本里,用户需要在FileAppender里嵌入一个PatternLayout。
  3. layout,格式化数据将event事件转化为字符串,解析的过程

配置文件式配置

logback会依次读取以下类型配置文件:

  1. logback.groovy
  2. logback-test.xml
  3. logback.xml

如果均不存在会采用默认配置

官网配置

https://logback.qos.ch/manual/configuration.html

1.在resource下新建logback.xml

2.写入logback.xml文件

<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg ok %n</pattern>
    </encoder>
  </appender>

  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

3.测试

    @Test
    void test(){
        final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
        logger.error("error");
        logger.warn("warn");
        logger.info("info");
        logger.debug("debug");
        logger.trace("trace");
    }

在这里插入图片描述

配置文件方式将日志写入文件中

1.在logback.xml中编写FIle类型Appender

加入name叫做FIle的Appender,让他应用ch.qos.logback.core.FileAppender

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>D://myApp.log</file>

        <encoder>
            <pattern>%date %level [%thread] %logger{10} [%file:%line] %msg -to file %n</pattern>
        </encoder>
    </appender>

2.引用appender配置

找到root节点,使用appender-ref进行引入

<root level="debug">
     <appender-ref ref="FILE" />
</root>

3.测试

在这里插入图片描述

自定义日志输出

我们还可以通过配置logger节点的方式,对单独的logger进行配置
如下在logback.xml中配置了叫test的logger直接在控制台进行输出,当然你还可以指定到文件,流中等这些都取决于你怎么配置

//name
final Logger logger = LoggerFactory.getLogger("test");
//logback.xml
  <logger name="test" additivity="false">
    <appender-ref ref="STDOUT" />
  </logger>

滚动日志(rollingFileAppender)

    <appender name="ROLL" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <!-- 设置按尺寸和时间(同时满足)分割 -->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>D://rolling.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>7</maxHistory>
            <totalSizeCap>100MB</totalSizeCap>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}-%msg%n</pattern>
        </encoder>

    </appender>

过滤器Filter

我们需要将filter配置在appender中

简单过滤器

<appender>
	 <filter class="chapters.filters.SampleFilter" />
</appender>

级别过滤器

LevelFilter 根据精确的级别匹配过滤事件。 如果事件的级别等于配置的级别,则过滤器接受或拒绝该事件,具体取决于 onMatch 和 onMismatch 属性的配置。

    <filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>INFO</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
    </filter>

属性

通过配置属性我们可以进行复用增强移植性

<property name="p1" value="define value"></property>

然后我们就可以通过${}来进行使用

异步日志

主线程应该用于执行业务,而日志不应该占用主线程,所以我们应该采用异步的方式进行优化
AsyncAppender 异步记录 ILoggingEvents。 它仅充当事件调度程序,因此必须引用另一个附加程序才能执行任何有用的操作

实际上我们是在外面配好appender然后到异步中进行ref引入的

  <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE" />
  </appender>
属性名称类型描述
queueSizeint阻塞队列的最大容量。 默认, queueSize 设置为 256。
discardingThresholdint默认情况下,当阻塞队列有 20% 容量时 剩下的,它将丢弃 TRACE、DEBUG 和 INFO 级别的事件, 只保留 WARN 和 ERROR 级别的事件。 为了保持所有 事件,将 discardingThreshold 为 0。
includeCallerDataboolean提取呼叫者数据可能相当昂贵。 改善 性能,默认情况下,与事件关联的调用者数据 事件添加到事件队列时不提取。 经过 等“廉价”数据 MDC 复制 你可以指导这个 appender 通过将 includeCallerData 属性设置为 true 来包含调用者数据。
maxFlushTimeint根据引用的 appender 的队列深度和延迟, 这 AsyncAppender可能需要不可接受的数量 是时候完全刷新队列了。 当。。。的时候 LoggerContext是 停了下来, AsyncAppender stop方法等待 直到这个超时,工作线程才能完成。 利用 maxFlushTime 指定最大队列刷新 以毫秒为单位的超时。 无法在此范围内处理的事件 窗口被丢弃。 此值的语义与 Thread.join(long) 。
neverBlockboolean如果 false(默认)appender 将阻塞 追加到一个完整的队列而不是丢失消息。 调成 trueappender 只会删除消息和 不会阻止您的应用程序。

log4j2

Apache Log4]2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有:

  1. 异常处理,在logback中,Appender中的异常不会被应用感知到,但是在log4j2中,提供了一些异常处理机制。
  2. 性能提升,log4j2相较于log4j和logback都具有很明显的性能提升,后面会有官方测试的数据。
  3. 自动重载配置,参考了logback的设计,当然会提供自动刷新参数配置,最实用的就是我们在生产上可以动态的修改日志的级别而不需要重启应用。
  4. 无垃圾机制,避免频繁的日志收集导致jvm的gc

官网地址

https://logging.apache.org/log4j/2.x/index.html

QuickStart

1. 依赖

        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.17.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.2</version>
        </dependency>

2.添加log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="error">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

3.测试

    @Test
    void test(){
        final Logger logger = LogManager.getLogger(this.getClass().getName());
        logger.fatal("fatal");
        logger.error("error");
        logger.info("info");
        logger.debug("debug");
        logger.trace("trace");
    }

在这里插入图片描述

使用Slf4j作为日志门面(推荐)

1.添加依赖

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.17.2</version>
        </dependency>

2.使用slf4j的测试

    @Test
    void test(){
        final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
        logger.error("error");
        logger.warn("warn");
        logger.info("info");
        logger.debug("debug");
        logger.trace("trace");
        //占位符输出方式
        logger.warn("{}first,{}second","zhangsan",18);
    }

在这里插入图片描述

配置相关

重加载配置文件

Configuration节点上配置monitorInterval属性
以下配置36000s重加载

<Configuration monitorInterval="30">

</Configuration>

配置文件

<Appenders>
  <File name="File1" fileName="output.log" bufferedIO="false" advertiseURI="file://path/to/output.log" advertise="true">
  ...
  </File>
</Appenders>

配置输出格式

//第一种
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
//第二种
<PatternLayout>
  <Pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Pattern>
</PatternLayout>

配置Logger

通过在loggers下配置子logger指定名称可以自定义单个logger

<Loggers>
    <Logger name="name1">
     	<AppenderRef ref="name"/>
      	<filter  ... />
	</Logger>
</Loggers>

官网建议以严格的格式进行配置

以下是官网的给出的建议配置

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="debug" strict="true" name="XMLConfigTest"
                   packages="org.apache.logging.log4j.test">
      <Properties>
        <Property name="filename">target/test.log</Property>
      </Properties>
      <Filter type="ThresholdFilter" level="trace"/>
     
      <Appenders>
        <Appender type="Console" name="STDOUT">
          <Layout type="PatternLayout" pattern="%m MDC%X%n"/>
          <Filters>
            <Filter type="MarkerFilter" marker="FLOW" onMatch="DENY" onMismatch="NEUTRAL"/>
            <Filter type="MarkerFilter" marker="EXCEPTION" onMatch="DENY" onMismatch="ACCEPT"/>
          </Filters>
        </Appender>
        <Appender type="Console" name="FLOW">
          <Layout type="PatternLayout" pattern="%C{1}.%M %m %ex%n"/><!-- class and line number -->
          <Filters>
            <Filter type="MarkerFilter" marker="FLOW" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
            <Filter type="MarkerFilter" marker="EXCEPTION" onMatch="ACCEPT" onMismatch="DENY"/>
          </Filters>
        </Appender>
        <Appender type="File" name="File" fileName="${filename}">
          <Layout type="PatternLayout">
            <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
          </Layout>
        </Appender>
      </Appenders>
     
      <Loggers>
        <Logger name="org.apache.logging.log4j.test1" level="debug" additivity="false">
          <Filter type="ThreadContextMapFilter">
            <KeyValuePair key="test" value="123"/>
          </Filter>
          <AppenderRef ref="STDOUT"/>
        </Logger>
     
        <Logger name="org.apache.logging.log4j.test2" level="debug" additivity="false">
          <AppenderRef ref="File"/>
        </Logger>
     
        <Root level="trace">
          <AppenderRef ref="STDOUT"/>
        </Root>
      </Loggers>
     
    </Configuration>

JSON格式配置

使用 JSON 进行配置

除了 XML,Log4j 还可以使用 JSON 进行配置。 JSON 格式与简洁的 XML 格式非常相似。 每个键代表一个插件的名称,与之关联的键/值对就是它的属性。 如果一个键包含的不仅仅是一个简单的值,它本身就是一个从属插件。 在下面的示例中,ThresholdFilter、Console 和 PatternLayout 都是插件,而 Console 插件的 name 属性将被分配一个 STDOUT 值,而 ThresholdFilter 将被分配一个调试级别。

    { "configuration": { "status": "error", "name": "RoutingTest",
                         "packages": "org.apache.logging.log4j.test",
          "properties": {
            "property": { "name": "filename",
                          "value" : "target/rolling1/rollingtest-$${sd:type}.log" }
          },
        "ThresholdFilter": { "level": "debug" },
        "appenders": {
          "Console": { "name": "STDOUT",
            "PatternLayout": { "pattern": "%m%n" },
            "ThresholdFilter": { "level": "debug" }
          },
          "Routing": { "name": "Routing",
            "Routes": { "pattern": "$${sd:type}",
              "Route": [
                {
                  "RollingFile": {
                    "name": "Rolling-${sd:type}", "fileName": "${filename}",
                    "filePattern": "target/rolling1/test1-${sd:type}.%i.log.gz",
                    "PatternLayout": {"pattern": "%d %p %c{1.} [%t] %m%n"},
                    "SizeBasedTriggeringPolicy": { "size": "500" }
                  }
                },
                { "AppenderRef": "STDOUT", "key": "Audit"}
              ]
            }
          }
        },
        "loggers": {
          "logger": { "name": "EventLogger", "level": "info", "additivity": "false",
                      "AppenderRef": { "ref": "Routing" }},
          "root": { "level": "error", "AppenderRef": { "ref": "STDOUT" }}
        }
      }
    }

YAML配置方式(推荐,个人最喜欢)

Log4j 还支持将 YAML 用于配置文件。 该结构遵循与 XML 和 YAML 配置格式相同的模式。

    Configuration:
      status: warn
      name: YAMLConfigTest
      properties:
        property:
          name: filename
          value: target/test-yaml.log
      thresholdFilter:
        level: debug
      appenders:
        Console:
          name: STDOUT
          target: SYSTEM_OUT
          PatternLayout:
            Pattern: "%m%n"
        File:
          name: File
          fileName: ${filename}
          PatternLayout:
            Pattern: "%d %p %C{1.} [%t] %m%n"
          Filters:
            ThresholdFilter:
              level: error
     
      Loggers:
        logger:
          -
            name: org.apache.logging.log4j.test1
            level: debug
            additivity: false
            ThreadContextMapFilter:
              KeyValuePair:
                key: test
                value: 123
            AppenderRef:
              ref: STDOUT
          -
            name: org.apache.logging.log4j.test2
            level: debug
            additivity: false
            AppenderRef:
              ref: File
        Root:
          level: error
          AppenderRef:
            ref: STDOUT
              

异步日志

异步日志记录可以通过在单独的线程中执行 I/O 操作来提高应用程序的性能。 Log4j 2 在这方面做了很多改进。

  • 异步 Loggers 是 Log4j 2 中的新增功能。它们的目的是尽快从对 Logger.log 的调用返回到应用程序。 您可以选择使所有 Logger 异步或混合使用同步和异步 Logger。 使所有 Loggers 异步将提供最佳性能,而混合则为您提供更大的灵活性。
  • LMAX 干扰器技术。 异步 Logger 在内部使用 Disruptor,一个无锁的线程间通信库,而不是队列,从而产生更高的吞吐量和更低的延迟。
  • 作为 Async Loggers 工作的一部分,Asynchronous Appenders 已得到增强,可以在批处理结束时(当队列为空时)刷新到磁盘。 这会产生与配置“immediateFlush=true”相同的结果,即所有接收到的日志事件始终在磁盘上可用,但效率更高,因为它不需要在每个日志事件上都接触磁盘。 (异步 Appender 在内部使用 ArrayBlockingQueue,并且不需要类路径上的中断器 jar。)

依赖

Log4j-2.9 和更高版本需要在类路径上使用disruptor-3.3.4.jar 或更高版本。 在 Log4j-2.9 之前,需要disruptor-3.0.0.jar 或更高版本。

<!-- https://mvnrepository.com/artifact/com.lmax/disruptor -->
<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>

全局异步

这个配置很简单,只要创建一个叫log4j2.component.properties的文件然后加入Log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
或者Log4j2.contextSelector=org.apache.logging.log4j.core.async.BasicAsyncLoggerContextSelector

混合异步(局部异步,最常用)

借助AsyncLogger即可设置异步日志,直接指明name这和普通的appender是一样的

  <Loggers>
    <AsyncLogger name="com.foo.Bar" level="trace" includeLocation="true">
      <AppenderRef ref="RandomAccessFile"/>
    </AsyncLogger>
  </Loggers>

关闭行号信息

即配置属性:includeLocation="false"为了让性能进一步提升

注意点

  1. 如果使用异步日志,AsyncAppender、AsyncLogger和全局日志,不要同时出现。性能会和AsyncAppender—致,降至最低。
  2. 设置includeLocation=false ,打印位置信息会急剧降低异步日志的性能,比同步日志还要慢。

无垃圾模式

从版本2.6开始,默认情况下Log4j以“无垃圾”模式运行,其中重用对象和缓冲区,并且尽可能不分配临时对象。还有一个“低垃圾”模式,它不是完全无垃圾,但不使用ThreadLocal字段。
Log4j2.6中的无垃圾日志记录部分通过重用ThreadLocal字段中的对象来实现,部分通过在将文本转换为字节时重用缓冲区来实现。

这个模式是默认开启的,大大的提升了性能

文档地址

https://logging.apache.org/log4j/2.x/manual/garbagefree.html

相关文章:

  • WebRTC Simulcast测试--用Janus
  • 2022年全球及中国有机粘土行业头部企业市场占有率及排名调研报告
  • 21年毕业,转行软件测试,薪资10K+,好运气都藏在你的实力里
  • java自定义工具类编写规范
  • 计算机组成原理_Cache的基本概念
  • 【Python零基础入门篇 · 1】:print()函数的使用和转义字符、原字符总结
  • Android集成腾讯TBS_X5内核的一些解决方法
  • spring cloud
  • 【MySQL】必知必会知识点
  • 复盘模型总结
  • Spring5学习笔记03--Bean的生命周期
  • 在小熊派BearPi-HM_Micro_small开发板上安装HAP
  • 基数排序(学习)
  • hive窗口函数最全总结
  • Vulnhub靶场 ICA: 1
  • Android 控件背景颜色处理
  • echarts的各种常用效果展示
  • SOFAMosn配置模型
  • VirtualBox 安装过程中出现 Running VMs found 错误的解决过程
  • 百度地图API标注+时间轴组件
  • 基于遗传算法的优化问题求解
  • 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
  • 前端相关框架总和
  • 算法之不定期更新(一)(2018-04-12)
  • 带你开发类似Pokemon Go的AR游戏
  • 浅谈sql中的in与not in,exists与not exists的区别
  • ​ 轻量应用服务器:亚马逊云科技打造全球领先的云计算解决方案
  • # 再次尝试 连接失败_无线WiFi无法连接到网络怎么办【解决方法】
  • #1014 : Trie树
  • #define与typedef区别
  • #HarmonyOS:Web组件的使用
  • $L^p$ 调和函数恒为零
  • (Oracle)SQL优化技巧(一):分页查询
  • (论文阅读32/100)Flowing convnets for human pose estimation in videos
  • (生成器)yield与(迭代器)generator
  • (算法)Travel Information Center
  • (提供数据集下载)基于大语言模型LangChain与ChatGLM3-6B本地知识库调优:数据集优化、参数调整、Prompt提示词优化实战
  • (状压dp)uva 10817 Headmaster's Headache
  • ***汇编语言 实验16 编写包含多个功能子程序的中断例程
  • . Flume面试题
  • .bat批处理(七):PC端从手机内复制文件到本地
  • .net core 客户端缓存、服务器端响应缓存、服务器内存缓存
  • .NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划
  • .Net Web项目创建比较不错的参考文章
  • .net 前台table如何加一列下拉框_如何用Word编辑参考文献
  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • .NET 中什么样的类是可使用 await 异步等待的?
  • .Net 转战 Android 4.4 日常笔记(4)--按钮事件和国际化
  • .Net6 Api Swagger配置
  • .net反混淆脱壳工具de4dot的使用
  • /var/lib/dpkg/lock 锁定问题
  • @for /l %i in (1,1,10) do md %i 批处理自动建立目录
  • @zabbix数据库历史与趋势数据占用优化(mysql存储查询)
  • [ C++ ] STL---string类的模拟实现
  • []FET-430SIM508 研究日志 11.3.31