免责声明 :这篇文章是关于名为Spark的Java微型Web框架的,而不是关于数据处理引擎Apache Spark的 。
在此博客文章中,我们将看到如何使用Spark构建简单的Web服务。 如免责声明中所述,Spark是受Ruby框架Sinatra启发的Java微型Web框架。 Spark的目的是简化操作,仅提供最少的功能集。 但是,它提供了用几行Java代码构建Web应用程序所需的一切。
入门
假设我们有一个带有一些属性的简单域类和一个提供一些基本CRUD功能的服务:
public class User {
private String id;
private String name;
private String email;
// getter/setter
}
public class UserService {
// returns a list of all users
public List<User> getAllUsers() { .. }
// returns a single user by id
public User getUser(String id) { .. }
// creates a new user
public User createUser(String name, String email) { .. }
// updates an existing user
public User updateUser(String id, String name, String email) { .. }
}
现在,我们希望将UserService的功能公开为RESTful API(为简单起见,我们将跳过REST的超媒体部分)。 为了访问,创建和更新用户对象,我们要使用以下URL模式:
得到 | /用户 | 获取所有用户的列表 |
得到 | / users / <id> | 获取特定用户 |
开机自检 | /用户 | 创建一个新用户 |
放 | / users / <id> | 更新用户 |
返回的数据应为JSON格式。
要开始使用Spark,我们需要以下Maven依赖项:
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.7</version>
</dependency>
Spark使用SLF4J进行日志记录,因此我们需要SLF4J活页夹才能查看日志和错误消息。 在此示例中,我们为此目的使用slf4j-simple依赖项。 但是,您也可以使用Log4j或您喜欢的任何其他绑定程序。 在类路径中使用slf4j-simple足以在控制台中查看日志输出。
我们还将使用GSON生成JSON输出,并使用JUnit编写简单的集成测试。 您可以在完整的pom.xml中找到这些依赖项。
返回所有用户
现在该创建一个负责处理传入请求的类了。 我们首先实现GET / users请求,该请求应返回所有用户的列表。
import static spark.Spark.*;
public class UserController {
public UserController(final UserService userService) {
get("/users", new Route() {
@Override
public Object handle(Request request, Response response) {
// process request
return userService.getAllUsers();
}
});
// more routes
}
}
注意第一行中的spark.Spark。*的静态导入。 这使我们可以访问各种静态方法,包括get(),post(),put()等。 在构造函数中,get()方法用于注册一个Route,该Route侦听/ users上的GET请求。 路由负责处理请求。 每当发出GET / users请求时,都会调用handle()方法。 在handle()内部,我们返回一个应发送给客户端的对象(在本例中为所有用户的列表)。
Spark从Java 8 Lambda表达式中受益匪浅。 Route是一个功能接口(仅包含一种方法),因此我们可以使用Java 8 Lambda表达式来实现它。 使用Lambda表达式,上面的Route定义如下所示:
get("/users", (req, res) -> userService.getAllUsers());
要启动该应用程序,我们必须创建一个简单的main()方法。 在main()内部,我们创建服务的实例,并将其传递给我们新创建的UserController:
public class Main {
public static void main(String[] args) {
new UserController(new UserService());
}
}
如果现在运行main(),Spark将启动一个侦听端口4567的嵌入式Jetty服务器。我们可以通过启动GET http:// localhost:4567 / users请求来测试我们的第一个路由。
如果服务返回包含两个用户对象的列表,则响应主体可能如下所示:
[com.mscharhag.sparkdemo.User@449c23fd, com.mscharhag.sparkdemo.User@437b26fe]
显然,这不是我们想要的回应。
Spark使用名为ResponseTransformer的接口将路由返回的对象转换为实际的HTTP响应。
ReponseTransformer看起来像这样:
public interface ResponseTransformer {
String render(Object model) throws Exception;
}
ResponseTransformer具有一个方法,该方法接受一个对象并返回此对象的String表示形式。 ResponseTransformer的默认实现只是在传递的对象上调用toString()(它创建如上所示的输出)。
由于我们要返回JSON,因此我们必须创建一个ResponseTransformer,将传递的对象转换为JSON。 为此,我们使用带有两个静态方法的小型JsonUtil类:
public class JsonUtil {
public static String toJson(Object object) {
return new Gson().toJson(object);
}
public static ResponseTransformer json() {
return JsonUtil::toJson;
}
}
toJson()是使用GSON将对象转换为JSON的通用方法。 第二种方法利用Java 8方法引用来返回ResponseTransformer实例。 ResponseTransformer还是一个功能接口,因此可以通过提供适当的方法实现(toJson())来满足它。 因此,每当调用json()时,我们都会获得一个新的ResponseTransformer,它利用了我们的toJson()方法。
在我们的UserController中,我们可以将ResponseTransformer作为第三个参数传递给Spark的get()方法:
import static com.mscharhag.sparkdemo.JsonUtil.*;
public class UserController {
public UserController(final UserService userService) {
get("/users", (req, res) -> userService.getAllUsers(), json());
...
}
}
再次注意第一行中JsonUtil。*的静态导入。 这使我们可以选择仅通过调用json()来创建新的ResponseTransformer。
现在,我们的响应如下所示:
[{
"id": "1866d959-4a52-4409-afc8-4f09896f38b2",
"name": "john",
"email": "john@foobar.com"
},{
"id": "90d965ad-5bdf-455d-9808-c38b72a5181a",
"name": "anna",
"email": "anna@foobar.com"
}]
我们还有一个小问题。 返回的响应带有错误的Content-Type 。 为了解决这个问题,我们可以注册一个设置JSON Content-Type的Filter:
after((req, res) -> {
res.type("application/json");
});
过滤器还是一个功能接口,因此可以通过一个简短的Lambda表达式实现。 在我们的Route处理完请求后,过滤器会将每个响应的Content-Type更改为application / json。 我们还可以使用before()代替after()来注册过滤器。 然后,在路由处理请求之前,将调用过滤器。
GET / users请求现在应该可以工作了!
返回特定用户
要返回特定用户,我们只需在UserController中创建一条新路由:
get("/users/:id", (req, res) -> {
String id = req.params(":id");
User user = userService.getUser(id);
if (user != null) {
return user;
}
res.status(400);
return new ResponseError("No user with id '%s' found", id);
}, json());
使用req.params(“:id”),我们可以从URL获取:id路径参数。 我们将此参数传递给我们的服务以获取相应的用户对象。 如果未找到具有传递ID的用户,则假定服务返回null。 在这种情况下,我们将HTTP状态代码更改为400(错误请求)并返回一个错误对象。
ResponseError是一个小的帮助程序类,我们用于将错误消息和异常转换为JSON。 看起来像这样:
public class ResponseError {
private String message;
public ResponseError(String message, String... args) {
this.message = String.format(message, args);
}
public ResponseError(Exception e) {
this.message = e.getMessage();
}
public String getMessage() {
return this.message;
}
}
现在,我们可以使用以下请求查询单个用户:
GET / users / 5f45a4ff-35a7-47e8-b731-4339c84962be
如果存在具有此ID的用户,我们将收到如下所示的响应:
{
"id": "5f45a4ff-35a7-47e8-b731-4339c84962be",
"name": "john",
"email": "john@foobar.com"
}
如果我们使用无效的用户ID,将创建ResponseError对象并将其转换为JSON。 在这种情况下,响应如下所示:
{
"message": "No user with id 'foo' found"
}
创建和更新用户
创建和更新用户非常容易。 就像返回所有用户的列表一样,这是通过单个服务调用完成的:
post("/users", (req, res) -> userService.createUser(
req.queryParams("name"),
req.queryParams("email")
), json());
put("/users/:id", (req, res) -> userService.updateUser(
req.params(":id"),
req.queryParams("name"),
req.queryParams("email")
), json());
要为HTTP POST或PUT请求注册路由,我们只需使用Spark的静态post()和put()方法。 在Route内部,我们可以使用req.queryParams()访问HTTP POST参数。
为了简单起见(并显示另一个Spark功能),我们不在路由内进行任何验证。 相反,我们假定如果传入无效值,则服务将引发IllegalArgumentException。
Spark为我们提供了注册ExceptionHandlers的选项。 如果在处理路由时引发Exception,则将调用ExceptionHandler。 ExceptionHandler是我们可以使用Java 8 Lambda表达式实现的另一个单一方法接口:
exception(IllegalArgumentException.class, (e, req, res) -> {
res.status(400);
res.body(toJson(new ResponseError(e)));
});
在这里,我们创建一个ExceptionHandler,如果抛出IllegalArgumentException则调用它。 捕获的Exception对象作为第一个参数传递。 我们将响应代码设置为400,并在响应正文中添加一条错误消息。
如果当email参数为空时服务抛出IllegalArgumentException,我们可能会收到如下响应:
{
"message": "Parameter 'email' cannot be empty"
}
控制器的完整资源可以在这里找到。
测试中
由于Spark的简单性质,因此为示例应用程序编写集成测试非常容易。
让我们从基本的JUnit测试设置开始:
public class UserControllerIntegrationTest {
@BeforeClass
public static void beforeClass() {
Main.main(null);
}
@AfterClass
public static void afterClass() {
Spark.stop();
}
...
}
在beforeClass()中,我们通过简单地运行main()方法来启动应用程序。 所有测试完成后,我们调用Spark.stop()。 这将停止运行我们的应用程序的嵌入式服务器。
之后,我们可以在测试方法中发送HTTP请求,并验证我们的应用程序返回了正确的响应。 一个发送创建新用户请求的简单测试如下所示:
@Test
public void aNewUserShouldBeCreated() {
TestResponse res = request("POST", "/users?name=john&email=john@foobar.com");
Map<String, String> json = res.json();
assertEquals(200, res.status);
assertEquals("john", json.get("name"));
assertEquals("john@foobar.com", json.get("email"));
assertNotNull(json.get("id"));
}
request()和TestResponse是两个小型的自制测试实用程序。 request()将HTTP请求发送到传递的URL,并返回TestResponse实例。 TestResponse只是一些HTTP响应数据的小包装。 request()和TestResponse的源包含在GitHub上的完整测试类中 。
结论
与其他Web框架相比,Spark仅提供了少量功能。 但是,它是如此简单,您可以在几分钟之内构建小型Web应用程序(即使您以前从未使用过Spark)。 如果您想研究Spark,则应该清楚地使用Java 8,它减少了您必须编写的代码量。
- 您可以在GitHub上找到示例项目的完整源代码。