• 使用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按照如下步骤介绍如何使用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 资源被删除

    二、restfulhttp服务

    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拆分为fristnamelastname,同时通过增加虚拟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"
            }
        }
    }
  • 相关阅读:
    手机分辨率对应表
    本地加密解密工具类
    缓存清理的工具类
    Javadoc常见的标记和含义
    Android 禁止进入activity自动弹出键盘
    ListView
    可以展开和收起的的LinearLayout
    Android 编码规范
    ToastUtils
    防微信左滑删除的效果
  • 原文地址:https://www.cnblogs.com/viwofer/p/13225958.html
Copyright © 2020-2023  润新知