栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Spring Boot 3.x- 构建RESTful API

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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接口。

整个项目架构设计如下:

1.新建项目导入依赖库


    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        3.0.0-M2
         
    
    com.example
    spring-boot-restful-api
    0.0.1-SNAPSHOT
    spring-boot-restful-api
    spring-boot-restful-api
    
        17
        1.4.2.Final
    
    
        
            org.mapstruct
            mapstruct
            ${org.mapstruct.version}
        
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-validation
        

        
            mysql
            mysql-connector-java
            runtime
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.boot
            spring-boot-starter-logging
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
                3.8.1
                
                    17 
                    17 
                    
                        
                            org.mapstruct
                            mapstruct-processor
                            ${org.mapstruct.version}
                        
                        
                            org.projectlombok
                            lombok
                            ${lombok.version}
                        
                        
                            org.projectlombok
                            lombok-mapstruct-binding
                            0.2.0
                        
                        
                    
                
            
        
    
    
        
            spring-milestones
            Spring Milestones
            https://repo.spring.io/milestone
            
                false
            
        
    
    
        
            spring-milestones
            Spring Milestones
            https://repo.spring.io/milestone
            
                false
            
        
    


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

实体设计:

@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 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 entityToResponse(List zoos);

    ZooResponse entityToResponse(Zoo zoo);

}

2.Repository接口实现

为了数据转换方便,直接继承ListCrudRepository。

@Transactional(readOnly = true)
public interface AnimalRepository extends ListCrudRepository {

    List findAnimalByZooIdIs(Integer zooId);
}

@Transactional(readOnly = true)
public interface ZooRepository extends ListCrudRepository {

}
3.Service实现

接口定义,在controller注入调用。

**AnimalService.java**
public interface AnimalService {
    AnimalResponse create(Integer zooId, AnimalRequest animalRequest) throws NoRecordFoundException;

    AnimalResponse detail(Integer id) throws NoRecordFoundException;

    List list();

    List 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 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 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 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 list() {
        List zoos = zooRepository.findAll();
        return ZooConverter.INSTANCE.entityToResponse(zoos);
    }

    @Transactional
    @Override
    public void delete(Integer id) throws NoRecordFoundException {
        Optional 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 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 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 list() {

        return AnimalConverter.INSTANCE.entityToResponse(animalRepository.findAll());
    }

    @Override
    public List listZooAnimals(Integer zooId) {
        List animals = animalRepository.findAnimalByZooIdIs(zooId);
        return AnimalConverter.INSTANCE.entityToResponse(animals);
    }
}

service中repository注入,使用构造函数的方式,这个是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;
    }

    
    @GetMapping()
    public ResponseEntity> list() {
        return ResponseEntity.ok(zooService.list());
    }

    
    @SneakyThrows
    @GetMapping(value = "/{id}")
    public ResponseEntity detail(@PathVariable("id") Integer id) {

        return ResponseEntity.ok(zooService.detail(id));
    }

    
    @PostMapping
    public ResponseEntity create(@RequestBody @Validated ZooRequest zooRequest) {
        return ResponseEntity.ok(zooService.create(zooRequest));
    }

    
    @SneakyThrows
    @DeleteMapping(value = "/{id}")
    public void delete(@PathVariable("id") Integer id) {
        zooService.delete(id);
    }

    
    @SneakyThrows
    @PutMapping(value = "/{id}")
    public ResponseEntity update(@PathVariable("id") Integer id, @RequestBody @Validated ZooRequest zooRequest) {
        return ResponseEntity.ok(zooService.update(id, zooRequest));
    }

    
    @SneakyThrows
    @PatchMapping(value = "/{id}")
    public ResponseEntity updatePart(@PathVariable("id") Integer id, @RequestParam(value = "telephone", required = true) String telephone) {
        return ResponseEntity.ok(zooService.updateTelephone(id, telephone));
    }

    
    @SneakyThrows
    @PostMapping(value = "/{zooId}/animals")
    public ResponseEntity createAnimal(@PathVariable("zooId") Integer zooId, @RequestBody AnimalRequest animalRequest) {
        return ResponseEntity.ok(animalService.create(zooId, animalRequest));
    }

    
    @GetMapping(value = "/{zooId}/animals")
    public ResponseEntity> 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 handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest webRequest) {
        return super.handleExceptionInternal(ex, body, headers, status, webRequest);
    }

    @Override
    protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        Map 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 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 handlerNoRecordFound(NoRecordFoundException ex) {
        return new ResponseEntity<>(new ErrorResult(HttpStatus.NOT_FOUND.value(), ex.getMessage(), null), HttpStatus.NOT_FOUND);
    }
}
 

ResponseEntityExceptionHandler默认实现了常用的异常处理。但是它输出的格式 是spring默认的。如果需要自定义格式,需要继承它然后重新输出内容。如上面例子所示。


总结

以上就是根据restful规范设计的简单api。随着接口越来越多,调用方怎样能一目了然的了解怎样使用你提供的接口,那么接口文档非常重要,下一节我们再讲。
完整代码:代码

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号