Spring Boot 3.x- 构建RESTful API

系列文章目录

系列文章:Spring Boot 3.x 系列教程


文章目录

  • 系列文章目录
  • 一、什么是REST
  • 二、RESTful API设计原则
  • 三、Spring Boot 3构建 RESTful API
    • 1.新建项目导入依赖库
    • 2.表结构设计
    • 3.接口设计
    • 4.对象转换&Repository &Service
      • 1.对象转化
      • 2.Repository接口实现
      • 3.Service实现
    • 5.controller
      • 测试&异常统一处理
  • 总结


一、什么是REST

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

二、RESTful API设计原则

  1. 通信协议

API通信协议使用HTTPS协议

  1. 部署域名

API部署到专有域名下: https://api.example.com 或者 https://example.com/api/

  1. API 版本

API版本号放入URL https://api.example.com/v1

  1. 面向资源

RESTful架构中,每个URI代表一种资源(resource),所以URI中不能有动词,只能有名词,而且所用的名词往往与数据库的表名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。

https://api.example.com/v1/zoos
https://api.example.com/v1/animals
https://api.example.com/v1/employees

  1. HTTP动词操作资源

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:删除某个指定动物园的指定动物

  1. ** 条件过滤**

?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 - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

  1. 错误处理

如果状态码是4xx,就应该向用户返回出错信息。一般来说,返回的信息中将error作为键名,出错信息作为键值即可。

{ 
error: “Invalid API key”
}

  1. 返回结果

GET /collection:返回资源对象的列表(数组)
GET /collection/resource:返回单个资源对象
POST /collection:返回新生成的资源对象
PUT /collection/resource:返回完整的资源对象
PATCH /collection/resource:返回完整的资源对象
DELETE /collection/resource:返回一个空文档

三、Spring Boot 3构建 RESTful API

完整代码:代码

本节使用Spring Boot 3+Spring data jpa+mysql+lombok+mapstruct构建Restful Api。实现动物园动物之间的CRUD接口。

整个项目架构设计如下:

Spring Boot 3.x- 构建RESTful API_第1张图片

1.新建项目导入依赖库


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

2.表结构设计

数据库结构使用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

实体设计:
Spring Boot 3.x- 构建RESTful API_第2张图片

@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;
}

3.接口设计

本次需要实现的接口如下:

接口 描述 返回
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;
    
}

4.对象转换&Repository &Service

1.对象转化

对象转换使用了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);

}

Spring Boot 3.x- 构建RESTful API_第3张图片

2.Repository接口实现

为了数据转换方便,直接继承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> {

}

3.Service实现

接口定义,在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);
    }
}

servicerepository注入,使用构造函数的方式,这个是Spring推荐的方式。service方法中业务异常直接抛出,上层统一处理,这样可以方便的格式化错误信息的输出。

5.controller

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;

}

测试&异常统一处理

Spring Boot 3.x- 构建RESTful API_第4张图片

接口在正常的响应下返回业务数据,没问题。如果在异常的情况下。需要包装成统一的返回格式。
Spring Boot 3.x- 构建RESTful API_第5张图片

@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。随着接口越来越多,调用方怎样能一目了然的了解怎样使用你提供的接口,那么接口文档非常重要,下一节我们再讲。
完整代码:代码

你可能感兴趣的:(Spring,Boot,3.x教程,restful,spring,boot,java,后端,数据库)