前言:
本文按照Spring官網構建REST服務的步驟測試,可以得到結論:
到底什麼樣的風格才是RESTful風格呢?
1,約束請求命令如下:
- GET,獲取資源。例如:/employees表示獲取列表資源,/employees/{id}表示獲取單個物件資源。
- POST,新增。例如:/employees,body為json物件,表示新增。
- PUT,更新。例如:/employees/{id},body為json物件,表示更新。
- DELETE,刪除。例如: /employees/{id},表示刪除。
2,約束返回結果: 返回資料為列表,則每個物件資源附加自己的資源連結、列表資源連結以及可操作性連結。
參考連結:
- 官網demo地址:https://spring.io/guides/tutorials/bookmarks/
- 官網demo的git地址:https://github.com/spring-guides/tut-rest/
官網demo按照如下步驟介紹如何使用SpringBoot構建REST服務,並強調
-
Pretty URLs like /employees/3 aren’t REST.
-
Merely using
GET
,POST
, etc. aren’t REST. -
Having all the CRUD operations laid out aren’t REST.
一、普通的http服務
pom.xml檔案如下所示:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.htkm.demo</groupId> <artifactId>demo-restful</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo-restful</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.0.RELEASE</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
JPA型別的實體類:
@Data @Entity public class Employee { @Id @GeneratedValue private Long id; private String name; private String role; public Employee() {} public Employee(String name, String role) { this.name = name; this.role = role; } }
使用H2記憶體資料庫,建立測試資料庫:
@Configuration @Slf4j public class LoadDatabase { @Bean CommandLineRunner initDatabase(EmployeeRepository repository) { return args -> { log.info("Preloading " + repository.save(new Employee("Bilbo Baggins", "burglar"))); log.info("Preloading " + repository.save(new Employee("Frodo Baggins", "thief"))); }; } }
JPA型別的DAO:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {}
EmployeeContrller控制器:
@RestController public class EmployeeController { @Autowired private EmployeeRepository repository; @GetMapping("/employees") List<Employee> all() { return repository.findAll(); } @PostMapping("/employees") Employee newEmployee(@RequestBody Employee newEmployee) { return repository.save(newEmployee); } @GetMapping("/employees/{id}") Employee one(@PathVariable Long id) { return repository.findById(id) .orElseThrow(() -> new EmployeeNotFoundException(id)); } @PutMapping("/employees/{id}") Employee replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) { return repository.findById(id) .map(employee -> { employee.setName(newEmployee.getName()); employee.setRole(newEmployee.getRole()); return repository.save(employee); }) .orElseGet(() -> { newEmployee.setId(id); return repository.save(newEmployee); }); } @DeleteMapping("/employees/{id}") void deleteEmployee(@PathVariable Long id) { repository.deleteById(id); } }
自定義異常:
public class EmployeeNotFoundException extends RuntimeException { public EmployeeNotFoundException(Long id) { super("Could not find employee " + id); } }
增加@ControllerAdvice註解,實現異常處理器:
@ControllerAdvice public class EmployeeNotFoundAdvice { @ResponseBody @ExceptionHandler(EmployeeNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) String employeeNotFoundHandler(EmployeeNotFoundException ex) { return ex.getMessage(); } }
使用postman或者curl工具測試執行:
1,GET http://localhost:8080/employees
[ { "id": 1, "name": "Bilbo Baggins", "role": "burglar" }, { "id": 2, "name": "Frodo Baggins", "role": "thief" } ]
2,GET http://localhost:8080/employees/1
{ "id": 1, "name": "Bilbo Baggins", "role": "burglar" }
3,DELETE http://localhost:8080/employees/1 資源被刪除
二、restful的http服務
pom.xml增加hateoas依賴 :
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency>
控制器更改,在原來的返回json基礎之上附加操作連結,紅色加粗部分可以重構簡化,在下一章節。
@GetMapping("/employees") CollectionModel<EntityModel<Employee>> all() { List<EntityModel<Employee>> employees = repository.findAll().stream() .map(employee -> EntityModel.of(employee, linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(), // 附加自身連結 linkTo(methodOn(EmployeeController.class).all()).withRel("employees"))) // 附加all操作連結 .collect(Collectors.toList()); return CollectionModel.of(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel()); // 附加自身連結 } @GetMapping("/employees/{id}") EntityModel<Employee> one(@PathVariable Long id) { Employee employee = repository.findById(id) // .orElseThrow(() -> new EmployeeNotFoundException(id)); return EntityModel.of(employee, linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel(), // 附加自身連結 linkTo(methodOn(EmployeeController.class).all()).withRel("employees")); // 附加all操作連結 }
Postman測試執行
1,GET http://localhost:8080/employees,可以看到附加連結
{ "_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" } } }
2,GET http://localhost:8080/employees/1
{ "id": 1, "name": "Bilbo Baggins", "role": "burglar", "_links": { "self": { "href": "http://localhost:8080/employees/1" }, "employees": { "href": "http://localhost:8080/employees" } } }
三、擴充套件的restful服務
擴充套件實體,將name拆分為fristname和lastname,同時通過增加虛擬的get/set保留name屬性:
@Data @Entity public class Employee { @Id @GeneratedValue private Long id; private String firstName; private String lastName; private String role; public Employee() {} public Employee(String firstName, String lastName, String role) { this.firstName = firstName; this.lastName = lastName; this.role = role; } public String getName() { return this.firstName + " " + this.lastName; } public void setName(String name) { String[] parts = name.split(" "); this.firstName = parts[0]; this.lastName = parts[1]; } }
重構簡化程式碼,增加物件包裝處理類 :
@Component class EmployeeModelAssembler implements RepresentationModelAssembler<Employee, EntityModel<Employee>> { @Override public EntityModel<Employee> toModel(Employee employee) { return EntityModel.of(employee, linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(), linkTo(methodOn(EmployeeController.class).all()).withRel("employees")); } }
控制器更改:
@RestController public class EmployeeController { @Autowired private EmployeeRepository repository; @Autowired private EmployeeModelAssembler assembler; @GetMapping("/employees") CollectionModel<EntityModel<Employee>> all() { List<EntityModel<Employee>> employees = repository.findAll().stream() .map(assembler::toModel) // 包裝物件 .collect(Collectors.toList()); return CollectionModel.of(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel()); // 附加自身連結 } @PostMapping("/employees") ResponseEntity<?> newEmployee(@RequestBody Employee newEmployee) { EntityModel<Employee> entityModel = assembler.toModel(repository.save(newEmployee)); return ResponseEntity .created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) .body(entityModel); // 返回狀態碼201,增加Location頭 http://localhost:8080/employees/3 } @GetMapping("/employees/{id}") EntityModel<Employee> one(@PathVariable Long id) { Employee employee = repository.findById(id) // .orElseThrow(() -> new EmployeeNotFoundException(id)); return assembler.toModel(employee); // 包裝物件 } @PutMapping("/employees/{id}") ResponseEntity<?> replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) { Employee updatedEmployee = repository.findById(id) // .map(employee -> { employee.setName(newEmployee.getName()); employee.setRole(newEmployee.getRole()); return repository.save(employee); }) // .orElseGet(() -> { newEmployee.setId(id); return repository.save(newEmployee); }); EntityModel<Employee> entityModel = assembler.toModel(updatedEmployee); return ResponseEntity .created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) .body(entityModel); // 返回狀態碼201,增加Location頭 http://localhost:8080/employees/3 } @DeleteMapping("/employees/{id}") ResponseEntity<?> deleteEmployee(@PathVariable Long id) { repository.deleteById(id); return ResponseEntity.noContent().build();// 返回狀態碼204 } }
curl測試執行
$ curl -v -X POST localhost:8080/employees -H 'Content-Type:application/json' -d '{"name": "Samwise Gamgee", "role": "gardener"}'
請求相應狀態為201,包含Location 響應頭
> 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 2018 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" } } }
$ curl -v -X PUT localhost:8080/employees/3 -H 'Content-Type:application/json' -d '{"name": "Samwise Gamgee", "role": "ring bearer"}'
> 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 2018 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" } } }
$ curl -v -X DELETE localhost:8080/employees/1
> DELETE /employees/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 204 < Date: Fri, 10 Aug 2018 21:30:26 GMT
四、附加可操作連結的restful服務
訂單實體轉換器,如果訂單狀態為可執行的訂單則附加取消和完成連結 :
@Component public 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 = EntityModel.of(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")); // 附加cancel連結 orderModel.add(linkTo(methodOn(OrderController.class).complete(order.getId())).withRel("complete")); // 附加complete連結 } return orderModel; } }
控制器中取消和完成操作
@DeleteMapping("/orders/{id}/cancel") ResponseEntity<?> 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) .header(HttpHeaders.CONTENT_TYPE, MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) .body(Problem.create() .withTitle("Method not allowed") .withDetail("You can't cancel an order that is in the " + order.getStatus() + " status")); } @PutMapping("/orders/{id}/complete") ResponseEntity<?> 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) .header(HttpHeaders.CONTENT_TYPE, MediaTypes.HTTP_PROBLEM_DETAILS_JSON_VALUE) .body(Problem.create() .withTitle("Method not allowed") .withDetail("You can't complete an order that is in the " + order.getStatus() + " status")); }
PostMan測試執行:
1,GET http://localhost:8080
{ "_links": { "employees": { "href": "http://localhost:8080/employees" }, "orders": { "href": "http://localhost:8080/orders" } } }
2,GET 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" } } }
3,DELETE http://localhost:8080/orders/3/cancel
{ "title": "Method not allowed", "detail": "You can't cancel an order that is in the COMPLETED status" }
4,DELETE http://localhost:8080/orders/4/cancel
{ "id": 4, "description": "iPhone", "status": "CANCELLED", "_links": { "self": { "href": "http://localhost:8080/orders/4" }, "orders": { "href": "http://localhost:8080/orders" } } }
5,POST localhost:8080/orders
設定header為Content-Type:application/json
body為{"name": "Samwise Gamgee", "role": "gardener"}'
{ "id": 5, "description": "新的訂單", "status": "IN_PROGRESS", "_links": { "self": { "href": "http://localhost:8080/orders/5" }, "orders": { "href": "http://localhost:8080/orders" }, "cancel": { "href": "http://localhost:8080/orders/5/cancel" }, "complete": { "href": "http://localhost:8080/orders/5/complete" } } }
五、總結
RESTful風格的http服務,包含以下特徵(百度百科摘錄):
- 1、每一個URI代表1種資源;
- 2、客戶端使用GET、POST、PUT、DELETE4個表示操作方式的動詞對服務端資源進行操作:GET用來獲取資源,POST用來新建資源(也可以用於更新資源),PUT用來更新資源,DELETE用來刪除資源;
- 3、通過操作資源的表現形式來操作資源;
- 4、資源的表現形式是XML或HTML;
- 5、客戶端與服務端之間的互動在請求之間是無狀態的,從客戶端到服務端的每個請求都必須包含理解請求所必需的資訊。
官網demo描述:
What’s important to realize is that REST, however ubiquitous, is not a standard, per se, but an approach, a style, a set of constraints on your architecture that can help you build web-scale systems.
到底什麼樣的風格才是為RESTful風格呢?
首先約束請求命令如下:
- GET,獲取資源。例如:/employees表示獲取列表資源,/employees/{id}表示獲取單個物件資源。
- POST,新增。例如:/employees,body為json物件,表示新增。
- PUT,更新。例如:/employees/{id},body為json物件,表示更新。
- DELETE,刪除。例如: /employees/{id},表示更新。
其次約束返回結果: 返回資料為列表,則每個物件資源附加自己的資源連結、列表資源連結以及可操作性連結。
以下例子是可能的返回結果:
{
"_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"
}
}
}