Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Building REST services with Spring :: Learn how to easily build RESTful services with Spring

NotificationsYou must be signed in to change notification settings

spring-guides/tut-rest

Repository files navigation

tagsprojects
rest
hateoas
hypermedia
security
testing
oauth
spring-framework
spring-hateoas
spring-security
spring-security-oauth

REST has quickly become the de facto standard for building web services on the web because REST services are easy to build and easy to consume.

A much larger discussion can be had about how REST fits in the world of microservices.However, for this tutorial, we look only at building RESTful services.

Why REST? REST embraces the precepts of the web, including its architecture, benefits, and everything else. This is no surprise, given that its author (Roy Fielding) was involvedin probably a dozen specs which govern how the web operates.

What benefits? The web and its core protocol, HTTP, provide a stack of features:

  • Suitable actions (GET,POST,PUT,DELETE, and others)

  • Caching

  • Redirection and forwarding

  • Security (encryption and authentication)

These are all critical factors when building resilient services. However, that is not all. The web is built out of lots of tiny specs. This architecture lets it easily evolve without getting bogged down in "standards wars".

Developers can draw upon third-party toolkits that implement these diverse specs and instantly have both client and server technology at their fingertips.

By building on top of HTTP, REST APIs provide the means to build:

  • Backwards compatible APIs

  • Evolvable APIs

  • Scaleable services

  • Securable services

  • A spectrum of stateless to stateful services

Note that REST, however ubiquitous, is not a standardper se but an approach, a style, a set ofconstraints on your architecture that can help you build web-scale systems. This tutorial uses the Spring portfolio to build a RESTful service while takin advantage of the stackless features of REST.

Getting Started

To get started, you need:

As we work through this tutorial, we useSpring Boot. Go toSpring Initializr and add the following dependencies to a project:

  • Spring Web

  • Spring Data JPA

  • H2 Database

Change the Name to "Payroll" and then chooseGenerate Project. A.zip file downloads. Unzip it. Inside, you should find a simple, Maven-based project that includes apom.xml build file. (Note: You can use Gradle. The examples in this tutorial will be Maven-based.)

To complete the tutorial, you could start a new project from scratch or you could look at thesolution repository in GitHub.

If you choose to create your own blank project, this tutorial walks you through building your application sequentially. You do not need multiple modules.

Rather than providing a single, final solution, thecompleted GitHub repository uses modules to separate the solution into four parts.The modules in the GitHub solution repository build on one another, with thelinks module containing the final solution.The modules map to the following headers:

The Story so Far

Note
This tutorial starts by building up the code in thenonrest module.

We start off with the simplest thing we can construct.In fact, to make it as simple as possible, we can even leave out the concepts of REST.(Later on, we add REST, to understand the difference.)

Big picture: We are going to create a simple payroll service that manages the employees of a company. We store employee objects in a (H2 in-memory) database, and access them (through something called JPA). Then we wrap that with something that allows access over the internet (called the Spring MVC layer).

The following code defines anEmployee in our system.

nonrest/src/main/java/payroll/Employee.java
link:nonrest/src/main/java/payroll/Employee.java[role=include]

Despite being small, this Java class contains much:

  • @Entity is a JPA annotation to make this object ready for storage in a JPA-based data store.

  • id,name, androle are attributes of ourEmployee domain object.id is marked with more JPA annotations to indicate that it is the primary key and is automatically populated by the JPA provider.

  • A custom constructor is created when we need to create a new instance but do not yet have anid.

With this domain object definition, we can now turn toSpring Data JPA to handle the tedious database interactions.

Spring Data JPA repositories are interfaces with methods that support creating, reading, updating, and deleting records against a back end data store. Some repositories also support data paging and sorting, where appropriate. Spring Data synthesizes implementations based on conventions found in the naming of the methods in the interface.

Note
There are multiple repository implementations besides JPA. You can useSpring Data MongoDB,Spring Data Cassandra, and others. This tutorial sticks with JPA.

Spring makes accessing data easy. By declaring the followingEmployeeRepository interface, we can automatically:

  • Create new employees

  • Update existing employees

  • Delete employees

  • Find employees (one, all, or search by simple or complex properties)

nonrest/src/main/java/payroll/EmployeeRepository.java
link:nonrest/src/main/java/payroll/EmployeeRepository.java[role=include]

To get all this free functionality, all we have to do is declare an interface that extends Spring Data JPA’sJpaRepository, specifying the domain type asEmployee and theid type asLong.

Spring Data’srepository solution makes it possible to sidestep data store specifics and, instead, solve a majority of problems by using domain-specific terminology.

Believe it or not, this is enough to launch an application! A Spring Boot application is, at a minimum, apublic static void main entry-point and the@SpringBootApplication annotation. This tells Spring Boot to help out wherever possible.

nonrest/src/main/java/payroll/PayrollApplication.java
link:nonrest/src/main/java/payroll/PayrollApplication.java[role=include]

@SpringBootApplication is a meta-annotation that pulls incomponent scanning,auto-configuration, andproperty support. We do not diveinto the details of Spring Boot in this tutorial. However, in essence, it starts a servlet container and serves up our service.

An application with no data is not very interesting, so we preload that it has data. The following class gets loaded automatically by Spring:

nonrest/src/main/java/payroll/LoadDatabase.java
link:nonrest/src/main/java/payroll/LoadDatabase.java[role=include]

What happens when it gets loaded?

  • Spring Boot runs ALLCommandLineRunner beans once the application context is loaded.

  • This runner requests a copy of theEmployeeRepository you just created.

  • The runner creates two entities and stores them.

Right-click andRunPayRollApplication, and you get:

Fragment of console output showing preloading of data
...20yy-08-09 11:36:26.169  INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=1, name=Bilbo Baggins, role=burglar)20yy-08-09 11:36:26.174  INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=2, name=Frodo Baggins, role=thief)...

This is not thewhole log, but only the key bits of preloading data.

HTTP is the Platform

To wrap your repository with a web layer, you must turn to Spring MVC. Thanks to Spring Boot, you need add only a little code. Instead, we can focus on actions:

nonrest/src/main/java/payroll/EmployeeController.java
link:nonrest/src/main/java/payroll/EmployeeController.java[role=include]
  • @RestController indicates that the data returned by each method is written straight into the response body instead of rendering a template.

  • AnEmployeeRepository is injected by constructor into the controller.

  • We have routes for each operation (@GetMapping,@PostMapping,@PutMapping and@DeleteMapping, corresponding to HTTPGET,POST,PUT, andDELETE calls). (We recommend reading each method and understanding what they do.)

  • EmployeeNotFoundException is an exception used to indicate when an employee is looked up but not found.

nonrest/src/main/java/payroll/EmployeeNotFoundException.java
link:nonrest/src/main/java/payroll/EmployeeNotFoundException.java[role=include]

When anEmployeeNotFoundException is thrown, this extra tidbit of Spring MVC configuration is used to render anHTTP 404 error:

nonrest/src/main/java/payroll/EmployeeNotFoundAdvice.java
link:nonrest/src/main/java/payroll/EmployeeNotFoundAdvice.java[role=include]
  • @RestControllerAdvice signals that this advice is rendered straight into the response body.

  • @ExceptionHandler configures the advice to only respond when anEmployeeNotFoundException is thrown.

  • @ResponseStatus says to issue anHttpStatus.NOT_FOUND — that is, anHTTP 404 error.

  • The body of the advice generates the content. In this case, it gives the message of the exception.

To launch the application, you can right-click thepublic static void main inPayRollApplication and selectRun from your IDE.

Alternatively, Spring Initializr creates a Maven wrapper, so you can run the following command:

$ ./mvnw clean spring-boot:run

Alternatively, you can use your installed Maven version, as follows:

$ mvn clean spring-boot:run

When the app starts, you can immediately interrogate it, as follows:

$ curl -v localhost:8080/employees

Doing so yields the following:

Details
*   Trying ::1...* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> GET /employees HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 200< Content-Type: application/json;charset=UTF-8< Transfer-Encoding: chunked< Date: Thu, 09 Aug 20yy 17:58:00 GMT<* Connection #0 to host localhost left intact[{"id":1,"name":"Bilbo Baggins","role":"burglar"},{"id":2,"name":"Frodo Baggins","role":"thief"}]

You can see the pre-loaded data in a compacted format.

Now try to query a user that doesn’t exist, as follows:

$ curl -v localhost:8080/employees/99

When you do so, you get the following output:

Details
*   Trying ::1...* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> GET /employees/99 HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 404< Content-Type: text/plain;charset=UTF-8< Content-Length: 26< Date: Thu, 09 Aug 20yy 18:00:56 GMT<* Connection #0 to host localhost left intactCould not find employee 99

This message nicely shows anHTTP 404 error with the custom message:Could not find employee 99.

It is not hard to show the currently coded interactions.

Caution

If you use Windows command prompt to issue cURL commands, the following command probably does not work properly. You must either pick a terminal that support single-quoted arguments, or use double quotation marks and then escape the quotation marks inside the JSON.

To create a newEmployee record, use the following command in a terminal (the$ at the beginning signifies that what follows it is a terminal command):

$ curl -X POST localhost:8080/employees -H 'Content-type:application/json' -d '{"name": "Samwise Gamgee", "role": "gardener"}'

Then it stores the newly created employee and sends it back to us:

{"id":3,"name":"Samwise Gamgee","role":"gardener"}

You can update the user. For example, you can change the role:

$ curl -X PUT localhost:8080/employees/3 -H 'Content-type:application/json' -d '{"name": "Samwise Gamgee", "role": "ring bearer"}'

Now we can see the change reflected in the output:

{"id":3,"name":"Samwise Gamgee","role":"ring bearer"}
Caution
The way you construct your service can have significant impacts. In this situation, we saidupdate, butreplace is a better description. For example, if the name was NOT provided, it would instead get nulled out.

Finally, you can delete users, as follows:

$ curl -X DELETE localhost:8080/employees/3# Now if we look again, it's gone$ curl localhost:8080/employees/3Could not find employee 3

This is all well and good, but do we have a RESTful service yet? (The answer is no.)

What’s missing?

What Makes a Service RESTful?

So far, you have a web-based service that handles the core operations that involve employee data. However, that is not enough to make things "RESTful".

  • Pretty URLs, such as`/employees/3`, aren’t REST.

  • Merely usingGET,POST, and so on is not REST.

  • Having all the CRUD operations laid out is not REST.

In fact, what we have built so far is better described asRPC (Remote Procedure Call), because there is no way to know how to interact with this service. If you published this today, you wouldd also have to write a document or host a developer’s portal somewhere with all the details.

This statement of Roy Fielding’s may further lend a clue to the difference betweenREST andRPC:

I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating.

What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?

The side effect of nnot including hypermedia in our representations is that clients must hard-code URIs to navigate the API. This leads to the same brittle nature that predated the rise of e-commerce on the web. It signifies that our JSON output needs a little help.

Spring HATEOAS

Now we can introduceSpring HATEOAS, a Spring project aimed at helping you write hypermedia-driven outputs. To upgrade your service to being RESTful, add the following to your build:

Note
If you are following along in thesolution repository, the next section switches to therest module.
Adding Spring HATEOAS todependencies section ofpom.xml
link:rest/pom.xml[role=include]

This tiny library gives us the constructs that define a RESTful service and then render it in an acceptable format for client consumption.

A critical ingredient to any RESTful service is addinglinks to relevant operations. To make your controller more RESTful, add links like the following to the existingone method inEmployeeController:

Getting a single item resource
link:rest/src/main/java/payroll/EmployeeController.java[role=include]

You also need to include new imports:

Details
importorg.springframework.hateoas.EntityModel;importstaticorg.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
Note

This tutorial is based on Spring MVC and uses the static helper methods fromWebMvcLinkBuilder to build these links.If you are using Spring WebFlux in your project, you must instead useWebFluxLinkBuilder.

This is very similar to what we had before, but a few things have changed:

  • The return type of the method has changed fromEmployee toEntityModel<Employee>.EntityModel<T> is a generic container from Spring HATEOAS that includes not only the data but a collection of links.

  • linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel() asks that Spring HATEOAS build a link to theone method ofEmployeeController and flag it as aself link.

  • linkTo(methodOn(EmployeeController.class).all()).withRel("employees") asks Spring HATEOAS to build a link to the aggregate root,all(), and call it "employees".

What do we mean by "build a link?" One of Spring HATEOAS’s core types isLink. It includes aURI and arel (relation).Links are what empower the web. Before the World Wide Web, other document systems would render information or links, but it was the linking of documents WITH this kind of relationship metadata that stitched the web together.

Roy Fielding encourages building APIs with the same techniques that made the web successful, and links are one of them.

If you restart the application and query the employee record ofBilbo, you get a slightly different response than earlier:

Tip
Curling prettier

When your curl output gets more complex it can become hard to read. Use this orother tips to prettify the json returned by curl:

# The indicated part pipes the output to json_pp and asks it to make your JSON pretty. (Or use whatever tool you like!)#                                  v------------------vcurl -v localhost:8080/employees/1 | json_pp
RESTful representation of a single employee
{"id":1,"name":"Bilbo Baggins","role":"burglar","_links":{"self":{"href":"http://localhost:8080/employees/1"},"employees":{"href":"http://localhost:8080/employees"}}}

This decompressed output shows not only the data elements you saw earlier (id,name, androle) but also a_links entry that contains two URIs. This entire document is formatted usingHAL.

HAL is a lightweightmediatype that allows encoding not only data but also hypermedia controls, alerting consumers to other parts of the API to which they can navigate. In this case,there is a "self" link (kind of like athis statement in code) along with a link back to theaggregate root.

To make the aggregate root also be more RESTful, you want to include top level links while also including any RESTful components within.

So we modify the following (located in thenonrest module of the completed code):

Getting an aggregate root
link:nonrest/src/main/java/payroll/EmployeeController.java[role=include]

We want the following (located in therest module of the completed code):

Getting an aggregate rootresource
link:rest/src/main/java/payroll/EmployeeController.java[role=include]

That method, which used to be merelyrepository.findAll(), is "all grown up."" Not to worry. Now we can unpack it.

CollectionModel<> is another Spring HATEOAS container. It is aimed at encapsulating collections of resources instead of a single resource entity, such asEntityModel<> from earlier.CollectionModel<>, too, lets you include links.

Do not let that first statement slip by. What does "encapsulating collections" mean? Collections of employees?

Not quite.

Since we are talking REST, it should encapsulate collections ofemployee resources.

That is why you fetch all the employees but then transform them into a list ofEntityModel<Employee> objects. (Thanks Java Streams!)

If you restart the application and fetch the aggregate root, you can see what it looks like now:

curl -v localhost:8080/employees | json_pp
RESTful representation of a collection of employee resources
{"_embedded":{"employeeList":[{"id":1,"name":"Bilbo Baggins","role":"burglar","_links":{"self":{"href":"http://localhost:8080/employees/1"},"employees":{"href":"http://localhost:8080/employees"}}},{"id":2,"name":"Frodo Baggins","role":"thief","_links":{"self":{"href":"http://localhost:8080/employees/2"},"employees":{"href":"http://localhost:8080/employees"}}}]},"_links":{"self":{"href":"http://localhost:8080/employees"}}}

For this aggregate root, which serves up a collection of employee resources, there is a top-level"self"link. The"collection" is listed underneath the"_embedded" section. This is how HAL represents collections.

Each individual member of the collection has their information as well as related links.

What is the point of adding all these links? It makes it possible to evolve REST services over time.Existing links can be maintained while new links can be added in the future.Newer clients may take advantage of the new links, while legacy clients can sustain themselves on the old links.This is especially helpful if services get relocated and moved around.As long as the link structure is maintained,clients can still find and interact with things.

Simplifying Link Creation

Note
If you are following along in thesolution repository, the next section switches to theevolution module.

In the code earlier, did you notice the repetition in single employee link creation? The code to providea single link to an employee, as well as to create an "employees" link to the aggregate root, was showntwice. If that raised a concern, good! There’s a solution.

You need to define a function that convertsEmployee objects toEntityModel<Employee> objects.While you could easily code this method yourself, SpringHATEOAS’sRepresentationModelAssembler interface does the work for you. Create a new classEmployeeModelAssembler:

evolution/src/main/java/payroll/EmployeeModelAssembler.java
link:evolution/src/main/java/payroll/EmployeeModelAssembler.java[role=include]

This simple interface has one method:toModel(). It is based on converting a non-model object(Employee) into a model-based object (EntityModel<Employee>).

All the code you saw earlier in the controller can be moved into this class. Also, by applying SpringFramework’s@Component annotation, the assembler is automatically created when the app starts.

Note
Spring HATEOAS’s abstract base class for all models isRepresentationModel. However, for simplicity, werecommend usingEntityModel<T> as your mechanism to easily wrap all POJOs as models.

To leverage this assembler, you have only to alter theEmployeeController by injecting the assembler in the constructor:

Injecting EmployeeModelAssembler into the controller
link:evolution/src/main/java/payroll/EmployeeController.java[role=include]  ...}

From here, you can use that assembler in the single-item employee methodone that already exists inEmployeeController:

Getting single item resource using the assembler
link:evolution/src/main/java/payroll/EmployeeController.java[role=include]

This code is almost the same, except that, instead of creating theEntityModel<Employee> instance here, youdelegate it to the assembler. Maybe that is not impressive.

Applying the same thing in the aggregate root controller method is more impressive. This change is also to theEmployeeController class:

Getting aggregate root resource using the assembler
link:evolution/src/main/java/payroll/EmployeeController.java[role=include]

The code is, again, almost the same. However, you get to replace all thatEntityModel<Employee> creation logic withmap(assembler::toModel). Thanks to Java method references, it is super easy to plug in and simplify your controller.

Important
A key design goal of Spring HATEOAS is to make it easier to do The Right Thing™. In this scenario, that means adding hypermedia to your service without hard coding a thing.

At this stage, you have created a Spring MVC REST controller that actually produces hypermedia-powered content. Clients that do not speak HAL can ignore the extra bits while consuming the pure data. Clients that do speak HAL can navigate your empowered API.

But that is not the only thing needed to build a truly RESTful service with Spring.

Evolving REST APIs

With one additional library and a few lines of extra code, you have added hypermedia to your application.But that is not the only thing needed to make your service RESTful. An important facet of REST is the factthat it is neither a technology stack nor a single standard.

REST is a collection of architectural constraints that, when adopted, make your application much moreresilient. A key factor of resilience is that when you make upgrades to your services, your clientsdo not suffer downtime.

In the "olden" days, upgrades were notorious for breaking clients. In other words, an upgrade to theserver required an update to the client. In this day and age, hours or even minutes of downtime spentdoing an upgrade can cost millions in lost revenue.

Some companies require that you present management with a plan to minimize downtime. In the past, youcould get away with upgrading at 2:00 a.m. on a Sunday when load was at a minimum. But in today’sInternet-based e-commerce with international customers in other time zones, such strategies are not as effective.

SOAP-based services andCORBA-based services were incredibly brittle. It was hard to roll out aserver that could support both old and new clients. With REST-based practices, it is much easier,especially using the Spring stack.

Supporting Changes to the API

Imagine this design problem: You have rolled out a system with thisEmployee-based record. The system is a major hit. You have sold your system to countless enterprises. Suddenly, the need for an employee’s name to be split intofirstName andlastName arises.

Uh oh. You did not think of that.

Before you open up theEmployee class and replace the single fieldname withfirstName andlastName, stop and think. Does that break any clients? How long will it take to upgradethem? Do you even control all the clients accessing your services?

Downtime = lost money. Is management ready for that?

There is an old strategy that precedes REST by years.

Never delete a column in a database.
— Unknown

You can always add columns (fields) to a database table. But do not take one away. The principle in RESTfulservices is the same.

Add new fields to your JSON representations, but do not take any away. Like this:

JSON that supports multiple clients
{"id":1,"firstName":"Bilbo","lastName":"Baggins","role":"burglar","name":"Bilbo Baggins","_links":{"self":{"href":"http://localhost:8080/employees/1"},"employees":{"href":"http://localhost:8080/employees"}}}

This format showsfirstName,lastName, andname. While it sports duplication of information, the purpose is to supportboth old and new clients. That means you can upgrade the server without requiring clients to upgrade at the same time. This is good movethat should reduce downtime.

Not only should you show this information in both the "old way" and the "new way", but you should also process incoming data both ways.

Employee record that handles both "old" and "new" clients
link:evolution/src/main/java/payroll/Employee.java[role=include]

This class is similar to the previous version ofEmployee, with a few changes:

  • Fieldname has been replaced byfirstName andlastName.

  • A "virtual" getter for the oldname property,getName(), is defined. It uses thefirstName andlastName fields to produce a value.

  • A "virtual" setter for the oldname property,setName(), is also defined. It parses an incoming string and stores it into the proper fields.

Of course, not change to your API is as simple as splitting a string or merging two strings. Butitis surely not impossible to come up with a set of transforms for most scenarios, right?

Important

Do not forget to change how you preload your database (inLoadDatabase) to use this new constructor.

link:evolution/src/main/java/payroll/LoadDatabase.java[role=include]

Proper Responses

Another step in the right direction involves ensuring that each of your REST methods returns a proper response. Update the POST method (newEmployee) in theEmployeeController:

POST that handles "old" and "new" client requests
link:evolution/src/main/java/payroll/EmployeeController.java[role=include]

You also need to add the imports:

Details
importorg.springframework.hateoas.IanaLinkRelations;importorg.springframework.http.ResponseEntity;
  • The newEmployee object is saved, as before. However, the resulting object is wrapped in theEmployeeModelAssembler.

  • Spring MVC’sResponseEntity is used to create anHTTP 201 Created status message. This type of response typically includes aLocation response header, and we use the URI derived from the model’s self-related link.

  • Additionally, the model-based version of the saved object is returned.

With these tweaks in place, you can use the same endpoint to create a new employee resource and use the legacyname field:

$ curl -v -X POST localhost:8080/employees -H 'Content-Type:application/json' -d '{"name": "Samwise Gamgee", "role": "gardener"}' | json_pp

The output is as follows:

Details
> POST /employees HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*> Content-Type:application/json> Content-Length: 46>< Location: http://localhost:8080/employees/3< Content-Type: application/hal+json;charset=UTF-8< Transfer-Encoding: chunked< Date: Fri, 10 Aug 20yy 19:44:43 GMT<{  "id": 3,  "firstName": "Samwise",  "lastName": "Gamgee",  "role": "gardener",  "name": "Samwise Gamgee",  "_links": {    "self": {      "href": "http://localhost:8080/employees/3"    },    "employees": {      "href": "http://localhost:8080/employees"    }  }}

This not only has the resulting object rendered in HAL (bothname as well asfirstName andlastName), but also theLocation header populated withhttp://localhost:8080/employees/3.A hypermedia-powered client could opt to "surf" to this new resource and proceed to interact with it.

The PUT controller method (replaceEmployee) inEmployeeController needs similar tweaks:

Handling a PUT for different clients
link:evolution/src/main/java/payroll/EmployeeController.java[role=include]

TheEmployee object built by thesave() operation is then wrapped in theEmployeeModelAssembler to create anEntityModel<Employee> object. Using thegetRequiredLink() method, you can retrieve theLink created by theEmployeeModelAssembler with aSELF rel. This method returns aLink, which must be turned into aURI with thetoUri method.

Since we want a more detailed HTTP response code than200 OK, we use Spring MVC’sResponseEntity wrapper. It has a handystatic method (created()) where we can plug in the resource’s URI.It is debatable whetherHTTP 201 Created carries the right semantics, since we do not necessarily "create" a new resource. However, it comes pre-loaded with aLocation response header, so we use it. Restart your application, run the following command, and observe the results:

$ curl -v -X PUT localhost:8080/employees/3 -H 'Content-Type:application/json' -d '{"name": "Samwise Gamgee", "role": "ring bearer"}' | json_pp
Details
* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> PUT /employees/3 HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*> Content-Type:application/json> Content-Length: 49>< HTTP/1.1 201< Location: http://localhost:8080/employees/3< Content-Type: application/hal+json;charset=UTF-8< Transfer-Encoding: chunked< Date: Fri, 10 Aug 20yy 19:52:56 GMT{"id": 3,"firstName": "Samwise","lastName": "Gamgee","role": "ring bearer","name": "Samwise Gamgee","_links": {"self": {"href": "http://localhost:8080/employees/3"},"employees": {"href": "http://localhost:8080/employees"}}}

That employee resource has now been updated and the location URI has been sent back. Finally, update the DELETE operation (deleteEmployee) inEmployeeController:

Handling DELETE requests
link:evolution/src/main/java/payroll/EmployeeController.java[role=include]

This returns anHTTP 204 No Content response. Restart your application, run the following command, and observe the results:

$ curl -v -X DELETE localhost:8080/employees/1
Details
* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> DELETE /employees/1 HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 204< Date: Fri, 10 Aug 20yy 21:30:26 GMT
Important
Making changes to the fields in theEmployee class requires coordination with your database team, sothat they can properly migrate existing content into the new columns.

You are now ready for an upgrade that does not disturb existing clients while newer clients can take advantage of the enhancements.

By the way, are you worried about sending too much information over the wire? In some systems where every byte counts,evolution of APIs may need to take a backseat. However, you should not pursue such premature optimization until you measure the impact of your changes.

Building links into your REST API

Note
If you are following along in thesolution repository, the next section switches to thelinks module.

So far, you have built an evolvable API with bare bones links.To grow your API and better serve your clients, you need to embrace the concept ofHypermedia as the Engine of Application State.

What does that mean? This section explores it in detail.

Business logic inevitably builds up rules that involve processes.The risk of such systems is we often carry such server-side logic into clients and build up strong coupling.REST is about breaking down such connections and minimizing such coupling.

To show how to cope with state changes without triggering breaking changes in clients, imagine adding a system that fulfills orders.

As a first step, define a newOrder record:

links/src/main/java/payroll/Order.java
link:links/src/main/java/payroll/Order.java[role=include]
  • The class requires a JPA@Table annotation that changes the table’s name toCUSTOMER_ORDER becauseORDER is not a valid name for table.

  • It includes adescription field as well as astatus field.

Orders must go through a certain series of state transitions from the time a customer submits an order and it is eitherfulfilled or cancelled. This can be captured as a Javaenum calledStatus:

links/src/main/java/payroll/Status.java
link:links/src/main/java/payroll/Status.java[role=include]

Thisenum captures the various states anOrder can occupy. For this tutorial, we keep it simple.

To support interacting with orders in the database, you must define a corresponding Spring Data repository calledOrderRepository:

Spring Data JPA’sJpaRepository base interface
interfaceOrderRepositoryextendsJpaRepository<Order,Long> {}

We also need to create a new exception class calledOrderNotFoundException:

Details
link:links/src/main/java/payroll/OrderNotFoundException.java[role=include]

With this in place, you can now define a basicOrderController with the required imports:

Import Statements
importjava.util.List;importjava.util.stream.Collectors;importstaticorg.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;importorg.springframework.hateoas.CollectionModel;importorg.springframework.hateoas.EntityModel;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RestController;
links/src/main/java/payroll/OrderController.java
link:links/src/main/java/payroll/OrderController.java[role=include]}
  • It contains the same REST controller setup as the controllers you have built so far.

  • It injects both anOrderRepository and a (not yet built)OrderModelAssembler.

  • The first two Spring MVC routes handle the aggregate root as well as a single itemOrder resource request.

  • The third Spring MVC route handles creating new orders, by starting them in theIN_PROGRESS state.

  • All the controller methods return one of Spring HATEOAS’sRepresentationModel subclasses to properly render hypermedia (or a wrapper around such a type).

Before building theOrderModelAssembler, we should discuss what needs to happen. You are modeling the flow of states betweenStatus.IN_PROGRESS,Status.COMPLETED, andStatus.CANCELLED. A natural thing when serving up such data to clients is tolet the clients make the decision about what they can do, based on this payload.

But that would be wrong.

What happens when you introduce a new state in this flow? The placement of various buttons on the UI would probably be erroneous.

What if you changed the name of each state, perhaps while coding international support and showing locale-specific text for each state?That would most likely break all the clients.

EnterHATEOAS orHypermedia as the Engine of Application State. Instead of clients parsing the payload, give them linksto signal valid actions. Decouple state-based actions from the payload of data. In other words, whenCANCEL andCOMPLETE are valid actions,you should dynamically add them to the list of links. Clients need to show users the corresponding buttons only when the links exist.

This decouples clients from having to know when such actions are valid, reducing the risk of the server and its clients gettingout of sync on the logic of state transitions.

Having already embraced the concept of Spring HATEOASRepresentationModelAssembler components, theOrderModelAssembleris the perfect place to capture the logic for this business rule:

links/src/main/java/payroll/OrderModelAssembler.java
link:links/src/main/java/payroll/OrderModelAssembler.java[role=include]

This resource assembler always includes theself link to the single-item resource as well as a link back to the aggregate root.However, it also includes two conditional links toOrderController.cancel(id) as well asOrderController.complete(id) (not yet defined). Theselinks are shown only when the order’s status isStatus.IN_PROGRESS.

If clients can adopt HAL and the ability to read links instead of simply reading the data of plain old JSON, they can tradein the need for domain knowledge about the order system. This naturally reduces coupling between client and server. Italso opens the door to tuning the flow of order fulfillment without breaking clients in the process.

To round out order fulfillment, add the following to theOrderController for thecancel operation:

Creating a "cancel" operation in the OrderController
link:links/src/main/java/payroll/OrderController.java[role=include]

It checks theOrder status before letting it be cancelled. If it is not a valid state, it returns anRFC-7807Problem,a hypermedia-supporting error container. If the transition is indeed valid, it transitions theOrder toCANCELLED.

Now we need to add this to theOrderController as well for order completion:

Creating a "complete" operation in the OrderController
link:links/src/main/java/payroll/OrderController.java[role=include]

This implements similar logic to prevent anOrder status from being completed unless in the proper state.

Let’s updateLoadDatabase to pre-load someOrder objectss along with theEmployee objects it was loading before.

Updating the database pre-loader
link:links/src/main/java/payroll/LoadDatabase.java[role=include]

Now you can test. Restart your application to make sure you are running the latest code changes. To use the newly minted order service, you can perform a few operations:

$ curl -v http://localhost:8080/orders | json_pp
Details
{  "_embedded": {    "orderList": [      {        "id": 3,        "description": "MacBook Pro",        "status": "COMPLETED",        "_links": {          "self": {            "href": "http://localhost:8080/orders/3"          },          "orders": {            "href": "http://localhost:8080/orders"          }        }      },      {        "id": 4,        "description": "iPhone",        "status": "IN_PROGRESS",        "_links": {          "self": {            "href": "http://localhost:8080/orders/4"          },          "orders": {            "href": "http://localhost:8080/orders"          },          "cancel": {            "href": "http://localhost:8080/orders/4/cancel"          },          "complete": {            "href": "http://localhost:8080/orders/4/complete"          }        }      }    ]  },  "_links": {    "self": {      "href": "http://localhost:8080/orders"    }  }}

This HAL document immediately shows different links for each order, based upon its present state.

  • The first order, beingCOMPLETED, only has the navigational links. The state transition links are not shown.

  • The second order, beingIN_PROGRESS, additionally has thecancel link as well as thecomplete link.

Now try cancelling an order:

$ curl -v -X DELETE http://localhost:8080/orders/4/cancel | json_pp
Note
You may need to replace the number 4 in the preceding URL, based on the specific IDs in your database. That information can be found from the previous/orders call.
Details
> DELETE /orders/4/cancel HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 200< Content-Type: application/hal+json;charset=UTF-8< Transfer-Encoding: chunked< Date: Mon, 27 Aug 20yy 15:02:10 GMT<{  "id": 4,  "description": "iPhone",  "status": "CANCELLED",  "_links": {    "self": {      "href": "http://localhost:8080/orders/4"    },    "orders": {      "href": "http://localhost:8080/orders"    }  }}

This response shows anHTTP 200 status code, indicating that it was successful. The response HAL document shows that order in itsnew state (CANCELLED). Also, the state-altering links gone.

Now try the same operation again:

$ curl -v -X DELETE http://localhost:8080/orders/4/cancel | json_pp
Details
* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> DELETE /orders/4/cancel HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 405< Content-Type: application/problem+json< Transfer-Encoding: chunked< Date: Mon, 27 Aug 20yy 15:03:24 GMT<{  "title": "Method not allowed",  "detail": "You can't cancel an order that is in the CANCELLED status"}

You can see anHTTP 405 Method Not Allowed response.DELETE has become an invalid operation. TheProblem responseobject clearly indicates that you are not allowed to "cancel" an order already in the "CANCELLED" status.

Additionally, trying to complete the same order also fails:

$ curl -v -X PUT localhost:8080/orders/4/complete | json_pp
Details
* TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> PUT /orders/4/complete HTTP/1.1> Host: localhost:8080> User-Agent: curl/7.54.0> Accept: */*>< HTTP/1.1 405< Content-Type: application/problem+json< Transfer-Encoding: chunked< Date: Mon, 27 Aug 20yy 15:05:40 GMT<{  "title": "Method not allowed",  "detail": "You can't complete an order that is in the CANCELLED status"}

With all this in place, your order fulfillment service is capable of conditionally showing what operations are available. Italso guards against invalid operations.

By using the protocol of hypermedia and links, clients can be made sturdier and be less likely to break simply becauseof a change in the data. Spring HATEOAS eases building the hypermedia you need to serve to your clients.

Summary

Throughout this tutorial, you have engaged in various tactics to build REST APIs. As it turns out, REST is not just about pretty URIs and returning JSON instead of XML.

Instead, the following tactics help make your services less likely to break existing clients you may or may not control:

  • Do not remove old fields. Instead, support them.

  • Use rel-based links so clients need not hard code URIs.

  • Retain old links as long as possible. Even if you have to change the URI, keep the rels so that older clientshave a path to the newer features.

  • Use links, not payload data, to instruct clients when various state-driving operations are available.

It may appear to be a bit of effort to build upRepresentationModelAssembler implementations for eachresource type and to use these components in all of your controllers. However, this extra bit of server-sidesetup (made easy thanks to Spring HATEOAS) can ensure the clients you control (and more importantly, thoseyou do not control) can upgrade with ease as you evolve your API.

This concludes our tutorial on how to build RESTful services using Spring. Each section of this tutorial is managed as a separatesubproject in a single github repo:

  • nonrest — Simple Spring MVC app with no hypermedia

  • rest — Spring MVC + Spring HATEOAS app with HAL representations of each resource

  • evolution — REST app where a field is evolved but old data is retained for backward compatibility

  • links — REST app where conditional links are used to signal valid state changes to clients

To view more examples of using Spring HATEOAS, seehttps://github.com/spring-projects/spring-hateoas-examples.

To do some more exploring, check out the following video by Spring teammate Oliver Drotbohm:

About

Building REST services with Spring :: Learn how to easily build RESTful services with Spring

Resources

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp