天天看点

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框架