使用Spring构建REST服务
一、REST简介
1、为什么要使用RESTful架构?
2、API请求方式与传统请求方式的区别
传统API请求 | 请求类型 | RESTful请求方式 |
---|---|---|
xxx/api/getDogs | GET | xxx/api/dogs |
xxx/api/addDogs | POST | xxx/api/dogs |
xxx/api/updateDogs/:dogId | PUT | xxx/api/dogs/:dogId |
xxx/api/deleteDogs/:dogId | DELETE | xxx/api/dogs/dogId |
3、RESTful请求设计
- GET :查询
- POST:新增
- PUT:全量更新
- PATCH:增量更新
- DELETE:删除
4、RESTful响应设计
二、示例代码
1、员工实体类 Employee.java
package rest;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Objects;
@Entity //是一个 JPA 注释,用于使此对象准备好存储在基于 JPA 的数据存储中
class Employee {
private @Id @GeneratedValue Long id;
private String name;
private String role;
Employee() {}
Employee(String name, String role) {
this.name = name;
this.role = role;
}
public Long getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getRole() {
return this.role;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setRole(String role) {
this.role = role;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Employee))
return false;
Employee employee = (Employee) o;
return Objects.equals(this.id, employee.id) && Objects.equals(this.name, employee.name)
&& Objects.equals(this.role, employee.role);
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.name, this.role);
}
@Override
public String toString() {
return "Employee{" + "id=" + this.id + ", name='" + this.name + '\'' + ", role='" + this.role + '\'' + '}';
}
}
2、访问数据 EmployeeRepository.java
继承JpaRepository即可自动的实现
- 创建新员工
- 更新员工
- 删除员工
- 查询员工(一个、全部)
package rest;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.JpaRepository;
@Configuration
interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
}
3、初始化数据 LoadDatabase.java
package rest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class LoadDatabase {
private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class);
@Bean
CommandLineRunner initDatabase(EmployeeRepository repository) {
return args -> {
log.info("Preloading " + repository.save(new Employee("Andy", "经理")));
log.info("Preloading " + repository.save(new Employee("Tom", "项目总监")));
};
}
}
4、异常处理 EmployeeNotFoundException.java
package rest;
class EmployeeNotFoundException extends RuntimeException {
EmployeeNotFoundException(Long id) {
super("Could not find employee " + id);
}
}
5、配置呈现HTTP 404 EmployeeNotFoundAdvice.java
package rest;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
class EmployeeNotFoundAdvice {
@ResponseBody
@ExceptionHandler(EmployeeNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
String employeeNotFoundHandler(EmployeeNotFoundException ex) {
return ex.getMessage();
}
}
6、 员工控制器 EmployeeController.java
package rest;
import java.util.List;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
class EmployeeController {
private final EmployeeRepository repository;
EmployeeController(EmployeeRepository repository) {
this.repository = repository;
}
// Aggregate root
// tag::get-aggregate-root[]
@GetMapping("/employees")
List<Employee> all() {
return repository.findAll();
}
// end::get-aggregate-root[]
@PostMapping("/employees")
Employee newEmployee(@RequestBody Employee newEmployee) {
return repository.save(newEmployee);
}
// Single item
@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);
}
}
三、测试结果
- 如图所示的请求方式就是按照RESTful的风格设计的
1、查询所有员工
2、根据id查询员工
- ①查询存在的员工
- ②查询不存在的员工
3、新增员工
新增员工成功
4、修改员工信息
- 如图所示id为3的员工信息已经修改
5、删除一名员工
- 如图所示id为3的员工已经删除
四、给RESTful服务添加链接到相关操作,使得接口更加 RESTful
1、在pom.xml添加如下依赖
2、调整根据员工id查询
@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"));
}
输出结果如下:
2、调整查询员工集合
@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")))
.collect(Collectors.toList());
return CollectionModel.of(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel());
}
员工集合的 restful 表示如下
{
"_embedded": {
"employeeList": [
{
"id": 1,
"name": "Andy",
"role": "经理",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
},
{
"id": 2,
"name": "Tom",
"role": "项目总监",
"_links": {
"self": {
"href": "http://localhost:8080/employees/2"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/employees"
}
}
}
五、简化链接创建
1、对象转换函数 EmployeeModelAssembler.java
package rest;
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 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"));
}
}
2、将员工模型组注入控制器
3、再次调整根据id查询员工,简化代码
@GetMapping("/employees/{id}")
EntityModel<Employee> one(@PathVariable Long id) {
Employee employee = repository.findById(id)
.orElseThrow(() -> new EmployeeNotFoundException(id));
return assembler.toModel(employee);
}
4、修改employees实体,经name字段改为 firstName 和 lastName
package rest;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Objects;
@Entity //是一个 JPA 注释,用于使此对象准备好存储在基于 JPA 的数据存储中
class Employee {
private @Id @GeneratedValue Long id;
private String firstName;
private String lastName;
private String role;
Employee(String firstName, String lastName, String role) {
this.firstName = firstName;
this.lastName = lastName;
this.role = role;
}
public Employee() {
}
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];
}
public Long getId() {
return this.id;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public String getRole() {
return this.role;
}
public void setId(Long id) {
this.id = id;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setRole(String role) {
this.role = role;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Employee))
return false;
Employee employee = (Employee) o;
return Objects.equals(this.id, employee.id) && Objects.equals(this.firstName, employee.firstName)
&& Objects.equals(this.lastName, employee.lastName) && Objects.equals(this.role, employee.role);
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.firstName, this.lastName, this.role);
}
@Override
public String toString() {
return "Employee{" + "id=" + this.id + ", firstName='" + this.firstName + '\'' + ", lastName='" + this.lastName
+ '\'' + ", role='" + this.role + '\'' + '}';
}
}
5、修改initDatabase
6、调整新增员工接口
7、通过这样的调整,可以使用相同的接口创建新员工,并使用原来的旧字段
8、修改员工信息进行如下调整
这里我们得到了一个比200 OK更详细的HTTP响应,即HTTP 201 Created
六、在 REST API 中构建链接
1、订单实体类 Order.java
package rest;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "CUSTOMER_ORDER")
class Order {
private @Id @GeneratedValue Long id;
private String description;
private Status status;
Order(String description, Status status) {
this.description = description;
this.status = status;
}
public Order() {
}
public Long getId() {
return this.id;
}
public String getDescription() {
return this.description;
}
public Status getStatus() {
return this.status;
}
public void setId(Long id) {
this.id = id;
}
public void setDescription(String description) {
this.description = description;
}
public void setStatus(Status status) {
this.status = status;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Order))
return false;
Order order = (Order) o;
return Objects.equals(this.id, order.id) && Objects.equals(this.description, order.description)
&& this.status == order.status;
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.description, this.status);
}
@Override
public String toString() {
return "Order{" + "id=" + this.id + ", description='" + this.description + '\'' + ", status=" + this.status + '}';
}
}
2、状态枚举类 Status.java
package rest;
enum Status {
IN_PROGRESS,
COMPLETED,
CANCELLED
}
3、异常处理类 OrderNotFoundException.java
package rest;
public class OrderNotFoundException extends RuntimeException{
OrderNotFoundException(Long id) {
super("Could not find order"+ id);
}
}
4、对象转换函数RepresentationModelAssembler.java
package rest;
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) {
EntityModel<Order> orderModel = EntityModel.of(order,
linkTo(methodOn(OrderController.class).one(order.getId())).withSelfRel(),
linkTo(methodOn(OrderController.class).all()).withRel("orders"));
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;
}
}
5、数据处理 OrderRepository.java
package rest;
import org.springframework.data.jpa.repository.JpaRepository;
interface OrderRepository extends JpaRepository<Order, Long> {
}
6、调整LoadDatabase
package rest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class LoadDatabase {
private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class);
@Bean
CommandLineRunner initDatabase(EmployeeRepository employeeRepository, OrderRepository orderRepository) {
return args -> {
employeeRepository.save(new Employee("Bilbo", "Baggins", "burglar"));
employeeRepository.save(new Employee("Frodo", "Baggins", "thief"));
employeeRepository.findAll().forEach(employee -> log.info("Preloaded " + employee));
orderRepository.save(new Order("MacBook Pro", Status.COMPLETED));
orderRepository.save(new Order("iPhone", Status.IN_PROGRESS));
orderRepository.findAll().forEach(order -> {
log.info("Preloaded " + order);
});
};
}
}
7、订单列表如下
客户端无需解析,而是通过链接即可收到有效操作。将基于状态的操作与数据的操作有效负载分离。
这样调整使得客户端不必知道操作何时有效,从而降低了服务器及客户端在状态转换逻辑上不同步的风险。
如下所示,仅当“取消”和“完成”是有效操作时,才动态的显示在对应的列表中。
-
第一个订单,已完成,只有导航链接。不显示状态转换链接。
-
第二个订单,正在进行,另外具有取消链接以及完整链接。
8、尝试取消订单
- 这里可以看到订单列表的链接随着状态也改变了
- 已经取消的订单再次取消
- 将取消的订单改为完成状态
9、尝试完成订单
- 此时id为4的订单已经改为完成状态
- 将完成状态的订单再次改成完成状态
- 将完成状态的订单改成取消状态
可见订单履行服务能够有条件地显示可用的操作,它还可以防止无效操作。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/122982.html