使用Ratpack和Spring Boot打造高性能的JVM微服务应用
2016-02-24 22:31
691 查看
使用Ratpack和Spring Boot打造高性能的JVM微服务应用
这是我为InfoQ翻译的文章,原文地址:Build High Performance JVM Microservices with Ratpack & Spring Boot,InfoQ上的中文地址:使用Ratpack与Spring Boot构建高性能JVM微服务。在微服务天堂中Ratpack和Spring Boot是天造地设的一对。它们都是以开发者为中心的运行于JVM之上的web框架,侧重于生产率、效率以及轻量级部署。他们在服务程序的开发中带来了各自的好处。Ratpack通过一个高吞吐量、非阻塞式的web层提供了一个反应式编程模型,而且对应用程序结构的定义和HTTP请求过程提供了一个便利的处理程序链;Spring Boot集成了整个Spring生态系统,为应用程序提供了一种简单的方式来配置和启用组件。Ratpack和Spring Boot是构建原生支持计算云的基于数据驱动的微服务的不二选择。
Ratpack并不关心应用程序底层使用了什么样的依赖注入框架。相反,应用程序可以通过Ratpack提供的DI抽象(被称为Registry)访问服务层组件。Ratpack的Registry是构成其基础设施的一部分,其提供了一个接口,DI提供者可以使用注册器回调(registry backing)机制来参与到组件解决方案序列中。
Ratpack直接为Guice和Spring Boot提供了注册器回调机制,开发人员可以为应用程序灵活选择使用的依赖注入框架。
在本文中我们将演示使用Ratpack和Spring Boot构建一个RESTful风格的基于数据驱动的微服务,背后使用了Spring Data用于操作数据。
开始构建Ratpack项目的最佳方式是创建Gradle脚本以及标准的Java项目结构。Gradle是Ratpack原生支持的构建系统,其实由于Ratpack只是一组简单的JVM库,所以其实它适用于任何构建系统(不管你的需求有多特别)。如果你还未安装Gradle,那么安装它最佳方式是通过Groovy enVironment Manager工具。示例项目的构建脚本如列表1所示。
列表1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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' } } |
在主类中,我们通过工厂方法构造了RatpackServer的一个实例,在start方法中提供了对应用程序的定义。该定义中我们编写了RESTful API处理器链。请参见列表2中对Main类的演示。注意Ratpack要求的编译环境为Java 8。
列表2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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")) ) ) ) ) ); } } |
在项目根目录下,我们可以通过命令行简单使用gradle的“run”命令运行该应用程序。这会启动web服务器并绑定到端口5050。为了演示当前项目的功能,确保处理器结构工作正常,我们可以在命令行中通过curl运行一些测试:
命令:curl http://localhost:5050, 期待输出:Received GET request
命令:curl -XPOST http://localhost:5050, 期待输出:Received POST request
命令:curl -XPUT http://localhost:5050, 期待输出:Received PUT request
命令:curl -XDELETE http://localhost:5050, 期待输出:Received DELETE request
可以看到,应用程序处理器链可以正确地路由请求,我们建立了RESTful API的结构。接下来需要改善这些API…
为了演示的缘故,让我们尽量保持简单,改造该微服务以便可以对一个User领域对象进行CRUD操作。通过REST接口,客户可以做以下事情:
通过一个GET请求来请求指定的用户账号,用户名作为路径变量(path variable);
GET请求中如果未指定用户名,则列出所有的用户;
通过POST一个JSON格式的用户对象来创建一个用户;
使用PUT请求,用户名作为路径变量来更新该用户的邮件地址;
使用DELETE请求,用户名作为路径变量来删除该用户。
在之前小节中我们定义的处理器已经包含了大多数处理这种需求的基础设施。但根据需求我们还需要做细微调整。例如,我们现在需要绑定处理器接收用户名作为路径变量。列表3中是更新后的代码,主类中的处理器可以满足现在的需求。
列表3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 2930 | 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) ) ) ) ) ); } } |
在(1)中我们修改了入口级前缀为/api/users;
在(2)中我们绑定了一个新的前缀处理器到:username路径变量上。任何到来的请求路径中的值会被转换,并且Ratpack处理器可以通过ctx.getPathTokens()中的表来访问该值。
在(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处理器,当创建新用户时该POST处理器会被调用。这里又一次从到来的请求中取出JSON数据;
最后在(7)中,我们附加了一个GET处理器,当客户端需要所有用户的列表时可以调用它。
再次启动该应用程序并进行一系列curl命令行调用,来测试这些端口操作是否符合预期:
命令:curl http://localhost:5050/api/users, 期望结果:”Received request to list all users”
命令: curl -d ‘{ “username”: “dan”, “email”: “danielpwoods@gmail.com” }’http://localhost:5050/api/users, 期望结果: “Received request to create a new user with JSON: { “username”: “dan”, “email”: “danielpwoods@gmail.com” }”
命令: curl http://localhost:5050/api/users/dan, 期望结果: “Received request for user: dan”
命令: curl -XPUT -d ‘{ “email”: “daniel.p.woods@gmail.com” }’http://localhost:5050/api/users/dan, 期望结果: “Received update request for user: dan, JSON: { “email”: “daniel.p.woods@gmail.com” }”
命令: curl -XDELETE http://localhost:5050/api/users/dan, 期望结果: “Received delete request for user: dan”
现在我们拥有了满足需求的API的基础框架,但仍需使其更加有用。我们可以开始设置服务层的依赖。在本例中,我们将使用Spring Data JPA组件作为数据访问对象;列表4展示了对构建脚本的修改。
列表4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 2930 | 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' } } |
有了新的依赖后,我们必须做的第一件事是建模我们的微服务领域对象:User。User类为了演示的目的尽可能的简单,列表5展示了一个正确建模的JPA领域实体。我们将其放置到项目的src/main/java/springpack/model/User.java类文件中。
列表5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 2930 | 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; } } |
列表6
1 2 3 4 5 6 7 8 9 10 | 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) } |
在修改API来使用UserRepository之前,我们首先必须定义Spring Boot 应用程序类。该类代表了一个配置入口,指向了Spring Boot自动配置引擎,并且可以构造一个Spring ApplicationContext,从而可以使用Ratpack应用程序中的注册器回调。列表7描述了该Spring Boot配置类。
列表7
1 2 3 4 5 6 7 8 9 1011 | 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(); } } |
@SpringBootApplication注解做了大部分事情。当初始化Spring Boot注册器回调时,该类会作为入口点。它的基础设施将使用该注解来扫描classpath中任何可用的组件,并自动装配这些组件到应用程序上下文中中,并且根据Spring Boot的约定规则来自动配置它们。例如,UserRepository类(使用了@Repository注解)存在于应用程序classpath中,所以Spring Boot将使用Spring Data引擎代理该接口,并配置其与H2嵌入式数据库一块工作,因为H2也在classpath中。借助Spring Boot我们无需其它多余的配置。
在实现API层之前我们需要做的另一个事情是构造Ratpack来使用Spring Boot应用程序作为注册器。Ratpack的Spring Boot集成组件提供了一个固定层来无缝转换Spring Boot应用程序为注册器回调程序,只需一行代码就可以合并这两个世界。列表8中的代码展示了更新后的主类,这次使用SpringBootConfig类作为API层的注册器。
列表8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 2930 | 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")) ) ) ) ) ); } } |
如果你仔细观察接下来的修改,就会理解Ratpack与传统的基于servlet的web应用是完全不同的。之前我们提及过,Ratpack的HTTP层构建在非阻塞的网络接口上,该web框架天然支持高性能。而基于servlet的web应用会为每个到来的请求产生一个新的线程,虽然会降低资源利用率,但每个请求处理流时是隔离的。在这种机制下,web应用处理请求时会采用阻塞式的方式,比如调用数据库并等待对应的结果然后返回,在等待期间(相对来说)并不关心这会影响它服务接下来的客户端的能力。在非阻塞式的web应用中,如果客户端或服务器端不发送数据,那么网络层并不会被阻塞,所以线程池中少量的“请求任务”线程就可以服务大量高并发的请求。然而这意味着如果应用程序代码阻塞了一个“请求任务”线程,那么吞吐量会显著影响。因此,阻塞操作(比如对数据库的操作)不能放置在请求线程中。
幸运的是,Ratpack通过在请求上下文中暴露一个阻塞接口来在应用程序中执行阻塞操作。该接口会把阻塞操作放置到另一个不同的线程池中,在维持高容量的情况服务新带来的请求的同时,这些阻塞调用也可以同步完成。一旦阻塞调用完成,处理流会返回到“请求任务”线程中,应答会被写回到客户端。在我们构建的API层中,我们要确保所有对UserRepository的操作都被路由到阻塞固定层中。列表9展示了API层的实现。
列表9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 2930 | 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后,可以运行一系列curl测试来确保该微服务符合预期:
命令: curl -d ‘{“username”: “dan”, “email”: “danielpwoods@gmail.com”}’http://localhost:5050/api/users, 期望结果: {“id”:1,“username”:“dan”,“email”:“danielpwoods@gmail.com”}
命令: curl http://localhost:5050/api/users, 期望结果: [{“id”:1,“username”:“dan”,“email”:“danielpwoods@gmail.com”}]
命令: curl -XPUT -d ‘{ “email”: “daniel.p.woods@gmail.com” }’http://localhost:5050/api/users/dan, 期望结果: {“id”:1,“username”:“dan”,“email”:“daniel.p.woods@gmail.com”}
命令: curl http://localhost:5050/api/users/dan, 期望结果: {“id”:1,“username”:“dan”,“email”:“daniel.p.woods@gmail.com”}
命令: curl -XDELETE http://localhost:5050/api/users/dan, 期望结果: empty
命令: curl http://localhost:5050/api/users/dan, 期望结果: {“message”:“NOT FOUND”,“status”:“404”}
通过上面的命令序列可以看出API层工作完全正确,我们拥有了一个完全正式的数据驱动的基于Ratpack和Spring Boot的微服务,并且使用了Spring Data JPA!
整个过程的最后一步是部署。部署的最简单方式是执行gradle installDist命令。这会打包应用程序以及整个运行时依赖到一个traball(.tar文件)和zip(.zip文件)存档文件中。它另外也会创建跨平台的启动脚本,可以在任何安装了Java 8的系统中启动我们的微服务。当installDist任务完成后,可以在项目的build/distributions目录中找到这些存档文件。
通过本文章你已经学会了如何利用Spring Boot提供的大量生态系统以及Ratpack提供的高性能特性来打造一个微服务应用程序。你可以使用该示例作为起点来构建JVM上原生支持云的数据驱动的微服务程序。
欢迎使用Ratpack和Srping Boot!
关于作者
Daniel Woods醉心于企业级Java、Groovy以及Grails开发。他在JVM软件开发领域拥有10余年的工作经验,并且乐于向开源项目(比如Grails和Ratpack)贡献他的经验。Dan曾在Gr8conf和SpringOne 2GX大会上做过演讲嘉宾,展示了他基于JVM的企业级应用程序架构的专业知识。相关文章推荐
- Rxjava基础
- 随堂笔记160224基础语法
- Struts2-工作原理
- java单例模式singleton回顾,如何实现单例模式
- JavaIO之-BIO(同步阻塞线程)
- Java的最佳实践
- Java反射机制分析指南
- JAVA语法基础 3
- 最全面的Java多线程用法解析
- Spring JTA事务配置JOTM
- Java实现配置加载机制
- Java新手问题集锦汇总
- JAVA SE 学习笔记
- java多线程-CyclicBarrier
- 【JAVA】19、多维数组
- Spring整合RabbitMQ进行消息队列开发
- 文件访问权限详解(eclipse和studio如何访问虚拟机内部的存储内容)
- java图片压缩策略说明
- dySE:一个 Java 搜索引擎的实现
- synchronized Java