前言
在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