文章目录
基于 SpringBoot 3.3.5
、MyBatis-Plus 3.5.5
和 Lombok
技术栈,封装通用 BaseController
实现 Controller 层 CRUD 逻辑复用。通过标准化注解、精简注释,确保代码可读性与可维护性,子类仅需继承即可拥有全套通用接口。
一、环境准备:依赖配置
在 pom.xml
中引入核心依赖,包含 SpringBoot 基础、MyBatis-Plus、Lombok 及 Swagger(接口文档,可选但推荐):
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<dependencies>
<!-- SpringBoot Web 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus 启动器(3.5.5 适配 SpringBoot 3.x) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- Lombok(简化实体类 getter/setter、构造器等) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- MySQL 驱动(根据数据库类型调整) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Swagger/OpenAPI(接口文档,便于调试) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>
</project>
二、核心工具类:ApprenticeUtil
提供 格式转换、QueryWrapper 自动构建、反射获取字段值 能力,为 BaseController 提供底层支持,所有方法均添加详细说明与参数注解。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 通用工具类:提供驼峰/下划线转换、QueryWrapper构建、反射字段值获取等能力
* @author 开发者名称
* @since JDK 17(SpringBoot 3.x 最低支持 JDK 17)
*/
public class ApprenticeUtil {
/** 驼峰转下划线正则:匹配大写字母 */
private static final Pattern HUMP_PATTERN = Pattern.compile("[A-Z]");
/** 下划线转驼峰正则:匹配下划线+后续字符 */
private static final Pattern LINE_PATTERN = Pattern.compile("_(\\w)");
/**
* 驼峰命名转下划线命名(如 userName → user_name)
* @param str 输入的驼峰格式字符串,可为空(为空时返回空)
* @return java.lang.String 转换后的下划线格式字符串,若输入为空则返回空
*/
public static String humpToLine(String str) {
if (ObjectUtils.isEmpty(str)) {
return str;
}
Matcher matcher = HUMP_PATTERN.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 下划线命名转驼峰命名(如 user_name → userName)
* @param str 输入的下划线格式字符串,可为空(为空时返回空)
* @return java.lang.String 转换后的驼峰格式字符串,若输入为空则返回空
*/
public static String lineToHump(String str) {
if (ObjectUtils.isEmpty(str)) {
return str;
}
str = str.toLowerCase();
Matcher matcher = LINE_PATTERN.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
return sb.toString();
}
/**
* 根据实体非空字段自动构建 QueryWrapper(仅生成 eq 条件)
* @param entity 实体对象,不可为空(为空时返回空 QueryWrapper)
* @param <E> 实体泛型类型
* @return com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<E> 构建后的查询条件,若反射异常返回空
*/
public static <E> QueryWrapper<E> getQueryWrapper(E entity) {
if (ObjectUtils.isEmpty(entity)) {
return new QueryWrapper<>();
}
Field[] fields = entity.getClass().getDeclaredFields();
QueryWrapper<E> queryWrapper = new QueryWrapper<>();
for (Field field : fields) {
// 跳过 final 修饰的常量字段
if (Modifier.isFinal(field.getModifiers())) {
continue;
}
field.setAccessible(true); // 突破私有字段访问限制
try {
Object fieldValue = field.get(entity);
// 字段值非空时,添加 eq 条件(字段名转下划线匹配数据库列)
if (!ObjectUtils.isEmpty(fieldValue)) {
String dbColumn = humpToLine(field.getName());
queryWrapper.eq(dbColumn, fieldValue);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
return queryWrapper;
}
/**
* 反射获取实体指定字段的值(通过 getter 方法,遵循 JavaBean 规范)
* @param entity 实体对象,不可为空(为空时返回空)
* @param fieldName 字段名(如 "id"、"name"),不可为空(为空时返回空)
* @param <E> 实体泛型类型
* @return java.lang.Object 字段的具体值,若反射异常(如字段不存在)则返回空
*/
public static <E> Object getValueForClass(E entity, String fieldName) {
if (ObjectUtils.isEmpty(entity) || ObjectUtils.isEmpty(fieldName)) {
return null;
}
Field field = null;
PropertyDescriptor propertyDescriptor = null;
try {
// 获取实体类的指定字段
field = entity.getClass().getDeclaredField(fieldName);
// 获取字段的 PropertyDescriptor(封装 getter/setter)
propertyDescriptor = new PropertyDescriptor(field.getName(), entity.getClass());
} catch (NoSuchFieldException | IntrospectionException e) {
e.printStackTrace();
return null;
}
// 调用 getter 方法获取字段值
Method getterMethod = Objects.requireNonNull(propertyDescriptor).getReadMethod();
return ReflectionUtils.invokeMethod(getterMethod, entity);
}
}
三、通用响应工具类:ResponseUtils
统一接口返回格式,包含状态码、提示信息、业务数据,使用 Lombok 简化代码。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 接口统一响应工具类:封装返回结果格式
* @author 开发者名称
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseUtils<T> {
/** 状态码:200=成功,500=失败,400=参数错误(可扩展更多状态码) */
private Integer code;
/** 提示信息:如 "操作成功"、"参数不能为空" */
private String msg;
/** 业务数据:如列表、单条实体、统计数量等 */
private T data;
/**
* 成功响应(无业务数据)
* @param msg 成功提示信息
* @return ResponseUtils<T> 统一响应对象
*/
public static <T> ResponseUtils<T> success(String msg) {
return new ResponseUtils<>(200, msg, null);
}
/**
* 成功响应(带业务数据)
* @param msg 成功提示信息
* @param data 业务数据
* @return ResponseUtils<T> 统一响应对象
*/
public static <T> ResponseUtils<T> success(String msg, T data) {
return new ResponseUtils<>(200, msg, data);
}
/**
* 失败响应(无业务数据)
* @param msg 失败提示信息
* @return ResponseUtils<T> 统一响应对象
*/
public static <T> ResponseUtils<T> error(String msg) {
return new ResponseUtils<>(500, msg, null);
}
/**
* 参数错误响应(无业务数据)
* @param msg 参数错误提示信息
* @return ResponseUtils<T> 统一响应对象
*/
public static <T> ResponseUtils<T> paramError(String msg) {
return new ResponseUtils<>(400, msg, null);
}
}
四、分页参数 DTO:PageParamDto
封装分页查询所需参数(页码、页大小、排序字段),使用 Lombok 简化代码,字段添加注释说明。
import lombok.Data;
import org.springframework.util.ObjectUtils;
/**
* 分页查询参数 DTO:统一分页与排序参数格式
* @author 开发者名称
* @param <E> 筛选条件实体类型(可为空,用于分页+条件查询)
*/
@Data
public class PageParamDto<E> {
/** 页码:默认1,不能小于1 */
private Integer page = 1;
/** 页大小:默认10,最大不超过100(避免性能问题) */
private Integer size = 10;
/** 升序排序字段:多字段用逗号分隔(如 "createTime,name"),可为空 */
private String asc;
/** 降序排序字段:多字段用逗号分隔(如 "updateTime,id"),可为空 */
private String desc;
/** 筛选条件实体:非空字段作为查询条件,可为空 */
private E entity;
/**
* 分页参数校验与修正:确保页码≥1,页大小≤100
*/
public void validateParam() {
this.page = ObjectUtils.isEmpty(this.page) || this.page < 1 ? 1 : this.page;
this.size = ObjectUtils.isEmpty(this.size) || this.size > 100 ? 100 : this.size;
}
}
五、MyBatis-Plus 分页配置
SpringBoot 3.x 环境下,通过配置类注入 MyBatis-Plus 分页插件,确保分页功能生效。
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus 配置类:注入分页插件(适配 SpringBoot 3.x)
* @author 开发者名称
*/
@Configuration
public class MyBatisPlusConfig {
/**
* 注册 MyBatis-Plus 拦截器链:添加分页插件
* @return com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor 拦截器对象
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件:指定数据库类型为 MySQL(其他数据库需修改 DbType,如 DbType.ORACLE)
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 设置单页最大条数(与 PageParamDto 保持一致,避免超大页查询)
paginationInterceptor.setMaxLimit(100L);
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
六、核心基类:BaseController
通过泛型适配任意实体与 Service,封装 8 个通用接口,所有参数与方法均添加详细注解说明。
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Controller 通用基类:封装 CRUD、分页、排序等通用接口
* @author 开发者名称
* @param <S> Service 类型:必须继承 MyBatis-Plus 的 IService 接口
* @param <E> 实体类型:与 Service 对应的数据库实体
*/
@Tag(name = "通用CRUD接口", description = "所有业务Controller继承此基类,自动获得通用接口")
public class BaseController<S extends IService<E>, E> {
/** 通用 Service:自动注入子类对应的 Service 实现(子类无需重复注入) */
@Autowired
protected S baseService;
/**
* 新增数据(单条)
* @param entity 实体对象:包含新增的业务数据(需符合数据库字段规则)
* @return ResponseUtils<E> 响应结果:成功返回200+提示,失败返回500+错误信息
*/
@Operation(summary = "新增数据", description = "单条数据新增,实体对象非空字段为新增内容")
@PostMapping("/insert")
public ResponseUtils<E> insert(
@Parameter(description = "新增实体对象", required = true) @RequestBody E entity
) {
boolean isSuccess = baseService.save(entity);
return isSuccess ? ResponseUtils.success("新增成功") : ResponseUtils.error("新增失败");
}
/**
* 批量删除数据(按ID列表)
* @param ids ID列表:需删除的实体ID集合(不可为空且不可包含无效ID)
* @return ResponseUtils<E> 响应结果:成功返回200+提示,失败返回500+错误信息
*/
@Operation(summary = "批量删除数据", description = "按ID列表批量删除,ID不可为空且需为有效数字")
@PostMapping("/deleteByIds")
public ResponseUtils<E> deleteByIds(
@Parameter(description = "删除ID列表(如[1,2,3])", required = true) @RequestBody List<Long> ids
) {
if (ids == null || ids.isEmpty()) {
return ResponseUtils.paramError("请选择需删除的记录ID");
}
boolean isSuccess = baseService.removeByIds(ids);
return isSuccess ? ResponseUtils.success("删除成功") : ResponseUtils.error("删除失败");
}
/**
* 按ID更新数据
* @param entity 实体对象:必须包含ID字段(用于定位数据),其他非空字段为更新内容
* @return ResponseUtils<E> 响应结果:成功返回200+提示,失败返回500+错误信息
*/
@Operation(summary = "按ID更新数据", description = "实体需包含ID字段,非空字段为更新内容")
@PostMapping("/updateById")
public ResponseUtils<E> updateById(
@Parameter(description = "更新实体对象(含ID)", required = true) @RequestBody E entity
) {
// 反射校验ID是否存在(简化版:实际可结合 ApprenticeUtil 完善)
Object id = ApprenticeUtil.getValueForClass(entity, "id");
if (id == null) {
return ResponseUtils.paramError("更新需指定ID");
}
boolean isSuccess = baseService.updateById(entity);
return isSuccess ? ResponseUtils.success("更新成功") : ResponseUtils.error("更新失败");
}
/**
* 按ID查询单条数据
* @param id 实体ID:不可为空且需为有效数字(如1、2)
* @return ResponseUtils<E> 响应结果:成功返回200+实体数据,无数据返回200+空数据
*/
@Operation(summary = "按ID查询数据", description = "根据实体ID查询单条记录,ID不可为空")
@GetMapping("/getById")
public ResponseUtils<E> getById(
@Parameter(description = "实体ID", required = true) @RequestParam Long id
) {
if (id == null || id < 1) {
return ResponseUtils.paramError("ID无效(需为正整数)");
}
E entity = baseService.getById(id);
return ResponseUtils.success("查询成功", entity);
}
/**
* 保存或更新数据(自动判断)
* @param entity 实体对象:含ID则更新,不含ID则新增(ID生成策略需配置,如自增、雪花算法)
* @return ResponseUtils<E> 响应结果:成功返回200+提示,失败返回500+错误信息
*/
@Operation(summary = "保存或更新数据", description = "自动判断:含ID则更新,不含ID则新增")
@PostMapping("/saveOrUpdate")
public ResponseUtils<E> saveOrUpdate(
@Parameter(description = "保存/更新实体对象", required = true) @RequestBody E entity
) {
boolean isSuccess = baseService.saveOrUpdate(entity);
return isSuccess ? ResponseUtils.success("操作成功") : ResponseUtils.error("操作失败");
}
/**
* 带条件查询列表
* @param entity 实体对象:非空字段作为查询条件(如name="张三"则查询name为张三的所有数据)
* @return ResponseUtils<List<E>> 响应结果:成功返回200+列表数据,无数据返回空列表
*/
@Operation(summary = "带条件查询列表", description = "实体非空字段作为eq条件,返回符合条件的所有数据")
@PostMapping("/list")
public ResponseUtils<List<E>> list(
@Parameter(description = "查询条件实体(非空字段为条件)", required = false) @RequestBody(required = false) E entity
) {
QueryWrapper<E> queryWrapper = ApprenticeUtil.getQueryWrapper(entity);
List<E> dataList = baseService.list(queryWrapper);
return ResponseUtils.success("查询成功", dataList);
}
/**
* 分页查询(支持排序)
* @param pageParamDto 分页参数:包含页码、页大小、排序字段、筛选条件实体
* @return ResponseUtils<Page<E>> 响应结果:成功返回200+分页数据(含总条数、当前页数据)
*/
@Operation(summary = "分页查询", description = "支持分页+多字段排序+条件筛选,页码默认1,页大小默认10")
@PostMapping("/page")
public ResponseUtils<Page<E>> page(
@Parameter(description = "分页参数(含排序、筛选条件)", required = true) @RequestBody PageParamDto<E> pageParamDto
) {
// 分页参数校验与修正
pageParamDto.validateParam();
// 初始化分页对象
Page<E> page = new Page<>(pageParamDto.getPage(), pageParamDto.getSize());
// 构建查询条件(含筛选+排序)
QueryWrapper<E> queryWrapper = ApprenticeUtil.getQueryWrapper(pageParamDto.getEntity());
// 处理升序排序
if (!org.springframework.util.ObjectUtils.isEmpty(pageParamDto.getAsc())) {
String[] ascFields = pageParamDto.getAsc().split(",");
queryWrapper.orderByAsc(ascFields);
}
// 处理降序排序
if (!org.springframework.util.ObjectUtils.isEmpty(pageParamDto.getDesc())) {
String[] descFields = pageParamDto.getDesc().split(",");
queryWrapper.orderByDesc(descFields);
}
// 执行分页查询
Page<E> resultPage = baseService.page(page, queryWrapper);
return ResponseUtils.success("分页查询成功", resultPage);
}
/**
* 按条件统计记录数
* @param entity 实体对象:非空字段作为统计条件(如status=1则统计状态为1的总条数)
* @return ResponseUtils<Long> 响应结果:成功返回200+统计数量,无数据返回0
*/
@Operation(summary = "按条件统计数量", description = "实体非空字段作为eq条件,返回符合条件的总条数")
@PostMapping("/count")
public ResponseUtils<Long> count(
@Parameter(description = "统计条件实体(非空字段为条件)", required = false) @RequestBody(required = false) E entity
) {
QueryWrapper<E> queryWrapper = ApprenticeUtil.getQueryWrapper(entity);
long total = baseService.count(queryWrapper);
return ResponseUtils.success("统计成功", total);
}
}
七、实体类示例:Blog(博客实体)
使用 Lombok 简化代码,字段添加一行注释说明数据库含义,配合 MyBatis-Plus 注解映射数据库表。
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 博客实体:映射数据库 blog 表
* @author 开发者名称
*/
@Data
@TableName("blog") // 数据库表名(若实体名与表名一致可省略)
public class Blog {
/** 博客ID:主键,自增 */
@TableId(type = IdType.AUTO)
private Long id;
/** 博客标题:非空,最长255字符 */
private String title;
/** 博客内容:非空,长文本 */
private String content;
/** 作者ID:关联用户表主键 */
private Long authorId;
/** 博客状态:0=草稿,1=发布,2=删除 */
private Integer status;
/** 创建时间:自动填充(需配置MyBatis-Plus自动填充) */
private LocalDateTime createTime;
/** 更新时间:自动填充(需配置MyBatis-Plus自动填充) */
private LocalDateTime updateTime;
}
八、Service 层示例:IBlogService 与实现
遵循 MyBatis-Plus 规范,Service 接口继承 IService
,实现类继承 ServiceImpl
,无需编写基础 CRUD 逻辑。
1. 接口:IBlogService
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.entity.Blog;
/**
* 博客 Service 接口:继承 MyBatis-Plus IService,获得基础 CRUD 能力
* @author 开发者名称
*/
public interface IBlogService extends IService<Blog> {
// 如需自定义业务方法,在此添加(如:根据作者ID查询博客列表)
}
2. 实现类:BlogServiceImpl
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.Blog;
import com.example.demo.mapper.BlogMapper;
import com.example.demo.service.IBlogService;
import org.springframework.stereotype.Service;
/**
* 博客 Service 实现类:继承 MyBatis-Plus ServiceImpl,无需编写基础 CRUD 实现
* @author 开发者名称
*/
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
// 基础 CRUD 逻辑由 MyBatis-Plus 自动实现,自定义方法在此实现
}
九、子类 Controller 示例:BlogController
继承 BaseController
,指定泛型(Service 类型 + 实体类型),无需编写代码即可拥有所有通用接口。
import com.example.demo.entity.Blog;
import com.example.demo.service.IBlogService;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 博客 Controller:继承 BaseController 获得通用 CRUD 接口
* @author 开发者名称
*/
@RestController
@RequestMapping("/api/blog") // 接口基础路径(前端请求路径:http://localhost:8080/api/blog/xxx)
@Tag(name = "博客管理", description = "博客相关操作:新增、删除、更新、查询、分页等")
public class BlogController extends BaseController<IBlogService, Blog> {
// 无需编写代码,已自动拥有以下接口:
// 1. POST /api/blog/insert 新增
// 2. POST /api/blog/deleteByIds 批量删除
// 3. POST /api/blog/updateById 按ID更新
// 4. GET /api/blog/getById 按ID查询
// 5. POST /api/blog/saveOrUpdate 保存或更新
// 6. POST /api/blog/list 条件查询列表
// 7. POST /api/blog/page 分页查询
// 8. POST /api/blog/count 条件统计数量
// 如需自定义接口(如:根据作者ID查询博客),在此新增即可
}
十、关键注意事项
- JDK 版本:SpringBoot 3.x 最低支持 JDK 17,需确保开发环境与部署环境一致;
- 实体 ID 规范:实体 ID 建议统一命名为
id
(Long 类型),若使用其他名称需修改BaseController
中反射获取 ID 的逻辑(ApprenticeUtil.getValueForClass(entity, "id")
); - 自动填充配置:
createTime
、updateTime
等字段建议配置 MyBatis-Plus 自动填充,避免手动设置; - 异常处理:实际项目中建议添加全局异常处理器(
@RestControllerAdvice
),统一捕获并处理反射异常、数据库异常等; - 权限控制:通用接口需结合权限框架(如 Spring Security、Sa-Token)添加权限校验,避免未授权访问。