天天看點

ratpack架構_使用Ratpack和Spring Boot建構高性能JVM微服務

ratpack架構

Ratpack和Spring Boot是微服務天堂中的佼佼者。 每個都是針對JVM的以開發人員為中心的Web架構,專注于生産力,效率和輕量級部署。 它們在微服務開發領域具有各自的優勢,因為它們帶來了不同的産品。 Ratpack帶來了一個React式程式設計模型,該模型具有高吞吐量,無阻塞的Web層以及友善的處理程式鍊,用于定義應用程式結構和HTTP請求處理; Spring Boot為整個Spring生态系統帶來了內建,并且提供了一種簡單的方法來将元件配置和自動裝配到應用程式中。 對于建構雲原生和資料驅動的微服務,它們是無與倫比的稱贊。

Ratpack對應用程式的基礎依賴注入架構沒有意見。 相反,它允許應用程式通過其DI抽象(稱為Registry)通路服務層元件。 Ratpack的系統資料庫是其基礎架構不可或缺的一部分,它為DI提供程式提供了一個接口,以通過系統資料庫支援來參與元件解析序列。

Ratpack開箱即用,帶有Guice和Spring Boot的系統資料庫支援,為開發人員提供了實作的靈活性。

在本文中,我們将示範如何建構一個RESTful資料驅動的Ratpack和Spring Boot微服務,該服務将在背景利用Spring Data。

開始使用Ratpack項目的最佳方法是建立Gradle建構腳本和标準Java項目結構。 Gradle是Ratpack支援的建構系統,但是由于Ratpack隻是JVM庫的集合,是以它實際上可以由任何建構系統建構(盡管您的行程可能會有所不同)。 如果尚未安裝Gradle,最簡單的方法是通過Groovy enVironment Manager 。 清單1中描述了我們項目的建構腳本。

清單1

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    classpath 'io.ratpack:ratpack-gradle:0.9.18'
  }
}

apply plugin: 'io.ratpack.ratpack-java'
apply plugin: 'idea'
apply plugin: 'eclipse'

repositories {
  jcenter()
}

dependencies {
  compile ratpack.dependency('spring-boot') (1)
}

mainClassName = "springpack.Main" (2)

eclipse {
  classpath {
    containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
    containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
  }
}
           

該建構腳本通過使用(1)處的Ratpack Gradle插件的ratpack.dependency(..)功能來導入Ratpack Spring Boot內建。 有了建構腳本和項目結構後,我們可以建立一個“主類”,它将成為可啟動類以啟動和運作我們的應用程式。 請注意,在(2)中,我們指定了主類名稱,以便指令行工具可以更好地工作。 這意味着我們的主類必須與此相對應,是以我們将在項目的src / main / java樹中建立一個springpack.Main類。

在主類中,我們通過工廠方法start建立一個RatpackServer執行個體,并向其提供應用程式的定義 。 在此定義中将存在我們的RESTful HTTP API處理程式鍊。 作為初始示範,請考慮清單2中所示的主類。請注意,Ratpack需要Java 8。

清單2

package springpack;

import ratpack.server.RatpackServer;

public class Main {

  public static void main(String[] args) throws Exception {
    RatpackServer.start(spec -> spec
      .handlers(chain -> chain (1)
          .prefix("api", pchain -> pchain (2)
            .all(ctx -> ctx (3)
              .byMethod(method -> method (4)
                .get(() -> ctx.render("Received GET request"))
                .post(() -> ctx.render("Received POST request"))
                .put(() -> ctx.render("Received PUT request"))
                .delete(() -> ctx.render("Received DELETE request"))
              )
            )
          )
      )
    );
  }
}
           

如果我們在主類中分解應用程式定義,我們可以确定一些關鍵區域,對于那些不熟悉Ratpack的人來說值得解釋。 第一點值得注意的是,Ratpack中的HTTP請求流經處理鍊,該鍊由(1)定義的處理程式部分定義。 處理程式在鍊中定義,用于描述它們能夠滿足的請求類型。 具體來說,在(2)處,我們定義了一個字首處理程式類型,并指定它應綁定到“ api” HTTP路由。 字首處理程式進而建立了一個新鍊,該鍊将委派給與“ / api”端點比對的傳入請求。 在(3)中,我們使用all處理程式類型來指定所有傳入請求都應通過提供的處理程式運作,在(4)中,我們使用Ratpack的byMethod機制将get,post,put和delete處理程式綁定到各自的HTTP方法。

現在,我們隻需在項目的根目錄下發出gradle“ run”指令,即可從指令行運作該應用程式。 這将啟動并在端口5050上綁定Web伺服器。為了示範項目的現有功能并確定處理程式結構按預期工作,我們可以從指令行對curl進行一些測試:

  • 指令:curl http:// localhost:5050 ,預期結果:收到GET請求
  • 指令:curl -XPOST http:// localhost:5050 ,預期結果:收到POST請求
  • 指令:curl -XPUT http:// localhost:5050 ,預期結果:收到PUT請求
  • 指令:curl -XDELETE http:// localhost:5050 ,預期結果:收到DELETE請求

如您所見,應用程式處理程式鍊正确地路由了請求,并且我們已經有了RESTful API的結構。 現在我們需要讓它做某事...

為了示範,讓我們保持簡單,并使此微服務負責與User域對象相關的CRUD操作。 通過REST端點,用戶端應該能夠:

  • 通過以使用者名作為路徑變量的GET請求請求特定的使用者帳戶;
  • 未指定使用者名時,通過GET請求列出所有使用者;
  • 通過釋出JSON編碼的使用者對象來建立使用者;
  • 通過發出以使用者名作為路徑變量的PUT請求來更新使用者的電子郵件位址;
  • 通過發出以使用者名作為路徑變量的DELETE請求來删除使用者。

用于處理這些需求的大多數基礎結構已經基于我們在上一節中定義的處理程式而到位,但是這些需求意味着我們将需要稍作更改。 例如,我們現在需要綁定接受使用者名路徑變量的處理程式。 清單3中更新的代碼顯示了主類,現在具有滿足要求的處理程式。

清單3

package springpack;

import ratpack.server.RatpackServer;

public class Main {

  public static void main(String[] args) throws Exception {
    RatpackServer.start(spec -> spec
      .handlers(chain -> chain
        .prefix("api/users", pchain -> pchain (1)
          .prefix(":username", uchain -> uchain (2)
            .all(ctx -> { (3)
              String username = ctx.getPathTokens().get("username");
              ctx.byMethod(method -> method (4)
                .get(() -> ctx.render("Received request for user: " + username))
                                               .put(() -> {
                  String json = ctx.getRequest().getBody().getText();
                  ctx.render("Received update request for user: " + username + ", JSON: " + json);
                })
                .delete(() -> ctx.render("Received delete request for user: " + username))
              );
            })
          )
          .all(ctx -> ctx (5)
            .byMethod(method -> method
              .post(() -> { (6)
                String json = ctx.getRequest().getBody().getText();
                ctx.render("Received request to create a new user with JSON: " + json);
              })
              .get(() -> ctx.render("Received request to list all users")) (7)
            )
          )
        )
      )
    );
  }
}
           

現在已對API進行了重組,以遵循以資源為中心的模式,圍繞我們的使用者域對象進行了以下更改:

  • 在(1)處 ,我們将入門級字首更改為/ api / users;
  • 在(2)處,這一次我們在:username路徑變量上綁定了一個新的字首處理程式。 傳入請求路徑中存在的任何值都将被轉換并通過ctx.getPathTokens()映射使Ratpack處理程式可以通路;
  • 在(3)處 ,我們按照/ api / users /:username URI模式為所有流量綁定處理程式;
  • 在(4),我們使用byMethod機制将處理程式附加到HTTP GET,PUT和DELETE方法。 這些處理程式使我們能夠了解針對給定使用者的用戶端操作的意圖。 在PUT處理程式中,我們進行ctx.getRequest()。getBody()。getText()調用,以從傳入請求中捕獲JSON。
  • 在(5)處 ,我們附加了一個處理程式以将所有傳入請求比對到/ api / users端點;
  • 在(6)處 ,我們再次在/ api / users處理程式中利用byMethod機制來附加在建立新使用者時調用的POST處理程式。 我們再次調用從傳入請求中捕獲JSON;
  • 最後,在(7)處 ,我們附加了GET處理程式,當用戶端需要所有使用者的清單時将調用該處理程式。

如果再次啟動該應用程式并進行一系列curl指令行調用,我們可以測試端點是否按預期運作:

  • 指令:curl http:// localhost:5050 / api / users ,預期結果:“已收到列出所有使用者的請求”
  • 指令:curl -d'{“使用者名”:“ dan”,“電子郵件”:“ [email protected]”}' http:// localhost:5050 / api / users ,預期結果:“已收到建立新請求的請求JSON使用者:{“ username”:“ dan”,“ email”:“ [email protected]”}“
  • 指令:curl http:// localhost:5050 / api / users / dan ,預期結果:“收到的使用者請求:dan”
  • 指令:curl -XPUT -d'{“ email”:“ [email protected]”}' http:// localhost:5050 / api / users / dan ,預期結果:“已收到使用者的更新請求: dan,JSON:{“ email”:“ [email protected]”}“
  • 指令:curl -XDELETE http:// localhost:5050 / api / users / dan ,預期結果:“收到使用者對dan的删除請求”

現在,我們已經有了可以表示API要求的支架,我們需要使它做一些有用的事情。 我們可以從為服務層設定依賴關系開始。 在這個例子中,我們将利用Spring Data JPA項目作為我們的資料通路對象。 清單4中反映了對buildscript的更改。

清單4

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    classpath 'io.ratpack:ratpack-gradle:0.9.18'
  }
}

apply plugin: 'io.ratpack.ratpack-java'
apply plugin: 'idea'
apply plugin: 'eclipse'

repositories {
  jcenter()
}

dependencies {
  compile ratpack.dependency('spring-boot')
  compile 'org.springframework.boot:spring-boot-starter-data-jpa:1.2.4.RELEASE' (1)
  compile 'com.h2database:h2:1.4.187' (2)
}

mainClassName = "springpack.Main"

eclipse {
  classpath {
    containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
    containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
  }
}
           

唯一的變化是在(1)處 ,我們現在包括Spring Boot Spring Data JPA依賴關系,在(2)處引入了H2嵌入式資料庫依賴關系。 當在類路徑上找到H2時,Spring Boot将自動配置Spring Data以将其用作記憶體資料源。 在項目頁面上很好地記錄了如何配置和使用Spring Data資料源。

有了新的依賴關系之後,我們要做的第一件事就是開始對微服務的域對象模組化:使用者。 為了便于示範,User類可以非常簡單,清單5中的代碼顯示了正确模組化的JPA域實體。 我們将其放置在項目内的src / main / java / springpack / model / User.java類檔案中。

清單5

package springpack.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class User {
  private static final long serialVersionUID = 1l;

  @Id
  @GeneratedValue
  private Long id;

  @Column(nullable = false)
  private String username;

  @Column(nullable = false)
  private String email;

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }
}
           

我們可以使用javax.persistence。*注釋,因為Spring Data現在位于項目的編譯時類路徑中。 Spring Boot使它無縫地啟動和運作資料通路對象,是以我們可以根據Spring Data提供的Repository服務類型對DAO進行模組化。 由于我們的API遵循相對簡單的CRUD操作,是以我們可以利用Spring Data提供的CrudRepository夾具來最小化UserRepository DAO實作所需的代碼。

清單6

package springpack.model;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<User, Long> {

  User findByUsername(String username); (1)
}
           

令人驚訝的是,清單6所示的UserRepository DAO實作對于我們的User域對象具有完整的服務層是必需的。 Spring Data提供的Repository接口允許我們根據要搜尋的實體的約定建立“幫助程式”查找方法。 根據需求,我們知道我們的API層需要按使用者名查找使用者,是以我們可以在<1>處添加findByUsername方法。 我們将UserRepository放入項目中的src / main / java / springpack / model / UserRepository.java類檔案中。

在繼續修改API以利用UserRepository之前,我們首先必須定義我們的Spring Boot應用程式類。 此類充當Spring Boot自動配置引擎的配置入口點,并構造一個Spring ApplicationContext,我們可以将其用作Ratpack應用程式中的系統資料庫支援。 清單7描述了Spring Boot配置類。

清單7

package springpack;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class SpringBootConfig {

  @Bean
  ObjectMapper objectMapper() { (1)
    return new ObjectMapper();
  }
}
           

我們的SpringBootConfig類所需的極少的代碼量進入src / main / java / springpack / SpringBootConfig.java類檔案中。 在此,我們顯式地為Jackson的ObjectMapper定義了bean定義。 我們将在API層中使用它來讀寫JSON。

@SpringBootApplication注釋在這裡完成了大部分繁重的工作。 當我們初始化Spring Boot系統資料庫支援時,我們将提供此類作為入口點。 然後,它的基礎結構将使用該注釋掃描類路徑中是否有任何可用的元件,将它們自動連接配接到應用程式上下文中,并根據Spring Boot的正常規則對其進行自動配置。 例如,在應用程式的類路徑中僅存在UserRepository類(用@Repository注釋)将使Spring Boot通過Spring Data Engine代理該接口,該接口也将配置為與H2嵌入式資料庫一起使用。在類路徑上。 至此,Spring Boot方面不再需要其他任何東西。

在實作API層之前,我們必須做的下一件事是訓示Ratpack将Spring Boot應用程式用作系統資料庫。 Ratpack的Spring Boot內建提供了一種工具,可将Spring Boot應用程式無縫轉換為系統資料庫支援,使其成為一行代碼即可合并兩個世界。 清單8中的代碼顯示了一個更新的主類,這次,SpringBootConfig類作為我們的API層的系統資料庫站起來了。

清單8

package springpack;

import ratpack.server.RatpackServer;
import ratpack.spring.Spring;
import springpack.config.SpringBootConfig;

public class Main {

  public static void main(String[] args) throws Exception {
    RatpackServer.start(spec -> spec
      .registry(Spring.spring(SpringBootConfig.class)) (1)
      .handlers(chain -> chain
        .prefix("api/users", pchain -> pchain
          .prefix(":username", uchain -> uchain
            .all(ctx -> {
              String username = ctx.getPathTokens().get("username");
              ctx.byMethod(method -> method
                .get(() -> ctx.render("Received request for user: " + username))
                .put(() -> {
                  String json = ctx.getRequest().getBody().getText();
                  ctx.render("Received update request for user: " + username + ", JSON: " + json);
                })
                .delete(() -> ctx.render("Received delete request for user: " + username))
              );
            })
          )
          .all(ctx -> ctx
            .byMethod(method -> method
              .post(() -> {
                String json = ctx.getRequest().getBody().getText();
                ctx.render("Received request to create a new user with JSON: " + json);
              })
              .get(() -> ctx.render("Received request to list all users"))
            )
          )
        )
      )
    );
  }
}
           

唯一必要的更改是在(1)處 ,我們為Ratpack應用程式定義提供了顯式的Registry實作。 現在我們可以開始實作API層了。

當您跟蹤即将發生的變化時,再次重要的是要了解Ratpack與傳統的基于servlet的Web應用程式有很大不同。 如前所述,Ratpack的HTTP層建立在非阻塞網絡接口上,該接口支援其作為高性能Web架構的性質。 基于Servlet的Web應用程式将為每個傳入的請求生成一個新線程,這雖然資源效率低下,但允許每個請求處理流獨立運作。 在這種範例中,Web應用程式能夠執行諸如調用資料庫并等待相應結果的操作,而不必擔心(相對)影響其為後續用戶端提供服務的能力。 在非阻塞的Web應用程式中,當用戶端或伺服器未發送資料時,網絡層不會阻塞,是以可以在少量的“請求接受”線程池中發出大量并發請求。 但是,這意味着,如果應用程式代碼在這些請求接受線程之一上阻塞,則吞吐量将受到嚴重影響。 在這種情況下,重要的是不要在請求線程内進行阻塞操作,例如對資料庫的調用。

幸運的是,Ratpack通過在請求的上下文中公開一個阻塞接口,使在應用程式中的阻塞操作變得容易。 這樣可以将阻塞操作排程到其他線程池,并允許這些調用同步完成,同時仍然為大量傳入的新請求提供服務。 一旦阻塞調用完成,處理流程将傳回到請求接收線程,并且可以将響應寫回到用戶端。 在建構API層時,我們需要確定所有使用UserRepository的調用都通過阻塞夾具進行路由,如清單9的API層實作所示。

清單9

package springpack;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import ratpack.exec.Promise;
import ratpack.handling.Context;
import ratpack.server.RatpackServer;
import ratpack.spring.Spring;
import springpack.model.User;
import springpack.model.UserRepository;

import java.util.HashMap;
import java.util.Map;

public class Main {
  private static final Map<String, String> NOT_FOUND = new HashMap<String, String>() {{
    put("status", "404");
    put("message", "NOT FOUND");
  }};
  private static final Map<String, String> NO_EMAIL = new HashMap<String, String>() {{
    put("status", "400");
    put("message", "NO EMAIL ADDRESS SUPPLIED");
  }};

  public static void main(String[] args) throws Exception {
    RatpackServer.start(spec -> spec
      .registry(Spring.spring(SpringBootConfig.class))
      .handlers(chain -> chain
        .prefix("api/users", pchain -> pchain
          .prefix(":username", uchain -> uchain
            .all(ctx -> {
              // extract the "username" path variable
              String username = ctx.getPathTokens().get("username");
              // pull the UserRepository out of the registry
              UserRepository userRepository = ctx.get(UserRepository.class);
              // pull the Jackson ObjectMapper out of the registry
              ObjectMapper mapper = ctx.get(ObjectMapper.class);
              // construct a "promise" for the requested user object. This will
              // be subscribed to within the respective handlers, according to what
              // they must do. The promise uses the "blocking" fixture to ensure
              // the DB call doesn't take place on a "request taking" thread.
              Promise<User> userPromise = ctx.blocking(() -> userRepository.findByUsername(username));
              ctx.byMethod(method -> method
                .get(() ->
                  // the .then() block will "subscribe" to the result, allowing
                  // us to send the user domain object back to the client
                  userPromise.then(user -> sendUser(ctx, user))
                )
                .put(() -> {
                  // Read the JSON from the request
                  String json = ctx.getRequest().getBody().getText();
                  // Parse out the JSON body into a Map
                  Map<String, String> body = mapper.readValue(json, new TypeReference<Map<String, String>>() {
                  });
                  // Check to make sure the request body contained an "email" address
                  if (body.containsKey("email")) {
                    userPromise
                      // map the new email address on to the user entity
                      .map(user -> {
                        user.setEmail(body.get("email"));
                        return user;
                      })
                      // and use the blocking thread pool to save the updated details
                      .blockingMap(userRepository::save)
                      // finally, send the updated user entity back to the client
                      .then(u1 -> sendUser(ctx, u1));
                  } else {
                    // bad request; we didn't get an email address
                    ctx.getResponse().status(400);
                    ctx.getResponse().send(mapper.writeValueAsBytes(NO_EMAIL));
                  }
                })
                .delete(() ->
                  userPromise
                    // make the DB delete call in a blocking thread
                    .blockingMap(user -> {
                      userRepository.delete(user);
                      return null;
                    })
                    // then send a 204 back to the client
                    .then(user -> {
                      ctx.getResponse().status(204);
                      ctx.getResponse().send();
                    })
                )
              );
            })
          )
          .all(ctx -> {
            // pull the UserRepository out of the registry
            UserRepository userRepository = ctx.get(UserRepository.class);
            // pull the Jackson ObjectMapper out of the registry
            ObjectMapper mapper = ctx.get(ObjectMapper.class);
            ctx.byMethod(method -> method
              .post(() -> {
                // read the JSON request body...
                String json = ctx.getRequest().getBody().getText();
                // ... and convert it into a user entity
                User user = mapper.readValue(json, User.class);
                // save the user entity on a blocking thread and
                // render the user entity back to the client
                ctx.blocking(() -> userRepository.save(user))
                  .then(u1 -> sendUser(ctx, u1));
              })
              .get(() ->
                // make the DB call, on a blocking thread, to list all users
                ctx.blocking(userRepository::findAll)
                  // and render the user list back to the client
                  .then(users -> {
                    ctx.getResponse().contentType("application/json");
                    ctx.getResponse().send(mapper.writeValueAsBytes(users));
                  })
              )
            );
          })
        )
      )
    );
  }

  private static void notFound(Context context) {
    ObjectMapper mapper = context.get(ObjectMapper.class);
    context.getResponse().status(404);
    try {
      context.getResponse().send(mapper.writeValueAsBytes(NOT_FOUND));
    } catch (JsonProcessingException e) {
      context.getResponse().send();
    }
  }

  private static void sendUser(Context context, User user) {
    if (user == null) {
      notFound(context);
    }

    ObjectMapper mapper = context.get(ObjectMapper.class);
    context.getResponse().contentType("application/json");
    try {
      context.getResponse().send(mapper.writeValueAsBytes(user));
    } catch (JsonProcessingException e) {
      context.getResponse().status(500);
      context.getResponse().send("Error serializing user to JSON");
    }
  }
}
           

API層實作中最值得注意的元素是阻塞機制的使用,可以從每個請求附帶的Context對象中提取阻塞機制。 調用ctx.blocking()時,将傳回Promise對象,該對象必須訂閱才能執行代碼。 這使我們可以按承諾(如prefix(“:username”)鍊所示)在不同的處理程式中重用,進而保持代碼的清潔。

現在已經實作了API,我們可以再次運作一系列的curl測試以確定微服務按預期工作:

  • 指令:curl -d'{“ username”:“ dan”,“ email”:“ [email protected]”}' http:// localhost:5050 / api / users ,預期響應:{“ id”:1, “使用者名”:“ dan”,“電子郵件”:“ [email protected]”}
  • 指令:curl http:// localhost:5050 / api / users ,預期響應:[{“ id”:1,“使用者名”:“ dan”,“電子郵件”:“ [email protected]”}]
  • 指令:curl -XPUT -d'{“ email”:“ [email protected]”}' http:// localhost:5050 / api / users / dan ,預期響應:{“ id”:1, “使用者名”:“ dan”,“電子郵件”:“ [email protected]”}
  • 指令:curl http:// localhost:5050 / api / users / dan ,預期響應:{“ id”:1,“使用者名”:“ dan”,“電子郵件”:“ [email protected]” }
  • 指令:curl -XDELETE http:// localhost:5050 / api / users / dan ,預期響應:空
    • 指令:curl http:// localhost:5050 / api / users / dan ,預期響應:{“ message”:“ NOT FOUND”,“ status”:“ 404”}

遵循此指令序列,我們确實可以看到我們的API層運作正常,并且我們有一個使用Spring Data JPA的完全形成的,資料驅動的Ratpack和Spring Boot微服務!

整個過程的最後一步是為部署做好準備。 完成此操作的最簡單方法是執行gradle installDist指令。 這會将您的應用程式及其所有運作時依賴項打包在tarball(.tar檔案)和zip(.zip檔案)檔案中。 它還将為您建立跨平台的啟動腳本,以便您可以在任何裝有Java 8的系統上啟動微服務。 installDist任務完成後,您可以在項目的build / distributions目錄中找到這些歸檔檔案。

通過這篇文章,您學習了如何建立一個微服務應用程式,該應用程式在利用Spring Boot提供的廣闊生态系統的同時,提高Ratpack的性能和易用性。 您可以将此示例用作在JVM上建構雲本機和資料驅動的微服務的起點。

快樂的拉包! Spring快樂引導!

翻譯自: https://www.infoq.com/articles/Ratpack-and-Spring-Boot/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

ratpack架構