Spring Cloud Sleuth 功能

1. 上下文传播

通过头部传播,线路从一个服务连接到另一个服务。 默认格式是B3。 与数据格式类似,只要跟踪和区间 ID 兼容 B3,你也可以配置替代的首部格式。最显著的是,这意味着痕迹ID和区间ID是小写十六进制,而不是UUID。 除了跟踪标识符外,还可以随请求传递其他属性(行李)。 远程行李必须预先定义,但其他方面灵活。spring-doc.cadn.net.cn

要使用提供的默认值,你可以设置Spring。侦探。传播。类型财产。 值可以是一个列表,这样你会传播更多的追踪头。spring-doc.cadn.net.cn

对于《勇敢》我们支持。AWS,B3,W3C传播类型。spring-doc.cadn.net.cn

你可以在本“如何实现”部分阅读更多关于如何提供自定义上下文传播的信息。spring-doc.cadn.net.cn

2. 抽样

Spring Cloud Sleuth 将采样决策推迟到示踪器实现。 不过,有些情况下你可以在运行时更改采样决策。spring-doc.cadn.net.cn

其中一种情况是对某些客户端时段进行跳过报告。 为此,你可以设置spring.sleuth.web.client.skip-pattern路径模式可以跳过。 另一种选择是提供你自己的定制org.springframework.cloud.sleuth.SamplerFunction<'org.springframework.cloud.sleuth.http.HttpRequest>实现并定义当给定HttpRequest不应采样。spring-doc.cadn.net.cn

3. 行李

分布式追踪通过在连接跟踪线的服务内部和跨服务之间传播字段来实现:尤其是traceId和spanId。 包含这些字段的上下文可以选择性地推送其他需要保持一致的字段,无论涉及多少服务。 这些额外字段的简单名称是“行李”。spring-doc.cadn.net.cn

Sleuth 允许你定义允许在 trace 上下文中存在哪些行李,包括使用哪些头部名称。spring-doc.cadn.net.cn

以下示例展示了使用 Spring Cloud Sleuth API 设置行李值的方法:spring-doc.cadn.net.cn

try (Tracer.SpanInScope ws = this.tracer.withSpan(initialSpan)) {
    BaggageInScope businessProcess = this.tracer.createBaggage(BUSINESS_PROCESS).set("ALM");
    BaggageInScope countryCode = this.tracer.createBaggage(COUNTRY_CODE).set("FO");
    try {
目前行李的数量和尺寸没有限制。 请记住,过多会降低系统吞吐量或增加RPC延迟。 在极端情况下,过多的包袱可能因超过传输级消息或头部容量而导致应用程序崩溃。

你可以用属性来定义没有特殊配置的字段,比如名称映射:spring-doc.cadn.net.cn

  • Spring侦探。行李。远程字段是一份用于接受并传播到远程服务的头部名称列表。spring-doc.cadn.net.cn

  • Spring.侦探.行李.本地字段是一份需要在本地传播的名称列表spring-doc.cadn.net.cn

这些键不使用任何前缀。 你设定的就是实际使用的。spring-doc.cadn.net.cn

在这两个属性中设置的名称都会导致行李同名。spring-doc.cadn.net.cn

要自动将行李值设置为 Slf4j 的 MDC,你必须设置春.侦探.行李.相关场带有允许的本地或远程键列表的属性。例如,spring.sleuth.baggage.correlation-fields=country-code将设定 的值国家代码行李进入了多伦多。spring-doc.cadn.net.cn

注意,额外的字段是从下游的跟踪上下文开始传播并添加到MDC的。 要在当前跟踪上下文中立即将额外字段添加到MDC,配置该字段在更新时刷新:spring-doc.cadn.net.cn

// configuration
@Bean
BaggageField countryCodeField() {
    return BaggageField.create("country-code");
}

@Bean
ScopeDecorator mdcScopeDecorator() {
    return MDCScopeDecorator.newBuilder()
            .clear()
            .add(SingleCorrelationField.newBuilder(countryCodeField())
                    .flushOnUpdate()
                    .build())
            .build();
}

// service
@Autowired
BaggageField countryCodeField;

countryCodeField.updateValue("new-value");
请记住,在MDC中添加条目会大幅降低应用的性能!

如果你想将行李条目添加为标签,以便通过行李条目搜索跨度,可以设置Spring.侦探.行李.标签字段还有一份允许携带的行李钥匙清单。 要禁用该功能,你必须通过spring.sleuth.propagation.tag.enabled=false财产。spring-doc.cadn.net.cn

3.1. 行李与标签

像跟踪ID一样,Bbagage通常附加在消息或请求上,通常作为头部。 标签是通过 Span 发送到 Zipkin 的密钥值对。 行李价值默认不是加的跨度,这意味着除非你选择加入,否则不能基于行李进行搜索。spring-doc.cadn.net.cn

要让行李也成为标签,请使用属性Spring.侦探.行李.标签字段这样:spring-doc.cadn.net.cn

spring:
  sleuth:
    baggage:
      foo: bar
      remoteFields:
        - country-code
        - x-vcap-request-id
      tagFields:
        - country-code

4. OpenZipkin Brave Tracer 集成

Spring Cloud Sleuth 通过 OpenZipkin Brave 追踪器中的桥接器集成,春云侦探勇敢模块。 在本节中,你可以阅读关于具体Brave集成的内容。spring-doc.cadn.net.cn

你可以选择直接在代码中使用 Sleuth 的 API 或 Brave API(例如 Sleuth 的示 踪或者说是勇敢者示 踪). 如果你想直接使用该追踪器实现的 API,请阅读他们的文档以了解更多相关内容。spring-doc.cadn.net.cn

4.1. 勇敢基础

以下是你可能使用的最核心类型:spring-doc.cadn.net.cn

以下是OpenZipkin Brave项目中最相关的链接:spring-doc.cadn.net.cn

4.2. 勇敢采样

采样仅适用于后端的追踪,比如 Zipkin。 无论采样率如何,日志中都会出现痕迹ID。 抽样是一种防止系统过载的方法,通过持续追踪部分请求,但并非全部请求。spring-doc.cadn.net.cn

默认每秒10次痕迹的速率由以下控制春季侦探采样率当我们知道侦探被用于Logging以外的用途时,属性和适用。 使用超过100次/秒的追踪速率且极其谨慎,因为这可能会使追踪系统过载。spring-doc.cadn.net.cn

采样器也可以由 Java 配置设置,如下示例所示:spring-doc.cadn.net.cn

@Bean
public Sampler defaultSampler() {
    return Sampler.ALWAYS_SAMPLE;
}
你可以设置HTTP首部B31或者,在进行消息传递时,你可以设置spanFlags头部 至1. 这样做会强制当前请求被采样,无论配置如何。

默认情况下,采样器可以配合刷新范围机制工作。 这意味着你可以在运行时更改采样属性,刷新应用程序,这些变化就会被反映出来。 然而,有时围绕采样器创建代理并过早调用它(来自@PostConstruct注释方法)可能导致死锁。 在这种情况下,要么显式创建采样豆,要么设置属性spring.sleuth.sampler.refresh.enabledfalse禁用刷新示波器支持。spring-doc.cadn.net.cn

4.3. Brave Baggage Java 配置

如果你需要做比上述更高级的作,不要定义属性,而是使用@Bean你用的行李栏配置。spring-doc.cadn.net.cn

4.4. 勇敢定制

勇敢。示 踪对象完全由侦探管理,所以你很少需要影响它。 话虽如此,《侦探》支持多种定制器类型,允许你用自动配置或属性配置Sleuth尚未完成的任何功能。spring-doc.cadn.net.cn

如果你将以下其中之一定义为侦探会调用它来自定义行为:spring-doc.cadn.net.cn

4.4.1. 勇敢采样定制

如果需要客户端/服务器采样,只需注册一个类型的豆子brave.sampler.SamplerFunction<HttpRequest>还给豆子起名字sleuthHttpClientSampler对于客户端采样器和sleuthHttpServerSampler用于服务器采样器。spring-doc.cadn.net.cn

为了方便@HttpClientSampler@HttpServerSampler注释可以用来注入正确的豆子,或通过静态字符串引用豆子名称名称领域。spring-doc.cadn.net.cn

看看Brave的代码,了解如何制作基于路径的采样器 github.com/openzipkin/brave/tree/master/instrumentation/http#sampling-policyspring-doc.cadn.net.cn

如果你想彻底重写Http追踪豆子你可以用SkipPatternProvider用于获取URL的接口模式对于不应采样的跨度。 下面你可以看到SkipPatternProvider在服务器端,Sampler<HttpRequest>.spring-doc.cadn.net.cn

@Configuration(proxyBeanMethods = false)
    class Config {
  @Bean(name = HttpServerSampler.NAME)
  SamplerFunction<HttpRequest> myHttpSampler(SkipPatternProvider provider) {
      Pattern pattern = provider.skipPattern();
      return request -> {
          String url = request.path();
          boolean shouldSkip = pattern.matcher(url).matches();
          if (shouldSkip) {
              return false;
          }
          return null;
      };
  }
}

4.5. 勇敢的信息

侦探自动配置消息追踪豆子作为消息工具(如Kafka或JMS)的基础。spring-doc.cadn.net.cn

如果需要定制生产者/消费者对消息轨迹的采样,只需注册一个类型的豆子brave.sampler.SamplerFunction<MessagingRequest>还给豆子起名字侦探制作人采样器对于制作人采样器和侦探消费者采样器供消费者试用。spring-doc.cadn.net.cn

为了方便@ProducerSampler@ConsumerSampler注释可以用来注入正确的豆子,或通过静态字符串引用豆子名称名称领域。spring-doc.cadn.net.cn

前任。 这里有一个采样器,每秒追踪100个消费者请求,除了“警报”通道。 其他请求将使用由描图元件。spring-doc.cadn.net.cn

@Configuration(proxyBeanMethods = false)
    class Config {
  @Bean(name = ConsumerSampler.NAME)
  SamplerFunction<MessagingRequest> myMessagingSampler() {
      return MessagingRuleSampler.newBuilder().putRule(channelNameEquals("alerts"), Sampler.NEVER_SAMPLE)
              .putRule(Matchers.alwaysMatch(), RateLimitingSampler.create(100)).build();
  }
}

4.6. 勇敢的Opentracing(勇敢的Opentracing)

你可以通过io.opentracing.brave:brave-opentracing桥。 只需将其添加到类路径和OpenTracining中即可示 踪将自动设置。spring-doc.cadn.net.cn

5. 向Zipkin发送Spans

Spring Cloud Sleuth 提供了与 OpenZipkin 分布式追踪系统的多种集成。 无论选择哪种追踪器实现,添加都足够了春云侦探齐普金发送到类路径,开始向 Zipkin 发送 span。 你可以选择通过HTTP或消息发送来实现。 你可以在“如何作”部分了解更多相关内容。spring-doc.cadn.net.cn

当跨度关闭时,数据通过HTTP发送到Zipkin。通信是异步的。 你可以通过设置spring.zipkin.baseUrl性质,具体如下:spring-doc.cadn.net.cn

spring.zipkin.baseUrl: https://192.168.99.100:9411/

如果你想通过服务发现找到 Zipkin,可以在 URL 中传递 Zipkin 的服务 ID,如下示例所示zipkinserver服务编号:spring-doc.cadn.net.cn

spring.zipkin.baseUrl: https://zipkinserver/

要禁用这个功能,只需设置spring.zipkin.discovery-client-enabledfalse.spring-doc.cadn.net.cn

当发现客户端功能启用时,Sleuth 会使用LoadBalancerClient以查找Zipkin服务器的网址。 这意味着你可以设置负载均衡配置。spring-doc.cadn.net.cn

如果你有Web,,ActiveMQ卡 夫 卡在 classpath 上,你可能需要选择如何将 spans 发送到 zipkin。 要做到这一点,设置Web,,ActiveMQ卡 夫 卡前往Spring.zipkin.sender.type财产。 以下示例展示了如何设置发送方类型Web:spring-doc.cadn.net.cn

spring.zipkin.sender.type: web

以定制Rest模板通过HTTP向Zipkin发送跨度,你可以注册ZipkinRestTemplateCustomizer豆。spring-doc.cadn.net.cn

@Configuration(proxyBeanMethods = false)
    class MyConfig {
    @Bean ZipkinRestTemplateCustomizer myCustomizer() {
        return new ZipkinRestTemplateCustomizer() {
            @Override
            void customize(RestTemplate restTemplate) {
                // customize the RestTemplate
            }
        };
    }
}

然而,如果你想控制创建的整个过程Rest模板你必须创建一个zipkin2.记者.发件人类型。spring-doc.cadn.net.cn

@Bean Sender myRestTemplateSender(ZipkinProperties zipkin,
        ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer) {
    RestTemplate restTemplate = mySuperCustomRestTemplate();
    zipkinRestTemplateCustomizer.customize(restTemplate);
    return myCustomSender(zipkin, restTemplate);
}

默认情况下,API 路径将设置为API/v2/spansAPI/v1/spans这取决于编码器版本。如果你想使用自定义 API 路径,可以用以下属性(空写,设置 “”)来配置它:spring-doc.cadn.net.cn

spring.zipkin.api-path: v2/path2

5.1. 海关服务名称

默认情况下,Sleuth 假设当你向 Zipkin 发送一个 span 时,你希望该 span 的服务名称等于spring.application.name财产。 不过,情况并非总是如此。 在某些情况下,你需要为申请中所有路段明确提供不同的服务名称。 为此,你可以将以下属性传递给应用程序来覆盖该值(示例为我的服务):spring-doc.cadn.net.cn

spring.zipkin.service.name: myService

5.2. 宿主定位器

本节讨论从服务发现中定义主机。 这不是通过服务发现找到Zipkin的问题。

要定义对应特定区间的主机,我们需要解析主机名称和端口。 默认方法是从服务器属性中获取这些值。 如果这些未设置,我们会尝试从网络接口中获取主机名称。spring-doc.cadn.net.cn

如果你启用了发现客户端,并且更愿意从服务注册表中注册的实例获取主机地址,你必须设置spring.zipkin.locator.discovery.enabled属性(适用于基于HTTP和基于流的跨度报告),具体如下:spring-doc.cadn.net.cn

spring.zipkin.locator.discovery.enabled: true

5.3. 报告跨度的定制化

在Sleuth中,我们生成一个固定名称的跨度。 有些用户希望根据标签的值修改名称。spring-doc.cadn.net.cn

侦探注意到SpanFilter可以自动跳过报告的命名模式。 该物业spring.sleuth.span-filter.span-name-patterns-to-skip包含跨度名称的默认跳跃模式。 该物业spring.sleuth.span-filter.additional-span-name-patterns-to-skip将提供的跨区间名称模式附加到现有的组合上。 要禁用这个功能,只需设置spring.sleuth.span-filter.enabledfalse.spring-doc.cadn.net.cn

5.3.1. 报告跨度的勇敢定制

本节仅适用于Brave猎空。

在报告跨度之前(例如向Zipkin报告),你可能想以某种方式修改该跨度。 你可以通过实现SpanHandler.spring-doc.cadn.net.cn

以下示例展示了如何注册两个实现SpanHandler:spring-doc.cadn.net.cn

@Bean
SpanHandler handlerOne() {
    return new SpanHandler() {
        @Override
        public boolean end(TraceContext traceContext, MutableSpan span, Cause cause) {
            span.name("foo");
            return true; // keep this span
        }
    };
}

@Bean
SpanHandler handlerTwo() {
    return new SpanHandler() {
        @Override
        public boolean end(TraceContext traceContext, MutableSpan span, Cause cause) {
            span.name(span.name() + " bar");
            return true; // keep this span
        }
    };
}

前述示例将报告跨距的名称更改为福巴尔,就在被报告之前(例如,给Zipkin)。spring-doc.cadn.net.cn

5.4. 覆盖 Zipkin 的自动配置

从 2.1.0 版本起,Spring Cloud Sleuth 支持向多个追踪系统发送追踪。为了实现这一点,每个追踪系统都需要有记者<斯潘>寄件人. 如果你想覆盖提供的豆子,你需要给它们一个具体的名字。 你可以分别使用ZipkinAutoConfiguration.REPORTER_BEAN_NAMEZipkinAutoConfiguration.SENDER_BEAN_NAME.spring-doc.cadn.net.cn

@Configuration(proxyBeanMethods = false)
protected static class MyConfig {

    @Bean(ZipkinAutoConfiguration.REPORTER_BEAN_NAME)
    Reporter<zipkin2.Span> myReporter(@Qualifier(ZipkinAutoConfiguration.SENDER_BEAN_NAME) MySender mySender) {
        return AsyncReporter.create(mySender);
    }

    @Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME)
    MySender mySender() {
        return new MySender();
    }

    static class MySender extends Sender {

        private boolean spanSent = false;

        boolean isSpanSent() {
            return this.spanSent;
        }

        @Override
        public Encoding encoding() {
            return Encoding.JSON;
        }

        @Override
        public int messageMaxBytes() {
            return Integer.MAX_VALUE;
        }

        @Override
        public int messageSizeInBytes(List<byte[]> encodedSpans) {
            return encoding().listSizeInBytes(encodedSpans);
        }

        @Override
        public Call<Void> sendSpans(List<byte[]> encodedSpans) {
            this.spanSent = true;
            return Call.create(null);
        }

    }

}

6. 对数积分

Sleuth 用包括服务名称在内的变量配置日志上下文(%{spring.zipkin.service.name}%{spring.application.name}如果之前未设置,则张成ID(%{spanId})以及迹迹ID(%{traceId}). 这些工具帮助你将日志与分布式追踪连接起来,并允许你选择使用哪些工具来排查服务。spring-doc.cadn.net.cn

一旦你找到任何有错误的日志,就可以在消息中查找追踪ID。 把它粘贴到分布式追踪系统中,无论第一个请求最终访问了多少服务,都能直观地看到整个追踪过程。spring-doc.cadn.net.cn

backend.log:  2020-04-09 17:45:40.516 ERROR [backend,5e8eeec48b08e26882aba313eb08f0a4,dcc1df555b5777b3] 97203 --- [nio-9000-exec-1] o.s.c.s.i.web.ExceptionLoggingFilter     : Uncaught exception thrown
frontend.log:2020-04-09 17:45:40.574 ERROR [frontend,5e8eeec48b08e26882aba313eb08f0a4,82aba313eb08f0a4] 97192 --- [nio-8081-exec-2] o.s.c.s.i.web.ExceptionLoggingFilter     : Uncaught exception thrown

你会注意到上面的追踪ID是5e8eeec48b08e26882aba313eb08f0a4例如。 这个日志配置是由Sleuth自动设置的。 你可以通过禁用侦探来禁用它。Spring.sleuth.enabled=false财产或你自己的财产logging.pattern.level财产。spring-doc.cadn.net.cn

如果你使用日志聚合工具(如KibanaSplunk等),可以对发生的事件进行排序。 Kibana的一个例子类似于以下图片:spring-doc.cadn.net.cn

对数与基巴纳的相关性

如果你想使用 Logstash,以下列表展示了 Logstash 的 Grok 图案:spring-doc.cadn.net.cn

filter {
  # pattern matching logback pattern
  grok {
    match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
  }
  date {
    match => ["timestamp", "ISO8601"]
  }
  mutate {
    remove_field => ["timestamp"]
  }
}
如果你想将Grok与Cloud Foundry的日志一起使用,必须使用以下模式:
filter {
  # pattern matching logback pattern
  grok {
    match => { "message" => "(?m)OUT\s+%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" }
  }
  date {
    match => ["timestamp", "ISO8601"]
  }
  mutate {
    remove_field => ["timestamp"]
  }
}

6.1. JSON Logstash 日志回溯

通常,你不想把日志存储在文本文件中,而是要存储在 Logstash 可以立即选择的 JSON 文件中。 为此,你需要做以下作(为了可读性,我们将依赖关系传递给groupId:artifactId:version符号)。spring-doc.cadn.net.cn

  1. 确保Logback在类路径上(ch.qos.logback:logback-core).spring-doc.cadn.net.cn

  2. 添加Logstash Logback编码。 例如,使用 version(变值)4.6net.logstash.logback:logstash-logback-encoder:4.6.spring-doc.cadn.net.cn

日志回溯设置spring-doc.cadn.net.cn

请考虑以下Logback配置文件(logback-spring.xml)的示例。spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <springProperty scope="context" name="springAppName" source="spring.application.name"/>
    <!-- Example for logging into the build folder of your project -->
    <property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}"/>

    <!-- You can override this to have a custom pattern -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>

    <!-- Appender to log to console -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- Minimum logging level to be presented in the console logs-->
            <level>DEBUG</level>
        </filter>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>

    <!-- Appender to log to file -->
    <appender name="flatfile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>utf8</charset>
        </encoder>
    </appender>
    <!-- Appender to log to file in a JSON format -->
    <appender name="logstash" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE}.json</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>UTC</timeZone>
                </timestamp>
                <pattern>
                    <pattern>
                        {
                        "timestamp": "@timestamp",
                        "severity": "%level",
                        "service": "${springAppName:-}",
                        "trace": "%X{traceId:-}",
                        "span": "%X{spanId:-}",
                        "pid": "${PID:-}",
                        "thread": "%thread",
                        "class": "%logger{40}",
                        "rest": "%message"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="console"/>
        <!-- uncomment this to have also JSON logs -->
        <!--<appender-ref ref="logstash"/>-->
        <!--<appender-ref ref="flatfile"/>-->
    </root>
</configuration>

那个日志回溯配置文件:spring-doc.cadn.net.cn

如果你用自定义logback-spring.xml你必须通过spring.application.name启动而非应用财产档案。 否则,你的自定义日志文件无法正确读取该属性。

7. 接下来要读什么

如果你想了解更多本节讨论的课程,可以直接浏览源代码。 如果你有具体问题,请参见操作指南部分。spring-doc.cadn.net.cn

如果你对 Spring Cloud Sleuth 的核心功能感到熟悉,可以继续阅读关于 Spring Cloud Sleuth 的集成内容。spring-doc.cadn.net.cn