使用 Spring 的微服務

工程 | Paul Chapman | 2015 年 7 月 14 日 | ...

簡介

注意: 已於 2019 年 7 月修訂

使用 Spring、Spring Boot 和 Spring Cloud 設定微服務系統的簡單範例。

微服務允許從許多協作元件構建大型系統。它在程序層級上做了 Spring 一直在元件層級上做的事情:鬆散耦合的程序而不是鬆散耦合的元件。

Shopping Application

例如,想像一個線上商店,針對使用者帳戶、產品目錄、訂單處理和購物車有不同的微服務。

不可避免地,您需要設定和配置許多移動元件才能構建這樣的系統。如何讓它們協同工作並不明顯 - 您需要非常熟悉 Spring Boot,因為 Spring Cloud 大量利用了它,並且需要幾個 Netflix 或其他 OSS 專案,當然,還有一些 Spring 配置「魔法」!

Demo Application

在本文中,我旨在透過逐步構建最簡單的系統來闡明事情的運作方式。因此,我將僅實作大型系統的一小部分 - 使用者帳戶服務。

Web 應用程式將使用 RESTful API 向帳戶服務微服務發出請求。我們還需要新增一個探索服務 - 這樣其他程序才能找到彼此。

此應用程式的程式碼位於此處:https://github.com/paulc4/microservices-demo

對其運作方式的描述是經過刻意詳細說明的。沒有耐心的讀者可能更喜歡直接查看程式碼。請注意,它在單一專案中包含三個微服務。

了解更多

  • 註冊參加 SpringOne Platform 2019 – 使用 Spring 構建可擴展微服務應用程式的首要會議。今年我們將於 10 月 7 日至 10 日在德州奧斯丁舉行。使用折扣碼 S1P_Save200 來節省您的門票費用。需要幫助說服您的主管嗎?使用此頁面
  • 取得免費電子書 遷移到雲原生應用程式架構,作者:Matt Stine
  • 網路研討會討論了可幫助您將單體應用程式重新平台化到現代雲端環境的工具和方法。
## 更新(2018 年 6 月)

自從我最初撰寫此部落格以來,發生了許多變化

  1. 關於討論在同一主機上使用同一服務的多個實例...已更新示範應用程式以符合。
  2. 關於 @LoadBalanced討論 - 自 Brixton 發布系列以來,其運作方式已變更Spring Cloud 1.1.0.RELEASE)。
  3. 將帳戶微服務的配置重構為其自身的類別 AccountsConfiguration
  4. 已升級到 Spring Boot 2,因此一些 Boot 類別已變更套件。
  5. 已將示範應用程式升級到 Spring Cloud Finchley 發布系列(包括結尾評論中的各種修復 - 感謝您的回饋)。
  6. Eureka 伺服器依賴項已變更為 spring-cloud-starter-netflix-eureka-server

先前版本使用 Spring Boot 1.5.10 和 Spring Cloud Edgeware SR3,可以作為 git 標籤 v1.2.0 取得。

 

好的,讓我們開始吧...

服務註冊

當多個程序協同工作時,它們需要找到彼此。如果您曾經使用 Java 的 RMI 機制,您可能還記得它依賴於一個中央註冊表,以便 RMI 程序可以找到彼此。微服務有相同的需求。

Netflix 的開發人員在構建其系統時遇到了這個問題,並建立了一個名為 Eureka(希臘語中的「我找到了它」)的註冊伺服器。對我們來說幸運的是,他們使他們的探索伺服器成為開源,而 Spring 已將其納入 Spring Cloud 中,使其更容易執行 Eureka 伺服器。以下是完整的探索伺服器應用程式

@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistrationServer {

  public static void main(String[] args) {
    // Tell Boot to look for registration-server.yml
    System.setProperty("spring.config.name", "registration-server");
    SpringApplication.run(ServiceRegistrationServer.class, args);
  }
}

它真的很簡單!

Spring Cloud 建立在 Spring Boot 之上,並使用父 POM 和起始 POM。 POM 的重要部分是

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <!-- Setup Spring Boot -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <!-- Setup Spring MVC & REST, use Embedded Tomcat -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <!-- Spring Cloud starter -->
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency>

        <dependency>
            <!-- Eureka for service registration -->
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>
    </dependencies>

   <!-- Spring Cloud dependencies -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

自我最初撰寫本文以來,此 POM 已變更為使用 Spring Boot 作為其父 POM,而不是 Spring Cloud。 Spring Cloud 依賴項透過依賴管理部分提供。

github 程式碼中也包含一個範例 gradle 建置檔案。

注意: Finchley.RELEASE 是目前的「發布系列」 - 一組協調的發布版本 -- 請參閱 Spring Cloud 首頁上的說明。

依預設,Spring Boot 應用程式會尋找 application.propertiesapplication.yml 檔案進行配置。透過設定 spring.config.name 屬性,我們可以告訴 Spring Boot 尋找不同的檔案 - 如果您在同一個專案中有多個 Spring Boot 應用程式,這會很有用 - 我稍後會這樣做。

此應用程式會尋找 registration-server.propertiesregistration-server.yml。以下是來自 registration-server.yml 的相關配置

# Configure this Discovery Server
eureka:
  instance:
    hostname: localhost
  client:  # Not a client, don't register with yourself (unless running
           # multiple discovery servers for redundancy)
    registerWithEureka: false
    fetchRegistry: false

server:
  port: 1111   # HTTP (Tomcat) port

依預設,Eureka 在連接埠 8761 上執行,但我們在此處改用連接埠 1111。此外,透過將註冊程式碼包含在我的程序中,我可能是伺服器或用戶端。配置指定我不是用戶端,並停止伺服器程序嘗試向自身註冊。

使用 Consul

Spring Cloud 也支援 Consul 作為 Eureka 的替代方案。您可以使用指令碼啟動 Consul Agent(其註冊伺服器),然後用戶端使用它來尋找其微服務。如需詳細資訊,請參閱此部落格文章或專案首頁

現在嘗試執行 RegistrationServer(請參閱下方以取得執行應用程式的說明)。您可以在此處開啟 Eureka 儀表板:https://127.0.0.1:1111,顯示應用程式的部分將會是空的。

從現在開始,我們將參考探索伺服器,因為它可以是 Eureka 或 Consul(請參閱側邊欄)。

建立微服務:帳戶服務

微服務是一個獨立的程序,用於處理定義完善的需求。

Beans vs Processes

在使用 Spring 配置應用程式時,我們強調鬆散耦合和緊密內聚。這些不是新概念(Larry Constantine 被認為在 1960 年代後期首次定義這些概念 - 參考文獻),但現在我們將它們應用於互動元件 (Spring Beans) 而不是互動程序。

在此範例中,我有一個簡單的帳戶管理微服務,它使用 Spring Data 來實作 JPA AccountRepository 和 Spring REST 來提供帳戶資訊的 RESTful 介面。在大多數方面,這是一個簡單的 Spring Boot 應用程式。

它之所以特別,是因為它會在啟動時向探索伺服器註冊自身。以下是 Spring Boot 啟動類別

@EnableAutoConfiguration
@EnableDiscoveryClient
@Import(AccountsWebApplication.class)
public class AccountsServer {

    @Autowired
    AccountRepository accountRepository;

    public static void main(String[] args) {
        // Will configure using accounts-server.yml
        System.setProperty("spring.config.name", "accounts-server");

        SpringApplication.run(AccountsServer.class, args);
    }
}

註解會執行此作業

  1. @EnableAutoConfiguration - 將其定義為 Spring Boot 應用程式。
  2. @EnableDiscoveryClient - 這會啟用服務註冊和探索。在本例中,此程序會使用其應用程式名稱向探索伺服器服務註冊自身(請參閱下方)。
  3. @Import(AccountsWebApplication.class) - 此 Java 配置類別會設定其他所有內容(如需更多詳細資訊,請參閱下方)。

使其成為微服務的是透過 @EnableDiscoveryClient探索伺服器註冊,並且其 YML 配置會完成設定

# Spring properties
spring:
  application:
     name: accounts-service

# Discovery Server Access
eureka:
  client:
    serviceUrl:
      defaultZone: https://127.0.0.1:1111/eureka/

# HTTP Server
server:
  port: 2222   # HTTP (Tomcat) port

請注意,此檔案

  1. 將應用程式名稱設定為 accounts-service。此服務會在此名稱下註冊,也可以透過此名稱存取 - 請參閱下方。
  2. 指定要接聽的自訂連接埠 (2222)。我的所有程序都在使用 Tomcat,它們不能全部在連接埠 8080 上接聽。
  3. Eureka Service 程序的 URL - 來自上一節。

Eureka Dashboard

現在執行 AccountsService 應用程式並讓它完成初始化。重新整理儀表板 https://127.0.0.1:1111,您應該會看到 ACCOUNTS-SERVICE 列在應用程式下方。註冊最多需要 30 秒(依預設),因此請耐心等待 - 檢查來自 RegistrationService 的日誌輸出

警告:請勿嘗試使用 Eclipse/STS 的內部網路檢視器顯示 XML 輸出,因為它無法執行此操作。請改用您最喜歡的網路瀏覽器。

如需更多詳細資訊,請前往此處:https://127.0.0.1:1111/eureka/apps/,您應該會看到類似這樣的內容

<applications>
    <versions__delta>1</versions__delta>
    <apps__hashcode>UP_1_</apps__hashcode>
    <application>
        <name>ACCOUNTS-SERVICE</name>
        <instance>
            <hostName>autgchapmp1m1.corp.emc.com</hostName>
            <app>ACCOUNTS-SERVICE</app>
            <ipAddr>172.16.84.1</ipAddr><status>UP</status>
            <overriddenstatus>UNKNOWN</overriddenstatus>
            <port enabled="true">3344</port>
            <securePort enabled="false">443</securePort>
            ...
        </instance>
    </application>
</applications>

或者前往 https://127.0.0.1:1111/eureka/apps/ACCOUNTS-SERVICE 並僅查看 AccountsService 的詳細資料 - 如果它未註冊,您將會收到 404。

組態選項

註冊時間: 註冊最多需要 30 秒,因為這是預設的客戶端重新整理時間。您可以透過將 eureka.instance.leaseRenewalIntervalInSeconds 屬性設定為較小的數字來更改此設定(在 demo 應用程式中,我已將其設定為 5)。在正式環境不建議這樣做。另請參閱這裡

eureka:
  instance:
    leaseRenewalIntervalInSeconds: 5         # DO NOT DO THIS IN PRODUCTION

註冊 ID: 一個程序(微服務)使用唯一的 ID 向探索服務(discovery-service)註冊。如果另一個程序使用相同的 ID 註冊,則會被視為重新啟動(例如某種故障轉移或恢復),並且第一個程序的註冊將被丟棄。這給了我們想要的容錯系統。

要運行相同程序的複數實例(用於負載平衡和彈性),它們需要使用唯一的 ID 註冊。 當我第一次寫這個部落格時,這是自動的,並且從 Brixton 版本系列開始,又再次變成自動的。

Angel 版本系列下,客戶端用來向探索伺服器註冊的實例 ID 是從客戶端的服務名稱(與 Spring 應用程式名稱相同)以及客戶端的主機名稱派生的。 因此,在同一主機上運行的相同程序將具有相同的 ID,因此一次只能註冊一個。

幸運的是,您可以透過客戶端的 Eureka Metadata Map 手動設定 ID 屬性,如下所示

eureka:
  instance:
    metadataMap:
      instanceId: ${spring.application.name}:${spring.application.instance_id:${server.port}}

Brixton 版本系列以來,這現在是預設行為。那麼它做了什麼呢?

我們將 instanceId 設定為 application-name:instance_id,但如果未定義 instance_id,我們將改用 application-name::server-port。請注意,spring.application.instance_id 在使用 Cloud Foundry 時設定,但它方便地為同一應用程式的每個實例提供唯一的 ID 號碼。當在其他地方運行時,我們可以使用伺服器端口(因為同一機器上的不同實例必須偵聽不同的端口)來做類似的事情。您經常看到的另一個示例是 ${spring.application.name}:${spring.application.instance_id:${random.value}},但我個人認為使用端口號可以輕鬆識別每個實例 - 隨機值只是沒有任何意義的長字串。

注意: 語法 ${x:${y}} 是 Spring 屬性簡寫,等同於 ${x} != null ? ${x} : ${y}

Brixton 版本以來,還有一個專用的屬性用於此

eureka:
  instance:
    instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}

存取微服務:Web-Service

要使用 RESTful 服務,Spring 提供了 RestTemplate 類別。這使您可以將 HTTP 請求發送到 RESTful 伺服器,並以多種格式(例如 JSON 和 XML)獲取資料。

注意: Accounts 微服務透過 HTTP 提供 RESTful 介面,但可以使用任何合適的協定。使用 AMQP 或 JMS 進行訊息傳遞是一種顯而易見的替代方案(在這種情況下,不再需要探索伺服器 - 相反,程序需要知道要對話的佇列名稱,請考慮使用 Spring Cloud Configuration Server來做到這點)。

可使用的格式取決於類別路徑上是否存在 marshaling 類別 - 例如,由於 JAXB 是 Java 的標準部分,因此始終會檢測到它。 如果類別路徑中存在 Jackson jars,則支援 JSON。

微服務(探索)客戶端可以使用 RestTemplate,並且 Spring 會自動將其配置為具有微服務感知能力(稍後會詳細介紹)。

封裝微服務存取

這是我的客戶端應用程式的 WebAccountService 的一部分

@Service
public class WebAccountsService {

    @Autowired        // NO LONGER auto-created by Spring Cloud (see below)
    @LoadBalanced     // Explicitly request the load-balanced template
                      // with Ribbon built-in
    protected RestTemplate restTemplate; 

    protected String serviceUrl;

    public WebAccountsService(String serviceUrl) {
        this.serviceUrl = serviceUrl.startsWith("http") ?
               serviceUrl : "http://" + serviceUrl;
    }

    public Account getByNumber(String accountNumber) {
        Account account = restTemplate.getForObject(serviceUrl
                + "/accounts/{number}", Account.class, accountNumber);

        if (account == null)
            throw new AccountNotFoundException(accountNumber);
        else
            return account;
    }
    ...
}

請注意,我的 WebAccountService 只是 RestTemplate 的一個包裝器,用於從微服務獲取資料。 有趣的部分是 serviceUrlRestTemplate

存取微服務

如下所示,serviceUrl 由主程式提供給 WebAccountController(後者又將其傳遞給 WebAccountService

@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(useDefaultFilters=false)  // Disable component scanner
public class WebServer {

    // Case insensitive: could also use: http://accounts-service
    public static final String ACCOUNTS_SERVICE_URL
                                        = "http://ACCOUNTS-SERVICE";

    public static void main(String[] args) {
        // Will configure using web-server.yml
        System.setProperty("spring.config.name", "web-server");
        SpringApplication.run(WebServer.class, args);
    }

    @LoadBalanced    // Make sure to create the load-balanced template
    @Bean
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * Account service calls microservice internally using provided URL.
     */
    @Bean
    public WebAccountsService accountsService() {
        return new WebAccountsService(ACCOUNTS_SERVICE_URL);
    }

    @Bean
    public WebAccountsController accountsController() {
         return new WebAccountsController
                       (accountsService());  // plug in account-service
    }
}

需要注意的幾點

  1. WebController 是一個典型的基於 Spring MVC 視圖的控制器,它返回 HTML。 應用程式使用 Thymeleaf 作為視圖技術(用於產生動態 HTML)
  2. WebServer 也是一個 @EnableDiscoveryClient,但在這種情況下,除了向探索伺服器註冊自己之外(這是沒有必要的,因為它不提供任何自身服務),它還使用 Eureka 來定位帳戶服務。
  3. 從 Spring Boot 繼承的預設元件掃描器設定會尋找 @Component 類別,在這種情況下,會找到我的 WebAccountController 並嘗試建立它。 但是,我想自己建立它,所以我禁用了掃描器,就像這樣 @ComponentScan(useDefaultFilters=false)
  4. 我傳遞給 WebAccountControllerservice-url 是服務用來向探索伺服器註冊自己的名稱 - 預設情況下,這與程序的 spring.application.name 相同,即 account-service - 請參閱上面的 account-service.yml。不要求使用大寫,但它確實有助於強調 ACCOUNTS-SERVICE 是一個邏輯主機(將透過探索獲得),而不是實際的主機。

負載平衡 RestTemplate

RestTemplate bean 將被 Spring Cloud 攔截和自動配置(由於 @LoadBalanced 註釋),以使用自訂的 HttpRequestClient,後者使用 Netflix Ribbon 來進行微服務查找。 Ribbon 也是一個負載平衡器,因此如果您有多個服務實例可用,它會為您選擇一個。(Eureka 和 Consul 本身都不執行負載平衡,因此我們改用 Ribbon 來執行它)。

注意:Brixton 版本系列(Spring Cloud 1.1.0.RELEASE)開始,不再自動建立 RestTemplate。 最初它是為您建立的,這導致了混淆和潛在的衝突(有時 Spring 可能過於樂於助人!)。

請注意,此實例使用 @LoadBalanced 進行限定。(註釋本身使用 @Qualifier 進行註釋 - 有關詳細訊息,請參閱這裡)。因此,如果您有多個 RestTemplate bean,您可以確保注入正確的 bean,如下所示

    @Autowired
    @LoadBalanced     // Make sure to inject the load-balanced template
    protected RestTemplate restTemplate;

如果您查看 RibbonClientHttpRequestFactory,您將看到此程式碼

    String serviceId = originalUri.getHost();
    ServiceInstance instance =
             loadBalancer.choose(serviceId);  // loadBalancer uses Ribbon
    ... if instance non-null (service exists) ...
    URI uri = loadBalancer.reconstructURI(instance, originalUri);

loadBalancer 取得邏輯服務名稱(以向探索伺服器註冊的名稱)並將其轉換為所選微服務的實際主機名稱。

RestTemplate 實例是線程安全的,可用於存取應用程式不同部分的任意數量服務(例如,我可能有一個 CustomerService 包裝相同的 RestTemplate 實例,存取客戶端資料微服務)。

組態

以下是 web-server.yml 中的相關組態。 它用於

  1. 設定應用程式名稱
  2. 定義用於存取探索伺服器的 URL
  3. 將 Tomcat 端口設定為 3333
# Spring Properties
spring:
  application:
     name: web-service

# Discovery Server Access
eureka:
  client:
    serviceUrl:
      defaultZone: https://127.0.0.1:1111/eureka/

# HTTP Server
server:
  port: 3333   # HTTP (Tomcat) port
# 如何運行 Demo

這個系統的一個小 demo 位於 http://github.com/paulc4/microservices-demo。複製它並載入您最喜歡的 IDE 或直接使用 maven。 關於如何運行 demo 的建議包含在專案首頁上的 README 中。


額外注意事項

關於這些應用程式使用 Spring Boot 的一些注意事項。 如果您不熟悉 Spring Boot,這將解釋一些「魔法」!

檢視樣板引擎 (View Templating Engines)

Eureka 儀表板 (位於 RegistrationServer 內部) 是使用 FreeMarker 樣板實作的,但其他兩個應用程式則使用 Thymeleaf。 為了確保每個應用程式都使用正確的檢視引擎,每個 YML 檔案中都有額外的設定。

這是 registration-server.yml 檔案的末尾,用於停用 Thymeleaf。

...
# Discovery Server Dashboard uses FreeMarker.  Don't want Thymeleaf templates
spring:
  thymeleaf:
    enabled: false     # Disable Thymeleaf spring:

由於 AccountServiceWebService 都使用 Thymeleaf,我們也需要將它們指向各自的樣板。以下是 account-server.yml 的一部分

# Spring properties
spring:
  application:
     name: accounts-service  # Service registers under this name
  freemarker:
    enabled: false      # Ignore Eureka dashboard FreeMarker templates
  thymeleaf:
    cache: false        # Allow Thymeleaf templates to be reloaded at runtime
    prefix: classpath:/accounts-server/templates/
                        # Template location for this application only
...

web-server.yml 類似,但其樣板由以下內容定義

   prefix: classpath:/web-server/templates/

請注意每個 spring.thymeleaf.prefix 類別路徑的結尾都有 / - 這至關重要

命令列執行

該 jar 編譯後,當從命令列呼叫時,會自動執行 io.pivotal.microservices.services.Main - 請參閱 Main.java

可以在 POM 中看到設定 start-class 的 Spring Boot 選項

    <properties>
        <!-- Stand-alone RESTFul application for testing only -->
        <start-class>io.pivotal.microservices.services.Main</start-class>
    </properties>

AccountsConfiguration 類別

@SpringBootApplication
@EntityScan("io.pivotal.microservices.accounts")
@EnableJpaRepositories("io.pivotal.microservices.accounts")
@PropertySource("classpath:db-config.properties")
public class AccountsWebApplication {
...
}

這是 AccountService 的主要配置類別,它是一個使用 Spring Data 的經典 Spring Boot 應用程式。 註釋完成了大部分工作

  1. @SpringBootApplication - 將其定義為 Spring Boot 應用程式。 這個方便的註釋結合了 @EnableAutoConfiguration@Configuration@ComponentScan (預設情況下,這會導致 Spring 搜尋包含此類別的套件及其子套件,以尋找元件 - 潛在的 Spring Beans:AccountControllerAccountRepository)。
  2. @EntityScan("io.pivotal.microservices.accounts") - 因為我正在使用 JPA,所以我需要指定 @Entity 類別的位置。 通常,這是在 JPA 的 persistence.xml 或建立 LocalContainerEntityManagerFactoryBean 時指定的選項。 Spring Boot 會為我建立這個 factory-bean,因為類別路徑上有 spring-boot-starter-data-jpa 依賴。 因此,指定 @Entity 類別位置的另一種方法是使用 @EntityScan。 這將找到 Account
  3. @EnableJpaRepositories("io.pivotal.microservices.accounts") - 尋找擴充 Spring Data 的 Repository 標記介面的類別,並使用 JPA 自動實作它們 - 請參閱 Spring Data JPA
  4. @PropertySource("classpath:db-config.properties") - 用於配置我的 DataSource 的屬性 -- 請參閱 db-config.properties

配置屬性

如上所述,Spring Boot 應用程式會尋找 application.propertiesapplication.yml 來配置自身。 由於此應用程式中使用的所有三個伺服器都在同一個專案中,因此它們會自動使用相同的配置。

為了避免這種情況,每個伺服器都透過設定 spring.config.name 屬性來指定替代檔案。

例如,這是 WebServer.java 的一部分。

public static void main(String[] args) {
  // Tell server to look for web-server.properties or web-server.yml
  System.setProperty("spring.config.name", "web-server");
  SpringApplication.run(WebServer.class, args);
}

在執行時,應用程式將在 src/main/resources 中尋找並使用 web-server.yml

記錄

預設情況下,Spring Boot 會為 Spring 設定 INFO 等級的記錄。 由於我們需要檢查日誌以驗證我們的微服務是否正常運作,我已將等級提高到 WARN 以減少記錄量。

為此,需要在每個 xxxx-server.yml 配置文件中指定記錄等級。 這通常是定義它們的最佳位置,因為記錄屬性無法在屬性檔案中指定 (在處理 @PropertySource 指令之前,已經初始化了記錄)。 Spring Boot 手冊中對此有一條註釋,但很容易錯過。

我沒有在每個 YAML 檔案中重複記錄配置,而是選擇將其放入 logback 配置文件中,因為 Spring Boot 使用 logback - 請參閱 src/main/resources/logback.xml。 所有三個服務將共享相同的 logback.xml

取得 Spring 電子報

隨時掌握 Spring 電子報的最新消息

訂閱

取得領先

VMware 提供培訓和認證,以加速您的進度。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看全部