[译] 第十三天:Dropwizard - 很棒的Java REST 服务栈
前言
在8年软件开发生涯中我俨然一位Java开发者了。大多数我写的程序,都用Spring框架或者Java EE.最近我在学Python Web开发,其中印象很深的一个是Flask框架。Flask框架是个微框架,使得写REST后端很简单。我今天的30天挑战,决定找一款Java框架替代Python的Flask。一番搜索后,我发现Dropwizard框架可以达到如Flask同样的功效。这篇博客,我们来学习怎样用Dropwizard构建RESTful Java MongoDB程序。
什么是Dropwizard?
Dropwizard是一款开源Java框架,用于ops友好开发,高性能RESTful后端开发。它由Yammer开发并提供基于JVM后端的支持。
Dropwizar延续最优Java库,嵌入到程序包,包括以下组件:
- 嵌入Jetty: 每个程序都打包成jar而不是war,启用自带的Jetty容器,没有WAR文件也没有外部servlet容器。
- JAX-RS: Jersey(JAX-RS的参考实现)用来写RESTful Web服务,这样就没白费你已懂的JAX-RS知识。
- JSON: REST服务使用JSON, Jackson库用来做所有JSON处理。
- Logging: 用Logback和SLF4J完成。
- Hibernate Validator: Dropwizard用Hbernate Vlidator API来验证声明。
- Metrics: Dropwizard支持用Metrics库进行检测,提供观察代码对生产做了什么的绝佳视觉。
为什么选择Dropwizard?
我学习Dropwizard的几点原因:
- 快速的项目引导:要是你用过Spring或者Java EE, 就会了解开发者要通过项目引导的痛苦,用Dropwizard,只需要添加一个依赖到pom.xml文件就好了。
- 项目Metrics: Dropwizard支持项目metrics, 它提供很有用的信息如请求/相应时间等,我们只需给出@Timed注解就可获得方法执行时间。
- 生产力:每个Dropwizard应用有一个启用Jetty容器的主程序,意味着可以在IDE里直接像主程序一样运行和调试,没有必要再编译或者部署WAR文件。
Github仓库
今天的demo放在github: day13-dropwizard-mongodb-demo-app.
前提准备
- 必须会Java基础。
- 下载和安装MongoDB数据库。
- 安装最新Java Development Kit(JDK), 可以装OpenJDK7或者Oracle JDK 7. OpenShift支持OpenJDK 6和7, 这篇博客,我们用JDK 7.
- 从官网下载最新的Eclipse,目前最新版本是Kepler.
安装Eclipse很简单,只需解压下载的安装包。在linux或者mac上,打开命令管理器输入以下命令。
$ tar -xzvf eclipse-jee-kepler-R-*.tar.gz
Windows上可以用7-zip或者其他解压工具解压,解压后,在你解压的路径会有一个eclipse的文件夹,可以给可执行文件创建快捷键。
第一步:新建Maven项目
打开Eclipse IDE导航到项目空间,新建项目,到File > New > Maven Project,选择maven-archetype-quichstart,输入Ground Id和Artifact Id,最后点Finish.
第二步:更新pom.xml
现在更新pom.xml, 加入dropwizard-core maven依赖,再更新Maven项目用Java 1.7版本,更新pom.xml后更新Maven项目(右击>Maven>Update Project)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.shekhar</groupId> <artifactId>blog</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>blog</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.yammer.dropwizard</groupId> <artifactId>dropwizard-core</artifactId> <version>0.6.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> </plugins> </build> </project>
第三步:创建配置类
每个Dropwizard程序都有一个配置类,用来指定特定环境变量。后面我们会添加MongoDB配置参数如host, port, db name.这个类继承com.yammer.dropwizard.config.Configuration。
import com.yammer.dropwizard.config.Configuration; public class BlogConfiguration extends Configuration{ }
第四步:创建服务类
Dropwizard由一个服务类引导,这个类涵盖所有提供基础功能的绑定和命令,也启用内嵌的Jetty服务,继承com.yammer.dropwizard.Service.
import com.yammer.dropwizard.Service; import com.yammer.dropwizard.config.Bootstrap; import com.yammer.dropwizard.config.Environment; public class BlogService extends Service<BlogConfiguration> { public static void main(String[] args) throws Exception { new BlogService().run(new String[] { "server" }); } @Override public void initialize(Bootstrap<BlogConfiguration> bootstrap) { bootstrap.setName("blog"); } @Override public void run(BlogConfiguration configuration, Environment environment) throws Exception { } }
以上代码做了以下动作:
- 这个类有一个主方法,作为服务的入口,在这个主方法里,创建了一个BlogService实例来调用run方法,server命令用参数形式传递,它会启动内嵌的Jetty服务。
- Initalize 方法在执行service run方法前调用,设置服务名为blog.
- 接下来,有一个run方法会在service运行时调用,后面,我们会添加JAX-RS资源到这个方法。
第五步:写IndexResource
来写第一个当GET请求 '/' url时会被引用的资源,新建一个JAX-RS资源,它会列出所有博客。
import java.util.Arrays; import java.util.List; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import com.yammer.metrics.annotation.Timed; @Path("/") public class IndexResource { @GET @Produces(value = MediaType.APPLICATION_JSON) @Timed public List<Blog> index() { return Arrays.asList(new Blog("Day 12: OpenCV--Face Detection for Java Developers", "https://www.openshift.com/blogs/day-12-opencv-face-detection-for-java-developers")); } }
以上代码是一个标准的JAX-RS资源类,注释了@Path,定义了index()方法。Index()返回博客的集合,这些博客会被转换成JSON文档,@Timed注释确定了Dropwizard基础时间。
以上IndexResource用一篇博客呈现,显示如下,博客的呈现用hibernate validator注释来确保内容有效性。例如,用@URL注释来确保只有有效的URL会存到MongoDB数据库。
import java.util.Date; import java.util.UUID; import org.hibernate.validator.constraints.NotBlank; import org.hibernate.validator.constraints.URL; public class Blog { private String id = UUID.randomUUID().toString(); @NotBlank private String title; @URL @NotBlank private String url; private final Date publishedOn = new Date(); public Blog() { } public Blog(String title, String url) { super(); this.title = title; this.url = url; } public String getId() { return id; } public String getTitle() { return title; } public String getUrl() { return url; } public Date getPublishedOn() { return publishedOn; } }
然后,在service类run方法里注册IndexResource, 用以下代码更新BlogService的run方法。
@Override public void run(BlogConfiguration configuration, Environment environment) throws Exception { environment.addResource(new IndexResource()); }
现在我们可以把BlogService作为主程序运行,右击>Run As>Java Application. 会启动内嵌Jetty容器然后看到程序运行在http://localhost:8080/.
$ curl http://localhost:8080 [{"id":"9bb43d53-5436-4dac-abaa-ac530c833df1","title":"Day 12: OpenCV--Face Detection for Java Developers","url":"https://www.openshift.com/blogs/day-12-opencv-face-detection-for-java-developers","publishedOn":1384090975372}]
点击Metrics可以查看IndexResource的Metricx, 数据是JSON格式。
"com.shekhar.blog.IndexResource" : { "index" : { "type" : "timer", "duration" : { "unit" : "milliseconds", "min" : 17.764, "max" : 17.764, "mean" : 17.764, "std_dev" : 0.0, "median" : 17.764, "p75" : 17.764, "p95" : 17.764, "p98" : 17.764, "p99" : 17.764, "p999" : 17.764 }, "rate" : { "unit" : "seconds", "count" : 1, "mean" : 7.246537731991882E-4, "m1" : 2.290184897291144E-12, "m5" : 3.551918562683463E-5, "m15" : 2.445031498756583E-4 } } },
第六步:配置MongoDB
在pom.xml里添加Mongo-jackson-mapper依赖。
<dependency> <groupId>net.vz.mongodb.jackson</groupId> <artifactId>mongo-jackson-mapper</artifactId> <version>1.4.2</version> </dependency>
更新BlogConfiguration类的MongoDB数据库信息,如host, port, database name.
import javax.validation.constraints.Max; import javax.validation.constraints.Min; import org.codehaus.jackson.annotate.JsonProperty; import org.hibernate.validator.constraints.NotEmpty; import com.yammer.dropwizard.config.Configuration; public class BlogConfiguration extends Configuration { @JsonProperty @NotEmpty public String mongohost = "localhost"; @JsonProperty @Min(1) @Max(65535) public int mongoport = 27017; @JsonProperty @NotEmpty public String mongodb = "mydb"; }
接下来新建一个MongoManaged类,让我们能控制程序启动和停止,它实现接口com.yammer.dropwizard.lifecycle.Managed.
import com.mongodb.Mongo; import com.yammer.dropwizard.lifecycle.Managed; public class MongoManaged implements Managed { private Mongo mongo; public MongoManaged(Mongo mongo) { this.mongo = mongo; } @Override public void start() throws Exception { } @Override public void stop() throws Exception { mongo.close(); } }
以上代码,我们在Stop方法里关掉MongoDB连接。
接下来我们创建一个MongoHealthCheck来检查MongoDB是连接还是断开的,Health check是Dropwizard在生产环境做执行时测试检测服务的行为的功能。
import com.mongodb.Mongo; import com.yammer.metrics.core.HealthCheck; public class MongoHealthCheck extends HealthCheck { private Mongo mongo; protected MongoHealthCheck(Mongo mongo) { super("MongoDBHealthCheck"); this.mongo = mongo; } @Override protected Result check() throws Exception { mongo.getDatabaseNames(); return Result.healthy(); } }
现在更新BlogService类, 加入MongoDB配置。
package com.shekhar.blog; import com.mongodb.Mongo; import com.yammer.dropwizard.Service; import com.yammer.dropwizard.config.Bootstrap; import com.yammer.dropwizard.config.Environment; public class BlogService extends Service<BlogConfiguration> { public static void main(String[] args) throws Exception { new BlogService().run(new String[] { "server" }); } @Override public void initialize(Bootstrap<BlogConfiguration> bootstrap) { bootstrap.setName("blog"); } @Override public void run(BlogConfiguration configuration, Environment environment) throws Exception { Mongo mongo = new Mongo(configuration.mongohost, configuration.mongoport); MongoManaged mongoManaged = new MongoManaged(mongo); environment.manage(mongoManaged); environment.addHealthCheck(new MongoHealthCheck(mongo)); environment.addResource(new IndexResource()); } }
以上代码:
- 用BlogConfiguration对象新建了一个Mongo实例。
- 新建了MongoManaged实例并添加到环境中。
- 添加了正常检测。
以主程序运行应用,可以通过http://localhost:8081/healthcheck的HealthCheck页面检测MongoDB是否在运行,如果没有运行,可以看到异常信息。
! MongoDBHealthCheck: ERROR ! can't call something : Shekhars-MacBook-Pro.local/192.168.1.101:27017/admin com.mongodb.MongoException$Network: can't call something : Shekhars-MacBook-Pro.local/192.168.1.101:27017/admin at com.mongodb.DBTCPConnector.call(DBTCPConnector.java:227) at com.mongodb.DBApiLayer$MyCollection.__find(DBApiLayer.java:305) at com.mongodb.DB.command(DB.java:160) at com.mongodb.DB.command(DB.java:183) at com.mongodb.Mongo.getDatabaseNames(Mongo.java:327) at com.shekhar.blog.MongoHealthCheck.check(MongoHealthCheck.java:17) at com.yammer.metrics.core.HealthCheck.execute(HealthCheck.java:195) at Caused by: java.io.IOException: couldn't connect to [Shekhars-MacBook-Pro.local/192.168.1.101:27017] bc:java.net.ConnectException: Connection refused at com.mongodb.DBPort._open(DBPort.java:228) at com.mongodb.DBPort.go(DBPort.java:112) at com.mongodb.DBPort.call(DBPort.java:79) at com.mongodb.DBTCPConnector.call(DBTCPConnector.java:218) ... 33 more * deadlocks: OK
启动MongoDB可以看到如下
* MongoDBHealthCheck: OK
* deadlocks: OK
第七步:创建BlogResource
现在来写BlogResource类,用于响应创建博客的入口。
import java.util.ArrayList; import java.util.List; import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import net.vz.mongodb.jackson.DBCursor; import net.vz.mongodb.jackson.JacksonDBCollection; import com.yammer.metrics.annotation.Timed; @Path("/blogs") @Produces(value = MediaType.APPLICATION_JSON) @Consumes(value = MediaType.APPLICATION_JSON) public class BlogResource { private JacksonDBCollection<Blog, String> collection; public BlogResource(JacksonDBCollection<Blog, String> blogs) { this.collection = blogs; } @POST @Timed public Response publishNewBlog(@Valid Blog blog) { collection.insert(blog); return Response.noContent().build(); } }
用Java程序类型运行BlogService类,要测试BlogResource, 设置一个curl请求。
$ curl -i -X POST -H "Content-Type: application/json" -d '{"title":"Day 12: OpenCV--Face Detection for Java Developers","url":"https://www.openshift.com/blogs/day-12-opencv-face-detection-for-java-developers"}' http://localhost:8080/blogs HTTP/1.1 204 No Content Date: Sun, 10 Nov 2013 14:08:03 GMT Content-Type: application/json
第八步:更新IndexResource
现在更新IndexResource index()方法,从MongoDB获取所有博客文档。
import java.util.ArrayList; import java.util.List; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import net.vz.mongodb.jackson.DBCursor; import net.vz.mongodb.jackson.JacksonDBCollection; import com.yammer.metrics.annotation.Timed; @Path("/") public class IndexResource { private JacksonDBCollection<Blog, String> collection; public IndexResource(JacksonDBCollection<Blog, String> blogs) { this.collection = blogs; } @GET @Produces(value = MediaType.APPLICATION_JSON) @Timed public List<Blog> index() { DBCursor<Blog> dbCursor = collection.find(); List<Blog> blogs = new ArrayList<>(); while (dbCursor.hasNext()) { Blog blog = dbCursor.next(); blogs.add(blog); } return blogs; } }
更新BlogService run方法,传递博客集合到IndexResource.
@Override public void run(BlogConfiguration configuration, Environment environment) throws Exception { Mongo mongo = new Mongo(configuration.mongohost, configuration.mongoport); MongoManaged mongoManaged = new MongoManaged(mongo); environment.manage(mongoManaged); environment.addHealthCheck(new MongoHealthCheck(mongo)); DB db = mongo.getDB(configuration.mongodb); JacksonDBCollection<Blog, String> blogs = JacksonDBCollection.wrap(db.getCollection("blogs"), Blog.class, String.class); environment.addResource(new IndexResource(blogs)); environment.addResource(new BlogResource(blogs)); }
用Java程序形式运行BlogService, 要测试BlogResource, 设置curl请求。
$ curl http://localhost:8080 [{"id":"527f9806300462bbd300687e","title":"Day 12: OpenCV--Face Detection for Java Developers","url":"https://www.openshift.com/blogs/day-12-opencv-face-detection-for-java-developers","publishedOn":1384093702592}]
第九步:发布到云
这有一个博客讲我们怎样发布Dropwizard程序到OpenShift, 参考博客。
这是今天的内容,继续给反馈吧。
原文:https://www.openshift.com/blogs/day-13-dropwizard-the-awesome-java-rest-server-stack