使用SpringBoot构建REST服务-什么是REST服务

前言:

本文按照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"?>


4.0.0
com.htkm.demo
demo-restful
0.0.1-SNAPSHOT
demo-restful
Demo project for Spring Boot

<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>



org.springframework.boot
spring-boot-starter-web


org.springframework.boot
spring-boot-starter-hateoas


org.springframework.boot
spring-boot-starter-data-jpa


org.projectlombok
lombok


com.h2database
h2
runtime





org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import






org.apache.maven.plugins
maven-compiler-plugin

1.8
1.8
UTF-8



org.springframework.boot
spring-boot-maven-plugin




复制代码
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依赖 :

org.springframework.boot spring-boot-starter-hateoas 控制器更改,在原来的返回json基础之上附加操作链接,红色加粗部分可以重构简化,在下一章节。

复制代码
@GetMapping("/employees")
CollectionModel<EntityModel> all() {
List<EntityModel> 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 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> {

@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> {
@Override
public EntityModel toModel(Order order) {
// Unconditional links to single-item resource and aggregate root
EntityModel 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”
}
}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值