Spring 提示:配置

工程 | Josh Long | 2020年4月23日 | ...

演讲者:Josh Long (@starbuxman)

嗨,Spring 粉丝们!欢迎来到 Spring 提示的另一期!在本期中,我们将探讨一些相当基础的内容,一些我希望早点讨论的内容:配置。不,我不是指函数式配置或 Java 配置之类的,我说的是告知代码如何执行的字符串值。你放在 application.properties 中的内容。*那就是*配置。

Spring 中的所有配置都源自 Spring 的Environment抽象。Environment有点像字典——一个带有键值对的映射。Environment只是一个接口,通过它我们可以询问关于Environment的信息。这个抽象存在于 Spring Framework 中,并在十多年前的 Spring 3 中引入。在此之前,有一个集中的机制允许集成配置,称为属性占位符解析。这种环境机制以及围绕该接口的类集合已经完全取代了旧的支持。如果你发现某个博客仍在使用这些类型,我建议你转向更新、更绿色的领域 :)。

让我们开始吧。访问 Spring Initializr 并生成一个新项目,确保选择Spring Cloud VaultLombokSpring Cloud Config Client。我将我的项目命名为configuration。点击Generate生成应用程序。在您喜欢的 IDE 中打开项目。如果你想跟着一起做,请确保禁用 Spring Cloud Vault 和 Spring Cloud Config Client 依赖项。我们现在不需要它们。

对于大多数 Spring Boot 开发人员来说,第一步是使用 application.properties。当你生成一个新项目时,Spring Initializr 甚至会将一个空的 application.properties 放到src/main/resources/application.properties文件夹中!非常方便。你确实是在 Spring Initializr 上创建项目的,对吧?你可以使用 application.properties 或 application.yml。我不太喜欢.yml文件,但如果你更喜欢它,也可以使用。

Spring Boot 在启动时会自动加载application.properties。你可以通过环境在 Java 代码中取消引用属性文件中的值。在application.properties文件中添加一个属性,如下所示。

message-from-application-properties=Hello from application.properties

现在,让我们编辑代码以读取该值。

package com.example.configuration;

import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

@Log4j2
@SpringBootApplication
public class ConfigurationApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigurationApplication.class, args);
    }

    @Bean
    ApplicationRunner applicationRunner(Environment environment) {
        return args -> {
            log.info("message from application.properties " + environment.getProperty("message-from-application-properties"));
        };
    }
}

运行此代码,你将在日志输出中看到来自配置文件的值。如果你想更改 Spring Boot 默认读取的文件,你也可以这样做。不过,这是一个先有鸡还是先有蛋的问题——你需要指定一个属性,Spring Boot 将使用该属性来确定在哪里加载所有属性。因此,你需要在 application.properties 文件之外指定此属性。你可以使用程序参数或环境变量来填充spring.config.name属性。

export SPRING_CONFIG_NAME=foo

现在使用作用域中的环境变量重新运行应用程序,它将失败,因为它将尝试加载foo.properties,而不是application.properties

顺便说一句,你也可以运行带有外部配置的应用程序,该配置位于 jar 文件旁边,如下所示。如果你这样运行应用程序,外部application.properties中的值将覆盖.jar内部的值。

.
├── application.properties
└── configuration-0.0.1-SNAPSHOT.jar

0 directories, 2 files

Spring Boot 也知道 Spring 配置文件。配置文件是一种机制,允许你标记对象和属性文件,以便可以在运行时有选择地激活或停用它们。如果你想拥有特定于环境的配置,这非常有用。你可以将 Spring bean 或配置文件标记为属于特定配置文件,当该配置文件被激活时,Spring 将自动为你加载它。

配置文件名称基本上是任意的。某些配置文件是魔术的——Spring 以某种特定方式使用它们。其中最有趣的是default,当没有其他配置文件处于活动状态时,它会被激活。但通常情况下,名称由你决定。我发现将我的配置文件映射到不同的环境非常有用:devqastagingprod等。

假设有一个名为dev的配置文件。Spring Boot 将自动加载application-dev.properties。它将除了 application.properties 之外加载它。如果两个文件中的值之间存在冲突,则更具体的-带有配置文件的文件-将胜出。你可以有一个默认值,在没有特定配置文件的情况下应用,然后在配置文件的配置中提供具体信息。

你可以通过几种不同的方式激活给定的配置文件,但最简单的方法是在命令行中指定它。或者你可以在 IDE 的运行配置对话框中打开它。IntelliJ 和 Spring Tool Suite 都提供了一个地方来指定在运行应用程序时使用的配置文件。你也可以设置一个环境变量SPRING_PROFILES_ACTIVE,或在命令行中指定一个参数--spring.profiles.active。两者都可以接受以逗号分隔的配置文件列表——你可以一次激活多个配置文件。

让我们试试这个。创建一个名为application-dev.properties的文件。将以下值放入其中。

message-from-application-properties=Hello from dev application.properties

此属性与application.properties中的属性具有相同的键。这里的 Java 代码与我们之前的一样。只需确保在启动 Spring 应用程序之前指定配置文件即可。你可以使用环境变量、属性等。你甚至可以在main()方法中构建SpringApplication时以编程方式定义它。

package com.example.configuration.profiles;

import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

@Log4j2
@SpringBootApplication
public class ConfigurationApplication {

    public static void main(String[] args) {
        // this works
        // export SPRING_PROFILES_ACTIVE=dev  
        // System.setProperty("spring.profiles.active", "dev"); // so does this
        new SpringApplicationBuilder()
            .profiles("dev") // and so does this
            .sources(ConfigurationApplication.class)
            .run(args);
    }

    @Bean
    ApplicationRunner applicationRunner(Environment environment) {
        return args -> {
            log.info("message from application.properties " + environment.getProperty("message-from-application-properties"));
        };
    }
}

运行应用程序,你将看到输出中反映的专用消息。

到目前为止,我们一直在使用 Environment 来注入配置。你也可以使用@Value注解将值作为参数注入。你可能已经知道这一点。但是你知道如果没有任何匹配的值,你也可以指定要返回的默认值吗?你可能有很多原因想要这样做。你可以使用它来提供回退值,并在有人误输属性拼写时使其更加透明。它也很有用,因为即使有人不知道他们需要激活配置文件或其他内容,你也会得到一个可能有用的值。

package com.example.configuration.value;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@Log4j2
@SpringBootApplication
public class ConfigurationApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigurationApplication.class, args);
    }

    @Bean
    ApplicationRunner applicationRunner(
        @Value("${message-from-application-properties:OOPS!}") String valueDoesExist,
        @Value("${mesage-from-application-properties:OOPS!}") String valueDoesNotExist) {
        return args -> {
            log.info("message from application.properties " + valueDoesExist);
            log.info("missing message from application.properties " + valueDoesNotExist);
        };
    }
}

方便吧?此外,请注意,你提供的默认字符串可以反过来内插其他属性。因此,你可以执行以下操作,假设你的应用程序配置中确实存在像default-error-message这样的键

${message-from-application-properties:${default-error-message:YIKES!}}

如果存在,它将评估第一个属性,然后评估第二个属性,最后评估字符串YIKES!

前面,我们了解了如何使用环境变量或程序参数指定配置文件。这种机制——使用环境变量或程序参数配置 Spring Boot——是一种通用机制。你可以将其用于任何任意键,Spring Boot 将为你规范化配置。你可以通过这种方式在外部指定你放在 application.properties 中的任何键。让我们看一些例子。假设你想为数据源连接指定 URL。你可以将该值硬编码在 application.properties 中,但这并不安全。创建一个仅存在于生产环境中的环境变量可能会更好。这样,开发人员就无法访问生产数据库的密钥等等。

让我们试试看。这是示例的 Java 代码。


package com.example.configuration.envvars;

import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

@Log4j2
@SpringBootApplication
public class ConfigurationApplication {

    public static void main(String[] args) {
        // simulate program arguments
        String[] actualArgs = new String[]{"spring.datasource.url=jdbc:postgres://127.0.0.1/some-prod-db"};
        SpringApplication.run(ConfigurationApplication.class, actualArgs);
    }

    @Bean
    ApplicationRunner applicationRunner(Environment environment) {
        return args -> {
            log.info("our database URL connection will be " + environment.getProperty("spring.datasource.url"));
        };
    }
}

在运行它之前,请确保导出你用来运行应用程序的 shell 中的环境变量,或者指定程序参数。我通过拦截我们在这里传递给 Spring Boot 应用程序的public static void main(String [] args)来模拟后者——程序参数。你也可以这样指定环境变量

export SPRING_DATASOURCE_URL=some-arbitrary-value
mvn -DskipTests=true spring-boot:run 

多次运行程序,尝试不同的方法,你将看到输出中的值。应用程序中没有自动配置可以连接到数据库,因此我们使用此属性作为示例。URL 不必是有效的 URL(至少在你将 Spring 的 JDBC 支持和 JDBC 驱动程序添加到类路径之前)。

Spring Boot 在其值的来源方面非常灵活。它并不关心你是否使用SPRING_DATASOURCE_URLspring.datasource.url等。Spring Boot 将此称为*宽松绑定*。它允许你以最适合不同环境的方式做事,同时仍然适用于 Spring Boot。

这个想法——从环境中外部化应用程序的配置——并不是什么新鲜事。它在12 要素宣言中得到了很好的理解和描述。12 要素宣言指出,特定于环境的配置应该存在于该环境中,而不是代码本身。这是因为我们希望为所有环境构建一个版本。应该更改的内容应该是外部的。到目前为止,我们已经看到 Spring Boot 可以从命令行参数(程序参数)和环境变量中提取配置。它还可以读取来自 JOpt 的配置。如果碰巧在带有此类上下文的应用程序服务器中运行,它甚至可以来自 JNDI 上下文!

Spring Boot能够读取任何环境变量,这在此处非常有用。它也比使用程序参数更安全,因为程序参数会显示在操作系统的工具输出中。环境变量更合适。

到目前为止,我们已经看到Spring Boot可以从许多不同的地方提取配置。它知道配置文件,知道.yml.properties文件。它非常灵活!但是,如果它不知道如何做你想让它做的事情呢?你可以很容易地使用自定义的PropertySource<T>来教它新技巧。例如,如果你想将你的应用程序与存储在外部数据库、目录或Spring Boot不知道的其他位置的配置集成,你可能需要这样做。


package com.example.configuration.propertysource;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.PropertySource;

@Log4j2
@SpringBootApplication
public class ConfigurationApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder()
            .sources(ConfigurationApplication.class)
            .initializers(context -> context
                .getEnvironment()
                .getPropertySources()
                .addLast(new BootifulPropertySource())
            )
            .run(args);
    }

    @Bean
    ApplicationRunner applicationRunner(@Value("${bootiful-message}") String bootifulMessage) {
        return args -> {
            log.info("message from custom PropertySource: " + bootifulMessage);
        };
    }
}

class BootifulPropertySource extends PropertySource<String> {

    BootifulPropertySource() {
        super("bootiful");
    }

    @Override
    public Object getProperty(String name) {

        if (name.equalsIgnoreCase("bootiful-message")) {
            return "Hello from " + BootifulPropertySource.class.getSimpleName() + "!";
        }

        return null;
    }
}


上面的例子是在足够早的阶段注册PropertySource的最安全方法,这样所有需要它的地方都能找到它。你也可以在Spring开始连接对象并在你访问已配置对象时在运行时进行,但我不能保证这在每种情况下都能工作。这就是它可能的样子。

package com.example.configuration.propertysource;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;

@Log4j2
@SpringBootApplication
public class ConfigurationApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigurationApplication.class, args);
    }

    @Bean
    ApplicationRunner applicationRunner(@Value("${bootiful-message}") String bootifulMessage) {
        return args -> {
            log.info("message from custom PropertySource: " + bootifulMessage);
        };
    }

    @Autowired
    void contributeToTheEnvironment(ConfigurableEnvironment environment) {
        environment.getPropertySources().addLast(new BootifulPropertySource());
    }
}

class BootifulPropertySource extends PropertySource<String> {

    BootifulPropertySource() {
        super("bootiful");
    }

    @Override
    public Object getProperty(String name) {

        if (name.equalsIgnoreCase("bootiful-message")) {
            return "Hello from " + BootifulPropertySource.class.getSimpleName() + "!";
        }

        return null;
    }
}

到目前为止,我们几乎完全关注了如何从其他地方获取属性值。然而,我们还没有讨论一旦字符串进入我们的工作内存并可用于应用程序,它们会变成什么。大多数情况下,它们只是字符串,我们可以按原样使用它们。但是,有时将它们转换为其他类型的值(整数、日期、双精度数等)很有用。这项工作——将字符串转换为其他类型——可以成为另一个Spring Tips视频的主题,也许我很快就会做一个。需要说明的是,这里有很多相互关联的部分——ConversionServiceConverter<T>、Spring Boot的Binder等等。对于常见情况,这会正常工作。例如,你可以指定属性server.port = 8080,然后将其作为整数注入到你的应用程序中。

@Value("${server.port}") int port

将这些值自动绑定到对象可能会有所帮助。这正是Spring Boot的ConfigurationProperties为你做的。让我们看看它是如何工作的。

假设你有一个包含以下属性的application.properties文件

bootiful.message = Hello from a @ConfiguratinoProperties 

然后你可以运行应用程序,并看到配置值已为我们绑定到对象。

package com.example.configuration.cp;

import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@Log4j2
@SpringBootApplication
@EnableConfigurationProperties(BootifulProperties.class)
public class ConfigurationApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigurationApplication.class, args);
    }
    
    @Bean
    ApplicationRunner applicationRunner(BootifulProperties bootifulProperties) {
        return args -> {
            log.info("message from @ConfigurationProperties " + bootifulProperties.getMessage());
        };
    }
 
}

@Data
@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties("bootiful")
class BootifulProperties {
    private final String message;
}

BootifulProperties对象上的@Data@RequiredArgsConstructor注解来自Lombok。@Data为final字段合成getter,为非final字段合成getter和setter。@RequiredArgsConstructor为类中的所有final字段合成一个构造函数。结果是一个一旦通过构造函数初始化就不可变的对象。Spring Boot的ConfigurationProperties机制默认情况下不知道不可变对象;你需要使用@ConstructorBinding注解(Spring Boot中一个相当新的补充)才能使其在此处正确工作。这在其他编程语言(如Kotlin(data class ...)和Scala(case class ...))中更有用,这些语言具有创建不可变对象的语法糖。

我们已经看到Spring可以加载应用程序.jar旁边的配置,并且它可以从环境变量和程序参数加载配置。将信息放入Spring Boot应用程序并不难,但它有点零散。很难对环境变量进行版本控制或保护程序参数。

为了解决其中一些问题,Spring Cloud团队构建了Spring Cloud Config Server。Spring Cloud Config Server是一个HTTP API,它位于后端存储引擎的前面。存储是可插入的,最常见的是Git存储库,尽管也支持其他存储库。这些包括Subversion、本地文件系统,甚至MongoDB

我们将设置一个新的Spring Cloud Config Server。转到Spring Initializr,选择“Config Server”,然后点击“Generate”。在您喜欢的IDE中打开它。

我们需要做两件事才能使其工作:首先,我们必须使用一个注解,然后提供一个配置值以将其指向包含我们的配置文件的Git存储库。以下是application.properties

spring.cloud.config.server.git.uri=https://github.com/joshlong/greetings-config-repository.git
server.port=8888

这就是你的主类应该是什么样子。

package com.example.configserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

运行应用程序——mvn spring-boot:run或只是在您喜欢的IDE中运行应用程序。它现在可用。它将充当Github存储库中Git配置的代理。其他客户端可以使用Spring Cloud Config Client从Spring Cloud Config Server中提取他们的配置,Spring Cloud Config Server又会从Git存储库中提取配置。注意:为了演示方便,我尽可能地降低了安全性,但是你可以也应该保护链中的两个链接——从配置客户端到配置服务器,以及从配置服务器到Git存储库。Spring Cloud Config Server、Spring Cloud Config Client和Github可以很好地协同工作,并且安全地工作。

现在,返回我们的配置应用程序的构建,并确保取消注释Spring Cloud Config Client依赖项。要启动Spring Cloud Config Server,它需要一些——你猜对了!——配置。一个经典的先有鸡还是先有蛋的问题。此配置需要在其余配置之前更早地进行评估。你可以将此配置放在名为bootstrap.properties的文件中。

你需要标识你的应用程序以赋予其名称,以便当它连接到Spring Cloud Config Server时,它将知道要提供哪个配置。我们在此处指定的名称将与Git存储库中的属性文件匹配。以下是应放入文件中的内容。

spring.cloud.config.uri=https://127.0.0.1:8888
spring.application.name=bootiful

现在我们可以读取Git存储库中bootiful.properties文件中的任何我们想要的值,其内容为

message-from-config-server = Hello, Spring Cloud Config Server

我们可以像这样提取配置文件

package com.example.configuration.configclient;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@Log4j2
@SpringBootApplication
public class ConfigurationApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigurationApplication.class, args);
    }

    @Bean
    ApplicationRunner applicationRunner(@Value("${message-from-config-server}") String configServer) {
        return args -> {
            log.info("message from the Spring Cloud Config Server: " + configServer);
        };
    }
}

你应该在输出中看到该值。不错!Spring Cloud Config Server为我们做了很多很酷的事情。它可以为我们加密值。它可以帮助对属性进行版本控制。我最喜欢的一件事是,你可以独立于代码库的更改来更改配置。你可以将其与Spring Cloud的@RefreshScope结合使用,以在应用程序启动运行后动态重新配置应用程序。(我应该做一个关于刷新范围及其多种用途的视频……)Spring Cloud Config Server之所以成为最受欢迎的Spring Cloud模块之一是有原因的——它可以与单体架构和微服务一起使用。

如果你正确配置Spring Cloud Config Server,它可以加密属性文件中的值。它有效。许多人还使用HashiCorp的优秀Vault产品,这是一个功能更全面的安全产品。Vault可以使用UI、CLI或HTTP API安全地存储和严格控制对令牌、密码、证书、用于保护秘密的加密密钥和其他敏感数据的访问。你也可以使用Spring Cloud Vault项目轻松地将其用作属性源。从构建中取消注释Sring Cloud Vault依赖项,让我们看看如何设置HashiCorp Vault。

下载最新版本,然后运行以下命令。我假设使用的是Linux或类Unix环境。尽管如此,将其转换为Windows应该相当简单。我不会尝试解释Vault的所有内容;相反,我会将你推荐给HashiCorp Vault的优秀的入门指南HashiCorp Vault。以下是我知道的设置和运行所有这些的最不安全但最快的方法。首先,运行Vault服务器。我在这里提供了一个root令牌,但你通常会使用Vault在启动时提供的令牌。

export VAULT_ADDR="https://127.0.0.1:8200"
export VAULT_SKIP_VERIFY=true
export VAULT_TOKEN=00000000-0000-0000-0000-000000000000
vault server --dev --dev-root-token-id="00000000-0000-0000-0000-000000000000"

启动后,在另一个shell中,将一些值安装到Vault服务器中,如下所示。

export VAULT_ADDR="https://127.0.0.1:8200"
export VAULT_SKIP_VERIFY=true
export VAULT_TOKEN=00000000-0000-0000-0000-000000000000
vault kv put secret/bootiful message-from-vault-server="Hello Spring Cloud Vault"

这将密钥message-from-vault-server和值Hello Spring Cloud Vault放入Vault服务中。现在,让我们更改应用程序以连接到该Vault实例以读取安全值。我们将需要一个bootstrap.properties,就像Spring Cloud Config Client一样。

spring.application.name=bootiful
spring.cloud.vault.token=${VAULT_TOKEN}
spring.cloud.vault.scheme=http

然后,你可以像使用任何其他配置值一样使用该属性。

package com.example.configuration.vault;

import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@Log4j2
@SpringBootApplication
public class ConfigurationApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigurationApplication.class, args);
    }

    @Bean
    ApplicationRunner applicationRunner(@Value("${message-from-vault-server:}") String valueFromVaultServer) {
        return args -> {
            log.info("message from the Spring Cloud Vault Server : " + valueFromVaultServer);
        };
    }
}

现在,在运行此程序之前,请确保还配置了我们在与vault CLI 的两次交互中使用的相同三个环境变量:VAULT_TOKENVAULT_SKIP_VERIFYVAULT_ADDR。然后运行它,你应该在控制台中看到写入HashiCorp Vault的值。

后续步骤

希望你已经了解了Spring中丰富多彩且引人入胜的配置世界。掌握了这些信息后,你就可以更好地使用支持属性解析的其他项目了。掌握了这些工作原理的知识后,你就可以集成来自不同Spring集成的配置了,这些集成有很多!你可能会使用Spring Cloud Netflix的Archaius集成,或者与Spring Cloud Kubernetes的Configmaps集成,或者Spring Cloud GCP的Google Runtime Configuration API集成,或者Spring Cloud Azure的Microsoft Azure Key Vault集成等等。

我在这里只提到了几个产品,但列表是否详尽无所谓,如果集成正确,它们的用法将相同:云是极限!

获取Spring时事通讯

与Spring时事通讯保持联系

订阅

领先一步

VMware提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部