前言:

本文按照Spring官网构建REST服务的步骤测试,可以得到结论:

到底什么样的风格才是RESTful风格呢?

1,约束请求命令如下:

  • GET,获取资源。例如:/employees表示获取列表资源,/employees/{id}表示获取单个对象资源。
  • POST,新增。例如:/employees,body为json对象,表示新增。
  • PUT,更新。例如:/employees/{id},body为json对象,表示更新。
  • DELETE,删除。例如: /employees/{id},表示更新。

2,约束返回结果:  返回数据为列表,则每个对象资源附加自己的资源链接、列表资源链接以及可操作性链接。

参考链接:

官网demo按照如下步骤介绍如何使用SpringBoot构建REST服务,并强调

  • Pretty URLs like /employees/3 aren’t REST.

  • Merely using GETPOST, 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"
}
}
}

最新文章

  1. SQL语句在数据库中是如何执行的
  2. 跟开涛老师学shiro -- shiro简介
  3. JavaScript之substring()方法讲解
  4. 第48条:如果需要精确的答案,请避免使用float和double
  5. SQL Prompt5 破解版+使用说明 [转]
  6. Apple Swift中英文开发资源
  7. paypal的IPN机制
  8. 导入android SlidingMenu 应用
  9. J2SE学习笔记
  10. python之面向对象篇6
  11. Xilinx FFT IP v9.0 使用
  12. js中apply(thisArg, [argsArray])的参数与ArrayLike的关系
  13. JSON.parse()——json字符串转JS
  14. atitit.手动配置列表文件的选择and 数据的层次结构 attilax总结最佳实践--yaml
  15. MR案例:多文件输出MultipleOutputs
  16. Windows 配置 nginx php 多版本切换
  17. js中random的应用
  18. bzoj3157: 国王奇遇记
  19. 洛谷P3385 [模板]负环 [SPFA]
  20. PHP系统学习1

热门文章

  1. Java实现 蓝桥杯VIP 算法训练 友好数
  2. Java实现 蓝桥杯VIP 算法提高 密码锁
  3. Java实现 LeetCode 144 二叉树的前序遍历
  4. java实现第七届蓝桥杯路径之谜
  5. 举一个有趣的例子,让你轻松搞懂JVM内存管理
  6. linux下gdb调试方法与技巧整理
  7. iOS-Swift版本自定义CStextView的实现
  8. 扩展.Django-权限系统
  9. 掌握SpringBoot-2.3的容器探针:实战篇
  10. phpstorm里面无法配置deployment?