系列文章:Spring Boot 3.x 系列教程
REST
(英文:Representational State Transfer,简称REST)描述了一个架构样式的网络系统,比如 web 应用程序。它首次出现在 2000 年 Roy Fielding 的博士论文中,Roy Fielding是 HTTP
规范的主要编写者之一。在目前主流的三种Web服务交互方案中,REST
相比于SOAP
(Simple Object Access protocol,简单对象访问协议)以及XML-RPC
更加简单明了,无论是对URL
的处理还是对Payload
的编码,REST
都倾向于用更加简单轻量的方法设计和实现。值得注意的是REST并没有一个明确的标准,而更像是一种设计的风格。
REST
指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful
。
API
通信协议使用HTTPS
协议
API
部署到专有域名下:https://api.example.com
或者https://example.com/api/
API
版本号放入URL
https://api.example.com/v1
在
RESTful
架构中,每个URI
代表一种资源(resource
),所以URI
中不能有动词,只能有名词,而且所用的名词往往与数据库的表名对应。一般来说,数据库中的表都是同种记录的"集合"(collection
),所以API
中的名词也应该使用复数。
https://api.example.com/v1/zoos
https://api.example.com/v1/animals
https://api.example.com/v1/employees
GET(SELECT
):从服务器取出资源(一项或多项)。
POST(CREATE)
:在服务器新建一个资源。
PUT(UPDATE)
:在服务器更新资源(客户端提供改变后的完整资源)。
PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
DELETE(DELETE)
:从服务器删除资源。
两个不常用的HTTP动词
HEAD
:获取资源的元数据。
OPTIONS
:获取信息,关于资源的哪些属性是客户端可以改变的。
例子
GET /zoos:列出所有动物园
POST /zoos:新建一个动物园
GET /zoos/ID:获取某个指定动物园的信息
PUT /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE /zoos/ID:删除某个动物园
GET /zoos/ID/animals:列出某个指定动物园的所有动物
DELETE /zoos/ID/animals/ID:删除某个指定动物园的指定动物
?limit=10:指定返回记录的数量
?offset=10:指定返回记录的开始位置。
?page=2&per_page=100:指定第几页,以及每页的记录数。
?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
?animal_type_id=1:指定筛选条件
7.** 状态码**
200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - []:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。
401 Unauthorized - []:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - []:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。
如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。
{
error: “Invalid API key”
}
GET /collection:返回资源对象的列表(数组)
GET /collection/resource:返回单个资源对象
POST /collection:返回新生成的资源对象
PUT /collection/resource:返回完整的资源对象
PATCH /collection/resource:返回完整的资源对象
DELETE /collection/resource:返回一个空文档
完整代码:代码
本节使用Spring Boot 3+Spring data jpa+mysql+lombok+mapstruct
构建Restful Api
。实现动物园
和动物
之间的CRUD接口。
整个项目架构设计如下:
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>3.0.0-M2version>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>spring-boot-restful-apiartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>spring-boot-restful-apiname>
<description>spring-boot-restful-apidescription>
<properties>
<java.version>17java.version>
<org.mapstruct.version>1.4.2.Finalorg.mapstruct.version>
properties>
<dependencies>
<dependency>
<groupId>org.mapstructgroupId>
<artifactId>mapstructartifactId>
<version>${org.mapstruct.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
exclude>
excludes>
configuration>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.8.1version>
<configuration>
<source>17source>
<target>17target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstructgroupId>
<artifactId>mapstruct-processorartifactId>
<version>${org.mapstruct.version}version>
path>
<path>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
path>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombok-mapstruct-bindingartifactId>
<version>0.2.0version>
dependency>
annotationProcessorPaths>
configuration>
plugin>
plugins>
build>
<repositories>
<repository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/milestoneurl>
<snapshots>
<enabled>falseenabled>
snapshots>
repository>
repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/milestoneurl>
<snapshots>
<enabled>falseenabled>
snapshots>
pluginRepository>
pluginRepositories>
project>
数据库结构使用Entity
自动生成表结构。
spring:
datasource:
#数据库驱动完整类名
driver-class-name: com.mysql.jdbc.Driver
#数据库连接url
url: jdbc:mysql://127.0.0.1:3306/spring-boot-data-learn
#数据库用户名
username: root
#数据库密码
password: 123456
jpa:
hibernate:
ddl-auto: update
debug: true
@Entity
@Data
@NoArgsConstructor
public class Zoo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private String address;
private String telephone;
@OneToMany(cascade = ALL, mappedBy = "zoo")
private Set<Animal> animals;
}
@Entity
@Data
public class Animal {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private Integer age;
@ManyToOne
@JoinColumn(name = "ZOO_ID", nullable = false)
private Zoo zoo;
}
本次需要实现的接口如下:
接口 | 描述 | 返回 |
---|---|---|
GET /zoos |
查询动物园列表 | List |
GET /zoos/{id} |
查询指定动物园详情 | ZooResponse |
POST /zoos |
新增动物园 | ZooResponse |
DELETE /zoos/{id} |
删除指定动物园 | void |
PUT /zoos/{id} |
更新指定动物园信息(全部属性) | ZooResponse |
PATCH /zoos/{id} |
更新指定动物园信息(部分属性) | ZooResponse |
POST /zoos/{zooId}/animals |
指定动物园新增动物 | ZooResponse |
GET /zoos/{zooId}/animals |
查询指定动物园动物列表 | List |
GET /animals/{id} |
查询指定动物详细信息 | AnimalResponse |
GET /animals |
获取所有的动物列表 | List |
接口响应对象:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ZooResponse implements Serializable {
private Integer id;
private String name;
private String address;
private String telephone;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AnimalResponse implements Serializable {
private Integer id;
private String name;
private Integer age;
}
接口请求对象:
@Data
@NoArgsConstructor
public class AnimalRequest implements Serializable {
@NotEmpty(message = "animal name not empty")
@Size(max = 100)
private String name;
@NotEmpty
@Min(value = 1)
private Integer age;
}
@Data
@NoArgsConstructor
public class ZooRequest implements Serializable {
@NotEmpty(message = "zoo name not empty")
@Size(max = 32)
private String name;
@NotEmpty
@Size(max = 255)
private String address;
@NotEmpty
@Size(max = 20)
private String telephone;
}
对象转换使用了mapstruct
工具,下面自定义需要转换的对象映射关系,工具会自动实现接口。
@Mapper
public interface ZooConverter {
ZooConverter INSTANCE = Mappers.getMapper(ZooConverter.class);
Zoo requestToEntity(ZooRequest zooRequest);
List<ZooResponse> entityToResponse(List<Zoo> zoos);
ZooResponse entityToResponse(Zoo zoo);
}
为了数据转换方便,直接继承ListCrudRepository
。
@Transactional(readOnly = true)
public interface AnimalRepository extends ListCrudRepository<Animal, Integer> {
List<Animal> findAnimalByZooIdIs(Integer zooId);
}
@Transactional(readOnly = true)
public interface ZooRepository extends ListCrudRepository<Zoo, Integer> {
}
接口定义,在controller
注入调用。
**AnimalService.java**
public interface AnimalService {
AnimalResponse create(Integer zooId, AnimalRequest animalRequest) throws NoRecordFoundException;
AnimalResponse detail(Integer id) throws NoRecordFoundException;
List<AnimalResponse> list();
List<AnimalResponse> listZooAnimals(Integer zooId);
}
**ZooService.java**
public interface ZooService {
ZooResponse create(ZooRequest zooRequest);
ZooResponse update(Integer id, ZooRequest zooRequest) throws NoRecordFoundException;
ZooResponse updateTelephone(Integer id, String telephone) throws NoRecordFoundException;
ZooResponse detail(Integer id) throws NoRecordFoundException;
List<ZooResponse> list();
void delete(Integer id) throws NoRecordFoundException;
@Service("zooService")
public class ZooServiceImpl implements ZooService {
private ZooRepository zooRepository;
private AnimalRepository animalRepository;
public ZooServiceImpl(ZooRepository zooRepository, AnimalRepository animalRepository) {
this.zooRepository = zooRepository;
this.animalRepository = animalRepository;
}
@Transactional
@Override
public ZooResponse create(ZooRequest zooRequest) {
Zoo zoo = ZooConverter.INSTANCE.requestToEntity(zooRequest);
zooRepository.save(zoo);
return ZooConverter.INSTANCE.entityToResponse(zoo);
}
@Override
public ZooResponse update(Integer id, ZooRequest zooRequest) throws NoRecordFoundException {
if (zooRepository.findById(id).isPresent()) {
Zoo zoo = ZooConverter.INSTANCE.requestToEntity(zooRequest);
zoo.setId(id);
return ZooConverter.INSTANCE.entityToResponse(zoo);
} else {
throw new NoRecordFoundException("no record found id=" + id + " for zoo");
}
}
@Override
public ZooResponse updateTelephone(Integer id, String telephone) throws NoRecordFoundException {
Optional<Zoo> optionalZoo = zooRepository.findById(id);
if (optionalZoo.isPresent()) {
Zoo zoo = optionalZoo.get();
zoo.setTelephone(telephone);
zooRepository.save(zoo);
return ZooConverter.INSTANCE.entityToResponse(zoo);
} else {
throw new NoRecordFoundException("no record found id=" + id + " for zoo");
}
}
@Override
public ZooResponse detail(Integer id) throws NoRecordFoundException {
Optional<Zoo> optionalZoo = zooRepository.findById(id);
if (optionalZoo.isPresent()) {
return ZooConverter.INSTANCE.entityToResponse(optionalZoo.get());
} else {
throw new NoRecordFoundException("no record found id=" + id + " for zoo");
}
}
@Override
public List<ZooResponse> list() {
List<Zoo> zoos = zooRepository.findAll();
return ZooConverter.INSTANCE.entityToResponse(zoos);
}
@Transactional
@Override
public void delete(Integer id) throws NoRecordFoundException {
Optional<Zoo> zoo = zooRepository.findById(id);
if (zoo.isPresent()) {
zooRepository.deleteById(id);
} else {
throw new NoRecordFoundException("no record found id=" + id + " for zoo");
}
}
}
@Service("animalService")
public class AnimalServiceImpl implements AnimalService {
private ZooRepository zooRepository;
private AnimalRepository animalRepository;
public AnimalServiceImpl(ZooRepository zooRepository, AnimalRepository animalRepository) {
this.zooRepository = zooRepository;
this.animalRepository = animalRepository;
}
@Override
public AnimalResponse create(Integer zooId, AnimalRequest animalRequest) throws NoRecordFoundException {
Optional<Zoo> optionalZoo = zooRepository.findById(zooId);
if (optionalZoo.isEmpty()) {
throw new NoRecordFoundException("no record found id=" + zooId + " for zoo");
}
Zoo zoo = optionalZoo.get();
Animal animal = AnimalConverter.INSTANCE.requestToEntity(animalRequest);
animal.setZoo(zoo);
animalRepository.save(animal);
return AnimalConverter.INSTANCE.entityToResponse(animal);
}
@Override
public AnimalResponse detail(Integer id) throws NoRecordFoundException {
Optional<Animal> optionalAnimal = animalRepository.findById(id);
if (optionalAnimal.isPresent()) {
return AnimalConverter.INSTANCE.entityToResponse(optionalAnimal.get());
} else {
throw new NoRecordFoundException("no record found id=" + id + " for animal");
}
}
@Override
public List<AnimalResponse> list() {
return AnimalConverter.INSTANCE.entityToResponse(animalRepository.findAll());
}
@Override
public List<AnimalResponse> listZooAnimals(Integer zooId) {
List<Animal> animals = animalRepository.findAnimalByZooIdIs(zooId);
return AnimalConverter.INSTANCE.entityToResponse(animals);
}
}
service
中repository
注入,使用构造函数的方式,这个是Spring
推荐的方式。service
方法中业务异常直接抛出,上层统一处理,这样可以方便的格式化错误信息的输出。
controller
非常薄的一层,没有过多的业务逻辑处理,主要是参数校验,调用service
方法。然后统一的异常处理返回统一格式。
@RestController
@RequestMapping("/zoos")
public class ZooController {
private ZooService zooService;
private AnimalService animalService;
public ZooController(ZooService zooService, AnimalService animalService) {
this.zooService = zooService;
this.animalService = animalService;
}
/**
* 查询所有动物园
*
* @return ZooResponse
*/
@GetMapping()
public ResponseEntity<List<ZooResponse>> list() {
return ResponseEntity.ok(zooService.list());
}
/**
* 获取动物园详情
*
* @param id 动物园id
* @return ZooResponse
*/
@SneakyThrows
@GetMapping(value = "/{id}")
public ResponseEntity<ZooResponse> detail(@PathVariable("id") Integer id) {
return ResponseEntity.ok(zooService.detail(id));
}
/**
* 新增一个动物园
*
* @param zooRequest 动物园信息
* @return ZooResponse
*/
@PostMapping
public ResponseEntity<ZooResponse> create(@RequestBody @Validated ZooRequest zooRequest) {
return ResponseEntity.ok(zooService.create(zooRequest));
}
/**
* 删除指定动物园
*
* @param id 动物园id
*/
@SneakyThrows
@DeleteMapping(value = "/{id}")
public void delete(@PathVariable("id") Integer id) {
zooService.delete(id);
}
/**
* 更新动物园信息,整个对象信息
*
* @param id 动物园id
* @param zooRequest 动物园全部信息
* @return ZooResponse
*/
@SneakyThrows
@PutMapping(value = "/{id}")
public ResponseEntity<ZooResponse> update(@PathVariable("id") Integer id, @RequestBody @Validated ZooRequest zooRequest) {
return ResponseEntity.ok(zooService.update(id, zooRequest));
}
/**
* 更新动物园信息,部分对象信息
*
* @param id 动物园id
* @param telephone 手机号
* @return ZooResponse
*/
@SneakyThrows
@PatchMapping(value = "/{id}")
public ResponseEntity<ZooResponse> updatePart(@PathVariable("id") Integer id, @RequestParam(value = "telephone", required = true) String telephone) {
return ResponseEntity.ok(zooService.updateTelephone(id, telephone));
}
/**
* 指定动物园新增动物
*
* @param zooId 动物园id
* @return 动物信息
*/
@SneakyThrows
@PostMapping(value = "/{zooId}/animals")
public ResponseEntity<AnimalResponse> createAnimal(@PathVariable("zooId") Integer zooId, @RequestBody AnimalRequest animalRequest) {
return ResponseEntity.ok(animalService.create(zooId, animalRequest));
}
/**
* 查询指定动物园下所有动物
*
* @param zooId 动物园id
* @return
*/
@GetMapping(value = "/{zooId}/animals")
public ResponseEntity<List<AnimalResponse>> listAnimals(@PathVariable("zooId") Integer zooId) {
return ResponseEntity.ok(animalService.listZooAnimals(zooId));
}
}
@SneakyThrows
这个是lombak
的注解,消去异常处理的模版代码。
@RequestBody @Validated ZooRequest
接受客户端json格式数据,并且校验数据是否合法。使用的是jakarta.validation
。
public class ZooRequest implements Serializable {
@NotEmpty(message = "zoo name not empty")
@Size(max = 32)
private String name;
@NotEmpty
@Size(max = 255)
private String address;
@NotEmpty
@Size(max = 20)
private String telephone;
}
接口在正常的响应下返回业务数据,没问题。如果在异常的情况下。需要包装成统一的返回格式。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResult implements Serializable {
/**
* 业务错误码
*/
private Integer code;
//错误信息
private String error;
/**
* 错误信息的具体描述
*/
private Object detail;
}
统一异常处理
@ControllerAdvice(basePackages = "com.example.springbootrestfulapi.controller")
public class ControllerExceptionAdvice extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest webRequest) {
return super.handleExceptionInternal(ex, body, headers, status, webRequest);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
Map<String, Object> detail = new HashMap<>();
ex.getFieldErrors().forEach(fieldError -> {
detail.put(fieldError.getField(), fieldError.getDefaultMessage());
});
return new ResponseEntity<>(new ErrorResult(status.value(), ex.getBody().getDetail(), detail), status);
}
@Override
protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<>(new ErrorResult(status.value(), ex.getErrorCode(), ex.getMessage()), status);
}
@ExceptionHandler(NoRecordFoundException.class)
protected ResponseEntity<Object> handlerNoRecordFound(NoRecordFoundException ex) {
return new ResponseEntity<>(new ErrorResult(HttpStatus.NOT_FOUND.value(), ex.getMessage(), null), HttpStatus.NOT_FOUND);
}
}
ResponseEntityExceptionHandler
默认实现了常用的异常处理。但是它输出的格式 是spring默认的。如果需要自定义格式,需要继承它然后重新输出内容。如上面例子所示。
以上就是根据restful
规范设计的简单api
。随着接口越来越多,调用方怎样能一目了然的了解怎样使用你提供的接口,那么接口文档非常重要,下一节我们再讲。
完整代码:代码