最近,我一直在使用Spark (一种Java的Web框架,与Apache Spark 不相关)编写RESTful服务。 当我们计划写这篇文章时,我已经做好了不可避免的接口,样板代码和深层层次结构的Java风格的准备。 我很惊讶地发现,对于局限于Java的开发人员来说,还存在一个替代世界。
在本文中,我们将了解如何使用JSON传输数据来为博客构建RESTful应用程序。 我们会看到:
- 如何在Spark中创建一个简单的Hello World
- 如何指定请求中期望的JSON对象的布局
- 如何发送帖子请求以创建新帖子
- 如何发送获取请求以检索帖子列表
我们不会看到如何在数据库中插入该数据。 我们只将列表保留在内存中(在我的实际服务中,我一直在使用sql2o )。
一些依赖
我们将使用Maven,因此我将首先创建一个新的pom.xml并添加一些内容。 基本上:
- 火花
- 杰克逊
- Lombok
- 番石榴
- Easymock(仅在测试中使用,本文中未介绍)
- 格森
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.sql2o</groupId>
<artifactId>sql2o</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.4-1201-jdbc41</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>me.tomassetti.BlogService</mainClass>
<arguments>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
火花你好世界
你有这一切吗? 太酷了,然后编写一些代码。
package me.tomassetti;
import static spark.Spark.get;
import static spark.Spark.post;
import spark.Request;
import spark.Response;
import spark.Route;
public class BlogService
{
public static void main( String[] args) {
get("/posts", (req, res) -> {
return "Hello Sparkingly World!";
});
}
}
现在,我们可以使用以下命令运行它:
mvn compile && mvn exec:java
让我们打开浏览器并访问localhost http:// localhost:4567 / posts 。 在这里我们要做一个简单的获取。 对于执行帖子,您可能想要在浏览器中使用Postman插件,或者只运行curl 。 一切为您服务。
使用Jackson和Lombok进行很棒的描述性交换对象
在典型的RESTful应用程序中,我们希望接收带有json对象的POST请求作为有效负载的一部分。 我们的工作将是检查代码是否为格式正确的JSON,是否与预期的结构相对应,值是否在有效范围内,等等。无聊且重复。 我们可以用不同的方式做到这一点。 最基本的一种是使用gson :
JsonParser parser = new JsonParser();
JsonElement responseData = parser.parse(response);
if (!responseData.isJsonObject()){
// send an error like: "Hey, you did not pass an Object!
}
JsonObject obj = responseData.getAsJsonObject();
if (!obj.hasField("title")){
// send an error like: "Hey, we were expecting a field name title!
}
JsonElement titleAsElem = obj.get("title");
if (!titleAsElem.isString()){
// send an error like: "Hey, title is not an string!
}
// etc, etc, etc
我们可能不想这样做。
指定我们期望的结构的更具声明性的方法是创建特定的类。
class NewPostPayload {
private String title;
private List<String> categories;
private String content;
public String getTitle() { ... }
public void setTitle(String title) { ... }
public List<String> getCategories() { ... }
public void setCategories(List<String> categories){ ... }
public String getContent() { ... }
public void setContent(String content) { ... }
}
然后我们可以使用Jackson:
try {
ObjectMapper mapper = new ObjectMapper();
NewPostPayload newPost = mapper.readValue(request.body(), NewPostPayload.class);
} catch (JsonParseException e){
// Hey, you did not send a valid request!
}
这样,杰克逊会自动为我们检查有效载荷是否具有预期的结构。 我们可能想验证是否遵守其他约束。 例如,我们可能要检查标题是否为空,并且至少指定了一个类别。 我们可以创建一个仅用于验证的接口:
interface Validable {
boolean isValid();
}
class NewPostPayload implements Validable {
private String title;
private List<String> categories;
private String content;
public String getTitle() { ... }
public void setTitle(String title) { ... }
public List<String> getCategories() { ... }
public void setCategories(List<String> categories){ ... }
public String getContent() { ... }
public void setContent(String content) { ... }
public boolean isValid() {
return title != null && !title.isEmpty() && !categories.isEmpty();
}
}
仍然有很多无聊的getter和setter方法。 它们的信息量不是很大,只会污染代码。 我们可以使用Lombok摆脱它们。 Lombok是一个注释处理器,可以为您添加重复方法(getter,setter,equals,hashCode等)。 您可以将其视为编译器的插件,该插件可查找注释(例如@Data )并基于注释生成方法。 如果将其添加到依赖项中,maven会很好,但是您的IDE无法自动完成Lombok添加的方法。 您可能要安装插件。 对于Intellij Idea,我使用的是Lombok插件版本0.9.1,它的效果很好。
现在,您可以将类NewPostPayload修改为:
@Data
class NewPostPayload {
private String title;
private List<String> categories;
private String content;
public boolean isValid() {
return title != null && !title.isEmpty() && !categories.isEmpty();
}
}
好多了,是吗?
一个完整的例子
我们基本上需要做两件事:
- 插入新帖子
- 检索整个帖子列表
第一个操作应实现为POST(具有副作用),而第二个操作应实现为GET。 它们都对posts集合进行操作,因此我们将使用端点/ posts 。
让我们从插入帖子开始。 首先我们要解析
// insert a post (using HTTP post method)
post("/posts", (request, response) -> {
try {
ObjectMapper mapper = new ObjectMapper();
NewPostPayload creation = mapper.readValue(request.body(), NewPostPayload.class);
if (!creation.isValid()) {
response.status(HTTP_BAD_REQUEST);
return "";
}
int id = model.createPost(creation.getTitle(), creation.getContent(), creation.getCategories());
response.status(200);
response.type("application/json");
return id;
} catch (JsonParseException jpe) {
response.status(HTTP_BAD_REQUEST);
return "";
}
});
然后查看如何检索所有帖子:
// get all post (using HTTP get method)
get("/posts", (request, response) -> {
response.status(200);
response.type("application/json");
return dataToJson(model.getAllPosts());
});
最后的代码是:
package me.tomassetti;
import static spark.Spark.get;
import static spark.Spark.post;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.Data;
import spark.Request;
import spark.Response;
import spark.Route;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collector;
import java.util.stream.Collectors;
public class BlogService
{
private static final int HTTP_BAD_REQUEST = 400;
interface Validable {
boolean isValid();
}
@Data
static class NewPostPayload {
private String title;
private List<String> categories = new LinkedList<>();
private String content;
public boolean isValid() {
return title != null && !title.isEmpty() && !categories.isEmpty();
}
}
// In a real application you may want to use a DB, for this example we just store the posts in memory
public static class Model {
private int nextId = 1;
private Map<Integer, Post> posts = new HashMap<>();
@Data
class Post {
private int id;
private String title;
private List<String> categories;
private String content;
}
public int createPost(String title, String content, List<String> categories){
int id = nextId++;
Post post = new Post();
post.setId(id);
post.setTitle(title);
post.setContent(content);
post.setCategories(categories);
posts.put(id, post);
return id;
}
public List<Post> getAllPosts(){
return posts.keySet().stream().sorted().map((id) -> posts.get(id)).collect(Collectors.toList());
}
}
public static String dataToJson(Object data) {
try {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
StringWriter sw = new StringWriter();
mapper.writeValue(sw, data);
return sw.toString();
} catch (IOException e){
throw new RuntimeException("IOException from a StringWriter?");
}
}
public static void main( String[] args) {
Model model = new Model();
// insert a post (using HTTP post method)
post("/posts", (request, response) -> {
try {
ObjectMapper mapper = new ObjectMapper();
NewPostPayload creation = mapper.readValue(request.body(), NewPostPayload.class);
if (!creation.isValid()) {
response.status(HTTP_BAD_REQUEST);
return "";
}
int id = model.createPost(creation.getTitle(), creation.getContent(), creation.getCategories());
response.status(200);
response.type("application/json");
return id;
} catch (JsonParseException jpe) {
response.status(HTTP_BAD_REQUEST);
return "";
}
});
// get all post (using HTTP get method)
get("/posts", (request, response) -> {
response.status(200);
response.type("application/json");
return dataToJson(model.getAllPosts());
});
}
}
使用PostMan尝试应用程序
如果您更喜欢命令行,则可能要改用curl。 我喜欢不必转义JSON并拥有基本的编辑器,因此可以使用PostMan(Chrome插件)。
让我们插入一个帖子。 我们将所有字段指定为插入到请求正文中的Json对象的一部分。 我们获取创建的帖子的ID。
然后,我们可以获得帖子列表。 在这种情况下,我们使用GET(请求中没有正文),并获取所有帖子的数据(仅是我们在上面插入的帖子)。
结论
我不得不说,我对该项目感到非常惊讶。 我已经准备好了变得更糟:这是一种需要基本逻辑和大量管道的应用程序。 我发现Python,Clojure和Ruby在解决此类问题方面都做得很好,而当我用Java编写简单的Web应用程序时,逻辑就被样板代码淹没了。 好吧,事情可能会有所不同。 Spark,Lombok,Jackson和Java 8的结合确实很诱人。 我非常感谢这些软件的作者,他们确实在改善Java开发人员的生活。 我认为这也是一个教训:出色的框架可以经常改进很多事情,而这超出了我们的想象。
编辑:我从reddit的好人那里收到了一个改进示例的建议。 谢谢! 请保持良好的建议来!