Graalvm aot编译springboot3.x webflux+r2dbc版

本文完整代码链接 https://github.com/coralloc8/springboot3-native.git

Graalvm基础自行去官网了解 https://www.graalvm.org/latest/docs/getting-started/

此文章包含如下功能节点,在下述功能节点上做AOT编译(基本能满足日常开发):

  • Opendoc:API接口文档,替代swagger
  • Webflux:响应式编程
  • ReactiveTransactionManager:异步事务、事务切面
  • R2dbc、proxy:异步jdbc链接,SQL日志自定义输出
  • Mapstruct:替代反射的方式做数据对象转换。dto转bo、vo转dto之类
  • Graalvm Feature:自定义Feature,运行时注册相关的类
  • Jackson:数据序列化/反序列化时类型转换,枚举转换之类
  • Aop:实现全局的日志打印
  • ErrorWebExceptionHandler:webflux 全局异常处理 可支持函数式编程方式
  • WebFilter:自定义各种filter,MDC,reactor数据透传请求上下文
  • Redis:异步redis读取 (2024-06-05 新增)

本文只对如何将springboot3.x项目进行aot编译说明,Graalvm版本 jdk17。


image.png

项目对springboot的版本依赖如下:

     <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>3.2.4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2023.0.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2023.0.0.0-RC1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

        </dependencies>
    </dependencyManagement>

如果项目的父类不是spring-boot-starter-parent的话

    <parent>
        <groupId>org.spring.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
    </parent>

那么在项目中则需要添加如下profiles (spring-boot-starter-parent项目中默认已经配置下面的profiles)

    <!-- 自定义profile -->
    <profiles>
        <profile>
            <id>native</id>
            <build>
                <pluginManagement>
                    <plugins>
                        <plugin>
                            <groupId>org.graalvm.buildtools</groupId>
                            <artifactId>native-maven-plugin</artifactId>
                            <executions>
                                <execution>
                                    <id>add-reachability-metadata</id>
                                    <goals>
                                        <goal>add-reachability-metadata</goal>
                                    </goals>
                                </execution>
                            </executions>
                        </plugin>
                        <plugin>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-maven-plugin</artifactId>
                            <executions>
                                <execution>
                                    <id>process-aot</id>
                                    <goals>
                                        <goal>process-aot</goal>
                                    </goals>
                                </execution>
                            </executions>
                        </plugin>
                    </plugins>
                </pluginManagement>
            </build>
        </profile>
        <profile>
            <id>nativeTest</id>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-launcher</artifactId>
                    <scope>test</scope>
                </dependency>
            </dependencies>
            <build>
                <pluginManagement>
                    <plugins>
                        <plugin>
                            <groupId>org.graalvm.buildtools</groupId>
                            <artifactId>native-maven-plugin</artifactId>
                            <executions>
                                <execution>
                                    <id>native-test</id>
                                    <goals>
                                        <goal>test</goal>
                                    </goals>
                                </execution>
                            </executions>
                        </plugin>
                        <plugin>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-maven-plugin</artifactId>
                            <executions>
                                <execution>
                                    <id>process-test-aot</id>
                                    <goals>
                                        <goal>process-test-aot</goal>
                                    </goals>
                                </execution>
                            </executions>
                        </plugin>
                    </plugins>
                </pluginManagement>
            </build>
        </profile>
    </profiles>

Graalvm aot 编译目前对于springcloud有很多的限制


img_6.png

如官方所说,采用bootstrap.yml的方式走不通。因此得采用spring.config.import的方式来引入naco中的配置文件。

# 远端配置
spring:
  config:
    import:
      - optional:nacos:${spring.cloud.nacos.config.prefix}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}?preference=remote

preference的方式来自nacos官方 https://github.com/alibaba/spring-cloud-alibaba/pull/2459
需要在nacos配置文件中配置 spring.cloud.nacos.config.preference=remote
配置成preference=remote的话此时配置文件的优先级会变成 nacos>application-{active}.yml>application.yml

nacos的配置文件 (命名空间ID:spring-native 配置文件名:confi-dev.yaml)


image.png
server:
    port: 8090

coral:
    datasource:
        url: 192.168.29.66:3306

coral.native:
    username: xiaoxiao23
    sex: male男a

Graalvm编译不支持 lambda表达式。因此需要自行实现graalvm的feature来支持。

public class RuntimeRegistrationFeature implements Feature {


    private static final String PACKAGE_NAME = "com.coral.test.spring.natives";

    @Override
    public void duringSetup(DuringSetupAccess access) {
        //      扫描指定包下IService IRepository的字类(实现类),然后全部注册到graalvm Lambda 序列化中
        RuntimeRegistrationFeature.findClasses(PACKAGE_NAME, Set.of(IService.class, IRepository.class))
                .forEach(RuntimeSerialization::registerLambdaCapturingClass);
        RuntimeSerialization.register(SerializedLambda.class, IGetter.class);
    }

    /**
     * 找到某个包下面指定的父类的所有子类
     *
     * @param packageName  包名
     * @param superClasses 父类
     * @return 子类集合
     */
    private static Set<Class<?>> findClasses(String packageName, Set<Class<?>> superClasses) {
        Set<Class<?>> classes = new HashSet<>();
        for (Class<?> superClass : superClasses) {
            classes.addAll(ClassUtil.scanPackageBySuper(packageName, superClass));
        }
        return classes;
    }

}

Graalvm不支持java中的反射操作,需要自行通过 native-image-agent来对java jar生成 native-image映射文件。
常规操作是先将 普通的springboot项目打包成 jar可执行文件,然后再用以下命令来生成 native-image 文件

java  -agentlib:native-image-agent=config-output-dir=./config -jar target/spring-native-test-1.0.0-SNAPSHOT.jar

config-output-dir 为生成的文件所保存的目录。
执行完上述命令后,jar文件应该正常运行起来了,此时访问 http://localhost:8090/doc.html 进入到项目api文档。最保险的做法是将文档中的所有api接口调用一遍,包括进入到首页时的一些静态文件,强制刷新首页。这样才能让native探针充分捕捉到项目所需要的native-image文件。操作完成后,jar进程就可以杀掉了。

此时查看 config目录,应该会发现生成了一系列文件。


image.png

这些文件中需要说明的是jni文件中需要增加如下配置,不然编译过程中会报dns空值异常。

{
  "name":"sun.net.dns.ResolverConfigurationImpl",
  "fields":[{"name":"os_nameservers"}, {"name":"os_searchlist"}]
}

本文生成native-image映射文件采用的另外一种方式。
通过spring-boot-maven-plugin插件来配置生成映射文件。如此只需要执行
spring-boot:start spring-boot:stop即可。
start启动项目后,访问项目doc.html接口文档,仍然需要将文档中的所有api接口调用一遍,包括进入到首页时的一些静态文件,强制刷新首页。让native探针充分捕捉到项目所需要的native-image文件。
stop最终杀掉项目。

   <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <jvmArguments>
                        -Dfile.encoding=UTF-8
                        --add-opens java.base/java.lang=ALL-UNNAMED
                        --add-opens java.base/java.io=ALL-UNNAMED
                        --add-opens java.base/java.math=ALL-UNNAMED
                        --add-opens java.base/java.net=ALL-UNNAMED
                        --add-opens java.base/java.nio=ALL-UNNAMED
                        --add-opens java.base/java.security=ALL-UNNAMED
                        --add-opens java.base/java.text=ALL-UNNAMED
                        --add-opens java.base/java.time=ALL-UNNAMED
                        --add-opens java.base/java.util=ALL-UNNAMED
                        --add-opens java.base/jdk.internal.access=ALL-UNNAMED
                        --add-opens java.base/jdk.internal.misc=ALL-UNNAMED
                        -agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/
                    </jvmArguments>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>

上述两种方式生成的映射文件最终都需要放在项目 resources目录下。在resources目录下新建 META-INF/native-image 目录,然后将上述生成的映射文件全部放进去。

image.png

springboot process-aot编译也会生成native-image映射文件。保存目录在META-INF/native-image/{groupId}/{artifactId}/下。因此在native-maven-plugin插件中新增如下编译配置。

<buildArg>
  -H:ReflectionConfigurationResources=META-INF/native-image/reflect-config.json,META-INF/native-image/com.coral.test/spring-native-test/reflect-config.json 
  -H:ResourceConfigurationResources=META-INF/native-image/resource-config.json,META-INF/native-image/com.coral.test/spring-native-test/resource-config.json
  -H:DynamicProxyConfigurationResources=META-INF/native-image/proxy-config.json,META-INF/native-image/com.coral.test/spring-native-test/proxy-config.json 
  -H:JNIConfigurationResources=META-INF/native-image/jni-config.json
</buildArg>

接下来正式开始AOT编译。


image.png

image.png

image.png

步骤执行完后会发现在target目录下生成了jar文件和exe文件

image.png

instrument.dll文件是因为引入了nacos。所以生成了该文件。

本文的项目中已经配置完整的编译。因此可以采用如下命令一键编译。

mvn -Pnative -Pdev native:compile-no-fork -f pom.xml

在编译过程中会报各种类文件编译时机不对的问题。有的类需要在build阶段,有的类需要在run阶段初始化。类似下面配置。哪个类有问题就增加哪个类。
如下述错误

Error: Classes that should be initialized at run time got initialized during image building:
 ch.qos.logback.core.util.StatusPrinter was unintentionally initialized at build time. To see why ch.qos.logback.core.util.StatusPrinter got initialized use --trace-class-initialization=ch.qos.logback.core.util.StatusPrinter
ch.qos.logback.classic.Logger was unintentionally initialized at build time. To see why ch.qos.logback.classic.Logger got initialized use --trace-class-initialization=ch.qos.logback.classic.Logger
ch.qos.logback.classic.Level was unintentionally initialized at build time. To see why ch.qos.logback.classic.Level got initialized use --trace-class-initialization=ch.qos.logback.classic.Level
ch.qos.logback.core.status.StatusBase was unintentionally initialized at build time. To see why ch.qos.logback.core.status.StatusBase got initialized use --trace-class-initialization=ch.qos.logback.core.status.StatusBase
ch.qos.logback.core.util.Loader was unintentionally initialized at build time. To see why ch.qos.logback.core.util.Loader got initialized use --trace-class-initialization=ch.qos.logback.core.util.Loader
ch.qos.logback.core.CoreConstants was unintentionally initialized at build time. To see why ch.qos.logback.core.CoreConstants got initialized use --trace-class-initialization=ch.qos.logback.core.CoreConstants
ch.qos.logback.core.status.InfoStatus was unintentionally initialized at build time. To see why ch.qos.logback.core.status.InfoStatus got initialized use --trace-class-initialization=ch.qos.logback.core.status.InfoStatus
org.slf4j.LoggerFactory was unintentionally initialized at build time. To see why org.slf4j.LoggerFactory got initialized use --trace-class-initialization=org.slf4j.LoggerFactory
To see how the classes got initialized, use --trace-class-initialization=ch.qos.logback.core.util.StatusPrinter,ch.qos.logback.classic.Logger,ch.qos.logback.classic.Level,ch.qos.logback.core.status.StatusBase,ch.qos.logback.core.util.Loader,ch.qos.logback.core.CoreConstants,ch.qos.logback.core.status.InfoStatus,org.slf4j.LoggerFactory
  <buildArg>
                            <!-- logback -->
                            --initialize-at-build-time=ch.qos.logback.core.model.processor.DefaultProcessor$1
                            --initialize-at-build-time=ch.qos.logback.core.model.processor.ChainedModelFilter$1
                            --initialize-at-build-time=ch.qos.logback.core.model.processor.ImplicitModelHandler$1
                            --initialize-at-build-time=ch.qos.logback.core.CoreConstants
                            --initialize-at-build-time=ch.qos.logback.core.subst.Token
                            --initialize-at-build-time=ch.qos.logback.core.subst.Parser$1
                            --initialize-at-build-time=ch.qos.logback.core.subst.NodeToStringTransformer$1
                            --initialize-at-build-time=ch.qos.logback.core.util.Duration
                            --initialize-at-build-time=ch.qos.logback.core.util.Loader
                            --initialize-at-build-time=ch.qos.logback.core.util.StatusPrinter
                            --initialize-at-build-time=ch.qos.logback.core.status.StatusBase
                            --initialize-at-build-time=ch.qos.logback.core.status.InfoStatus
                            --initialize-at-build-time=ch.qos.logback.core.status.WarnStatus
                            --initialize-at-build-time=ch.qos.logback.classic.Level
                            --initialize-at-build-time=ch.qos.logback.classic.Logger
                            --initialize-at-build-time=ch.qos.logback.classic.model.processor.LogbackClassicDefaultNestedComponentRules
                            <!-- slf4j -->
                            --initialize-at-build-time=org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.slf4j.MDC,org.slf4j.impl.StaticLoggerBinder
                            <!-- netty -->
                            --initialize-at-run-time=io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler,io.netty.handler.codec.http2.Http2ServerUpgradeCodec
                            <!-- -->
                            --initialize-at-run-time=sun.net.dns.ResolverConfigurationImpl
                            --initialize-at-build-time=cn.hutool.core.util.ClassLoaderUtil
                            --initialize-at-build-time=cn.hutool.core.convert.BasicType
                            --initialize-at-build-time=cn.hutool.core.util.CharsetUtil
                            <!-- features -->
                            --features=com.coral.test.spring.natives.core.feature.RuntimeRegistrationFeature
                        </buildArg>

最终生成的完整编译文件为

          <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <configuration>
                    <!-- imageName用于设置生成的二进制文件名称 -->
                    <imageName>${project.artifactId}</imageName>
                    <!-- mainClass用于指定main方法类路径 -->
                    <mainClass>com.coral.test.spring.natives.MainApplication</mainClass>
                    <!--                    <useArgFile>true</useArgFile>-->
                    <skip>false</skip>
                    <!-- 如果要启用调试信息的生成,请在插件配置中提供以下内容-->
                    <debug>false</debug>
                    <!--启用详细输出-->
                    <verbose>false</verbose>
                    <!--
                     -H:ConfigurationFileDirectories=:指定配置文件的直接目录,多个项目之间用逗号分隔。在该目录中按默认方式的命名的 json 配置文件都可以被自动识别。
                     -H:ConfigurationResourceRoots=:指定配置资源的根路径,多个项目之间用逗号分隔。配置文件不仅可以被当作外部文件读取,也可以被当作 resource 资源读取。这种方式适用于读取存放在 jar 文件中的配置文件。
                     -H:XXXConfigurationFiles=:指定某一种类型的配置文件,多个项目之间用逗号分隔。这里的 XXX 可以是 Reflection、DynamicProxy、Serialization、SerializationDeny、Resource、JNI 或 PredefinedClasses。
                     -H:XXXConfigurationResources=:指定某一种类型的配置资源的路径,多个项目之间用逗号分隔。这里的 XXX 可以是 Reflection、DynamicProxy、Serialization、SerializationDeny、Resource、JNI 或 PredefinedClasses。
                     -->
                    <buildArgs combine.children="append">
                        <buildArg>
                            --verbose
                            -Djavax.xml.accessExternalDTD=all
                            -Dfile.encoding=UTF-8
                            -H:+AddAllCharsets
                            -H:+ReportExceptionStackTraces
                            <!--                            -H:+PrintClassInitialization-->
                            --allow-incomplete-classpath
                            --report-unsupported-elements-at-runtime

                        </buildArg>
                        <!-- 反射 -->
                        <buildArg>
                            -H:ReflectionConfigurationResources=META-INF/native-image/reflect-config.json,META-INF/native-image/com.coral.test/spring-native-test/reflect-config.json
                            -H:ResourceConfigurationResources=META-INF/native-image/resource-config.json,META-INF/native-image/com.coral.test/spring-native-test/resource-config.json
                            -H:DynamicProxyConfigurationResources=META-INF/native-image/proxy-config.json,META-INF/native-image/com.coral.test/spring-native-test/proxy-config.json
                            -H:JNIConfigurationResources=META-INF/native-image/jni-config.json
                        </buildArg>
                        <buildArg>
                            <!-- logback -->
                            --initialize-at-build-time=ch.qos.logback.core.model.processor.DefaultProcessor$1
                            --initialize-at-build-time=ch.qos.logback.core.model.processor.ChainedModelFilter$1
                            --initialize-at-build-time=ch.qos.logback.core.model.processor.ImplicitModelHandler$1
                            --initialize-at-build-time=ch.qos.logback.core.CoreConstants
                            --initialize-at-build-time=ch.qos.logback.core.subst.Token
                            --initialize-at-build-time=ch.qos.logback.core.subst.Parser$1
                            --initialize-at-build-time=ch.qos.logback.core.subst.NodeToStringTransformer$1
                            --initialize-at-build-time=ch.qos.logback.core.util.Duration
                            --initialize-at-build-time=ch.qos.logback.core.util.Loader
                            --initialize-at-build-time=ch.qos.logback.core.util.StatusPrinter
                            --initialize-at-build-time=ch.qos.logback.core.status.StatusBase
                            --initialize-at-build-time=ch.qos.logback.core.status.InfoStatus
                            --initialize-at-build-time=ch.qos.logback.core.status.WarnStatus
                            --initialize-at-build-time=ch.qos.logback.classic.Level
                            --initialize-at-build-time=ch.qos.logback.classic.Logger
                            --initialize-at-build-time=ch.qos.logback.classic.model.processor.LogbackClassicDefaultNestedComponentRules
                            <!-- slf4j -->
                            --initialize-at-build-time=org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.slf4j.MDC,org.slf4j.impl.StaticLoggerBinder
                            <!-- netty -->
                            --initialize-at-run-time=io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler,io.netty.handler.codec.http2.Http2ServerUpgradeCodec
                            <!-- -->
                            --initialize-at-run-time=sun.net.dns.ResolverConfigurationImpl
                            --initialize-at-build-time=cn.hutool.core.util.ClassLoaderUtil
                            --initialize-at-build-time=cn.hutool.core.convert.BasicType
                            --initialize-at-build-time=cn.hutool.core.util.CharsetUtil
                            <!-- features -->
                            --features=com.coral.test.spring.natives.core.feature.RuntimeRegistrationFeature
                        </buildArg>
                        <buildArg>
                            --add-opens java.base/java.lang=ALL-UNNAMED
                            --add-opens java.base/java.io=ALL-UNNAMED
                            --add-opens java.base/java.math=ALL-UNNAMED
                            --add-opens java.base/java.net=ALL-UNNAMED
                            --add-opens java.base/java.nio=ALL-UNNAMED
                            --add-opens java.base/java.security=ALL-UNNAMED
                            --add-opens java.base/java.text=ALL-UNNAMED
                            --add-opens java.base/java.time=ALL-UNNAMED
                            --add-opens java.base/java.util=ALL-UNNAMED
                            --add-opens java.base/jdk.internal.access=ALL-UNNAMED
                            --add-opens java.base/jdk.internal.misc=ALL-UNNAMED
                        </buildArg>
                    </buildArgs>
                </configuration>

            </plugin>


r2dbc的使用说明: https://docs.spring.io/spring-data/r2dbc/docs/current-SNAPSHOT/reference/html/#r2dbc.core


redis 编译相关

java.lang.UnsatisfiedLinkError: jdk.jfr.internal.JVM.getHandler(Ljava/lang/Class;)Ljava/lang/Object; [symbol: Java_jdk_jfr_internal_JVM_getHandler or Java_jdk_jfr_internal_JVM_getHandler__Ljava_lang_Class_2]
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.jni.access.JNINativeLinkage.getOrFindEntryPoint(JNINativeLinkage.java:152) ~[na:na]
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.jni.JNIGeneratedMethodSupport.nativeCallAddress(JNIGeneratedMethodSupport.java:53) ~[na:na]
        at jdk.jfr@17.0.10/jdk.jfr.internal.JVM.getHandler(Native Method) ~[na:na]
        at jdk.jfr@17.0.10/jdk.jfr.internal.Utils.getHandler(Utils.java:449) ~[na:na]
        at jdk.jfr@17.0.10/jdk.jfr.internal.MetadataRepository.getHandler(MetadataRepository.java:174) ~[na:na]
        at jdk.jfr@17.0.10/jdk.jfr.internal.MetadataRepository.register(MetadataRepository.java:135) ~[na:na]
        at jdk.jfr@17.0.10/jdk.jfr.internal.MetadataRepository.register(MetadataRepository.java:130) ~[na:na]
        at jdk.jfr@17.0.10/jdk.jfr.FlightRecorder.register(FlightRecorder.java:136) ~[na:na]
        at io.lettuce.core.event.connection.JfrConnectionCreatedEvent.<clinit>(JfrConnectionCreatedEvent.java) ~[auth-service:6.3.1.RELEASE/12e6995]
        at java.base@17.0.10/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) ~[auth-service:na]
        at java.base@17.0.10/java.lang.reflect.Constructor.newInstance(Constructor.java:480) ~[auth-service:na]
        at io.lettuce.core.event.jfr.JfrEventRecorder.createEvent(JfrEventRecorder.java:108) ~[na:na]
 reactor.core.Exceptions 304 warn - throwIfFatal detected a jvm fatal exception, which is thrown and logged below:java.lang.NoClassDefFoundError: Could not initialize class io.lettuce.core.event.connection.JfrConnectionCreatedEvent
        at java.base@17.0.10/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
        Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
        *__checkpoint 鈬?Handler com.coral.test.spring.natives.controller.EventLogController#findEventLog(String) [DispatcherHandler]
Original Stack Trace:
                at java.base@17.0.10/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
                at java.base@17.0.10/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
                at io.lettuce.core.event.jfr.JfrEventRecorder.createEvent(JfrEventRecorder.java:108)

解决方案 (2024-06-05 官方说jfr在windows上暂不支持)
2024-06-12 经测试在centos7上确实不会报此错误

https://github.com/oracle/graal/issues/8623
https://www.graalvm.org/latest/reference-manual/native-image/guides/build-and-run-native-executable-with-jfr/
https://github.com/oracle/graal/issues/5558

另外一种方式是将jfr功能禁用掉(2024-06-17)
https://github.com/redis/lettuce/wiki/Connection-Events#java-flight-recorder-events-since-61

io.lettuce.core.event.jfr.EventRecorderHolder类中该变量的

private static final String JFR_ENABLED_KEY = "io.lettuce.core.jfr";

private static final boolean JFR_ENABLED = Boolean.parseBoolean(SystemPropertyUtil.get(JFR_ENABLED_KEY, "true"));

因此需要设置

System.setProperty("io.lettuce.core.jfr", "false"); 

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容