Java快速开发框架:基于Spring Boot与MyBatis-Plus的接口高效开发实践
1. 项目概述为什么我们需要一个“快速”的接口框架干了这么多年后端开发最头疼的事情之一就是每次新项目启动都要花大量时间在那些重复、繁琐但又不得不做的“基础建设”上。比如一个用户注册接口核心业务逻辑可能就几行代码校验参数、查重、加密密码、入库。但为了这几行代码你得先搭好项目骨架配置好数据库连接池定义好统一的响应格式写好参数校验的注解处理全局异常再考虑一下接口文档怎么自动生成……一套组合拳下来半天时间就没了而且每个项目都差不多。这感觉就像每次做饭都得先自己烧砖垒灶台而不是直接开火炒菜。“基于Java的接口快速开发框架”要解决的就是这个“垒灶台”的问题。它的核心目标不是替代Spring Boot这类成熟的生态而是在其之上做一层高度封装和约定把那些每个项目都要做的、模式固定的“脏活累活”标准化、自动化。让你拿到需求后能立刻、马上、专注于写那几行核心的业务逻辑代码而不是被技术细节缠住手脚。简单说它想成为Java后端开发者的“瑞士军刀”或“脚手架生成器”大幅降低从零到一、从一到N的启动和迭代成本。这个框架面向的主要是中小型团队、快速迭代的业务项目、需要快速验证想法的创业公司或者是在大公司里经常需要承接各种内部工具、运营后台的开发者。对于他们来说开发效率、交付速度和代码规范的一致性往往比追求极致的性能或架构灵活性更重要。这个框架的价值就在于用一套经过验证的最佳实践把这些诉求打包成一个“开箱即用”的解决方案。2. 框架核心设计思路约定优于配置封装通用能力一个合格的快速开发框架绝不是把一堆开源库胡乱堆砌在一起。它的设计必须要有清晰的哲学和边界。我总结下来核心思路就八个字“约定优于配置封装通用能力”。2.1 “约定”的力量减少决策提升一致性“约定优于配置”Convention Over Configuration是Ruby on Rails带火的概念但在Java世界同样威力巨大。框架会预先定义好一整套开发规范比如项目结构约定controller,service,mapper,model,config这些包放在哪里叫什么名字。响应格式约定所有接口返回的JSON统一为{“code”: 200, “msg”: “success”, “data”: {}}这样的结构。异常处理约定业务异常、参数校验异常、系统异常分别怎么抛出怎么被全局捕获并转换成约定的响应格式。数据库操作约定实体类如何映射表通用的CRUD方法叫什么名字。开发者不需要在每一个新项目里都去争论和决策这些事直接遵循框架的约定即可。这带来的好处是巨大的团队协作成本降低新人上手极快代码风格统一后期维护也更容易。框架通过这种强约定把开发者从无尽的“选择困难症”中解放出来。2.2 “封装”的智慧提炼共性暴露简洁接口快速开发框架的另一个核心是封装。它会把那些通用、繁琐但必要的技术组件进行深度封装只暴露出最简单、最直观的API给开发者。数据访问层封装基于MyBatis-Plus或Spring Data JPA封装通用的BaseMapper和BaseService。你只需要让你的Mapper接口继承BaseMapper就能立刻拥有单表CRUD、分页、条件构造等全套能力无需写任何XML。对于简单的增删改查甚至一行SQL都不用写。参数校验封装整合Validation如Hibernate Validator但提供更友好的校验注解和全局异常处理。框架会自动捕获校验失败异常并转换成格式友好的错误信息返回给前端开发者只需要在DTO字段上加NotBlank、Email这样的注解即可。全局上下文封装比如用户登录信息。框架可以提供一个ThreadLocal封装的UserContext工具类在拦截器中自动将JWT解析出的用户信息注入在业务代码的任何地方都能通过UserContext.getCurrentUser()直接获取无需在每个Controller方法参数里传递。第三方集成封装对常用的OSS文件上传、短信发送、邮件推送、分布式锁等功能提供“一键配置”的Starter和简洁的Service类。你只需要在application.yml里填好AK/SK和端点就能像调用本地方法一样使用这些服务。注意封装不是“黑盒”。好的框架会在提供便利的同时保持足够的扩展性。当默认行为不满足需求时开发者应该能通过实现特定接口、覆盖配置类等方式进行定制而不是被框架“锁死”。3. 核心模块拆解与实操要点一个完整的快速开发框架通常由以下几个核心模块组成。我们逐一拆解并看看在实际项目中如何应用。3.1 统一响应与异常处理模块这是框架的“门面”决定了所有接口的“长相”和行为一致性。实现要点定义统一响应体创建一个泛型类如R。Data AllArgsConstructor NoArgsConstructor public class RT { private Integer code; // 状态码如200成功500失败 private String msg; // 提示信息 private T data; // 响应数据 public static T RT success(T data) { return new R(200, “操作成功”, data); } public static T RT error(String msg) { return new R(500, msg, null); } // 可以定义更多工厂方法如 success(), error(Integer code, String msg)等 }全局异常处理器使用RestControllerAdvice或ControllerAdvice。RestControllerAdvice public class GlobalExceptionHandler { // 处理业务异常 ExceptionHandler(BusinessException.class) public RVoid handleBusinessException(BusinessException e) { log.error(“业务异常”, e); return R.error(e.getCode(), e.getMessage()); } // 处理参数校验异常MethodArgumentNotValidException 或 BindException ExceptionHandler(MethodArgumentNotValidException.class) public RVoid handleValidException(MethodArgumentNotValidException e) { String message e.getBindingResult().getAllErrors() .stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.joining(“, “)); log.warn(“参数校验失败{}”, message); return R.error(400, message); } // 处理其他所有未捕获异常 ExceptionHandler(Exception.class) public RVoid handleException(Exception e) { log.error(“系统异常”, e); // 生产环境可以返回更模糊的信息如“系统繁忙” return R.error(500, “系统内部错误”); } }自定义业务异常类让业务层能抛出有明确语义的异常。public class BusinessException extends RuntimeException { private Integer code; public BusinessException(Integer code, String message) { super(message); this.code code; } // getters... }实操心得状态码设计不要直接用HTTP状态码如404 500作为业务码。可以定义两套体系HTTP状态码反映网络请求状态如200成功400客户端错误500服务端错误业务码反映具体的业务结果如1001用户不存在1002密码错误。R类里的code通常指业务码。异常日志业务异常BusinessException通常只打WARN级别日志因为这是可预见的业务逻辑分支。而未知的Exception必须打ERROR级别并打印堆栈方便排查。前端对接和前端同学约定好他们只关心R结构里的code和data。任何非200的HTTP状态码都视为网络或框架层异常应由前端统一进行网络错误提示。3.2 数据访问与MyBatis-Plus深度集成模块这是提升CRUD效率最显著的部分。MyBatis-Plus简称MP是这里的首选。实现要点引入依赖与配置在pom.xml中引入mybatis-plus-boot-starter并配置Mapper扫描路径。创建通用基类BaseEntity定义所有实体共有的字段如id,createTime,updateTime并配合MP的TableLogic逻辑删除、TableField(fill FieldFill.INSERT/UPDATE)自动填充等注解。Data public abstract class BaseEntity { TableId(type IdType.ASSIGN_ID) // 分布式ID private Long id; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic // 逻辑删除标记 private Integer deleted; }BaseMapper与BaseService你的Mapper接口继承MP的BaseMapperService层可以封装一个IService和ServiceImpl。// 自定义的通用Mapper可以添加一些全局方法 public interface MyBaseMapperT extends BaseMapperT { // 例如批量插入MySQL方言MP本身有这里只是示例 Integer insertBatchSomeColumn(CollectionT entityList); } // 自定义的通用Service接口 public interface MyBaseServiceT extends IServiceT { // 可以定义一些公共的业务方法如带缓存的查询 T getByIdWithCache(Long id); }配置自动填充与插件通过实现MetaObjectHandler接口来自动填充createTime等字段。配置分页插件PaginationInnerInterceptor、乐观锁插件等。实操心得慎用QueryWrapper在Service层使用QueryWrapper构建查询条件时要避免将前端参数直接拼接防止SQL注入。更推荐使用MP的LambdaQueryWrapper它是类型安全的。// 推荐Lambda方式编译时检查 LambdaQueryWrapperUser wrapper Wrappers.UserlambdaQuery() .eq(User::getUsername, username) .eq(User::getStatus, 1); // 不推荐字符串方式容易拼错 QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(“username”, username).eq(“status”, 1);逻辑删除的坑启用TableLogic后MP的delete*方法会变为更新deleted字段select*方法会自动加上deleted0条件。如果需要查询已删除的数据需要自己写SQL或使用wrapper忽略逻辑删除条件.ignoreLogicDel()需谨慎。分页查询规范统一使用MP的分页对象Page。在Controller中可以定义一个通用的分页查询参数类PageParam接收pageNum和pageSize在Service中转换为Page对象。3.3 身份认证与权限控制模块几乎所有的接口都需要知道“谁在请求”以及“他有没有权限”。这块是安全的重中之重。实现要点以JWT为例JWT工具类封装生成Token、解析Token、刷新Token的方法。依赖jjwt库。Component public class JwtUtil { Value(“${jwt.secret}”) private String secret; Value(“${jwt.expiration}”) private Long expiration; public String generateToken(String username, MapString, Object claims) { // ... 使用JJWT API构建Token return Jwts.builder() .setClaims(claims) .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expiration * 1000)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public Claims parseToken(String token) { // ... 解析并验证Token return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } // ... 其他方法 }认证拦截器实现HandlerInterceptor在preHandle方法中验证Token。public class AuthenticationInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(“Authorization”); if (StringUtils.isBlank(token)) { throw new BusinessException(401, “缺少认证令牌”); } try { Claims claims jwtUtil.parseToken(token.replace(“Bearer “, “”)); String username claims.getSubject(); // 将用户信息存入上下文如UserContext UserContext.setCurrentUser(username); // 可以将claims中的其他信息如userId role也存入上下文 return true; } catch (Exception e) { throw new BusinessException(401, “认证令牌无效或已过期”); } } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后清除上下文防止内存泄漏 UserContext.clear(); } }权限注解与切面使用Spring AOP实现基于注解的权限检查。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequiresPermissions { String[] value(); // 权限标识符数组如 [“user:add”, “user:edit”] } Aspect Component public class PermissionAspect { Around(“annotation(requiresPermissions)”) public Object checkPermission(ProceedingJoinPoint joinPoint, RequiresPermissions requiresPermissions) throws Throwable { String[] permissions requiresPermissions.value(); // 从UserContext获取当前用户权限列表 SetString userPerms UserContext.getCurrentPermissions(); for (String perm : permissions) { if (!userPerms.contains(perm)) { throw new BusinessException(403, “没有操作权限”); } } return joinPoint.proceed(); } }注册拦截器与配置通过WebMvcConfigurer将拦截器注册到Spring MVC并配置放行路径如登录接口、Swagger文档。实操心得Token存储与刷新JWT Token最好在客户端如浏览器的localStorage或cookie注意HttpOnly和SameSite中存储。可以设计一个/auth/refresh接口用旧的、未过期的Token来换取新的Token实现无感刷新提升用户体验。权限数据缓存每次请求都去数据库查用户权限列表是性能瓶颈。一定要将用户-角色-权限的关联关系缓存起来比如用RedisKey可以是user:perms:${userId}。接口放行列表对于登录、注册、公开API等接口一定要在拦截器配置中明确放行否则会陷入“需要Token才能获取Token”的死循环。3.4 接口文档与工具集成模块“快速开发”也意味着“快速对接”。清晰、实时、可调试的API文档至关重要。这里首推Knife4jSwagger的增强版。实现要点引入依赖引入knife4j-spring-boot-starter。配置类创建一个SwaggerConfig配置类配置API文档的基本信息、分组、扫描的包路径等。Configuration EnableSwagger2WebMvc public class SwaggerConfig { Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage(“com.yourpackage.controller”)) // 扫描的Controller包 .paths(PathSelectors.any()) .build() .securitySchemes(securitySchemes()) // 配置认证如JWT .securityContexts(securityContexts()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title(“你的项目API文档”) .description(“基于快速开发框架构建”) .version(“1.0”) .build(); } // 配置全局的JWT认证参数这样在Swagger UI里就可以直接填Token了 private ListSecurityScheme securitySchemes() { ApiKey apiKey new ApiKey(“Authorization”, “Authorization”, “header”); return Collections.singletonList(apiKey); } }在Controller中使用注解在Controller类和接口方法上使用Api,ApiOperation,ApiParam等注解来丰富文档描述。RestController RequestMapping(“/user”) Api(tags “用户管理”) public class UserController { GetMapping(“/{id}”) ApiOperation(“根据ID查询用户”) public RUserVO getUser(PathVariable ApiParam(“用户ID”) Long id) { // ... } }实操心得实体类描述别忘了给你的请求/响应DTOData Transfer Object字段加上ApiModelProperty注解说明字段含义和示例。这能让前端开发者一目了然。Data ApiModel(“用户创建请求”) public class UserCreateDTO { NotBlank(message “用户名不能为空”) ApiModelProperty(value “用户名”, required true, example “zhangsan”) private String username; Email(message “邮箱格式不正确”) ApiModelProperty(value “邮箱”, example “zhangsanexample.com”) private String email; }生产环境关闭通过Profile配置确保Swagger/Knife4j只在开发、测试环境启用在生产环境一定要关闭避免暴露接口信息。# application-dev.yml knife4j: enable: true # application-prod.yml knife4j: enable: false离线文档Knife4j支持将文档导出为Markdown、HTML、Word等格式方便与团队其他成员如产品经理、测试离线共享。4. 从零开始使用框架快速构建一个用户管理接口理论说了这么多我们来实战一下。假设我们要开发一个简单的用户管理模块包含“新增用户”和“分页查询用户列表”两个接口。4.1 环境准备与项目初始化使用Spring Initializr访问 start.spring.io选择Project: MavenLanguage: JavaSpring Boot: 选择稳定版本如3.xDependencies: 勾选Spring Web,Lombok,MyBatis Framework,MySQL Driver。导入IDE将生成的项目导入到你的IDE如IntelliJ IDEA。引入快速开发框架这里假设我们的框架已经打包成了一个Starter。在你的pom.xml中引入它实际可能是公司内部的Maven仓库地址。dependency groupIdcom.yourcompany/groupId artifactIdquick-dev-spring-boot-starter/artifactId version1.0.0/version /dependency这个Starter会帮你自动引入MP、Knife4j、JWT、工具类等所有依赖和配置。配置数据库在application.yml中配置数据源。spring: datasource: url: jdbc:mysql://localhost:3306/quick_dev_demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发环境开启SQL日志 global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段名 logic-delete-value: 1 # 逻辑已删除值 logic-not-delete-value: 0 # 逻辑未删除值4.2 编写实体类与Mapper创建用户实体类继承框架提供的BaseEntity。Data EqualsAndHashCode(callSuper true) TableName(“sys_user”) // 指定表名 ApiModel(“系统用户实体”) public class User extends BaseEntity { ApiModelProperty(“用户名”) private String username; ApiModelProperty(“密码加密后存储”) private String password; ApiModelProperty(“昵称”) private String nickname; ApiModelProperty(“邮箱”) private String email; ApiModelProperty(“状态0-禁用1-启用”) private Integer status; }创建Mapper接口继承框架的MyBaseMapper。Mapper // MyBatis注解 public interface UserMapper extends MyBaseMapperUser { // 如果需要复杂的联合查询可以在这里定义方法并在对应的XML中写SQL // 但简单的CRUD继承的BaseMapper已经全部提供了 }4.3 编写Service层创建Service接口继承框架的MyBaseService。public interface UserService extends MyBaseServiceUser { // 声明业务方法 RString createUser(UserCreateDTO dto); RPageResultUserVO getUserPage(PageParam pageParam, UserQueryDTO queryDTO); }创建Service实现类继承框架的ServiceImpl并实现自己的接口。Service Slf4j public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService { Autowired private PasswordEncoder passwordEncoder; // 框架可能提供的密码加密器 Override public RString createUser(UserCreateDTO dto) { // 1. 校验用户名是否已存在 LambdaQueryWrapperUser wrapper Wrappers.UserlambdaQuery() .eq(User::getUsername, dto.getUsername()); if (this.count(wrapper) 0) { return R.error(“用户名已存在”); } // 2. DTO 转 Entity User user new User(); BeanUtils.copyProperties(dto, user); // 使用Spring的工具类 // 3. 密码加密 user.setPassword(passwordEncoder.encode(dto.getPassword())); user.setStatus(1); // 默认启用 // 4. 保存MP的save方法 this.save(user); log.info(“创建用户成功用户名{}”, dto.getUsername()); return R.success(“用户创建成功”); } Override public RPageResultUserVO getUserPage(PageParam pageParam, UserQueryDTO queryDTO) { // 1. 构建分页对象和查询条件 PageUser page new Page(pageParam.getPageNum(), pageParam.getPageSize()); LambdaQueryWrapperUser wrapper Wrappers.UserlambdaQuery(); // 动态拼接查询条件 if (StringUtils.isNotBlank(queryDTO.getUsername())) { wrapper.like(User::getUsername, queryDTO.getUsername()); } if (queryDTO.getStatus() ! null) { wrapper.eq(User::getStatus, queryDTO.getStatus()); } wrapper.orderByDesc(User::getCreateTime); // 按创建时间倒序 // 2. 执行分页查询MP的page方法 PageUser userPage this.page(page, wrapper); // 3. 将Entity Page转换为VO Page PageResultUserVO result new PageResult(); result.setTotal(userPage.getTotal()); result.setPages(userPage.getPages()); result.setList(userPage.getRecords().stream() .map(this::convertToVO) // 假设有一个convertToVO方法 .collect(Collectors.toList())); return R.success(result); } private UserVO convertToVO(User user) { UserVO vo new UserVO(); BeanUtils.copyProperties(user, vo); // 可以在这里做一些字段转换比如不返回密码字段 vo.setPassword(null); return vo; } }4.4 编写Controller层这是最简洁的一层得益于框架的封装。RestController RequestMapping(“/api/v1/user”) Api(tags “用户管理接口”) public class UserController { Autowired private UserService userService; PostMapping(“/”) ApiOperation(“创建用户”) public RString createUser(Valid RequestBody UserCreateDTO dto) { // 参数校验已由Valid和全局异常处理器完成 // 业务逻辑完全交给Service return userService.createUser(dto); } GetMapping(“/page”) ApiOperation(“分页查询用户列表”) public RPageResultUserVO getUserPage(PageParam pageParam, UserQueryDTO queryDTO) { return userService.getUserPage(pageParam, queryDTO); } }看到了吗Controller非常干净只做三件事定义路由、接收参数、调用Service。参数校验、统一响应、异常处理、SQL打印、事务控制Transactional通常加在Service方法上等全部由框架在背后默默完成。4.5 验证与测试启动应用运行Spring Boot主类。访问API文档打开浏览器访问http://localhost:8080/doc.htmlKnife4j的地址你将看到一个美观的API文档页面里面已经列出了我们刚写的两个接口。在线调试在Knife4j的界面中找到“创建用户”接口填写JSON请求体点击“发送”。观察控制台SQL日志查看数据库数据是否插入成功。测试分页同样在Knife4j中测试分页接口尝试传入不同的pageNum,pageSize和查询条件。整个过程我们没有手动配置过一处AOP没有写一行异常处理代码没有操心过响应格式甚至连简单的单表CRUD SQL都没写。这就是一个“快速开发框架”带来的效率提升。5. 进阶框架的定制与扩展没有哪个框架能100%满足所有项目。好的框架必须提供扩展点。5.1 自定义数据源与多租户对于需要连接多个数据库或者需要根据请求动态切换数据源多租户SaaS系统的场景框架需要支持。实现思路继承框架的AbstractRoutingDataSourceSpring提供了这个抽象类来实现动态数据源路由。自定义注解与切面定义DataSource(“master”/”slave”)注解通过AOP在方法执行前将数据源Key设置到ThreadLocal中。在AbstractRoutingDataSource中重写determineCurrentLookupKey()方法从ThreadLocal中获取Key返回对应的实际数据源Bean。框架集成可以将这套动态数据源机制打包成一个模块通过配置spring.datasource.dynamic.enabletrue来启用并在配置文件中定义多个数据源连接信息。5.2 集成工作流引擎对于一些审批流、状态机复杂的业务可以集成轻量级的工作流引擎如Flowable或Activiti。框架层面的支持提供Starter自动配置Flowable的ProcessEngine、RepositoryService、TaskService等Bean。封装常用操作提供FlowableService封装流程部署、启动实例、查询任务、完成任务、查询历史等通用操作。与业务实体关联提供工具方法方便将业务主键如订单ID与流程实例ID进行绑定和查询。统一用户体系将框架自身的用户、角色与Flowable的用户、组进行同步或映射。5.3 分布式锁与幂等性保障在高并发场景下防止重复提交、保证接口幂等性是刚需。框架集成方案基于Redis的分布式锁提供DistributedLock工具类封装tryLock和unlock方法支持锁自动续期和超时释放。Component public class RedisDistributedLock { Autowired private StringRedisTemplate redisTemplate; public boolean tryLock(String lockKey, String requestId, long expireSeconds) { // 使用SET key value NX EX 命令保证原子性 return Boolean.TRUE.equals(redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS)); } public boolean unlock(String lockKey, String requestId) { // 使用Lua脚本保证判断和删除的原子性防止误删其他客户端的锁 String script “if redis.call(‘get’, KEYS[1]) ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”; Long result redisTemplate.execute(new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), requestId); return result ! null result 0; } }幂等性注解提供Idempotent注解可以标记在Controller方法上。通过AOP拦截在方法执行前根据请求参数或Token接口路径生成唯一Key去Redis中查询。如果已存在则认为是重复请求直接返回之前的结果需缓存结果或抛出幂等异常如果不存在则执行业务并将结果缓存一段时间。与全局异常处理结合定义IdempotentException在全局异常处理器中捕获并返回友好的提示信息如“请勿重复提交”。6. 常见问题与排查技巧实录在实际使用中你肯定会遇到各种问题。这里记录几个我踩过的坑和解决方法。6.1 MyBatis-Plus字段映射问题问题描述实体类中使用了LocalDateTime类型的createTime字段数据库是datetime类型。插入数据时时间变成了null或者不对。排查与解决检查数据库驱动与时区确保MySQL连接URL中包含了serverTimezoneAsia/Shanghai或你所在的时区。这是最常见的原因。检查MP的自动填充如果你使用了TableField(fill FieldFill.INSERT)必须实现MetaObjectHandler。检查你的insertFill方法是否被正确调用。Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, “createTime”, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, “updateTime”, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, “updateTime”, LocalDateTime.class, LocalDateTime.now()); } }检查JDBC版本确保使用的MySQL Connector/J版本与你的Java和MySQL版本兼容。对于Java 8和MySQL 5.7建议使用mysql-connector-java版本8.0.x。6.2 全局异常处理器不生效问题描述在Controller中抛出的自定义BusinessException没有被全局处理器捕获前端收到的是Spring默认的Whitelabel Error Page。排查与解决检查包扫描确保你的GlobalExceptionHandler类所在的包在Spring Boot的主应用类SpringBootApplication注解的类的扫描范围内。通常放在主类同级或子包下。检查注解确认类上加了RestControllerAdvice如果只处理REST接口或ControllerAdvice。检查异常类型匹配ExceptionHandler注解中指定的异常类型是否是你抛出的异常或其父类。确保没有更具体的异常处理器“拦截”了你的异常。检查Filter中的异常如果你的异常是在Filter或Interceptor的preHandle中抛出的ControllerAdvice是捕获不到的。需要在Filter中自己处理异常或者使用Spring提供的OncePerRequestFilter并重写其doFilterInternal方法用try-catch包裹。6.3 事务不回滚问题描述在Service方法上加了Transactional方法中抛出了异常但数据库操作没有被回滚。排查与解决检查异常类型默认情况下Transactional只在遇到RuntimeException和Error时回滚。如果你抛出的BusinessException继承自Exception而非RuntimeException则不会回滚。需要在注解中显式指定Transactional(rollbackFor Exception.class)。检查方法可见性Transactional是基于AOP代理的。如果事务方法被定义成了private、protected或者是在同一个类内部的其他方法中调用this.method()事务注解会失效。因为代理对象无法拦截内部调用。解决方法是将事务方法放到另一个Service中通过Autowired注入来调用或者使用AopContext.currentProxy()来获取代理对象再调用不推荐有侵入性。检查数据库引擎确认MySQL表使用的引擎是InnoDBMyISAM引擎不支持事务。6.4 分页查询总数异常缓慢问题描述使用MP的分页查询当数据量很大时SELECT COUNT(*)语句执行非常慢。排查与解决优化COUNT语句MP的分页插件默认会先执行一条COUNT(*)语句获取总数。对于超大的表这个操作可能很耗时。可以考虑使用page.setSearchCount(false)如果你不需要知道总记录数和总页数只是需要分页数据可以关闭总数查询。自定义COUNT查询如果表有复杂的查询条件可以自己写一个优化的COUNT查询SQL通过Select注解或XML映射到Mapper的一个方法上然后在Service中手动调用并设置到Page对象里。数据库层面优化给经常用于WHERE条件的字段加索引。对于COUNT(*)在MySQL的InnoDB引擎下直接查询主键索引通常是最快的因为InnoDB的主键索引存储了行数但这是近似值对于事务隔离级别有要求。如果条件复杂可能需要建立复合索引。考虑其他分页方案对于深度分页如pageNum很大LIMIT offset, size效率极低。可以考虑使用“游标分页”或“基于ID范围的分页”即记录上一页最后一条记录的ID下一页查询条件为WHERE id lastId LIMIT size。但这需要业务逻辑配合且无法直接跳转到任意页。6.5 接口文档字段缺失或错乱问题描述Knife4j/Swagger生成的文档中某些实体类的字段没有显示或者类型显示不正确。排查与解决检查注解确保实体类或DTO的字段上加了ApiModelProperty注解。如果字段是Boolean类型注意基本类型boolean和包装类型Boolean在Swagger中的默认值展示可能不同。检查泛型如果返回的是RPageResultUserVO这种多层嵌套的泛型Swagger有时可能无法正确解析内部的UserVO。可以尝试在Controller方法上使用ApiOperation的response属性直接指定或者使用ApiResponses注解。ApiOperation(value “分页查询”, response UserVO.class) ApiResponses({ ApiResponse(code 200, message “成功”, response PageResult.class) // 可能需要单独定义PageResult的模型 })更可靠的做法是为PageResult也加上ApiModel注解。检查循环引用如果两个实体类互相引用如User里有ListRoleRole里有ListUser会导致Swagger在生成模型时陷入死循环。需要使用JsonIgnore或ApiModelProperty(hidden true)在某一方忽略这个属性。重启与清理缓存有时IDE或Spring Boot的DevTools缓存会导致文档没有更新。尝试清理项目mvn clean并重启应用。