ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Sun Frame:基于SpringBoot的轻量级可插拔开发框架实践指南

Sun Frame:基于SpringBoot的轻量级可插拔开发框架实践指南 每个后端开发者在接手一个新项目时大概都经历过这种场面从 Spring Initializr 生成一个空壳工程然后开始漫长的“组装”之路——配统一返回体、配全局异常处理、配 Redis 工具类、写 JWT 拦截器……一套下来正经业务一行没写时间全花在搬砖上。今天想聊的 Sun Frame就是为了解决这件事而生的一个个人开源项目一个基于 SpringBoot 的轻量级开发框架核心思路是把高频通用能力收拢成可插拔的 starter让新项目能在 5 分钟内进入业务开发。这篇文章我会从设计思路、自动装配原理、实操过程到避坑记录完整过一遍这个框架适合正在规划自己脚手架的人也适合做毕设、做内部管理系统但不想天天重复造轮子的朋友。1. Sun Frame 的由来为什么要自己做一个轻量级框架1.1 从重复劳动到自研脚手架在 SpringBoot 已经统治 Java 后端的今天按理说项目初始化应该很轻松了但实际体验并不是这样。Spring Initializr 能帮你生成的是“结构正确”的工程里面没有统一响应体没有全局异常处理没有接口日志没有鉴权骨架。这些代码每个项目都要写但每个项目写出来的版本千奇百怪。我见过不少公司的项目光一个 Result 类就有三种格式有的用 Map 直接往里面塞 code、msg、data有的是各自封装了个 ResponseUtil。这些问题到联调阶段就开始爆发前端要适配多个团队的不同接口规范Mock 数据都写得想死。Sun Frame 最初的想法很简单——把我自己在多个项目里沉淀的那套通用代码整理成一个足够轻、不绑架业务、可以按需引入的框架。另一个触发点是市面上现成的脚手架。大而全的框架功能确实丰富单是代码生成、权限管理、定时任务就有一大堆但拿到手之后会发现问题模块之间耦合明显很多功能当前项目根本用不到光删代码就得删半天。对于中小系统、个人项目、课程设计、毕业设计这类场景其实我们需要的是一个“中间态”的解决方案比 Spring Initializr 多提供一些约定和通用组件又不像重型脚手架那样一上来就全家桶。Sun Frame 就定位在这个中间态。1.2 框架定位与设计原则Sun Frame 不是什么颠覆性技术它更像是一名后端老兵的项目习惯的表达方式。框架定了四个原则这几个原则贯穿了所有模块的设计轻量核心工程不引入任何重量级中间件作为强制依赖Redis、MinIO 这些外部组件全部按需通过 starter 引入。低侵入你不会被迫继承某个 BaseController也不会被要求必须实现某个框架接口。框架提供的类你愿意用就用不愿意用可以直接绕过。约定优先统一返回体、统一异常、统一日志格式等通过默认配置生效但如果项目有需要可以改。面向真实业务框架里的每个模块都是从实际业务里抽出来的东西不是为“设计感”凑出来的抽象。这里也说明一下适用边界。Sun Frame 适合管理后台、内容管理类 API 服务、教学项目、个人工具类 Web 应用如果要做海量并发、复杂分布式任务调度这类高难度场景那需要的不是这种轻量框架而是更完整的中台能力。想清楚边界才不会被“什么都能干”的心态拖垮。2. 核心模块与自动装配原理拆解2.1 整体模块划分Sun Frame 采用多模块 Maven 结构目的是让各部分可以独立发布、独立使用。目前分为这样几个模块模块职责依赖范围主要功能sun-frame-common基础公共工程无外部中间件依赖统一返回体 Result、错误码枚举、业务异常体系、通用工具类、用户上下文sun-frame-webWeb 层通用配置common spring-boot-starter-web全局异常处理、参数校验统一处理、CORS 策略、接口日志、请求追踪sun-frame-jwt认证鉴权模块common spring-boot-starter-security可选或拦截器JWT 生成/解析、RequireLogin 注解、白名单放行、登录用户注入sun-frame-redis-spring-boot-starterRedis 能力封装common spring-data-redisRedisTemplate 序列化、分布式锁、缓存工具方法sun-frame-minio-spring-boot-starterMinIO 对象存储封装common minio文件上传、下载、删除、预签名 URL、Bucket 管理sun-frame-mybatis-spring-boot-starter持久层增强common mybatis-plus分页插件配置、字段自动填充、MyBatis-Plus 常用能力初始化这种模块化设计带来的直接好处是一个“最简可启动”的 Sun Frame 工程只需要引入 common 和 web 两个模块够了。其他能力比如对象存储需要时再加一行依赖不需要时一点侵入都没有。这跟我前面说的“可插拔”是对应上的。2.2 自动装配的工作原理先补一个基础认知。SpringBoot 与普通 Spring 的一个巨大差异在于“自动配置”。SpringBoot 项目里的 SpringBootApplication 注解核心其实是三个注解的组合SpringBootConfiguration、ComponentScan、EnableAutoConfiguration。前两个好理解扫描配置类嘛关键是第三个 EnableAutoConfiguration它是整个自动装备体系的总开关。自动配置的加载逻辑是SpringBoot 启动时SpringFactoriesLoader 会从 classpath 里扫描所有 META-INF 目录下的配置文件。在 SpringBoot 2.7 之前对应的文件叫META-INF/spring.factories在里面通过org.springframework.boot.autoconfigure.EnableAutoConfiguration\配置类的全限定名列表来注册自动配置类。从 2.7 开始SpringBoot 提供了新的注册机制META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件里直接一行一个自动配置类全限定名。3.x 之后就全面切到新的 imports 文件了。Sun Frame 的每个 starter 在做自动配置时用的是下面这套完整链路第一步定义配置属性类比如 MinIO 的接入参数。配置项前缀定义为sun.minio使用者只需要在 application.yml 里写 sun.minio.endpoint、sun.minio.access-key、sun.minio.secret-key、sun.minio.bucket 等SpringBoot 就会把这些配置值绑定成配置类的属性。这里的关键点是ConfigurationProperties(prefix sun.minio)这个注解。第二步写真正的自动配置类。这个类必须用 AutoConfiguration 注解标记同时配合条件注解来决定是否生效。比如只有 classpath 里存在 MinioClient 类时才加载 MinIO 相关配置对应注解是ConditionalOnClass(MinioClient.class)再看配置项里有没有打开开关对应ConditionalOnProperty(prefix sun.minio, name enabled, havingValue true, matchIfMissing true)最后用ConditionalOnMissingBean保证如果使用者已经自己定义过同类型 Bean框架就不再覆盖。第三步在 resources 目录下新建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件内容写入自动配置类全限定名。这一步经常有人忘记或者是文件名写错、目录层级写错最后自动配置静默失败查半天才发现是注册文件有问题。Sun Frame 选择这套机制而不是直接写一堆 Component 让启动时全量扫描原因很简单可插拔能力依赖注册机制。架子搭好引入依赖就生效去掉依赖就消失完全不用改业务代码。对使用者来说框架的存在感被降到了最低。2.3 内置通用组件的封装思路统一返回体系是 Sun Frame 最基础的一个模块也是我觉得最“普惠”的设计。Result 类是一个泛型封装包含 code、message、data、traceId 四个字段。code 是业务错误码message 是给前端或者调用方看的信息data 是真正的业务数据traceId 则用来串联日志链路。所有的成功返回都走Result.success(data)业务异常走Result.failed(code, message)。这个模型不新鲜但统一的价值很大前后端联调时接口结构一致Swagger/API 文档也更好维护。全局异常处理这块我用RestControllerAdvice做了统一出口。核心是三个异常处理器第一个是业务异常处理器捕获 sun-frame-common 里定义的 BizException直接按异常里携带的错误码返回第二个是参数校验处理器捕获 MethodArgumentNotValidException 和 ConstraintViolationException把校验失败的具体字段信息提取出来返回而不是给前端甩一个笼统的“参数错误”第三个是兜底处理器捕获 Exception记录完整堆栈后统一返回“系统繁忙”这种安全信息。作为框架使用者你只需要抛异常或者加校验注解返回什么格式框架帮你管好了。Redis 模块里一个很容易踩坑的点是序列化。Spring Data Redis 默认用 JDK 序列化key 会变成\xAC\xED\x00\x05t\x00...这种乱码而且 JDK 序列化对象体积大、跨语言困难。Sun Frame 里默认把 key 的序列化器换成 StringRedisSerializervalue 的序列化器换成 Jackson 的 GenericJackson2JsonRedisSerializer同时注入自有的 RedisUtils 组件封装了缓存查询、缓存写入、分布式锁等常用操作。字段填充、分页这些 MyBatis-Plus 的能力也在持久层 starter 里直接配好引入依赖后分页查询不用再额外注册拦截器。3. 实操从零搭建一个 Sun Frame 服务3.1 项目创建与依赖引入先动手把项目拉起来。Sun Frame 的工程代码在 Gitee/GitHub 上开源拿到代码后先做本地安装进入根目录执行 maven 构建命令mvn clean install -DskipTests。这个命令会把 common、web、jwt、redis、minio、mybatis 等模块全部构建并安装到本地 Maven 仓库。之后新建业务项目时只需要像引普通依赖一样引入 Sun Frame 的模块即可。新建业务项目时pom.xml 大概长这样parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.sunframe/groupId artifactIdsun-frame-web/artifactId version1.0.0/version /dependency dependency groupIdorg.sunframe/groupId artifactIdsun-frame-redis-spring-boot-starter/artifactId version1.0.0/version /dependency dependency groupIdorg.sunframe/groupId artifactIdsun-frame-mybatis-spring-boot-starter/artifactId version1.0.0/version /dependency /dependencies这个 pom 除了引入 SpringBoot 官方父工程其他就是 Sun Frame 自己的模块依赖。你可能注意到我选了 SpringBoot 2.7.18 这个版本原因待会在避坑章节展开。最重要的是业务代码里不需要加任何核心依赖到自己的工程——Sun Frame 会把需要的 SpringBoot 场景依赖通过模块传递过来。启动类的写法和标准 SpringBoot 完全一样SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }是的没有任何继承框架基类的要求这体现了低侵入的设计原则。3.2 配置与第一组接口在 application.yml 里除了 SpringBoot 常规的数据源、端口配置Sun Frame 的组件配置通过各自的前缀开关生效。一个典型配置长这样server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/sun_demo?useUnicodetruecharacterEncodingutf8 username: root password: 123456 redis: host: localhost port: 6379 sun: jwt: enabled: true secret: sun-frame-jwt-secret-change-me expire-minutes: 120 white-list: /api/auth/login, /api/auth/register minio: enabled: true endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket: demo-bucket写一个最简单的实体类和 Mapper 接口。结合 MyBatis-Plus实体类就一个 TableName 注解表明对应的表Mapper 接口继承 BaseMapper 之后基础的增删改查就都有了完全不用写 SQLTableName(tb_user) public class User { TableId(type IdType.AUTO) private Long id; private String username; private String email; }public interface UserMapper extends BaseMapperUser { }然后是 Service 和 Controller 层。Controller 层最爽的一点是直接返回 Result 对象不需要在方法里手动处理响应值RestController RequestMapping(/api/user) public class UserController { Resource private UserMapper userMapper; GetMapping(/{id}) public ResultUser getUser(PathVariable Long id) { User user userMapper.selectById(id); if (user null) { throw new BizException(ErrorCode.NOT_FOUND); } return Result.success(user); } PostMapping public ResultBoolean createUser(RequestBody User user) { return Result.success(userMapper.insert(user) 0); } }你会发现整个链路里没有 System.out 打印、没有手写异常 try-catch、没有手动构建 Map 响应。接口日志由 sun-frame-web 里的 AOP 切面自动处理了包括请求路径、方法名、入参、耗时和执行结果。这就是框架层帮你砍掉的那些“隐形业务”。3.3 后端服务如何与前端工程集成很多做毕设或者中小项目的人面临的另一个实际问题是前端是 Vue 工程后端是 SpringBoot 工程部署时想要打包成一个 jar 方便运行。这个需求其实不算复杂只是首次操作容易踩路径坑。思路很简单前端项目先执行 npm run build 生成 dist 目录然后把 dist 里面的文件复制到 SpringBoot 的src/main/resources/static目录下最后 maven package 打成 fat jar。启动 jar 后直接访问http://localhost:8080SpringBoot 会将请求映射到 static 目录中的 index.html前端路由由 Vue Router 在浏览器端处理。要注意的是前端访问后端接口时应使用相对路径/api不要写成http://localhost:8080这种绝对地址否则部署到服务器换端口后又要改代码。复制文件这一步我一般用前端构建的拷贝插件在 Vue 的 vite.config.js 或 vue.config.js 里配置打包后自动拷贝到 SpringBoot 的 static 目录。这样整个发布流程只需要两步前端 build然后 maven package。Sun Frame 本身不干预这个过程因为它的 web 模块不会对静态资源映射做特殊限制所以这种单 jar 部署模式可以直接用。3.4 为框架扩展一个自定义 Starter框架作者视角的操作体验同样重要。假设你现在想给 Sun Frame 新增一个短信发送能力的 starter步骤是非常标准的四步。第一步在 sun-frame 父工程下新建sun-frame-sms-spring-boot-starter模块引入 common 模块依赖。第二步创建配置属性类 SmsProperties用 ConfigurationProperties 绑定以 sun.sms 开头的配置项。第三步创建自动配置类 SmsAutoConfiguration在里面根据配置创建 SmsClient Bean并在类上使用 ConditionalOnProperty 控制是否启用。第四步在 resources 下新建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件把自动配置类全限定名写入。然后回到根目录执行 maven install再到业务项目里引入依赖配置 sun.sms.enabledtrue服务就具备短信发送能力了。这个“四步走”的过程其实就是把 SpringBoot 的 SPI 机制摸透之后形成的肌肉记忆。等做过头两个 starter 之后后面任何一个新能力接入基本都是复制粘贴改改类名耗时不超过半小时。4. 使用与开发过程中踩过的坑4.1 自动配置不生效的排查套路框架开发和使用中最常见的现象是依赖引入了配置也写了但功能没生效。比如 JWT 拦截器没有拦截任何路径比如 MinIO Client 没有注入成功。这类问题的排查套路我总结了一套比较稳定的流程。第一步先看自动装配报告。在启动命令里加上--debug参数启动日志会输出一份完整的自动装配报告Positive matches 是已生效的自动配置Negative matches 是被判定不生效的配置其中会明确给出不生效的原因是条件不满足还是类不存在。这份报告是定位问题最好的地图。java -jar demo.jar --debug第二步对照条件注解检查ConditionalOnClass 判断的类是否真的在 classpath 中、ConditionalOnProperty 判断的配置项是否写对了前缀和值。很多人在配置里写 sun.minio.url但前缀定义的是 sun.minio.endpoint配置对不上条件装配直接放弃执行。第三步确认自动配置类的注册方式。SpringBoot 2.7 之前用 META-INF/spring.factories2.7 之后支持新的 imports 文件SpringBoot 3.x 则只能走 imports 文件。如果你的项目是 2.7 却只放 spring.factories结果可能还是能跑但如果升级到 3.x自动配置会静默失效。文件路径和文件名错一个字母整个能力完全失效而且不会有任何显式报错。这个坑我建议每个做 starter 的人都提前熟悉。4.2 SpringBoot 版本选择与升级冲突前面提到我在 Sun Frame 父工程里选的是 SpringBoot 2.7.18为什么不是最新的版本这要从实际兼容性说起。热搜词里有一条“springboot版本太高”这个现象在真实项目里确实存在。SpringBoot 3.x 将基线提升到 JDK 17同时包名从 javax 改成了 jakarta。如果你的目标环境是 JDK 8比如老服务器、不少高校实验环境那就只能使用 2.x。如果本地环境已经是 JDK 17我反而建议直接用 3.x毕竟新版本在性能优化和模块化上更有优势。另一个和版本强相关的是代理机制。SpringBoot 2.x 默认会优先使用 CGLIB 代理SDK 目标类没有实现接口时也能正常代理SpringBoot 3.x 同样保持这个行为。这个点在实际开发中的影响是如果你用 Transactional 调同类内部方法代理不生效事务会失效。这不是框架的问题是 Spring AOP 代理机制的老知识了但每个排查到这里的同学都容易先在配置上翻半天。我的建议是第一项目用什么 JDK 版本直接决定你选 SpringBoot 2 还是 3第二如果要做自定义 starter 的开源发布尽量兼容两个大版本Sun Frame 的 common 与 web 模块在代码层面避免使用 jakarta 与 javax 强绑定的 API需要里用条件编译或者分版本维护时要注意隔离。4.3 Redis 与 MinIO 集成时的典型问题Redis 序列化问题我在前面提过。实际使用 Sun Frame 的 redis starter 时有个额外问题也很常见用 Jackson 做 value 序列化之后存入 Redis 的值会带 class 字段标记真实类名。业务代码反序列化时如果类的包名或者结构发生了变化比如从实体里加了个字段旧缓存直接反序列化失败。这是 Jackson 序列化方案的通病并不是框架缺陷。解决方案有两种要么在 RedisUtils 里封装 string 类型的读写场景业务层自己负责对象的序列化和反序列化要么缓存 key 里带版本号发版后自动失效全部缓存。我更倾向第二种操作成本最低。MinIO 的坑主要集中在访问地址和 Bucket 策略。一个很经典的场景服务器上 MinIO 的 endpoint 配的是http://127.0.0.1:9000开发环境本地看没问题部署到生产后前端拿到的是127.0.0.1的地址自然访问不通。正确姿势是配置和公网可达地址一致的 endpoint或者让 MinIO 走反向代理对外统一暴露一个域名。另外通过预签名 URL 访问私有 Bucket 对象时要确认 bucket 策略与生成 URL 的客户端配置匹配否则会出现可以下载但无法在线预览的现象。4.4 常见问题速查表症状原因解决方案新加的自动配置完全无日志AutoConfiguration.imports 文件路径/名称错误检查 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 是否存在且内容正确启动报 NoSuchMethodError / ClassNotFoundExceptionSpringBoot/依赖版本冲突mvn dependency:tree 查依赖树排除重复或者版本不一致的依赖Redis 的 key 显示为 \xAC\xED 乱码默认 JDK 序列化器使用 sun-frame-redis-starter 自带序列化配置或手动指定 StringRedisSerializer接口返回 401 但白名单路径也拦截了白名单配置路径与实际请求路径不一致检查 sun.jwt.white-list 路径是否以 / 开头且不含上下文路径MyBatis-Plus 分页不生效分页插件未注册或拦截器顺序被覆盖确认只引入了 sun-frame-mybats-starter未手动注册重复拦截器上传文件到 MinIO 后无法访问Bucket 访问策略或 endpoint 不通bucket 设置为 public 或使用预签名 URLendpoint 用公网可达地址这些坑没有一个是高深的原理问题全是工程实践里的细碎东西。但恰恰是这些细碎的东西决定了框架好不好用、项目能不能快速跑通。Sun Frame 把这些常见问题通过组件封装提前规避剩下的就交给使用者的正确配置了。5. 后续方向与一点个人心得Sun Frame 目前的版本更像是一个基于我自己项目经验的“精选集”很多能力是从真实业务里长出来的。后续如果有时间我计划往几个方向扩展增加基于注解的幂等控制组件、内置 OpenAPI 文档配置、支持多租户数据隔离的 mybatis 扩展、再补一个基于虚拟线程的异步任务模块。不过这些能不能落地得看项目使用反馈和我的业余时间开源项目的节奏本来就应该稳着走。根据我自己这两年的实践体会做这种个人开源框架最大的收获不是代码本身而是把 SpringBoot 自动配置、模块化设计、版本兼容这些知识彻底吃透了。你可以把 Sun Frame 当成一个现成的脚手架来用也可以当成一个拆解 SpringBoot 原理的案例来学。如果你想开始自己的第一个开源项目我强烈建议也从一个这样小而美的 starter 开始——不要想着一次做完美能解决自己一类实际问题就值得被分享出去。
返回列表