REST API with Play Framework in Java – 在Java中使用Play框架的REST API

最后修改: 2016年 10月 11日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The purpose of this tutorial is to explore the Play Framework and learn how to build REST services with it using Java.

本教程的目的是探索Play框架,并学习如何使用Java构建REST服务。

We’ll put together a REST API to create, retrieve, update, and delete student records.

我们将组建一个REST API来创建、检索、更新和删除学生记录。

In such applications, we would normally have a database to store student records. The Play Framework has a built-in H2 database, along with support for JPA with Hibernate and other persistence frameworks.

在这样的应用程序中,我们通常会有一个数据库来存储学生记录。Play框架有一个内置的H2数据库,同时支持JPA与Hibernate和其他持久性框架。

However, to keep things simple and focus on the most important stuff, we will use a simple map to store student objects with unique IDs.

然而,为了使事情简单化,并将注意力集中在最重要的东西上,我们将使用一个简单的地图来存储具有唯一ID的学生对象。

2. Create a New Application

2.创建一个新的应用程序

Once we’ve installed the Play Framework as described in our Introduction to the Play Framework, we’re ready to create our application.

一旦我们按照Play框架介绍中的描述安装了Play框架,我们就可以准备创建我们的应用程序了。

Let’s use the sbt command to create a new application called student-api using play-java-seed:

让我们使用sbt命令,使用play-java-seed创建一个名为student-api的新应用程序。

sbt new playframework/play-java-seed.g8

3. Models

3.模型

With our application scaffolding in place, let’s navigate to student-api/app/models and create a Java bean for handling student information:

有了我们的应用程序支架,让我们导航到student-api/app/models并创建一个处理学生信息的Java bean。

public class Student {
    private String firstName;
    private String lastName;
    private int age;
    private int id;

    // standard constructors, getters and setters
}

We’ll now create a simple data store – backed by a HashMap – for student data, with helper methods to perform CRUD operations:

现在我们将为学生数据创建一个简单的数据存储–由HashMap支持–,用辅助方法来执行CRUD操作。

public class StudentStore {
    private Map<Integer, Student> students = new HashMap<>();

    public Optional<Student> addStudent(Student student) {
        int id = students.size();
        student.setId(id);
        students.put(id, student);
        return Optional.ofNullable(student);
    }

    public Optional<Student> getStudent(int id) {
        return Optional.ofNullable(students.get(id));
    }

    public Set<Student> getAllStudents() {
        return new HashSet<>(students.values());
    }

    public Optional<Student> updateStudent(Student student) {
        int id = student.getId();
        if (students.containsKey(id)) {
            students.put(id, student);
            return Optional.ofNullable(student);
        }
        return null;
    }

    public boolean deleteStudent(int id) {
        return students.remove(id) != null;
    }
}

4. Controllers

4.控制器

Let’s head over to student-api/app/controllers and create a new controller called StudentController.java. We’ll step through the code incrementally.

让我们前往student-api/app/controllers,创建一个新的控制器,名为StudentController.java。我们将循序渐进地完成代码。

First off, we need to configure an HttpExecutionContext. We’ll implement our actions using asynchronous, non-blocking code. This means that our action methods will return CompletionStage<Result> instead of just Result. This has the benefit of allowing us to write long-running tasks without blocking.

首先,我们需要配置一个HttpExecutionContext我们将使用异步、非阻塞的代码实现我们的动作。这意味着我们的动作方法将返回CompletionStage<Result>,而不是仅仅返回Result。这样做的好处是,我们可以在不阻塞的情况下编写长期运行的任务。

There is just one caveat when dealing with asynchronous programming in a Play Framework controller: we have to provide an HttpExecutionContext. If we don’t supply the HTTP execution context, we’ll get the infamous error “There is no HTTP Context available from here” when calling the action method.

在Play Framework控制器中处理异步编程时,只有一个注意事项:我们必须提供一个HttpExecutionContext.如果我们不提供HTTP执行上下文,在调用动作方法时,我们会得到臭名昭著的错误 “There is no HTTP Context available from here”。

Let’s inject it:

让我们来注射它。

private HttpExecutionContext ec;
private StudentStore studentStore;

@Inject
public StudentController(HttpExecutionContext ec, StudentStore studentStore) {
    this.studentStore = studentStore;
    this.ec = ec;
}

Notice we have also added the StudentStore and injected both fields in the constructor of the controller using the @Inject annotation. Having done this, we can now proceed with the implementation of the action methods.

注意我们还添加了StudentStore,并使用@Inject注解在控制器的构造函数中注入了这两个字段。做完这些,我们现在可以继续执行动作方法了。

Note that Play ships with Jackson to allow for data processing – so we can import any Jackson classes we need without external dependencies.

请注意,Play与Jackson一起,允许进行数据处理–所以我们可以导入任何我们需要的Jackson类,而不需要依赖外部。

Let’s define a utility class to perform repetitive operations. In this case, building HTTP responses.

让我们定义一个实用类来执行重复性的操作。在这种情况下,构建HTTP响应。

So let’s create student-api/app/utils package and add Util.java in it:

因此,让我们创建student-api/app/utils包并在其中添加Util.java

public class Util {
    public static ObjectNode createResponse(Object response, boolean ok) {
        ObjectNode result = Json.newObject();
        result.put("isSuccessful", ok);
        if (response instanceof String) {
            result.put("body", (String) response);
        } else {
            result.putPOJO("body", response);
        }
        return result;
    }
}

With this method, we’ll be creating standard JSON responses with a boolean isSuccessful key and the response body.

通过这个方法,我们将创建标准的JSON响应,其中有一个布尔值isSuccessful键和响应体。

We can now step through the actions of the controller class.

现在我们可以逐步完成控制器类的动作。

4.1. The create Action

4.1.创建行动

Mapped as a POST action, this method handles the creation of the Student object:

作为一个POST动作,该方法处理Student对象的创建。

public CompletionStage<Result> create(Http.Request request) {
    JsonNode json = request.body().asJson();
    return supplyAsync(() -> {
        if (json == null) {
            return badRequest(Util.createResponse("Expecting Json data", false));
        }

        Optional<Student> studentOptional = studentStore.addStudent(Json.fromJson(json, Student.class));
        return studentOptional.map(student -> {
            JsonNode jsonObject = Json.toJson(student);
            return created(Util.createResponse(jsonObject, true));
        }).orElse(internalServerError(Util.createResponse("Could not create data.", false)));
    }, ec.current());
}

We use a call from the injected Http.Request class to get the request body into Jackson’s JsonNode class. Notice how we use the utility method to create a response if the body is null.

我们使用从注入的Http.Request类中的调用来获取请求体到Jackson的JsonNode类。请注意,如果主体是null,我们如何使用实用方法来创建一个响应。

We’re also returning a CompletionStage<Result>, which enables us to write non-blocking code using the CompletedFuture.supplyAsync method.

我们还将返回一个CompletionStage<Result>,这使得我们能够使用CompletedFuture.supplyAsync方法编写非阻塞代码。

We can pass to it any String or a JsonNode, along with a boolean flag to indicate status.

我们可以向它传递任何StringJsonNode,以及一个boolean标志来表示状态。

Notice also how we use Json.fromJson() to convert the incoming JSON object into a Student object and back to JSON for the response.

请注意我们是如何使用Json.fromJson()将传入的JSON对象转换为Student对象,然后再转换为JSON作为响应。

Finally, instead of ok() which we are used to, we are using the created helper method from the play.mvc.results package. The idea is to use a method that gives the correct HTTP status for the action being performed within a particular context. For example, ok() for HTTP OK 200 status, and created() when HTTP CREATED 201 is the result status as used above. This concept will come up throughout the rest of the actions.

最后,我们使用了play.mvc.results包中的created辅助方法,而不是我们所习惯的ok()。我们的想法是使用一个方法,为在特定上下文中执行的动作提供正确的HTTP状态。例如,ok()用于HTTP OK 200状态,而created()则用于HTTP CREATED 201的结果状态。这个概念将在其余的动作中出现。

4.2. The update Action

4.2.update行动

A PUT request to http://localhost:9000/ hits the StudentController.update method, which updates the student information by calling the updateStudent method of the StudentStore:

http://localhost:9000/ PUT 请求击中StudentController.update方法,该方法通过调用StudentStoreupdateStudent方法更新学生信息。

public CompletionStage<Result> update(Http.Request request) {
    JsonNode json = request.body().asJson();
    return supplyAsync(() -> {
        if (json == null) {
            return badRequest(Util.createResponse("Expecting Json data", false));
        }
        Optional<Student> studentOptional = studentStore.updateStudent(Json.fromJson(json, Student.class));
        return studentOptional.map(student -> {
            if (student == null) {
                return notFound(Util.createResponse("Student not found", false));
            }
            JsonNode jsonObject = Json.toJson(student);
            return ok(Util.createResponse(jsonObject, true));
        }).orElse(internalServerError(Util.createResponse("Could not create data.", false)));
    }, ec.current());
}

4.3. The retrieve Action

4.3.retrieve行动

To retrieve a student, we pass in the id of the student as a path parameter in a GET request to http://localhost:9000/:id. This will hit the retrieve action:

为了检索一个学生,我们在GET请求中把学生的id作为路径参数传给http://localhost:9000/:id。这将触发retrieve动作。

public CompletionStage<Result> retrieve(int id) {
    return supplyAsync(() -> {
        final Optional<Student> studentOptional = studentStore.getStudent(id);
        return studentOptional.map(student -> {
            JsonNode jsonObjects = Json.toJson(student);
            return ok(Util.createResponse(jsonObjects, true));
        }).orElse(notFound(Util.createResponse("Student with id:" + id + " not found", false)));
    }, ec.current());
}

4.4. The delete Action

4.4.delete行动

The delete action is mapped to http://localhost:9000/:id. We supply the id to identify which record to delete:

删除行动被映射到http://localhost:9000/:id。我们提供id来识别要删除的记录。

public CompletionStage<Result> delete(int id) {
    return supplyAsync(() -> {
        boolean status = studentStore.deleteStudent(id);
        if (!status) {
            return notFound(Util.createResponse("Student with id:" + id + " not found", false));
        }
        return ok(Util.createResponse("Student with id:" + id + " deleted", true));
    }, ec.current());
}

4.5. The listStudents Action

4.5.listStudents动作

Finally, the listStudents action returns a list of all the students that have been stored so far. It’s mapped to http://localhost:9000/ as a GET request:

最后,listStudents动作返回一个到目前为止已经存储的所有学生的列表。它被映射到http://localhost:9000/作为一个GET请求。

public CompletionStage<Result> listStudents() {
    return supplyAsync(() -> {
        Set<Student> result = studentStore.getAllStudents();
        ObjectMapper mapper = new ObjectMapper();
        JsonNode jsonData = mapper.convertValue(result, JsonNode.class);
        return ok(Util.createResponse(jsonData, true));
    }, ec.current());
}

5. Mappings

5.映射

Having set up our controller actions, we can now map them by opening the file student-api/conf/routes and adding these routes:

设置好控制器动作后,我们现在可以通过打开student-api/conf/routes文件并添加这些路由来映射它们。

GET     /                           controllers.StudentController.listStudents()
GET     /:id                        controllers.StudentController.retrieve(id:Int)
POST    /                           controllers.StudentController.create(request: Request)
PUT     /                           controllers.StudentController.update(request: Request)
DELETE  /:id                        controllers.StudentController.delete(id:Int)
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)

The /assets endpoint must always be present for downloading static resources.

下载静态资源时必须始终存在/assets端点。

After this, we’re done with building the Student API.

在这之后,我们就完成了对Student API的构建。

To learn more about defining route mappings, visit our Routing in Play Applications tutorial.

要了解有关定义路由映射的更多信息,请访问我们的Play 应用程序中的路由教程。

6. Testing

6.测试

We can now run tests on our API by sending requests to http://localhost:9000/ and adding the appropriate context. Running the base path from the browser should output:

现在我们可以通过向http://localhost:9000/发送请求并添加适当的上下文来对我们的API运行测试。从浏览器中运行基本路径应该会输出。

{
     "isSuccessful":true,
     "body":[]
}

As we can see, the body is empty since we haven’t added any records yet. Using curl, let’s run some tests (alternatively, we can use a REST client like Postman).

正如我们所看到的,主体是空的,因为我们还没有添加任何记录。使用curl,让我们运行一些测试(或者,我们可以使用像Postman这样的REST客户端)。

Let’s open up a terminal window and execute the curl command to add a student:

让我们打开一个终端窗口,执行curl命令,添加一个学生

curl -X POST -H "Content-Type: application/json" \
 -d '{"firstName":"John","lastName":"Baeldung","age": 18}' \
 http://localhost:9000/

This will return the newly created student:

这将返回新创建的学生。

{ 
    "isSuccessful":true,
    "body":{ 
        "firstName":"John",
        "lastName":"Baeldung",
        "age":18,
        "id":0
    }
}

After running the above test, loading http://localhost:9000 from the browser should now give us:

运行上述测试后,从浏览器加载http://localhost:9000,现在应该给我们。

{ 
    "isSuccessful":true,
    "body":[ 
        { 
            "firstName":"John",
            "lastName":"Baeldung",
            "age":18,
            "id":0
        }
    ]
}

The id attribute will be incremented for every new record we add.

我们每增加一条新记录,id属性就会递增。

To delete a record we send a DELETE request:

为了删除一条记录,我们发送一个DELETE请求。

curl -X DELETE http://localhost:9000/0
{ 
    "isSuccessful":true,
    "body":"Student with id:0 deleted"
}

In the above test, we delete the record created in the first test, now let’s create it again so that we can test the update method:

在上面的测试中,我们删除了在第一个测试中创建的记录,现在让我们再次创建它,以便我们可以测试update方法

curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"Baeldung","age": 18}' \
http://localhost:9000/
{ 
    "isSuccessful":true,
    "body":{ 
        "firstName":"John",
        "lastName":"Baeldung",
        "age":18,
        "id":0
    }
}

Let’s now update the record by setting the first name to “Andrew” and age to 30:

现在让我们更新记录,将名字设为 “Andrew”,年龄设为30岁。

curl -X PUT -H "Content-Type: application/json" \
-d '{"firstName":"Andrew","lastName":"Baeldung","age": 30,"id":0}' \
http://localhost:9000/
{ 
    "isSuccessful":true,
    "body":{ 
        "firstName":"Andrew",
        "lastName":"Baeldung",
        "age":30,
        "id":0
    }
}

The above test demonstrates the change in the value of the firstName and age fields after updating the record.

上面的测试展示了更新记录后firstNameage字段的数值变化。

Let’s create some extra dummy records, we’ll add two: John Doe and Sam Baeldung:

让我们创建一些额外的假记录,我们将添加两个。John Doe和Sam Baeldung。

curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"Doe","age": 18}' \
http://localhost:9000/
curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"Sam","lastName":"Baeldung","age": 25}' \
http://localhost:9000/

Now, let’s get all the records:

现在,让我们拿回所有的记录。

curl -X GET http://localhost:9000/
{ 
    "isSuccessful":true,
    "body":[ 
        { 
            "firstName":"Andrew",
            "lastName":"Baeldung",
            "age":30,
            "id":0
        },
        { 
            "firstName":"John",
            "lastName":"Doe",
            "age":18,
            "id":1
        },
        { 
            "firstName":"Sam",
            "lastName":"Baeldung",
            "age":25,
            "id":2
        }
    ]
}

With the above test, we are ascertaining the proper functioning of the listStudents controller action.

通过上述测试,我们确定了listStudents控制器动作的正常运行。

7. Conclusion

7.结论

In this article, we’ve shown how to build a full-fledged REST API using the Play Framework.

在这篇文章中,我们展示了如何使用Play框架构建一个成熟的REST API。

As usual, the source code for this tutorial is available over on GitHub.

像往常一样,本教程的源代码可以在GitHub上获得