Spring Boot 4 + JDK 21 开发标准 RESTful 接口的方法
今天用一套生产级栈搭「标准 RESTful 接口」:Spring Boot 4.1.0 + JDK 21 + MyBatis-Plus + Druid + MySQL 8,对象转换用 MapStruct-Plus,接口测试用 Apifox。从建表到跑通测试,复制就能跑。
先说个坑:Spring Boot 4 必须用专门的 starter——MyBatis-Plus 要 mybatis-plus-spring-boot4-starter、Druid 要 druid-spring-boot-4-starter,用 SB3 的旧 starter 启动直接报 factoryBeanObjectType 类型错。下面会标出来。
一、先说规矩:标准长什么样
RESTful 不是教条,是让接口「不让人猜」的约定。记住这几条:
· 路径用名词复数:/api/users 而不是 /api/getUser
· 动词交给 HTTP 方法:GET 查、POST 增、PUT 改、DELETE 删
· 状态码要真:成功 200/201,参数错 400,找不到 404,服务器错 500
· 响应体统一结构:外层 code / msg / data 不变
· 出错也走统一结构:别把异常直接抛出去
二、pom 锁版本(SB4 专属 starter)
重点看 MyBatis-Plus 和 Druid 的 artifactId,都带 -boot4- / -boot-4-,用错就启动失败:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<druid.version>1.2.28</druid.version>
<mapstruct-plus.version>1.5.1</mapstruct-plus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MyBatis-Plus:SB4 必须用 -boot4-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Druid:SB4 必须用 -boot-4-starter(1.2.28 起支持) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-4-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- MySQL 8 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MapStruct-Plus:已内置 MapStruct,别再单独引 mapstruct -->
<dependency>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-spring-boot-starter</artifactId>
<version>${mapstruct-plus.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
<annotationProcessorPaths>
<path>
<groupId>io.github.linpeilie</groupId>
<artifactId>mapstruct-plus-processor</artifactId>
<version>${mapstruct-plus.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>三、建表 + 配数据源
application.yml 配 Druid 连接池 + MyBatis-Plus:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
stat-view-servlet:
enabled: true
url-pattern: /druid/*
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
global-config:
db-config:
id-type: auto
server:
port: 8080启动后访问 http://localhost:8080/druid 能看到 Druid 监控台,说明连接池就位了。
四、实体 + DTO,MapStruct-Plus 自动转
实体类用 MyBatis-Plus 注解;DTO 上加 @AutoMapper,编译期自动生成双向转换,运行时零反射:
@TableName("user")
@Data
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String email;
private Integer age;
private LocalDateTime createTime;
}
// 出参 VO:标 @AutoMapper,自动生成 User <-> UserVO
@Data
@AutoMapper(target = User.class)
public class UserVO {
private Long id;
private String username;
private String email;
private Integer age;
private LocalDateTime createTime;
}
// 入参 Request:标 @AutoMapper,自动生成 CreateUserRequest -> User
@Data
@AutoMapper(target = User.class)
public class CreateUserRequest {
@NotBlank private String username;
@Email private String email;
@Min(0) @Max(150) private Integer age;
}五、Mapper + Service + 分页配置
Mapper 继承 BaseMapper,单表 CRUD 直接白嫖;Service 继承 ServiceImpl,连手写都不用:
@Mapper
public interface UserMapper extends BaseMapper<User> {}
@Service
public class UserService extends ServiceImpl<UserMapper, User> {}分页要用 MyBatis-Plus 的分页拦截器,否则 page() 查全表不生效:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}六、统一响应 + 通用分页 + Controller
先包一层 Result<T>,前端解析逻辑只写一套:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
// 响应码:200成功,500失败
private Integer code;
// 响应提示信息
private String msg;
// 响应数据
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data);
}
public static <T> Result<T> success() {
return new Result<>(200, "操作成功", null);
}
public static <T> Result<T> error(String msg) {
return new Result<>(500, msg, null);
}
}分页再包一层通用 PageResult<T>——纯数据类,不绑死 MyBatis-Plus,前端解析统一;UserPageQuery 是分页入参,GET 接口 Spring 自动把 query string 绑到字段上:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult<T> {
private long total; // 总条数
private long pageNum; // 当前页码
private long pageSize; // 每页条数
private List<T> records; // 当前页数据
}
@Data
public class UserPageQuery {
private Integer pageNum = 1;
private Integer pageSize = 10;
private String username; // 可选:用户名模糊查询
}Controller 里 GET/POST/PUT/DELETE 各就各位,用 Converter 做 Entity↔DTO 转换:
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final Converter converter;
@GetMapping
public Result<PageResult<UserVO>> list(UserPageQuery query) {
Page<User> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.like(StringUtils.hasText(query.getUsername()), User::getUsername, query.getUsername())
.orderByDesc(User::getId);
userService.page(page, wrapper);
List<UserVO> vos = converter.convert(page.getRecords(), UserVO.class);
return Result.success(new PageResult<>(page.getTotal(), page.getCurrent(), page.getSize(), vos));
}
@GetMapping("/{id}")
public Result<UserVO> get(@PathVariable Long id) {
User user = userService.getById(id);
if (user == null) throw new NotFoundException("用户不存在: " + id);
return Result.success(converter.convert(user, UserVO.class));
}
@PostMapping
public Result<UserVO> create(@Valid @RequestBody CreateUserRequest req) {
User user = converter.convert(req, User.class);
userService.save(user);
return Result.success(converter.convert(user, UserVO.class));
}
@PutMapping("/{id}")
public Result<UserVO> update(@PathVariable Long id, @Valid @RequestBody CreateUserRequest req) {
if (userService.getById(id) == null) throw new NotFoundException("用户不存在: " + id);
User user = converter.convert(req, User.class);
user.setId(id);
userService.updateById(user);
return Result.success(converter.convert(userService.getById(id), UserVO.class));
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.removeById(id);
return Result.success();
}
}
LambdaQueryWrapper 的 like 第一个参数是条件开关——username 有值才拼 like,否则查全表,省得为「带条件/不带条件」写两个接口。
七、全局异常处理:错误也统一
一个 @RestControllerAdvice 兜住所有异常,HTTP 状态码区分错误类型(400/404/500),body 统一走 Result.error(msg):
public class NotFoundException extends RuntimeException {
public NotFoundException(String msg) { super(msg); }
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Result<Void>> handleValid(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.collect(Collectors.joining("; "));
return ResponseEntity.status(400).body(Result.error(msg));
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Result<Void>> handleNotFound(NotFoundException ex) {
return ResponseEntity.status(404).body(Result.error(ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleOther(Exception ex) {
return ResponseEntity.status(500).body(Result.error("服务器开小差了"));
}
}八、Apifox 跑一遍
接口写完得验。Apifox 比 curl 直观,能存集合、写断言、批量跑。步骤:
· 建环境:项目设置 → 环境 → 新建「本地」,baseUrl = http://localhost:8080
· 建集合:新建「用户接口」集合,依次加 5 个请求:
POST {{baseUrl}}/api/users · GET {{baseUrl}}/api/users · GET {{baseUrl}}/api/users/1 · PUT {{baseUrl}}/api/users/1 · DELETE {{baseUrl}}/api/users/1
POST 请求 Body 选 JSON,填:
{
"username": "竹雨",
"email": "zy@demo.com",
"age": 28
}在后置脚本里加断言,校验统一响应结构:
// 期望 code=200 且返回了 id
pm.test("创建成功", function () {
var json = pm.response.json();
pm.expect(json.code).to.eql(200);
pm.expect(json.data.id).to.be.above(0);
});再建一条「非法参数」用例(name 空、email 非法、age 越界),断言 body.code=500(失败)且 HTTP 状态码 400。点集合「运行」批量跑一遍,全绿就算达标。
分页接口单独测:先 POST 几条数据,再请求 GET {{baseUrl}}/api/users?pageNum=1&pageSize=10,带条件就加 &username=竹。断言分页结构:
pm.test("分页成功", function () {
var json = pm.response.json();
pm.expect(json.code).to.eql(200);
pm.expect(json.data.total).to.be.above(0);
pm.expect(json.data.records).to.be.an("array");
pm.expect(json.data.records.length).to.be.at.most(json.data.pageSize);
});兼容性提示(避坑)
· MyBatis-Plus:SB4 必须用 mybatis-plus-spring-boot4-starter(3.5.16),用 -boot-starter 或 -boot3-starter 会报 Invalid value type for attribute 'factoryBeanObjectType'。
· Druid:SB4 用 druid-spring-boot-4-starter(1.2.28 起),别用 -3-starter。
· MapStruct-Plus:1.5.1 官方标注支持到 SB2~3 / JDK8~17。SB4 + JDK21 下,注解处理器(编译期生成代码)不依赖 Spring 运行时、可正常用;starter 的自动配置基本兼容,落地前先本地跑一次转换单测,确认 Converter 能正常注入再上业务。
· web 依赖:SB4 已废弃 spring-boot-starter-web(deprecated),改用 spring-boot-starter-webmvc(仅 MVC 服务端)。若用到 RestTemplate / RestClient,再单独加 spring-boot-starter-restclient。
状态码对、结构齐、错误不裸奔——这套接口就算「达标」了。JDK 21 的虚拟线程也已经正式 GA,接口要扛并发,application.properties 里加一行 spring.threads.virtual.enabled=true 就能白捡一层并发红利。
你们项目接口现在是哪套栈?MyBatis-Plus 还是 JPA,转换还在手写吗?
MySQL8 里建张 user 表:
CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(64) NOT NULL, `email` VARCHAR(128) DEFAULT NULL, `age` INT DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
到此这篇关于Spring Boot 4 + JDK 21 开发标准 RESTful 接口的方法的文章就介绍到这了,更多相关Spring Boot JDK 开发RESTful 接口内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
详解springboot项目带Tomcat和不带Tomcat的两种打包方式
这篇文章主要介绍了详解springboot项目带Tomcat和不带Tomcat的两种打包方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2020-09-09
spring-boot-maven-plugin 插件的作用详解
添加了spring-boot-maven-plugin插件后,当运行maven打包的命令,项目会被打包成一个可以直接运行的jar包,使用"java -jar"可以直接运行。这篇文章主要给大家介绍spring-boot-maven-plugin 插件的作用,感兴趣的朋友一起看看吧2018-10-10
java9新特性Reactive Stream响应式编程 API
这篇文章主要为大家介绍了java9新特性响应式编程API的特点详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪2022-03-03


最新评论