【Spring Boot】REST与RESTful详解,基于Spring Boot的RESTful API实现

文章目录

  • 前言
  • 一、REST与RESTful
    • [1.1 背景](#1.1 背景)
    • [1.2 REST风格](#1.2 REST风格)
    • [1.3 RESTful](#1.3 RESTful)
    • [1.4 使用REST的优缺点](#1.4 使用REST的优缺点)
    • [1.5 小结](#1.5 小结)
  • [二、Spring Boot中的RESTful落地指南](#二、Spring Boot中的RESTful落地指南)
    • 2.1项目构建
    • [2.2 RESTful服务搭建](#2.2 RESTful服务搭建)
      • [2.2.1 资源路由定义](#2.2.1 资源路由定义)
      • [2.2.2 控制器方法的HTTP语义定义](#2.2.2 控制器方法的HTTP语义定义)
      • [2.2.3 状态码的返回](#2.2.3 状态码的返回)
      • [2.2.4 返回状态码的工程化](#2.2.4 返回状态码的工程化)
    • [2.3 RESTful服务示例](#2.3 RESTful服务示例)
      • [2.3.1 HttpGet](#2.3.1 HttpGet)
      • [2.3.2 HttpPost](#2.3.2 HttpPost)
      • [2.3.3 HttpPut](#2.3.3 HttpPut)
      • [2.3.4 HttpPatch](#2.3.4 HttpPatch)
      • [2.3.5 HttpDelete](#2.3.5 HttpDelete)
  • 总结

前言

提起WebAPI的开发,REST风格的API是不得不提及的一个话题。REST风格的WebAPI也是目前最流行的一种WebAPI风格,除了REST风格外,还有通过WebService或者WCF开发的基于SOAP协议的WebAPI,不过这些都在时代的潮流中或被沉淀,或又通过新的面貌重新进入我们的视野。

REST是一种基于 HTTP 协议的软件架构风格,而RESTful则是基于这种风格的实践。本文将详尽的介绍REST,并且通过Spring Boot开发RESTful WebAPI。


一、REST与RESTful

1.1 背景

REST(Representational State Transfer),这是一种基于HTTP协议的软件架构风格。由计算机科学家Roy Fielding在2000年的博士论文中提出(Roy博士也是 HTTP 协议的主要设计者之一)。

REST的理念的提出也是源自Web本身的设计原则(如URI、HTTP方法),目的是在让Web架构更符合其初衷,通过HTTP的语义来使用HTTP协议。

REST的提出是建立在对早期WebAPI服务协议的一种精简,开发过WebService的朋友应该能明细感觉到,这种基于SOAP协议,利用XML报文的形式传递数据的方式用起来很 "重" 。而且因为API风格大多使用RPC这种面向过程的方式,仅仅把HTTP当成传输数据的通道,并不关心HTTP谓词。而是通过方法实现HTTP语义,比如/Project/GetAll、/Project/GetByld?id=7、/Project/Update、/Project/DeleteByld/7。

RPC这种通过相应的业务驱动的方式,比较自然,但是未充分利用HTTP语义,容易导致API语义不清晰。并且随着业务的更改与扩张,大量的代码需要修改,版本管理复杂扩展性较差。

聊完RPC这种"面向过程的风格",接下来我们来对比了解下遵循REST风格设计的Web API ------RESTful。

1.2 REST风格

REST是一种基于HTTP协议的软件架构风格,它使用URL表示资源,使用HTTP方法(GET、POST、PUT、DELETE)表示对资源的操作。本质上是一组设计原则。

  • 按照HTTP的语义来使用HTTP协议
  • 将资源作为核心抽象,通过不同HTTP语义实现对资源的操作
  • 无状态通信,每个请求独立
  • 支持多种数据格式

1.3 RESTful

RESTful是遵循REST架构风格的API实现。一个API被称为RESTful,意味着它严格遵守REST原则。我们可以这样简单的总结一下RESTful

  • 使用名词而非动词命名URL(如/users而非/getAllUsers),URL用于资源的定位
  • 服务器端要通过状态码来反映资源获取的结果(如200成功、201新增成功、403没有权限,404 未找到)

1.4 使用REST的优缺点

按照HTTP的语义来使用HTTP协议的这种约定大于配置的方式的,确实能节约不少开发上的工作,但是也变相要求开发人员对REST原则更了解、并且有更多的设计能力。以下分别总结几点优缺点。

  • REST优点
    • 通过URL对资源定位,语义更清晰通过HTTP谓词表示不同的操作,接口自描述。
    • 可以对GET、PUT、DELETE请求进行重试(分布式架构里的中间网关服务器,自动重发)
    • 可以用GET请求做缓存(依据幂等性质,安全的设置缓存)
    • 通过HTTP状态码反映服务器端的处理结果统一错误处理机制
    • 网关等可以分析请求处理结果(网关服务器解析API响应,通过请求结果判断系统是否健壮)
  • REST缺点
    • 真实系统中的资源非常复杂,难以优雅表达。很难清晰地进行资源的划分,对技术人员的业务和技术水平要求高。
    • 安全性依赖外部机制,认证授权需依赖JWT等外部方案,增加复杂性。
    • 状态管理不足,客户端维护更多状态。

1.5 小结

REST总体而言是一种偏学术化的概念,但实际工作生产中,面对复杂的情况,切记不要抱着学术化的概念固步自封。灵活的选择和裁剪REST(比方说URL定位结合QueryString查询数据),更有利于我们在实际工作中的落地REST。

二、Spring Boot中的RESTful落地指南

2.1项目构建

  1. 打开idea,基于Spring Boot脚手架成标准的项目模板
  2. 添加相应的依赖
  3. pojo层定于统一返回类型Result类与Student学生属性类
    统一的结果返回类型
java 复制代码
package org.araby.restful.pojo;

import lombok.Data;

@Data
public class Result<T> {
    private T data;
    private Integer code;
    private String message;

    public static<T> Result<T> success(T data){
    	Result<T> result = new Result<T>();
    	result.setData(data);
    	result.setCode(1);
    	result.setMessage("success");
    	return result;
    }

    public static<T> Result<T> success(){
        return success(null);
    }

    public static<T> Result<T> error(T data){
        Result<T> result = new Result<T>();
        result.setData(data);
        result.setCode(-1);
        result.setMessage("error");
        return result;
    }

    public static<T> Result<T> error(){
        return error(null);
    }
}

Student 类

java 复制代码
package org.araby.restful.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    public Integer id;
    public String name;
    public Integer age;
    public String sex;
    public String classNo;
    public LocalDate createTime;
    public LocalDate updateTime;
}
  1. mapper层定于StudentMapper,基于MyBatis实现和数据库的交互
java 复制代码
package org.araby.restful.mapper;

import org.apache.ibatis.annotations.*;
import org.araby.restful.pojo.Student;

import java.util.List;

@Mapper
public interface StudentMapper {
    @Select("select id, name, age, sex, class_no, create_time, update_time from student")
    List<Student> findAll();

    @Select("select id, name, age, sex, class_no, create_time, update_time from student where id = #{id}")
    Student findById(Integer id);

    @Delete("delete from student where id = #{id}")
    Integer deleteById (Integer id);

    @Insert("insert into student(name, age, sex, class_no,create_time, update_time) values(#{name}, #{age}, #{sex}, #{classNo},#{createTime},#{updateTime})")
    Integer insert(Student student);

    @Update("update student set name = #{name}, age = #{age}, sex = #{sex}, class_no = #{classNo} ,update_time = #{updateTime} where id = #{id}")
    Integer update(Student student);

}
  1. service层定于StudentService,封装业务逻辑
java 复制代码
package org.araby.restful.service;

import org.araby.restful.pojo.Student;
import java.util.List;

public interface StudentService {
    List<Student> findAll();
    Student findById(Integer id);
    Boolean deleteById(Integer id);

    Boolean insert(Student student);
    Boolean update(Student student);

    Boolean patchStudent(Integer id, Student updateStudent);
}
java 复制代码
package org.araby.restful.service.impl;

import org.araby.restful.mapper.StudentMapper;
import org.araby.restful.pojo.Student;
import org.araby.restful.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class StudentServiceImpl implements StudentService {
    private  final StudentMapper studentMapper;
    @Autowired
    public StudentServiceImpl( StudentMapper studentMapper) {
        this.studentMapper = studentMapper;
    }

    @Override
    public List<Student> findAll() {
        return studentMapper.findAll();
    }
    @Override
    public Student findById(Integer id) {
        return studentMapper.findById(id);
    }
    @Override
    public Boolean deleteById(Integer id) {
        return  studentMapper.deleteById(id) > 0;
    }

    @Override
    public Boolean insert(Student student) {
        student.setCreateTime(java.time.LocalDate.now());
        student.setUpdateTime(java.time.LocalDate.now());
        return studentMapper.insert(student) > 0;
    }

    @Override
    public Boolean update(Student student) {
        student.setUpdateTime(java.time.LocalDate.now());
        return studentMapper.update(student) > 0;
    }

    @Override
    public Boolean patchStudent(Integer id, Student updateStudent) {
        Student existingStudent = studentMapper.findById(id);
        if (existingStudent == null) {
            return false;
        }
        if (updateStudent.getName() != null) {
            existingStudent.setName(updateStudent.getName());
        }
        if (updateStudent.getAge() != null) {
            existingStudent.setAge(updateStudent.getAge());
        }
        if (updateStudent.getSex() != null) {
            existingStudent.setSex(updateStudent.getSex());
        }
        if (updateStudent.getClassNo() != null) {
            existingStudent.setClassNo(updateStudent.getClassNo());
        }
        existingStudent.setUpdateTime(java.time.LocalDate.now());
        return studentMapper.update(existingStudent) > 0;
    }


}
  1. controller层定于StudentRESTfulController
java 复制代码
@RestController
public class StudentRESTfulController {

}

至此,准备工作全部完成,下面开始搭建RESTful的服务。

2.2 RESTful服务搭建

2.2.1 资源路由定义

java 复制代码
@RestController
public class StudentRESTfulController {
    private final StudentServiceImpl studentService;
    @Autowired
    public StudentRESTfulController(StudentServiceImpl studentService) {
        this.studentService = studentService;
    }
}

在Spring Boot中,控制器专注于接口的,需要用@RestController注解修饰。 这个注解本身是是个组合注解,内部包含@Controller和@ResponseBody。前者是向Spring标记本类是请求处理器,并且需要作为bean自动扫描注册到bean容器里。后者是将Controller内的所有控制器方法的返回值,序列化成JSON作为响应返回。

如果想给控制器里每个控制器方法添加一个统一的访问路径,可借助RequestMapping,传入访问路径

java 复制代码
@RequestMapping("/api")
@RestController
public class StudentRESTfulController {

2.2.2 控制器方法的HTTP语义定义

在Spring Boot中,通过对控制器方法前添加诸如@GetMapping,@PostMapping,@PutMapping,@PatchMapping,@DeleteMapping。注解内部也可以定义路由参数,比如下面示例里的"{id}"是路由模板中的占位符,表示该方法需要接收一个名为id的参数,且该参数会从URL中提取

java 复制代码
@GetMapping("/students")
    public Result<List<Student>> findAll() {

@GetMapping("/students/{id}")
    public Result<Student> findById(@PathVariable(required = true) Integer id) {

@DeleteMapping("/students")
    public Result<Void> deleteById( Integer id) {

值得注意的控制器方法参数可以值得为必须传入,通过PathVariable注解的required = true属性

HTTP方法

HTTP 操作类型 幂等性 典型场景
GET 读取 获取单个资源(/api/students/1)或资源列表(/api/students)
POST 创建 创建新资源
PUT 全量更新 通过提供全部字段,完整替换资源
PATCH 部分更新 增量更新资源,修改部分属性
DELETE 删除 删除资源

2.2.3 状态码的返回

标注的REST风格是完全按照HTTP的语义来使用HTTP协议,也就是说返回结果里也要按照HTTP协议返回相应的状态码。

Spring Boot提供了ResponseEntity,我们可以通过设置ResponseEntity的status来决定放回的响应状态码,让我们开发能够方便的返回各种状态码。body内就是本身的返回类型。

java 复制代码
@GetMapping("/students/{id}")
    public ResponseEntity<Result<Student>>findById(@PathVariable(required = true) Integer id) {
        Student student = studentService.findById( id);
        if (student == null){
            return ResponseEntity
                    .status(HttpStatus.NOT_FOUND) 
                    .body(Result.error(student)); 
        }
       else{
            return ResponseEntity.ok(Result.success(student));
        }
    }

查询一个不存在的结果,响应结果是404

一般是在全局异常拦截器里返回对应的响应状态码,这里仅作演示用。

成功状态码

状态码 ResponseEntity.status 含义 典型场景
200 OK HttpStatus.OK 请求成功,返回数据 GET 请求返回资源列表或单个资源
201 Created HttpStatus.CREATED 资源创建成功 POST 请求创建新资源
202 Accepted HttpStatus.ACCEPTED 请求已接受但未完成处理 异步操作(如后台任务处理),需在响应头中包含 Location 指向状态查询 URI
204 No Content HttpStatus.OK 请求成功但无返回内容 PUT/PATCH/DELETE 操作成功后(无需返回数据)

客户端错误状态码

状态码 ResponseEntity.status 含义 典型场景
400 Bad Request HttpStatus.BAD_REQUEST 请求参数无效或格式错误 模型验证失败或请求体格式错误
401 Unauthorized HttpStatus.UNAUTHORIZED 未认证 未登录
403 Forbidden HttpStatus.FORBIDDEN 已认证但权限不足 比如JWT Token过期失效
404 Not Found HttpStatus.NOT_FOUND 资源不存在 请求的 ID 对应的资源不存在(如 /api/users/999)
405 Method Not Allowed HttpStatus.METHOD_NOT_ALLOWED 指定 URI 不支持该 HTTP 方法 对 /api/users 用 DELETE(通常 DELETE 应针对单个资源)
409 Conflict HttpStatus.CONFLICT 请求冲突(如唯一约束冲突) 创建或更新资源时版本冲突(乐观锁)
415Unsupported Media Type HttpStatus.UNSUPPORTED_MEDIA_TYPE 不支持的请求格式(如 Content-Type 错误) 发送 application/xml 但 API 仅支持 application/json

服务器错误状态码

状态码 ResponseEntity.status 含义 典型场景
500 Internal Server Error HttpStatus.INTERNAL_SERVER_ERROR 服务器内部错误 服务器内部执行出现重大错误
501 Not Implemented HttpStatus.NOT_IMPLEMENTED 方法未实现 API 端点尚未实现(如占位方法)
503 Service Unavailable HttpStatus.SERVICE_UNAVAILABLE 服务不可用(临时过载或维护) 服务器暂时无法处理请求(如限流、数据库维护)

2.2.4 返回状态码的工程化

HTTP的状态码并不适合用来表示业务层面的错误码,它是一个用来表示技术层面信息的状态码。工作中我们也能经常发现仅仅是凭借状态码,很难详细的将一个错误完整的描述清楚。

就比如404 Not Found这个状态码。请求一个不存在的URL和对于一个已存在的URL查询出一个不存在的结果都会返回404 Not Found。这其实就有一个业务和技术错误耦合在一起,导致仅仅凭借状态码难以描述清楚。

一种建议是业务方面的错误使用200返回,但是在报文内部定义指定的错误码和错误信息进行补充。

并且对于实际生成项目中来说,给每一个方法手动重复指定响应状态码是愚蠢的,还是需要通过一个全局异常拦截器来统一处理,通过注册自定义的异常处理逻辑里来返回响应状态码

2.3 RESTful服务示例

2.3.1 HttpGet

GetMapping特性的内部可以指定参数,比如下面示例的"{id}" 是路由模板中的占位,表示该方法需要接收一个名为id的参数,且该参数会从URL中提取。用@PathVariable注解修饰模板占位参数,name里的值便是模板占位符

java 复制代码
    @GetMapping("/students")
    public Result<List<Student>> findAll() {
        List<Student> students = studentService.findAll();
        return Result.success(students);
    }

    @GetMapping("/students/{id}")
    public Result<Student> findById(@PathVariable(name = "id") Integer id) {
        Student students = studentService.findById( id);
        return Result.success(students);
    }

2.3.2 HttpPost

java 复制代码
    @PostMapping("/students")
    public Result<Void> insert(@RequestBody Student student) {
        if ( studentService.insert(student)){
            return Result.success();
        }
        else {
            return Result.error();
        }
    }

2.3.3 HttpPut

HttpPut是全量更新,需要传入完整对象。这里用@RequestBody的注解修饰参数,表明该数据从请求body中获取。

java 复制代码
@PutMapping("/students")
    public Result<Void> update(@RequestBody Student student) {
        if(studentService.update(student)){
            return Result.success();
        }
        else {
            return Result.error();
        }
    }

2.3.4 HttpPatch

增量更新,不一定是幂等。比方说累加器。

java 复制代码
    @PatchMapping("/students/{id}")
    public Result<Void> patchStudent(
            @PathVariable Integer id, // 路径参数:学生ID(必选)
            @RequestBody Student updateStudent // 请求体:仅含需要修改的字段
    ){
        if (studentService.patchStudent(id, updateStudent)){
            return Result.success();
        }
        else {
            return Result.error();
        }
    }

2.3.5 HttpDelete

csharp 复制代码
    @DeleteMapping("/students/{id}")
    public Result<Void> deleteById(@PathVariable Integer id) {
        if(studentService.deleteById(id)){
            return Result.success();
        }
        else {
            return Result.error();
        }
    }

总结

本文介绍了REST架构风格及RESTful API的概念,阐述了其基于HTTP语义的设计原则,并结合Spring Boot演示了RESTful API的具体实现,包括路由设计、HTTP方法映射及状态码规范等。

相关推荐
程序定小飞2 小时前
基于springboot的学院班级回忆录的设计与实现
java·vue.js·spring boot·后端·spring
攀小黑2 小时前
基于若依-内容管理动态修改,通过路由字典配置动态管理
java·vue.js·spring boot·前端框架·ruoyi
dreams_dream3 小时前
Django序列化器
后端·python·django
懷淰メ3 小时前
python3GUI--短视频社交软件 By:Django+PyQt5(前后端分离项目)
后端·python·django·音视频·pyqt·抖音·前后端
郝开4 小时前
Spring Boot 2.7.18(最终 2.x 系列版本)1 - 技术选型:连接池技术选型对比;接口文档技术选型对比
java·spring boot·spring
有意义4 小时前
从零搭建:json-server+Bootstrap+OpenAI 全栈 AI 小项目
前端·后端·llm
安冬的码畜日常4 小时前
【JUnit实战3_30】第十八章:REST API 接口测试(下)—— REST API 接口的 MockMvc + JUnit 5 测试实战
测试工具·junit·单元测试·restful·rest api·junit5
知兀4 小时前
【Spring/SpringBoot】SSM(Spring+Spring MVC+Mybatis)方案、各部分职责、与Springboot关系
java·spring boot·spring
汤姆yu5 小时前
基于springboot的民间救援队救助系统
java·spring boot·后端·救援队