有些东西,用久了会有一种“明明很简单,却每次都重复做”的烦躁感。SpringBoot 确实帮我们省了大量配置,但真正落到一个业务项目里,还是免不了要搭统一返回体、写全局异常捕获、接 Redis、接 MinIO、做参数校验、整操作日志。我做了几年后端以后,慢慢意识到自己真正需要的不是一个什么都会的大而全平台,而是一套“刚刚好”的骨架。Sun Frame 就是基于这个想法诞生的一个基于 SpringBoot 的轻量级开发框架,也是我个人维护了挺久的开源项目。它不是微服务全家桶,不强制你用云原生,也不搞代码生成的神话,就是老老实实把 SpringBoot 项目里最常用的一些能力收敛起来,打包成 starter,做到“加依赖、写配置、直接用”。如果你正在做一个单体应用、中小系统,或者想看一个 SpringBoot 自动装配的落地案例,这篇文章应该能给你不少真实可用的东西。
1. 为什么会做Sun Frame:个人项目背后的真实需求
1.1 被重复代码逼出来的决定
我刚开始用 SpringBoot 的时候,和绝大多数人一样:新建一个项目,复制上一套项目的工具类,改包名,再手动加一堆依赖。最痛苦的还不是配置过程,而是每个项目的 Controller 返回结构都不一样。你写一个Map<String, Object>也能返回,但到前端对接的时候就乱了,同一个接口今天返回{code: 200},明天另一个项目返回{status: 0},前端同学恨不能为每个系统单独写一套拦截逻辑。
我印象很深,有次帮朋友排查一个线上问题,那个项目里光“统一返回体”就有三套实现,分散在好几个模块里。有的是R.ok(),有的是Result.success(),还有的直接往 HttpServletResponse 里写 JSON。当时我就觉得,这种基础能力应该在一开始就固化下来,而不是等项目越写越大以后再回头去收敛。Sun Frame 的雏形,其实就是把我自己项目里的Result<T>、全局异常处理、枚举定义这些基础代码抽出来,形成了一个独立的公共模块。
另外,个人开源项目和公司内部项目不一样,你没有团队给你写文档,也没有人帮你 review 设计合理性。所以我一开始就给自己定了一个目标:一切设计都以“最简单的 SpringBoot 体验”为准则。也就是说,使用者拿到依赖之后,最好连代码都不用看,光凭配置项就能跑起来。这个原则后来贯穿了 Sun Frame 的整个版本演进,也避免了很多“为了抽象而抽象”的设计。
1.2 选择“轻量级”而不是“全家桶”的原因
现在市面上的开发脚手架很多,有重型的微服务框架,也有非常完善的中后台快速开发平台。那么 Sun Frame 为什么坚持走“轻量级”路线?我自己的体会是:绝多数后端项目其实根本不需要一开始就用上分布式事务、服务网格、消息队列这些重型武器。一个运营后台、一个内容管理系统、一个教务考勤系统,单体 SpringBoot 就足够了。你把框架搞得太重,反而会造成学习和排查的成本。
“轻量级”在我这里的定义有三条。第一,依赖收敛,能不强引的包就不强引;比如你不做文件上传,那 MinIO 相关的自动配置就不会加载。第二,约定优先,配置项必须有默认值,不配置也能跑,但这不意味着功能缺失,而是把常见场景的默认值调好。第三,容易替换,Sun Frame 不会侵入你的业务代码,你想换掉某个组件,直接去掉依赖,再补一个自己的 Bean 就行。
我一直觉得,个人开源项目的生命力不在于功能数量,而在于能不能让一个完全陌生的人快速上手。如果你做的东西连作者自己都要看半天文档才跑起来,那别人更不可能用。轻量级还有一层好处:当使用者遇到问题,他看源码的门槛低,几个关键类翻一翻就能理解整个调用链,这也是很多开发框架流行的原因之一。
2. Sun Frame整体设计与核心能力拆解
2.1 模块划分:core、web、data、security、generator
Sun Frame 从结构上不是一个大 jar 包,而是拆成了几个可以按需引用的模块。这个设计参考了很多知名开源项目的做法,但进一步做了减法。核心模块sunframe-core是最底层的东西,包含通用返回体、业务异常定义、分页对象和基础工具方法。这部分几乎不依赖第三方框架,只有几个必要的默认依赖,所以引入成本极低。
sunframe-spring-boot-starter-web提供 Web 场景下的能力,比如全局异常处理器、参数校验的自动开启、请求日志埋点、统一响应体包装。它会在 SpringMVC 挂载一些自动配置类,但不会影响你原有的@RestController写法。sunframe-spring-boot-starter-data则围绕数据访问层做了封装,包括 MyBatis-Plus 的通用 BaseMapper 扩展、自动填充创建时间和更新时间、分页插件配置和简单数据权限支持。这部分是我自己在业务里被 MyBatis-Plus 的重复配置折磨后的成果。
还有sunframe-spring-boot-starter-security,它不是重新造一套权限系统,而是基于 Spring Security 做了简化配置,把常见的登录认证、Token 管理、接口鉴权封装成可继承配置类。最后sunframe-generator是一个独立工具模块,不是让你在网页上点来点去生成代码,而是通过命令行或者 Maven 插件方式生成 Controller、Service、Mapper 的基础代码,这样能让骨架代码保持统一,同时不会像很多在线生成器一样生成一坨没人维护的代码。
2.2 自动装配原理:SpringBoot Starter机制怎么被我用起来
Sun Frame 能“加依赖即用”,靠的是 SpringBoot 的自动配置机制。这个机制本质上是 Spring 容器在启动时通过spring.factories或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,找到我们声明的自动配置类列表,再根据条件注解决定是否创建对应的 Bean。
我举个最简单例子。sunframe-spring-boot-starter-web里有一个WebAutoConfiguration,类上标注了@AutoConfiguration和@ConditionalOnClass(DispatcherServlet.class),方法上标注@ConditionalOnMissingBean(GlobalExceptionHandler.class)。也就是说,只有当你的项目里存在 SpringMVC 环境,并且你自己没有定义全局异常处理器的时候,Sun Frame 才会帮你创建一个默认的异常处理器。这个机制非常关键,它保证了框架的“不强占”特性——你想用自己的实现,直接定义一个同名 Bean 就能覆盖掉默认行为。
很多人会担心,使用这种 starter 会不会被“暗箱操作”了?其实你只要记住一句话:SpringBoot 自动配置的 Bean 默认是兜底,不是强制覆盖。@ConditionalOnMissingBean就是给使用者留的扩展口子。我在 Sun Frame 的文档里也一直强调,如果某个配置和你的项目有冲突,优先考在配置类加排除,比如@SpringBootApplication(exclude = WebAutoConfiguration.class),而不是盲目改依赖版本。这个思路在我自己接手过的一些老项目里尤其有用。
2.3 对比市面通用脚手架:若依/RuoYi、pigen等
既然聊到开发框架,很多人会拿 Sun Frame 和 RuoYi、pig 这类开源项目对比。我不是说这类项目不好,它们功能非常完整,很多企业项目甚至直接基于它们二次开发。但它们的定位是“后台权限管理系统”或者“微服务脚手架”,自带菜单权限、部门数据权限、代码生成、系统监控等一大堆功能。你引入之后,需要先理解他们的业务表结构和封装逻辑,然后才能开始写自己的业务。
Sun Frame 想解决的是另一个问题:如果你已经有一个自己的项目结构,或者你不想把整个项目交给一个大平台约束,你需要的只是一组可复用的 starter 模块。它更像是一个“积木盒子”,而不是“样板间”。我用 Sun Frame 做过几个新项目,也试过在旧项目里混用,效果都还不错。最直接的好处是代码结构统一了:不管哪个项目,Controller 的返回响应长得一样,全局异常格式一样,分页参数一样,内部人员维护起来没有心智负担。
当然,轻量级也有代价。你不会开箱就得到一整套后台管理页面,也没有菜单权限的数据模型。但如果你只是需要一个快速搭建业务接口的能力,以及规范化的返回结构,Sun Frame 的价值会非常直接。我经常和朋友说,选脚手架不是看谁功能多,而是看谁“删起来容易”。Sun Frame 的每个自动配置类都可以独立排除、独立替换,这一点在长期维护里太重要了。
3. 实操:如何从零集成Sun Frame到你的SpringBoot项目
3.1 环境准备与依赖引入
先说基本环境。Sun Frame 推荐使用 JDK 1.8 及以上版本,目前主分支基于 Spring Boot 2.7.18 构建,同时提供 spring-boot-3.x 的兼容分支。如果你还在用旧版本 Spring Boot,建议先升级到 2.5 以上;如果已经上了 Spring Boot 3,注意包名从javax换成了jakarta,部分老版本 starter 可能会因为类名问题启动失败。我在 2.7 上验证过绝大多数功能,3.x 分支主要用于新项目探索。
引入方式很简单。你只需要在pom.xml里加一个依赖:
<dependency> <groupId>com.sunframework</groupId> <artifactId>sunframe-spring-boot-starter-web</artifactId> <version>1.2.0</version> </dependency>如果你想做数据访问,再加一个sunframe-spring-boot-starter-data。这几个模块并没有强制依赖关系,所以你可以按需组合。依赖加好之后,SpringBoot 主类不需要加任何额外的@EnableSomething注解,因为自动配置会帮你完成。我在设计时特意避免了任何“开启注解”,因为从我经验看,注解越多,越容易忘记加导致诡异问题。
这里有个实际操作技巧:引入依赖后第一次启动时,多加一个面板配置debug=true。这样 SpringBoot 启动日志里会打印所有自动配置的匹配报告,你能清楚看到 Sun Frame 的哪些自动配置类生效了,哪些因为条件不满足被跳过。这个排查手段能省下大量时间,尤其当你觉得某个功能没生效时。
3.2 配置项说明与默认值
Sun Frame 的配置项前缀是sunframe,用 YAML 格式来说大概长这样:
sunframe: web: response-wrapped: true global-exception-enabled: true trace-id-enabled: true data: auto-fill: create-time-field: createTime update-time-field: updateTime page: default-size: 10 max-size: 100 security: token: secret: "change-me" expire-minutes: 120每一项都有默认值。比如response-wrapped默认开启,但如果你不习惯框架自动包装返回值,可以把它关掉。我不建议关,因为前端对接统一返回体是刚需。trace-id-enabled是给日志追踪用的,会在每个请求响应头里加上一个X-Trace-Id,并且在 MDC 里写入同一个值,这样查日志时可以按 TraceId 把整个请求串起来。这个小功能实际上很多团队后期都会自己加,我干脆在框架里默认做掉了。
关于配置项,我比较坚持的一个原则是“少而明确”。Sun Frame 的配置项控制在三十个以内,如果一个配置项对 95% 的用户来说都用不上,就不暴露。这样使用者的学习成本很低,文档也不用写太多。有些项目把配置项搞出上百个,反而降低了可维护性。实际写代码时,配置项的注入也尽量用@ConfigurationProperties(prefix = "sunframe")统一管理,避免一堆散落的@Value注解。
3.3 第一个接口:统一响应与全局异常处理
接下来我们写一个最简单的接口,看看 Sun Frame 帮我们做了什么。假设有一个 UserController:
@RestController @RequestMapping("/user") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping("/{id}") public UserVO getUser(@PathVariable Long id) { return userService.getById(id); } }没有显式包Result<T>,但最终返回给前端的数据会是:
{ "code": 0, "message": "success", "data": { "id": 1, "name": "张三" }, "traceId": "a8b9c0d1e2f3" }这是 Sun Frame 通过ResponseBodyAdvice实现的统一响应包装。它只包装正常的返回值和特定的业务异常,其它交给全局异常处理器处理。如果你在方法上标记了@IgnoreResponseWrap,那就保持原样返回,比如对接第三方接口时不需要统一包装,直接返回原始数据即可。
全局异常处理的规则也很直接:业务异常沿用 HTTP 200 返回,因为网络层没有错,是业务逻辑层面的问题;系统级错误则返回到 500,且 message 不直接展示堆栈信息,避免泄露代码细节。同时框架会自动记录一条带 TraceId 的 error 日志,方便后面排查。我在这个设计上踩过不少坑,最深刻的教训是:不要把异常堆栈直接放到 message 里传回前端,既危险又不专业。
3.4 集成MyBatis-Plus的数据访问
Sun Frame 的 data 模块,默认集成 MyBatis-Plus(目前是 3.5.3 版本)。它能提供自动填充、分页插件和基础 CRUD 扩展。比如你的实体类上有createTime和updateTime字段,只需要在类上加上@TableField(fill = FieldFill.INSERT),Sun Frame 的MetaObjectHandler扩展就会在插入和更新时自动帮你填充时间,省掉一堆手动 set。
很多人问,为什么选 MyBatis-Plus 而不是直接 JPA?我的理由很简单:国内团队大多数业务 SQL 都掌握得比较好,而且 MyBatis-Plus 在简单 CRUD 上的体验接近 JPA,复杂查询又可以转 XML 写原生 SQL,适配度很高。Sun Frame 里封装了一个BaseEntity,包含主键、创建时间、更新时间、逻辑删除标记和乐观锁版本号这几个常见字段,你可以选择继承它,也可以不继承。非侵入性还是第一位。
分页插件也是现成的。你只需要在 Service 层接收一个PageQuery,然后传给Page参数,就能拿到PageResult<T>对象返回给前端。它的结构里预先处理了“当前页、每页条数、总条数、总页数、数据列表”这些常见字段。我再补充一点:分页查询时千万不要把max-size设得太大,一个后台管理系统的列表,一页显示 10 到 20 条已经完全够用,我见过不少因为单页拉取太多数据导致数据库压力山大的事故。
3.5 把MinIO集成到SpringBoot文件上传中
文件存储是很多后台项目的刚需。Sun Frame 的 web 模块里预留了文件上传的抽象接口FileStorageService,默认实现是本地磁盘存储,但你可以很轻松地把它换成 MinIO。MinIO 是一个兼容 S3 协议的对象存储服务,个人项目里面也足够好用,开源,部署简单,能在本地用 Docker 跑起来:
docker run -p 9000:9000 -p 9001:9001 \ -e "MINIO_ROOT_USER=admin" \ -e "MINIO_ROOT_PASSWORD=admin123456" \ minio/minio server /data --console-address ":9001"启动之后,只需要在 Sun Frame 配置里填入地址和密钥,然后实现FileStorageService接口的upload方法,就能用 MinIO 存储了。为什么特意做这个抽象?因为文件存储方案很容易被业务绑定死:早期用本地磁盘,后来服务器磁盘不够了,换云对象存储或者 MinIO,如果代码里到处都是FileInputStream和FileOutputStream,改起来会非常痛苦。我见过好几个项目因为文件存储方案替换导致大面积返工,所以 Sun Frame 把这个切换成本降到了最低。
实际写 MinIO 的上传代码时,有一个容易忽略的点:上传时必须把Content-Type设置正确,不能让 MinIO 把图片、PDF 都识别成application/octet-stream,否则前端预览和下载都会出问题。而且生成文件名时,不要直接使用原始文件名,最好用 UUID 或者时间戳加随机数来重命名,避免中文文件名和路径冲突。这些经验不是从文档里看来的,都是线上踩过的坑。
4. 关键实现细节:自动配置、starter与banner定制
4.1 自定义starter的三步走
很多读者可能好奇,Sun Frame 的 starter 到底是怎么写出来的。这里我总结成三步。第一步,准备一个普通的 Maven 项目,创建自动配置类;第二步,注册自动配置类,在resources/META-INF下新建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,把自动配置类的全限定名写在里面;第三步,写spring-configuration-metadata.json提供配置项的元数据提示,这样使用者在 IDEA 里写sunframe.web这些配置时会有提示。
自动配置类的核心是条件注解组合。我会在类上同时使用@AutoConfiguration和@ConditionalOnProperty,实现“配置项没打开就不初始化”的效果。比如下面的示例:
@AutoConfiguration @ConditionalOnProperty(prefix = "sunframe.web", name = "enabled", havingValue = "true", matchIfMissing = true) public class WebAutoConfiguration { @Bean @ConditionalOnMissingBean(GlobalExceptionHandler.class) public GlobalExceptionHandler globalExceptionHandler() { return new GlobalExceptionHandler(); } @Bean @ConditionalOnMissingBean(ResponseBodyAdvice.class) public ResponseBodyAdvice<Object> responseBodyAdvice() { return new ResponseWrapAdvice(); } }这段代码看起来简单,但里面有非常多的门道。比如ResponseBodyAdvice的实现,如果要包装返回值,一定得判断当前请求是否走的是 SpringMVC 的@ResponseBody,而且不能重复包装。否则你可能会陷入“包装了一次后又包装一次”的递归问题。具体到代码里,我会通过instanceof语句判断返回值是不是Result,如果是就直接放行。这种细节只有自己实现过一遍,才能理解为什么很多 starter 源码里会有那么多防御性判断。
4.2 把项目Banner换成Sun Frame
SpringBoot 启动时那个字符画 banner 其实是个很好的品牌展示位。不少公司项目里都会把它替换成企业 Logo。Sun Frame 也提供了一个自定义 Banner 类,但我更想说的是 banner 生成技巧。我喜欢用在线 banner 生成器,选择纯字模风格,再用\033转义序列把文字加上颜色。这样项目启动时,控制台会打出Sun Frame字样,配合版本号,一眼就能看出来这个项目用的是哪个框架版本。
不过这个功能要谨慎使用。如果你的团队有人用 Windows cmd,注意 ANSI 颜色在部分终端下会显示成乱码符号。我建议默认 banner 不加颜色,只保留 ASCII 字符,这样最安全。如果你也想在项目里做自定义 banner,可以保持一个banner.txt文件放到src/main/resources下,SpringBoot 会自动识别。个人项目里这种小细节会被很多用户看到,我第一次发现有人把 banner 截图发到群里时,还挺有成就感的。
4.3 版本兼容性与SpringBoot版本选择
前面提到 SpringBoot 3.x 和 2.x 有javax到jakarta的坑,这里展开说一下。Spring Boot 3 发布于 2022 年底,它不再支持 JDK 8,默认从 JDK 17 起步。如果你的项目还停留在 JDK 8 环境,想用 Spring Boot 3 基本不可能。所以 Sun Frame 的定位是双轨并行:2.7.x分支给大多数存量项目用,3.x分支给新项目用。
实际操作中,版本兼容问题是引依赖时最容易翻车的点。比如在 Spring Boot 3 的 starter 里,如果你还没把javax.servlet-api换成jakarta.servlet-api,项目启动时会报NoClassDefFoundError。即使代码上通过spring.factories注册自动配置类,Spring Boot 3 也不再从spring.factories读取自动配置,必须改为AutoConfiguration.imports文件。所以如果你要兼容两个版本,这套映射关系就得维护两份。
我的经验是:个人框架不要一味追最新,等一个 SpringBoot 大版本发布后至少半年,生态稳定了再迁移会舒服很多。特别是 MyBatis-Plus、MinIO 这些第三方库,它们对大版本的适配有自己的节奏。过早跟进新版本,可能会因为某个依赖还没有适配,导致整个项目启动不了。
5. 完整项目结构示例与本地部署验证
5.1 demo工程目录结构
光说不练是假把式,这里给一个完整 demo 项目的结构。假设我们要做一个简单的“图书借阅管理”系统,使用 Sun Frame + SpringBoot + MyBatis-Plus + MinIO。项目骨架如下:
sunframe-demo ├── pom.xml ├── src/main/java/com/example/demo │ ├── DemoApplication.java │ ├── common │ │ └── Result.java │ ├── config │ │ └── MinioConfig.java │ ├── controller │ │ └── BookController.java │ ├── entity │ │ └── Book.java │ ├── mapper │ │ └── BookMapper.java │ └── service │ ├── BookService.java │ └── impl/BookServiceImpl.java └── src/main/resources ├── application.yml ├── banner.txt └── mapper/BookMapper.xml这个结构里,Result.java是 Sun Frame 提供的通用响应体,你通常不需要自己写。DemoApplication.java上只需要标准的@SpringBootApplication注解。如果你想让分页插件和自动填充生效,也只要在启动类上继承一下BaseAutoConfiguration,或者更简单:因为 starter 已经自动配置了,你什么都不用加。
很多初学者会犯一个错误:以为用框架就一定需要各种@EnableXXX,结果要么找不到注解,要么重复配置导致 Bean 冲突。Sun Frame 的设计哲学是:能用自动配置解决的绝不开启手动开关。你只需要把依赖加对了,写业务就行。要是遇到不生效的问题,先开debug=true看自动配置报告,再决定是否需要手动声明。
5.2 启动流程与常见错误
启动之前,检查一下application.yml里的数据源配置。我这里推荐用 H2 内存库先做快速联调,毕竟不想让读者为了跑 demo 还要专门装 MySQL:
spring: datasource: url: jdbc:h2:mem:testdb;MODE=MySQL;DATABASE_TO_LOWER=TRUE driver-class-name: org.h2.Driver username: sa password: "" sunframe: web: trace-id-enabled: true启动时常见的报错有两大类。第一类,ClassNotFoundException: javax.servlet.*,这基本就是 SpringBoot 版本和 Sun Frame 模块版本不匹配。第二类,Error creating bean with name 'globalExceptionHandler',多半是你自己项目里已经有一个同名 Bean,和自动配置的默认 Bean 撞了。解决方法是把你自己的 Bean 删掉,或者在自动配置类上用@ConditionalOnMissingBean处理,Sun Frame 已经处理过这种情况,但如果你自己又加了一个全局异常处理器,还是可能会冲突。
我在本地验证时通常会用mvn spring-boot:run启动,配合 IDEA 的 Rest Client 工具直接调接口。第一次跑通后,把这个启动过程写成一个 README 文档,方便以后其他同事加入项目时快速上手。一个好的 README 应该包含:运行环境、启动命令、默认端口、测试账号、配置项说明和常见问题。这些内容如果只存在个人脑子里,项目交接时就是灾难。
5.3 Vue打包后如何放进SpringBoot运行
还有一个很常见的问题:前后端分离项目,前端 Vue 工程打包后怎么放进 SpringBoot 一起发布。我在 Sun Frame 的 demo 里也做了一版。简单说,把 Vue 构建出来的dist目录里的文件复制到 SpringBoot 的src/main/resources/static目录下,然后后端访问http://localhost:8080/就能看到前端页面。但是要注意前端路由如果是 history 模式,刷新子路由时会出现 404,因为 SpringBoot 没有把非接口路径转发到index.html。
解决这个问题的方案是添加一个简单的路由转发控制器,或者写一个WebMvcConfigurer注册PathResourceResolver。我在 demo 里是这么处理的:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/") .resourceChain(true) .addResolver(new PathResourceResolver()); } }这种方案能覆盖绝大多数普通场景。不过如果你在 SpringBoot 内置了 JWT 鉴权,还得在安全配置里把前端的静态资源路径排除掉,否则第一次加载首页就会被拦截。很多人会在这一步卡很久,其实只需要在安全过滤链里允许/,/index.html,/assets/**这些路径匿名访问就行。
6. 个人开源项目维护心得与避坑清单
6.1 文档比代码更值得花时间
一个开源项目,代码写得再好,没有清晰的文档,使用者很容易在最初的十分钟里就放弃。我自己维护 Sun Frame 的体感是:每写一段代码,就要花同样甚至双倍的时间去写 README、写注释、写示例。尤其是 Starter 的配置项,一定要配上完整的 YAML 示例,并且拿真实项目跑出来的结果截图放在文档里。使用者看到实际的返回 JSON 结构,会比看抽象定义更放心。
文档里我还坚持放一个“快速开始”,目标是让用户在三分钟内启动一个带接口的项目。如果一个新用户能在三分钟内看到自己的第一个接口返回Result.success,他对这个框架的好感度会大幅提升。这个快速开始的脚本,我会在每次发布新版本前,用一台干净的虚拟机按照文档从零跑一遍,确保上面的步骤没有遗漏。有一次我发版前没有跑文档,结果用户按文档操作时发现少了一个依赖,那个问题还被提了 issue,后来我就再也不敢偷懒了。
6.2 版本管理与发布到Maven中央仓库的经验
个人项目要发布到 Maven Central 其实不是很难,但流程细节很多。首先你得准备一个 Sonatype 账号,然后要有一个自己的域名来绑定 groupId,否则只能用io.github.xxx这样的前缀。接下来需要配置 GPG 签名,每次发布都要对构件进行签名验证。我一开始觉得这很麻烦,但一旦配好,后面的发布就是执行一条命令的事情。
版本管理我采用语义化版本规范:主版本号变化意味着可能有破坏性更新,次版本号变化是新增功能保持兼容,修订号则是缺陷修复。发布前我会把所有依赖的版本也确认一遍,避免因为某个传递依赖更新导致使用者构建失败。这里有个细节:SpringBoot 的 BOM 会在父子项目之间传递依赖版本,但如果你把 Sun Frame 作为一个独立依赖引入别人的项目,不要在你的 pom 里把依赖范围设成provided,否则一些本来应该打包的类会被漏掉,我踩过一次,快被用户骂惨了。
另外,版本发布不是越多越好。小步快跑的策略在业务项目里很好用,但在框架项目里,频繁发版会让使用者不断面临依赖更新和兼容性测试的负担。我一般是一个功能稳定后,集中测试完再发一个小版本,确保每个发布版本都能在 demo 项目里跑通。
6.3 后续规划:分布式支持、代码生成器扩展
最后聊聊 Sun Frame 后续的方向。目前它更适合单体应用,但我也确实考虑做一个扩展模块,支持基于 Redis 的分布式锁和简单的多租户数据隔离。分布式锁其实没那么神秘,核心是拿到 Redis 的SETNX锁,再设置过期时间,需要注意的是一定要把 key 和 value 绑定到业务维度,并判断是否是自己的锁,不能简单解锁。这个功能放进去之后,Sun Frame 在中小型分布式场景下也能有用武之地。
代码生成器也是很多人催的功能。我打算把它做成一个 Maven 插件,而不是一个单独的启动器界面。因为在线生成的代码往往第一次能跑,后面维护就会越来越痛苦。我更倾向于用模板在工程里生成规范一致的代码文件,同时保留手工改动的余地。生成器要支持策略选择,比如生成 Controller、Service、Mapper、Entity 的开关,以及覆盖保护。如果生成器不做保护,不小心覆盖掉用户已经写好的业务逻辑,那就惹祸了。
维护开源项目最真实的感受是:它不像写公司业务代码那样闭环交差就行,而是要长期和用户打交道,持续打磨边界场景。如果你也想做自己的开源框架,我的建议是先从一个两三万行的小工具起步,解决一个你自己最痛的问题,比想做一个通用平台要靠谱得多。Sun Frame 的下一步,大概率还是继续做减法,把那些不常用、维护成本又高的模块移出主分支,让核心越来越稳定。我始终相信,一个好的开发框架,不是让你觉得它强大,而是让你意识不到它的存在,专心写业务就好。