SpringBoot 2 build REST service-build REST API in your application

So far, we have established an evolvable API with simple links. In order to develop our API and better serve our clients, we need to embrace the concept of Hypermedia as the application state engine .

what does this mean? In this part, we will study it in detail.

Business logic inevitably establishes rules that involve processes. The risk of this type of system is that we often bring this type of server-side logic to the client and establish a strong coupling. REST aims to dismantle this type of connection and minimize this coupling.

To illustrate how to respond to a change in state without triggering a change in the client, imagine adding a system that can receive orders.

The first step, the definition of a Orderrecord:
links/src/main/java/payroll/Order.java

package payroll;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Data
@Table(name = "CUSTOMER_ORDER")
class Order {

  private @Id @GeneratedValue Long id;

  private String description;
  private Status status;

  Order() {}

  Order(String description, Status status) {

    this.description = description;
    this.status = status;
  }
}
  • This class requires a JPA's @Tableannotation will change the name of the table CUSTOMER_ORDER, because ORDERthe effective name is not the table;
  • It includes descriptionfields and statusfields.

From the time the client submits the order to when the order is completed or cancelled, the order must undergo a series of specific state transitions. It can be captured as Java enum:

package payroll;

enum Status {

  IN_PROGRESS,
  COMPLETED,
  CANCELLED;
}

The enumcapture various states of Order can hold. For this tutorial, let us keep it simple.

To support interaction with orders in the database, we must define a corresponding Spring Data repository:

Spring Data JPA's JpaRepositorybasic interface

interface OrderRepository extends JpaRepository<Order, Long> {
}

Now, we can define a basic OrderController:
links/src/main/java/payroll/OrderController.java

@RestController
class OrderController {

  private final OrderRepository orderRepository;
  private final OrderModelAssembler assembler;

  OrderController(OrderRepository orderRepository,
                    OrderModelAssembler assembler) {

    this.orderRepository = orderRepository;
    this.assembler = assembler;
  }

  @GetMapping("/orders")
  CollectionModel<EntityModel<Order>> all() {

    List<EntityModel<Order>> orders = orderRepository.findAll().stream()
      .map(assembler::toModel)
      .collect(Collectors.toList());

    return new CollectionModel<>(orders,
      linkTo(methodOn(OrderController.class).all()).withSelfRel());
  }

  @GetMapping("/orders/{id}")
  EntityModel<Order> one(@PathVariable Long id) {
    Order order = orderRepository.findById(id)
        .orElseThrow(() -> new OrderNotFoundException(id));

    return assembler.toModel(order);
  }

  @PostMapping("/orders")
  ResponseEntity<EntityModel<Order>> newOrder(@RequestBody Order order) {

    order.setStatus(Status.IN_PROGRESS);
    Order newOrder = orderRepository.save(order);

    return ResponseEntity
      .created(linkTo(methodOn(OrderController.class).one(newOrder.getId())).toUri())
      .body(assembler.toModel(newOrder));
  }
}
  • It contains the same REST controller settings as the controller we built so far;
  • It is a simultaneous injection OrderRepositoryand a (not yet built) OrderModelAssembler;
  • The first two routes Spring MVC polymerization process and single root Orderresource request;
  • The third Spirng MVC Reuters by IN_PROGRESSstate they start to create a new order processing;
  • All controller method will return Spring HATEOAS of RepresentationModelone sub-class to display the correct hypermedia (or package wrapper class).

In the building OrderModelAssemblerbefore, let's discuss what needs to happen. We are modeling Status.IN_PROGRESS, Status.COMPLETEDand Status.CANCELLEDstatus between the stream. When providing this type of data to the client, it is natural for the client to decide what it can do based on the payload.

But this is wrong.

What happens when a new state is introduced in the process? The placement of various buttons on the UI may be wrong.

If we changed the name of each state, maybe it was when writing international support and displaying locale-specific text for each state? That will likely destroy all clients.

Enter HATEOAS or Hypermedia as the application state engine . Rather than let the client parse the payload, let the client link to signal a valid action. Separate state-based operations from the payload of data. In other words, when CANCEL and COMPLETE are valid actions, they are dynamically added to the link list. When the link exists, the client only needs to display the corresponding button to the user.

This eliminates the need for the client to know when these operations are valid, thereby reducing the risk of the server and its client being logically out of sync with state transitions.

Spring HATEOAS have embraced the ResourceAssemblerconcept of assembly, such logic will be placed OrderModelAssemblerin the ideal place to capture the business rule:
links/src/main/java/payroll/OrderModelAssembler.java

package payroll;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;

import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.stereotype.Component;

@Component
class OrderModelAssembler implements RepresentationModelAssembler<Order, EntityModel<Order>> {

  @Override
  public EntityModel<Order> toModel(Order order) {

    // Unconditional links to single-item resource and aggregate root

    EntityModel<Order> orderModel = new EntityModel<>(order,
      linkTo(methodOn(OrderController.class).one(order.getId())).withSelfRel(),
      linkTo(methodOn(OrderController.class).all()).withRel("orders")
    );

    // Conditional links based on state of the order

    if (order.getStatus() == Status.IN_PROGRESS) {
      orderModel.add(
        linkTo(methodOn(OrderController.class)
          .cancel(order.getId())).withRel("cancel"));
      orderModel.add(
        linkTo(methodOn(OrderController.class)
          .complete(order.getId())).withRel("complete"));
    }

    return orderModel;
  }
}

The resource assembler always includes its own links to individual resources and links to aggregate roots. But it also includes the two OrderController.cancel(id)and OrderController.complete(id)(not yet defined) conditions link. Only when the order status is Status.IN_PROGRESSwhen these links will be displayed.

If the client can adopt HAL and has the ability to read the link, instead of simply reading ordinary old JSON data, it can introduce a demand for knowledge of the order system domain. This naturally reduces the coupling between the client and the server. It opens the door for adjusting the order fulfillment process without destroying the shortcomings in the process.

To improve order fulfillment, to add the following OrderControllerto perform cancelthe operation:

In the OrderControllerCreate a "cancel" operation

@DeleteMapping("/orders/{id}/cancel")
ResponseEntity<RepresentationModel> cancel(@PathVariable Long id) {

  Order order = orderRepository.findById(id).orElseThrow(() -> new OrderNotFoundException(id));

  if (order.getStatus() == Status.IN_PROGRESS) {
    order.setStatus(Status.CANCELLED);
    return ResponseEntity.ok(assembler.toModel(orderRepository.save(order)));
  }

  return ResponseEntity
    .status(HttpStatus.METHOD_NOT_ALLOWED)
    .body(new VndErrors.VndError("Method not allowed", "You can't cancel an order that is in the " + order.getStatus() + " status"));
}

It will check the order status before canceling Orderstate. If the status is invalid, Spring HATEOAS is returned VndError, which is an error container that supports hypermedia. If the conversion really works, they will Orderswitch to CANCELLED.

And added to OrderControllerin order to complete the order:

Create a "Done" action in OrderController

@PutMapping("/orders/{id}/complete")
ResponseEntity<RepresentationModel> complete(@PathVariable Long id) {

    Order order = orderRepository.findById(id).orElseThrow(() -> new OrderNotFoundException(id));

    if (order.getStatus() == Status.IN_PROGRESS) {
      order.setStatus(Status.COMPLETED);
      return ResponseEntity.ok(assembler.toModel(orderRepository.save(order)));
    }

    return ResponseEntity
      .status(HttpStatus.METHOD_NOT_ALLOWED)
      .body(new VndErrors.VndError("Method not allowed", "You can't complete an order that is in the " + order.getStatus() + " status"));
}

This achieves a similar logic to prevent the Orderstate can not be completed unless the appropriate state.

By to LoadDatabaseadd some extra initialization code:

Update the database preloader

orderRepository.save(new Order("MacBook Pro", Status.COMPLETED));
orderRepository.save(new Order("iPhone", Status.IN_PROGRESS));

orderRepository.findAll().forEach(order -> {
  log.info("Preloaded " + order);
});

… We can test it!

To use the newly created order service, just perform a few operations:

$ curl -v http://localhost:8080/orders

{
  "_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"
    }
  }
}

The HAL document immediately displays different links for each order based on its current status:

  • COMPLETED 's first order has only navigation links. The state transition link is not displayed;
  • The second order is IN_PROGRESS , with cancel link and complete link.

Try to cancel the order:

$ curl -v -X DELETE http://localhost:8080/orders/4/cancel

> 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 2018 15:02:10 GMT
<
{
  "id": 4,
  "description": "iPhone",
  "status": "CANCELLED",
  "_links": {
    "self": {
      "href": "http://localhost:8080/orders/4"
    },
    "orders": {
      "href": "http://localhost:8080/orders"
    }
  }
}

The response displays an HTTP 200 status code indicating success . The responding HAL document CANCELLEDdisplays the order with a new status ( ). The link that changed the state also disappeared.

If we try the same operation again ...

$ curl -v -X DELETE http://localhost:8080/orders/4/cancel

* 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/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Mon, 27 Aug 2018 15:03:24 GMT
<
{
  "logref": "Method not allowed",
  "message": "You can't cancel an order that is in the CANCELLED status"
}

… We see HTTP 405 Method Not Allowed . DELETE has become an invalid operation. VndErrorThe response object clearly indicates that we are not allowed to "cancel" orders that are already in the "cancelled" state.

In addition, attempts to complete the same order will fail:

$ curl -v -X PUT localhost:8080/orders/4/complete

* 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/hal+json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Mon, 27 Aug 2018 15:05:40 GMT
<
{
  "logref": "Method not allowed",
  "message": "You can't complete an order that is in the CANCELLED status"
}

After completing all these operations, our order fulfillment service can conditionally display the available operations. It also prevents invalid operations.

By using hypermedia and link protocols, the client can be made more robust, and it is less likely to crash due to data changes only. Spring HATEOAS simplifies the process of building the hypermedia needed to serve customers.

Published 232 original articles · Liked 14 · Visits 20,000+

Guess you like

Origin blog.csdn.net/stevenchen1989/article/details/105620148