[spring] Spring Boot REST API - CRUD 操作

Spring Boot REST API - CRUD 操作

这里主要提一下 spring boot 创建 rest api,并对其进行 CRUD 操作

jackson & gson

目前浏览器和服务端主流的交互方式是使用 JSON(JavaScript Object Notation),但是 JSON 没有办法直接和 Java 的 POJO 创建对应关系,因此就需要一些库去实现这个转换的功能:

  • 将 JSON 转换成 Java POJO
  • 将 Java POJO 转化成 JSON
  • 实现序列化和反序列化

目前比较主流的两个库是 jackson 和 gson,这里选用 jackson,不需要做任何的配置,spring 默认使用 jackson,并且在默认情况下使用 setter/getter 对 POJO 的属性进行赋值

POM

项目依旧使用 spring initializer 创建,这里是额外需要勾选的两个库:

xml 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>

其中 lombok 可选,我只是懒得写 boilerplate code 所以直接安装了 lombok,配制方法在 Intellij 安装配置 lombok,这里不多赘述。如果 IDE 没有配置 lombok 可能会导致这个工具没法用

创建一个 rest controller

实现如下:

java 复制代码
package com.example.demo.rest;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class DemoRestController {
    // add code for the "/hello" endpoint
    @GetMapping("/hello")
    public String sayHello() {
        return "Hello World";
    }
}

效果如下:

这里几个注解的用途如下:

  • @RestController 告知 spring boot 这是一个 restful api 的 controller

    是传统 spring mvc 里 @Controller + @ResponseBody 的结合

  • @RequestMapping

    这个注解 spring mvc 里就有,表示处理的所有 rest api 都会 map 到 /test 这个路径下

  • @GetMapping

    表示这里会接受一个 HTTP 的 Get 请求,对应的路径是 /hello

    比较新版本的 sping mvc 也应该有这个注解

POJO

这里就是非常简单的定义一个 java class:

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String firstName;
    private String lastName;
}

其中三个注解来自于 lombok

Rest Controller 实现

CRUD 的实现,关于具体的 API 设计结尾处会稍微提一下

获取全部学生

实现如下:

java 复制代码
@RestController
@RequestMapping("/api")
public class StudentRestController {
    // define endpoint for "/students" - return a list of students
    @GetMapping("/students")
    public List<Student> getStudents() {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Peter", "Parker"));
        students.add(new Student("Stephen", "Strange"));
        students.add(new Student("Steve", "Rodgers"));
        return students;
    }
}

这里没有连接数据库,所以用一个 ArrayList 放所有的对象,并进行返回。可以看到返回值是一个正常的 JSON

路径变量 Path Variables

path variable 是一种可以从 URL 路径中获取变量的方式,如 client 可以调用这个路径: /api/students/{studentId}, 那么 studentId 就是路径变量

简单重构

开始之前先做一下简单重构,这样可以不用反复创建新的 ArrayList:

java 复制代码
public class StudentRestController {
    private List<Student> students;

    // define @PostConstruct to load the student data, it will only load data once
    @PostConstruct
    public void loadData() {
       this.students = new ArrayList<>();
        students.add(new Student("Peter", "Parker"));
        students.add(new Student("Stephen", "Strange"));
        students.add(new Student("Steve", "Rodgers"));
    }

    // define endpoint for "/students" - return a list of students
    @GetMapping("/students")
    public List<Student> getStudents() {
        return students;
    }

}

@PostConstruct 是 JavaEE 的规范之一,会在容器初始化后当前 bean 后被调用,且只会被调用一次,因此这里用来实现数据的赋值

路径变量实现

实现比较粗暴,直接获取对应下标的值:

java 复制代码
    // define ent point for "students/{studentId}" - return student at index
    @GetMapping("/students/{studentId}")
    // by default, param should match
    public Student getStudent(@PathVariable int studentId) {
        return this.students.get(studentId);
    }

实现效果如下:

⚠️:函数中的变量名和路径变量中的名称应当保持一致

异常处理

假设 studentId 并不是一个合法的参数,如 ArrayList 中只有三条数据,但是提供的 id 为 99,或者提供的不是数字,而是字符串,那么就会出现对应的异常:

这种情况下,用户其实并不需要了解这么多的信息,ta 可能只需要知道传过去的 id 不对,数据库找不到对应的数据即可。spring mvc 也提供了一个 @ExceptionHandler 去处理报错信息。实现方法如下:

  1. 创建对应的 error response POJO
  2. 创建对应的 exception 类
  3. 更新对应的 rest 实现,抛出在第 2 步里实现的 exception
  4. 使用 @ExceptionHandler 捕获对应异常,并且返回一个对应的 ResponseEntity<T>, 其中 T 为第 1 步里创建的 POJO,jackson 会将其转换成对应的 JSON 对象

定义 error res pojo

实现如下,非常简单:

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class StudentErrorResponse {
    private String message;
    private int status;
    private long timeStamp;
}

依旧使用 Lombok 解决大部分的问题

创建 custom exception

这里实现的是 not found exception,因为没有用默认参数,也没有用全参,所以没有使用 Lombok

java 复制代码
public class StudentNotFoundException extends RuntimeException {
    public StudentNotFoundException(String message) {
        super(message);
    }

    public StudentNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }

    public StudentNotFoundException(Throwable cause) {
        super(cause);
    }
}

抛出异常

java 复制代码
    // define ent point for "students/{studentId}" - return student at index
    @GetMapping("/students/{studentId}")
    public Student getStudent(@PathVariable int studentId) {
        // check the studentId against list size
        if (studentId >= this.students.size() || studentId < 0) {
            throw new StudentNotFoundException(("Student id not found - " + studentId));
        }
        return this.students.get(studentId);
    }

这里主要处理的是 index out of bound 的异常,如果参数类型不对则需要 overload 方法:

捕获异常

使用 ExceptionHandler 去捕获对应的异常,并且将 error code 修改成 404,表示无法根据当前 id 获取对应数据

java 复制代码
    // add the exception handler
    @ExceptionHandler
    public ResponseEntity<StudentErrorResponse> handleException(StudentNotFoundException exec) {
        // create a studentErrorResponse
        StudentErrorResponse error = new StudentErrorResponse();
        error.setStatus(HttpStatus.NOT_FOUND.value());
        error.setMessage(exec.getMessage());
        error.setTimeStamp(System.currentTimeMillis());

        // return ResponseEntity
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

显示结果如下:

添加 generic 报错处理

这个时候如果传入 string 的话,抛出的异常还是不太好看:

所以这里可以添加一个 generic 的报错信息,表示传进来的参数不对,是 bad request 即可:

java 复制代码
    @ExceptionHandler
    public ResponseEntity<StudentErrorResponse> handleException(Exception e) {
        // create a studentErrorResponse
        StudentErrorResponse error = new StudentErrorResponse();
        error.setStatus(HttpStatus.BAD_REQUEST.value());
        error.setMessage(e.getMessage());
        error.setTimeStamp(System.currentTimeMillis());

        // return ResponseEntity
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

显示结果如下:

注意这里所有的处理都是在 controller 中实现的:

全局异常处理

这里会使用 @ControllerAdvice 这个注解去实现,这是一个 AOP 的具体实现------即向已经存在的代码中注入新的行为(advice)

这里实现的方式很简单

  1. 创建一个新的 exception handler class,添加 @ControllerAdvice 注解

    java 复制代码
    @ControllerAdvice
    public class StudentRestExceptionHandler {}
  2. 重构

    将 controller 中的 exception handling 删掉

    同时将 exception handling 贴到 StudentRestExceptionHandler 中去

实现后的结构如下:

这样这个 handler 就能捕捉全局的报错,如修改一下 DemoRestController 中的代码,使其同样报错,也是可以捕获到的:

API 设计

写一些比较常识性的内容,已经对 RESTful 有了解的可以跳过

设计 API 的时候主要需要考虑三个点:

  • 谁会使用这个 API

    这个主要看的是目标用户,如这个 API 是会在同一个项目使用?公司内部使用?还是公开项目?

  • API 将会被怎样使用

    也就是说 API 的使用情况,如交易系统需要考虑付款、退款(部分退款/全部退款)、查看付款状态

    目前来说主流是 RESTful,不过使用 GraphQL 又是不同的情况

  • 任务需求是什么

    resource 的名称,支持的 HTTP 请求头等

目前来说主流的 API 设计规范如下:

HTTP Method Endpoint CRUD 操作
POST /employee Create
GET /employee/ Read
GET /employee/{id} Read
PUT /employee/{id} Update
DELETE /employee/{id} Delete

之前看到一些比较反常识的操作有一个: /api/deleteEmployee,如果是 delete 的话,应该是 HTTP 使用 DELETE,Endpoint 用 employees

相关推荐
chuanauc1 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴17 分钟前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao24 分钟前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc78727 分钟前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野6 小时前
【Java|集合类】list遍历的6种方式
java·python·list
二进制person7 小时前
Java SE--方法的使用
java·开发语言·算法
小阳拱白菜8 小时前
java异常学习
java