一、前言
在开发 JunoYi 框架 的过程中,我越来越明确一件事:一个后台管理系统如果只是"能跑起来",单体结构当然够用;但如果它要长期维护、持续开源、被别人二次开发,并且逐步沉淀成一个企业级 Java 开发框架,那么项目结构就不能只服务于今天的功能,而要服务于未来的演进。
JunoYi现在的定位是:
安全内建、开箱即用、企业级 Java 开发框架。基于 Spring Boot 3.5 + Java 21 打造的现代化企业应用开发脚手架。
所以我没有把所有代码都放在一个 src/main/java 下面,而是把后端拆成了:
text
JunoYi
├── junoyi-dependencies
├── junoyi-framework
├── junoyi-module
└── junoyi-server
这篇文章就结合 JunoYi 的真实项目结构,解释为什么我把 Spring Boot 项目拆成 framework、module、server,这种 Maven 模块设计到底解决了什么问题,以及它和主流开源框架的结构有什么相似之处。
二、先说结论:这不是为了"看起来高级",而是为了控制复杂度
很多项目一开始都是这样的:
src/main/java
└── com.xxx
├── controller
├── service
├── mapper
├── domain
├── config
├── utils
├── security
├── redis
├── excel
├── file
└── ...
早期这样写没有问题,甚至开发速度很快。
但当项目逐渐加入下面这些能力时,单体目录就开始变得吃力:
- 登录认证
- 权限控制
- 字段级权限
- 数据权限
- Redis 缓存
- 文件存储
- Excel 导入导出
- API 文档
- XSS 防护
- SQL 注入防护
- 多平台登录
- 微信登录
- 企业微信能力
- 系统管理
- 用户、角色、菜单、部门、字典、参数配置
- 操作日志
- 登录日志
- 在线会话
- 前后端分离接口
如果这些内容全部堆在一个应用模块里,项目会慢慢出现几个问题:
- 基础能力和业务代码混在一起。
- 改一个工具类,可能影响一堆业务。
- 业务模块无法单独拆卸或复用。
- 新人不知道哪些代码是框架能力,哪些代码是具体业务。
- 后续想做脚手架、代码生成、多租户、微服务版本,会越来越困难。
所以 JunoYi 的拆分目标不是"微服务化",也不是"为了拆而拆",而是把一个 Spring Boot 单体应用先整理成一个清晰的模块化单体。
模块化单体的核心思想是:部署时仍然可以是一个 Spring Boot 应用,但代码结构、依赖关系、职责边界要先清楚。
三、JunoYi 当前的 Maven 模块结构
JunoYi 根工程是一个 Maven 聚合工程:
xml
<modules>
<module>junoyi-dependencies</module>
<module>junoyi-framework</module>
<module>junoyi-module</module>
<module>junoyi-server</module>
</modules>
整体结构可以理解成:
JunoYi
├── junoyi-dependencies # 统一依赖版本管理
├── junoyi-framework # 框架基础能力
│ ├── junoyi-framework-core
│ ├── junoyi-framework-web
│ ├── junoyi-framework-log
│ ├── junoyi-framework-security
│ ├── junoyi-framework-permission
│ ├── junoyi-framework-datasource
│ ├── junoyi-framework-redis
│ ├── junoyi-framework-json
│ ├── junoyi-framework-excel
│ ├── junoyi-framework-file
│ ├── junoyi-framework-captcha
│ ├── junoyi-framework-event
│ ├── junoyi-framework-api-doc
│ ├── junoyi-framework-platform
│ ├── junoyi-framework-wechat
│ ├── junoyi-framework-wework
│ └── junoyi-framework-boot-starter
├── junoyi-module # 业务功能模块
│ ├── junoyi-module-system
│ └── junoyi-module-oauth
└── junoyi-server # 启动入口与部署模块
#mermaid-svg-EvKN5wvPIUhoXnFJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EvKN5wvPIUhoXnFJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EvKN5wvPIUhoXnFJ .error-icon{fill:#552222;}#mermaid-svg-EvKN5wvPIUhoXnFJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EvKN5wvPIUhoXnFJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EvKN5wvPIUhoXnFJ .marker.cross{stroke:#333333;}#mermaid-svg-EvKN5wvPIUhoXnFJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EvKN5wvPIUhoXnFJ p{margin:0;}#mermaid-svg-EvKN5wvPIUhoXnFJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EvKN5wvPIUhoXnFJ .cluster-label text{fill:#333;}#mermaid-svg-EvKN5wvPIUhoXnFJ .cluster-label span{color:#333;}#mermaid-svg-EvKN5wvPIUhoXnFJ .cluster-label span p{background-color:transparent;}#mermaid-svg-EvKN5wvPIUhoXnFJ .label text,#mermaid-svg-EvKN5wvPIUhoXnFJ span{fill:#333;color:#333;}#mermaid-svg-EvKN5wvPIUhoXnFJ .node rect,#mermaid-svg-EvKN5wvPIUhoXnFJ .node circle,#mermaid-svg-EvKN5wvPIUhoXnFJ .node ellipse,#mermaid-svg-EvKN5wvPIUhoXnFJ .node polygon,#mermaid-svg-EvKN5wvPIUhoXnFJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EvKN5wvPIUhoXnFJ .rough-node .label text,#mermaid-svg-EvKN5wvPIUhoXnFJ .node .label text,#mermaid-svg-EvKN5wvPIUhoXnFJ .image-shape .label,#mermaid-svg-EvKN5wvPIUhoXnFJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-EvKN5wvPIUhoXnFJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EvKN5wvPIUhoXnFJ .rough-node .label,#mermaid-svg-EvKN5wvPIUhoXnFJ .node .label,#mermaid-svg-EvKN5wvPIUhoXnFJ .image-shape .label,#mermaid-svg-EvKN5wvPIUhoXnFJ .icon-shape .label{text-align:center;}#mermaid-svg-EvKN5wvPIUhoXnFJ .node.clickable{cursor:pointer;}#mermaid-svg-EvKN5wvPIUhoXnFJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EvKN5wvPIUhoXnFJ .arrowheadPath{fill:#333333;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EvKN5wvPIUhoXnFJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EvKN5wvPIUhoXnFJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EvKN5wvPIUhoXnFJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EvKN5wvPIUhoXnFJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EvKN5wvPIUhoXnFJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EvKN5wvPIUhoXnFJ .cluster text{fill:#333;}#mermaid-svg-EvKN5wvPIUhoXnFJ .cluster span{color:#333;}#mermaid-svg-EvKN5wvPIUhoXnFJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EvKN5wvPIUhoXnFJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EvKN5wvPIUhoXnFJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-EvKN5wvPIUhoXnFJ .icon-shape,#mermaid-svg-EvKN5wvPIUhoXnFJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EvKN5wvPIUhoXnFJ .icon-shape p,#mermaid-svg-EvKN5wvPIUhoXnFJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EvKN5wvPIUhoXnFJ .icon-shape .label rect,#mermaid-svg-EvKN5wvPIUhoXnFJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EvKN5wvPIUhoXnFJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EvKN5wvPIUhoXnFJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EvKN5wvPIUhoXnFJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} junoyi-server
启动入口、应用配置、最终打包
junoyi-module
业务功能模块
junoyi-framework
框架基础能力
junoyi-dependencies
统一版本管理
这个结构的关键点是:
- dependencies 解决版本统一问题。
- framework 解决基础设施复用问题。
- module 解决业务功能内聚问题。
- server 解决最终装配和启动问题。
四、为什么需要 junoyi-dependencies:先把版本控制住
JunoYi 有很多第三方依赖,例如:
- Spring Boot 3.5.0
- Java 21
- MyBatis-Plus
- Druid
- MySQL Driver
- Hutool
- EasyExcel
- Redisson
- Lock4j
- Fastjson2
- JJWT
- SpringDoc
- Knife4j
- MinIO
- Aliyun OSS
- Qiniu
- 微信 SDK
如果每个模块都自己写版本号,很容易出现这种情况:
xml
<!-- A 模块 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- B 模块 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>
短期看不明显,长期一定会变成依赖地狱。
所以 JunoYi 单独设计了 junoyi-dependencies:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-core</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-module-system</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
然后根工程再引入:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-dependencies</artifactId>
<version>${revision}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
这样每个子模块只需要声明"我要用什么",不需要关心"用哪个版本"。
xml
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-boot-starter</artifactId>
</dependency>
这种设计的好处是:
- 版本集中管理,升级有入口。
- 内部模块版本统一,避免各模块版本不一致。
- 第三方依赖冲突更容易排查。
- 开源使用者阅读 POM 时更清楚。
- 后续发布 BOM 或脚手架时更自然。
Maven 官方文档里提到,多模块项目由 Maven Reactor 负责收集模块、排序模块,并按照依赖关系构建模块。也就是说,Maven 本身就支持这种聚合式工程结构,并且会根据模块之间的依赖关系决定构建顺序。参考:Apache Maven Guide to Working with Multiple Modules。
所以 junoyi-dependencies 不是一个业务模块,它是整个项目的依赖治理中心。
五、为什么需要 junoyi-framework:把"框架能力"和"业务能力"分开
JunoYi 不是只做一个后台管理系统,而是想沉淀成一个企业级 Java 开发框架。
这就意味着,有些代码不应该属于某个具体业务,而应该属于框架能力。
比如下面这些模块:
junoyi-framework-core
junoyi-framework-web
junoyi-framework-log
junoyi-framework-security
junoyi-framework-permission
junoyi-framework-datasource
junoyi-framework-redis
junoyi-framework-json
junoyi-framework-excel
junoyi-framework-file
junoyi-framework-captcha
junoyi-framework-event
junoyi-framework-api-doc
junoyi-framework-platform
junoyi-framework-wechat
junoyi-framework-wework
这些模块不是"用户管理"或"角色管理"这种具体业务,而是所有业务都会用到的底座能力。
例如:
1. core 模块
junoyi-framework-core 放的是基础能力:
com.junoyi.framework.core.domain.module.R
com.junoyi.framework.core.domain.page.PageQuery
com.junoyi.framework.core.domain.page.PageResult
com.junoyi.framework.core.domain.base.BaseEntity
com.junoyi.framework.core.utils.StringUtils
com.junoyi.framework.core.utils.SpringUtils
com.junoyi.framework.core.utils.TreeBuildUtils
com.junoyi.framework.core.utils.ServletUtils
这些东西不属于某一个业务模块,而是整个框架的基础语言。
比如统一响应对象:
java
public class R<T> {
private int code;
private String msg;
private T data;
}
分页对象:
java
public class PageQuery {
private Integer pageNum;
private Integer pageSize;
}
基础实体:
java
public class BaseEntity {
private Long createBy;
private LocalDateTime createTime;
private Long updateBy;
private LocalDateTime updateTime;
}
这些能力一旦放到业务模块里,就会导致其他模块依赖业务模块,依赖方向会变乱。
所以它们应该放在 framework-core。
2. web 模块
junoyi-framework-web 负责 Web 层通用能力,例如:
WebMvcConfiguration
CorsConfiguration
GlobalExceptionHandler
BaseController
XssFilter
SqlInjectionFilter
AccessLogInterceptor
这些能力也不是某个业务专属,而是整个 Web 应用都需要。
比如统一异常处理:
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e) {
return R.fail(e.getMessage());
}
}
如果没有独立的 web 模块,这些配置就会散落在启动模块或业务模块中。项目越大,越难维护。
3. security 模块
junoyi-framework-security 放的是认证、安全上下文、Token、会话、平台范围等能力:
TokenAuthenticationTokenFilter
ApiEncryptFilter
SecurityContext
LoginUser
UserSession
TokenPair
JwtTokenHelper
SessionHelper
AuthHelper
PlatformScope
PlatformScopeInterceptor
JunoYi 的登录体系支持双 Token 机制:
- AccessToken
- RefreshToken
并且支持不同平台定制不同会话时长,例如:
后台管理 WEB
前台 WEB
小程序
H5
移动端 APP
桌面端
这种能力明显不应该写死在 system 模块里。因为以后不管是系统管理、商城模块、工作流模块,还是第三方业务模块,都可能需要认证和会话能力。
所以它属于 framework-security。
4. permission 模块
junoyi-framework-permission 是 JunoYi 比较重要的设计点。
它不仅支持接口权限,还支持字段级权限:
Permission
FieldPermission
PermissionAspect
FieldPermissionSerializer
MaskUtils
PermissionHelper
PermissionLoader
例如:
java
@Permission("user.api.list.add")
@PostMapping("/user")
public R<Void> addUser(@RequestBody UserDTO dto) {
return userService.addUser(dto);
}
字段读取权限可以这样理解:
java
public class UserVO {
@FieldPermission(value = "user.data.phonenumber.read")
private String phonenumber;
@FieldPermission(value = "user.data.idCard.read")
private String idCard;
}
这样权限不只是控制"能不能访问某个接口",还可以控制"能不能读取某个字段"。
这就是 JunoYi 和传统 RBAC 后台框架不太一样的地方。
传统 RBAC 通常围绕用户、角色、菜单、权限标识来做控制。JunoYi 在此基础上,把权限继续细化到:
页面权限
按钮权限
接口权限
字段读取权限
字段写入权限
所以 permission 是框架能力,而不是业务模块的一部分。
5. datasource 模块
junoyi-framework-datasource 放的是数据源、MyBatis-Plus、数据权限、SQL 美化、慢 SQL 等能力:
MyBatisPlusConfig
DataSource
DataSourceAspect
DataScope
IgnoreDataScope
DataScopeHandler
SlowSqlInterceptor
SqlBeautifyInterceptor`
例如数据权限注解:
java
@DataScope
public List<SysUserVO> listUsers(SysUserQuery query) {
return userMapper.selectUserList(query);
}
这种能力在系统管理里会用,在未来其他业务模块里也会用。所以它应该沉淀到 framework。
6. redis、excel、file、json、captcha、event
这些模块也是一样的逻辑。
Redis:
RedisUtils
CacheUtils
QueueUtils
PlusSpringCacheManager
RedissonProperties
Excel:
ExcelUtils
ExcelListener
ExcelDictFormat
ExcelEnumFormat
CellMerge
文件存储:
FileStorage
LocalFileStorage
AliyunOssFileStorage
FileStorageFactory
UploadStrategy
事件总线:
EventBus
Event
EventHandler
EventListener
SpringEventBridge
验证码:
CaptchaHelper
CaptchaGenerator
RedisCaptchaStore
CaptchaScene
CaptchaType
这些都属于"可复用基础设施"。
如果直接塞进业务模块,短期能跑,长期会导致业务模块越来越像一个垃圾桶。
六、为什么需要 junoyi-framework-boot-starter:降低使用成本
JunoYi 的junoyi-framework-boot-starter是一个很关键的模块。
它的 POM 中聚合了框架常用能力:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-core</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-datasource</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-event</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-excel</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-json</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-permission</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-redis</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-security</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-web</artifactId>
</dependency>
</dependencies>
它的作用是:让业务模块不用一个个引入 framework 子模块。
比如 junoyi-module-system 只需要依赖:
xml
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-boot-starter</artifactId>
</dependency>
这样系统模块就获得了框架基础能力。
这和 Spring Boot 官方推荐的 starter 思路是一致的。
Spring Boot 官方文档在"Creating Your Own Starter"中说明,自定义 starter 的目的就是提供一个依赖入口,让使用者添加一个 starter 后获得使用某个能力所需的典型依赖集合。参考:Spring Boot Creating Your Own Auto-configuration。
所以 junoyi-framework-boot-starter 的意义不是写很多业务代码,而是提供一个"开箱即用"的依赖组合。
它解决的问题是:
没有 starter:
业务模块需要自己引入 core、web、redis、security、permission、json、file、excel...
有 starter:
业务模块只依赖 junoyi-framework-boot-starter
这就是框架体验。
七、为什么需要 junoyi-module:业务模块应该独立生长
junoyi-module 是业务功能层。
目前 JunoYi 有两个业务模块:
- junoyi-module-system
- junoyi-module-oauth
其中 junoyi-module-system 负责系统管理能力,例如:
- 用户管理
- 角色管理
- 部门管理
- 菜单管理
- 权限管理
- 字典管理
- 参数配置
- 文件管理
- 操作日志
- 登录日志
- 在线会话
从代码结构看,system 模块里包含:
- controller
- service
- mapper
- domain
- listener
- enums
- util
例如:
SysUserController
SysRoleController
SysDeptController
SysMenuController
SysDictTypeController
SysDictDataController
SysPermissionController
SysSessionController
SysOperLogController
SysAuthLogController
SysFileController
SysFileUploadController
这些是明确的业务功能,不应该放进 framework。
因为 framework 应该回答的是:
框架能提供什么基础能力?
而 module 应该回答的是:
这个系统具体实现了什么业务?
这两者必须分开。
八、system 模块为什么不是 framework 的一部分
有些人会问:用户、角色、菜单、权限不是每个后台管理系统都有吗?为什么不直接放到 framework 里?
我的理解是:它们是通用业务,不是底层框架。
例如:
- BaseEntity 是框架基础对象。
- R 是框架统一响应对象。
- PermissionAspect 是框架权限切面。
- JwtTokenHelper 是框架 Token 工具。
- SysUserController 是系统管理业务接口。
- SysRoleService 是系统管理业务服务。
- SysMenuMapper 是系统管理数据库访问。
用户、角色、菜单虽然常见,但它们依然依赖具体数据库表、具体页面、具体业务规则。
所以我把它们放在 junoyi-module-system,而不是 junoyi-framework。
这样做有几个好处:
- framework 可以保持通用性。
- system 可以独立演进。
- 以后如果有人不想用 JunoYi 默认的系统管理模块,可以替换或裁剪。
- 后续新增 mall、workflow、crm、tenant 等模块时,可以和 system 平级。
- 权限底座可以复用,但权限业务可以变化。
一个简单判断标准是:
如果它离开 JunoYi 的系统管理业务仍然成立,它更可能属于 framework。
如果它依赖具体业务表、业务流程、页面语义,它更可能属于 module。
例如:
framework-permission:
Permission 注解
FieldPermission 注解
PermissionAspect
PermissionHelper
module-system:
权限池管理
用户权限分配
角色权限分配
菜单权限配置
这就是框架能力和业务能力的边界。
九、为什么需要 junoyi-server:启动入口必须保持薄
junoyi-server 是最终启动模块。
它依赖:
xml
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-module-system</artifactId>
</dependency>
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-module-oauth</artifactId>
</dependency>
启动类也非常薄:
java
@SpringBootApplication(scanBasePackages = {"com.junoyi"})
@EnableCaching
public class JunoYiServerApplication {
public static void main(String[] args) {
JunoYiApplication.run(JunoYiServerApplication.class, args);
}
}
这说明 server 做的是装配工作,而不是业务实现。
它主要负责:
- 引入需要启用的 framework 能力。
- 引入需要启用的业务模块。
- 提供启动类。
- 放置应用配置文件。
- 完成最终打包。
junoyi-server 里面有:
application.yml
application-local.yml
keys/private.pem
keys/public.pem
public/LOGO.png
JunoYiServerApplication.java也就是说,server 更像一个"应用组装层"。
如果未来要做不同发行版,可以很自然地扩展:
junoyi-server-admin
junoyi-server-openapi
junoyi-server-tenant
junoyi-server-demo
每个 server 选择不同模块组合即可。
例如:
xml
<!-- 管理后台版本 -->
<dependency>
<artifactId>junoyi-module-system</artifactId>
</dependency>
<!-- OAuth 版本 -->
<dependency>
<artifactId>junoyi-module-oauth</artifactId>
</dependency>
<!-- 未来的工作流版本 -->
<dependency>
<artifactId>junoyi-module-workflow</artifactId>
</dependency>
这样 JunoYi 就不再是一个固定死的后台系统,而是可以组合的开发框架。
十、这种结构和 Spring Boot 官方思路是一致的
Spring Boot 生态中大量使用 starter 模式。
例如:
spring-boot-starter-web
spring-boot-starter-data-redis
spring-boot-starter-validation
spring-boot-starter-security
使用者不需要关心底层到底引入了多少依赖,只需要声明 starter。
JunoYi 的 junoyi-framework-boot-starter 也是类似思路:
junoyi-framework-boot-starter
├── core
├── web
├── datasource
├── redis
├── security
├── permission
├── json
├── excel
├── file
├── captcha
├── api-doc
└── platform
Spring Boot 官方文档明确提到,如果你在公司、开源项目或商业库中开发共享库,可以创建自己的自动配置;starter 可以提供自动配置代码以及典型依赖集合。参考:Spring Boot 官方文档:Creating Your Own Auto-configuration。
JunoYi 现在的设计还可以继续向更标准的 Spring Boot Starter 演进,例如:
junoyi-framework-security-autoconfigure
junoyi-framework-security-starter
或者继续保持现在这种更易理解的内部 starter 结构。
重点不是名字,而是思路:
框架能力沉淀为可复用依赖。
业务模块只使用能力,不重复建设能力。
启动模块只做装配,不承载复杂业务。
十一、这种结构也符合模块化单体的发展方向
Spring 官方还有一个项目叫 Spring Modulith,它强调在 Spring Boot 应用中构建结构良好的应用模块,并支持模块验证、模块级集成测试、模块级观察和文档生成。参考:Spring Modulith 官方项目页。
这说明一个趋势:不是所有系统都必须一上来做微服务,但大型 Spring Boot 应用一定要重视模块边界。
JunoYi 目前的结构,本质上就是一种模块化单体:
部署形态:
一个 Spring Boot 应用
代码形态:
多 Maven 模块
清晰依赖方向
framework 和 module 分离
server 统一装配
模块化单体的好处是:
- 保留单体部署简单的优势。
- 避免微服务过早引入分布式复杂度。
- 代码边界比普通单体清晰。
- 后续如果真的需要拆服务,已有模块边界可以作为拆分依据。
- 更适合开源框架和企业脚手架。
我不认为所有项目都应该一开始上微服务。很多中后台系统,最合适的起点就是模块化单体。
十二、对标其他开源框架:JunoYi 的结构不是孤例
JunoYi 这种拆法,在国内外很多企业级开源框架里都能看到类似思想。
1. RuoYi-Vue-Plus / RuoYi-Plus 类框架
RuoYi-Plus 相关文档中,后端结构也会区分:
ruoyi-admin # 启动入口
ruoyi-common # 通用工具模块
ruoyi-modules # 业务模块
ruoyi-extend # 扩展模块
其官方文档中对 ruoyi-admin 的说明是:系统启动入口,负责整合所有模块并提供统一部署打包。对 ruoyi-common 的说明是:提供系统基础设施和通用功能。对 ruoyi-modules 的说明是:包含业务子模块,实现具体业务功能。参考:RuoYi-Plus-UniApp 后端项目结构文档。
这和 JunoYi 的结构高度相似:
RuoYi-Plus:
ruoyi-common -> 通用基础能力
ruoyi-modules -> 业务模块
ruoyi-admin -> 启动入口
JunoYi:
junoyi-framework -> 框架基础能力
junoyi-module -> 业务模块
junoyi-server -> 启动入口
区别是命名不同:
common 更偏"通用工具"
framework 更强调"框架底座"
admin 更偏"后台入口"
server 更强调"服务端装配"
JunoYi 使用 framework,是因为我希望它不仅是工具包,而是具备安全、权限、平台、文件、事件、数据源等完整框架能力的基础层。
2. RuoYi-Vue-Plus 的结构解耦观点
RuoYi-Vue-Plus 的项目说明中也强调后端结构采用插件化、扩展包形式,目的是结构解耦、易于扩展。参考:RuoYi-Vue-Plus GitHub 项目说明。
这和 JunoYi 的目标一致:框架越大,越不能让所有能力互相缠绕。
JunoYi 中:
junoyi-framework-file
junoyi-framework-excel
junoyi-framework-captcha
junoyi-framework-wechat
junoyi-framework-wework
这些模块都可以看成一种"扩展能力"。
比如:
- 项目需要文件上传,就引入 file。
- 项目需要微信生态,就引入 wechat。
- 项目需要企业微信,就引入 wework。
- 项目需要验证码,就引入 captcha。
- 项目需要 Excel,就引入 excel。
即使现在 boot-starter 默认组合了部分模块,底层仍然保持了模块粒度,后续可以更精细地做可选启用。
十三、为什么不直接一个 module 搞定所有 framework 能力
有人可能会说:既然都是 framework,为什么不做一个 junoyi-framework jar,把所有基础能力都塞进去?
原因是:框架层内部也需要继续分治。
如果全部塞进一个模块,会出现几个问题:
- Redis、Excel、文件、微信、企业微信等依赖全部被迫引入。
- 不使用某个能力,也要承担它的依赖体积和配置复杂度。
- 某个基础能力变更时,影响范围不清楚。
- 模块职责不清晰。
- 后续做 starter 组合会困难。
所以 JunoYi 把 framework 再拆成多个子模块:
core # 基础对象、工具、常量
web # Web MVC、跨域、异常、过滤器、拦截器
security # 登录认证、Token、会话、安全上下文
permission # 权限注解、字段权限、权限切面
datasource # 数据源、MyBatis-Plus、数据权限
redis # Redis、Redisson、缓存工具
excel # Excel 导入导出
file # 文件存储
json # JSON 序列化
event # 事件总线
captcha # 验证码
api-doc # OpenAPI / Knife4j
platform # 多平台抽象
wechat # 微信生态
wework # 企业微信生态
这就是高内聚、低耦合。
每个模块只处理一类问题。
十四、JunoYi 的依赖方向应该怎么理解
JunoYi 的依赖方向应该是单向的:
server -> module -> framework -> dependencies
或者:
server -> framework
server -> module
module -> framework
framework -> dependencies
不能出现:
framework -> module
这是非常重要的。
因为 framework 是底座,它不能知道上层业务模块的存在。
举例:
正确:
java
// module-system 使用 framework-permission 提供的注解
@Permission("system.user.list")
@GetMapping("/list")
public R<PageResult<SysUserVO>> list(SysUserQuery query) {
return R.ok(userService.list(query));
}
错误:
java
// framework-permission 直接依赖 SysUserService
@Autowired
private SysUserService sysUserService;
一旦 framework 依赖了 module,整个架构就倒过来了。
正确的做法应该是:
framework 定义接口或扩展点
module 实现接口
server 装配它们
例如权限加载可以这样理解:
java
public interface PermissionLoader {
Set<String> loadPermissions(Long userId);
}
framework 只知道 PermissionLoader,不知道权限来自数据库、Redis、远程接口还是配置文件。
然后 system 模块提供实现:
java
@Service
public class SysPermissionLoader implements PermissionLoader {
@Override
public Set<String> loadPermissions(Long userId) {
return permissionMapper.selectPermissionsByUserId(userId);
}
}
这样 framework 和 module 的边界就是清晰的。
配图建议:放一张"正确依赖方向 vs 错误依赖方向"的对比图。左边是 server -\> module -\> framework,右边是 framework 反向依赖 module,并标红。
十五、这种拆分带来的第一个好处:业务开发更清楚
当一个新人进入 JunoYi 项目时,他可以很快理解:
我要改登录 Token:
看 junoyi-framework-security
我要改字段权限:
看 junoyi-framework-permission
我要改用户管理:
看 junoyi-module-system
我要改微信登录:
看 junoyi-module-oauth 或 junoyi-framework-wechat
我要改启动端口、环境配置:
看 junoyi-server
如果没有模块拆分,新人只能在一个巨大的包结构里搜索。
模块化之后,目录本身就是文档。
好的项目结构应该让人少猜。
十六、第二个好处:框架能力可以复用
比如未来我新增一个模块:
junoyi-module-workflow
它可以直接依赖:
xml
<dependency>
<groupId>com.junoyi</groupId>
<artifactId>junoyi-framework-boot-starter</artifactId>
</dependency>
然后 workflow 模块就能使用:
- 统一响应
- 分页对象
- 基础实体
- 权限注解
- 字段权限
- 数据权限
- Redis
- 文件上传
- Excel 导入导出
- 日志
- 全局异常
- API 文档
不用重复造轮子。
这就是 framework 层的价值。
十六、第三个好处:未来更容易做"可插拔模块"
JunoYi 现在有:
junoyi-module-system
junoyi-module-oauth
未来可能会有:
junoyi-module-generator
junoyi-module-workflow
junoyi-module-job
junoyi-module-tenant
junoyi-module-mall
junoyi-module-crm
junoyi-module-ai
如果所有业务都在一个模块里,想裁剪会很难。
但如果每个业务能力都是独立 Maven 模块,那么 server 可以选择性引入:
xml
<dependencies>
<dependency>
<artifactId>junoyi-framework-boot-starter</artifactId>
</dependency>
<dependency>
<artifactId>junoyi-module-system</artifactId>
</dependency>
<dependency>
<artifactId>junoyi-module-workflow</artifactId>
</dependency>
</dependencies>
不需要的模块就不引入。
这对开源框架尤其重要,因为不同使用者的需求不一样。
有人只需要后台管理。
有人需要 OAuth。
有人需要工作流。
有人需要多租户。
有人需要商城。
有人只想拿 framework 做二次开发。
模块化可以给使用者选择权。
十八、第四个好处:后续演进微服务更自然
我并不建议项目一开始就盲目微服务化。
微服务会带来很多额外复杂度:
- 服务注册发现
- 服务间调用
- 分布式事务
- 链路追踪
- 配置中心
- 网关
- 服务治理
- 部署编排
- 运维成本
但如果 JunoYi 未来需要拆成微服务,现在的模块边界会很有帮助。
比如:
junoyi-module-system -> system-service
junoyi-module-oauth -> oauth-service
junoyi-module-file -> file-service
junoyi-module-workflow -> workflow-service
模块化单体不是微服务,但它是通向微服务的更稳妥前置阶段。
先把代码边界整理清楚,再决定是否拆部署边界。
这是更务实的架构路线。
十九、第五个好处:测试和维护成本更低
模块拆分之后,测试也可以更有针对性。
例如:
framework-permission:
测试权限匹配
测试字段脱敏
测试权限注解切面
framework-security:
测试 Token 生成
测试 Token 刷新
测试会话管理
测试平台限制
module-system:
测试用户、角色、菜单、部门、字典等业务流程
server:
测试应用能否完整启动
这比在一个大模块里写测试更清晰。
Maven 多模块也支持按模块构建。例如:
sh
mvn -pl junoyi-framework/junoyi-framework-permission test
或者构建某个模块及其依赖:
sh
mvn -pl junoyi-module/junoyi-module-system -am package
这正是 Maven Reactor 的价值。Maven 官方文档说明,Reactor 会收集模块、按依赖关系排序并构建,同时支持 --also-make、--resume-from 等命令行能力。参考:Apache Maven 多模块官方文档。
二十、JunoYi 的模块设计可以这样记
我自己对 JunoYi 后端模块的理解是:
junoyi-dependencies:
负责版本,不负责功能。
junoyi-framework:
负责能力,不负责业务。
junoyi-module:
负责业务,不负责启动。
junoyi-server:
负责装配,不负责复杂实现。
也可以换一种说法:
dependencies 是地基材料清单。
framework 是基础设施。
module 是业务房间。
server 是最终交付的整栋楼。
在代码层面,应该遵守:
基础能力向下沉。
业务能力向上收。
启动模块保持薄。
依赖方向保持单向。
二十一、一个具体例子:权限系统为什么这样拆
以 JunoYi 的权限能力为例。
如果放在单体里,可能是这样:
controller
service
mapper
security
permission
annotation
aspect
utils
domain
看起来都能跑,但长期会混乱。
JunoYi 的拆法是:
junoyi-framework-permission
Permission 注解
FieldPermission 注解
PermissionAspect
FieldPermissionSerializer
PermissionMatcher
PermissionHelper
PermissionLoader 接口
junoyi-framework-security
LoginUser
SecurityContext
TokenHelper
JwtTokenHelper
SessionHelper
TokenAuthenticationTokenFilter
junoyi-module-system
用户表
角色表
菜单表
权限表
权限池管理
用户权限分配
角色权限分配
junoyi-server
引入 system 模块
引入 oauth 模块
启动整个应用
这样权限系统就被拆成了三层:
权限机制:framework-permission
登录身份:framework-security
权限数据:module-system
这比全部写在 system 里更合理。
因为权限机制可以复用,权限数据可以变化。
二十二、一个具体例子:文件上传为什么放 framework-file
文件上传也是类似。
JunoYi 的 junoyi-framework-file 中有:
FileStorage
LocalFileStorage
AliyunOssFileStorage
FileStorageFactory
UploadStrategy
ImageUploadStrategy
VideoUploadStrategy
DocumentUploadStrategy
AvatarUploadStrategy
FileInfo
FileStorageProperties
这些是文件基础设施。
而 junoyi-module-system 里可以有:
SysFileController
SysFileUploadController
也就是说:
framework-file:
解决文件怎么存、怎么校验、怎么选择存储策略。
module-system:
解决系统管理里如何提供文件上传接口、文件列表、文件记录。
如果以后有商城模块,也可以复用 framework-file:
商品图片上传
订单附件上传
售后凭证上传
如果文件能力写死在 system 模块,商城模块就会被迫依赖 system,这就不合理。
二十三、一个具体例子:多平台能力为什么放 framework-platform
JunoYi 支持多平台概念,例如:
后台管理 WEB
前台 WEB
小程序
H5
移动端 APP
桌面端
junoyi-framework-platform 中有:
PlatformManager
OAuthProvider
PayProvider
Platform
PayStatus
TradeType
OAuthRequest
OAuthResponse
PayRequest
PayResponse
这说明 JunoYi 把平台登录、支付、第三方授权抽象成了框架能力。
具体到微信生态,则由:
junoyi-framework-wechat
提供:
WeChatMpOauthProvider
WeChatPayProvider
WxMaConfiguration
WxPayConfiguration
WxMpProperties
而 OAuth 业务模块:
junoyi-module-oauth
则提供具体接口:
WeChatMpAuthController
IWeChatMpAuthService
WeChatMpAuthServiceImpl
WechatMpLoginDTO
OauthUserInfoVO
这就是抽象和业务的分离:
platform/wechat:
提供平台能力和 SDK 封装。
oauth module:
提供用户登录流程和业务接口。
二十四、为什么这种结构适合开源框架
开源框架和公司内部项目不一样。
公司内部项目只要满足当前业务就行,开源框架要考虑更多:
- 别人能不能看懂。
- 别人能不能裁剪。
- 别人能不能二次开发。
- 别人能不能只使用一部分能力。
- 项目能不能长期维护。
- 后续版本升级能不能保持兼容。
- 新模块加入时会不会破坏老结构。
JunoYi 的 framework/module/server 结构,就是为了回答这些问题。
尤其是 framework 层,它让 JunoYi 不只是一个后台管理系统,而是一个可以沉淀能力的框架。
module 层让 JunoYi 保持业务扩展能力。
server 层让 JunoYi 保持装配灵活性。
dependencies 层让 JunoYi 保持依赖可控。
二十五、我对模块边界的设计原则
在 JunoYi 中,我判断一个类应该放在哪个模块,主要看下面几个问题。
1. 它是不是具体业务?
如果是具体业务,放 junoyi-module。
例如:
SysUserController
SysRoleService
SysMenuMapper
SysDictDataController
WeChatMpAuthController
2. 它是不是多个业务都会用的基础能力?
如果是基础能力,放 junoyi-framework。
例如:
R
PageQuery
GlobalExceptionHandler
TokenHelper
PermissionAspect
RedisUtils
ExcelUtils
FileStorage
EventBus
3. 它是不是只负责启动和配置?
如果是启动、配置、打包,放 junoyi-server。
例如:
JunoYiServerApplication
application.yml
application-local.yml
public/LOGO.png
keys/public.pem
keys/private.pem
4. 它是不是只负责版本?
如果是版本管理,放 junoyi-dependencies。
例如:
spring.boot.version
mybatis-plus.version
redisson.version
easyexcel.version
jjwt.version
springdoc.version
二十六、这种设计不是没有成本
模块化不是免费的。
它会带来一些额外成本:
- POM 数量增加。
- 新增模块时需要考虑依赖关系。
- 包扫描范围需要设计。
- 模块之间不能随便互相调用。
- 初学者理解成本略高。
但我认为,对 JunoYi 这种开源框架来说,这些成本是值得的。
因为它换来的是长期可维护性。
一个项目越往后,真正昂贵的不是"多写几个 POM",而是"边界不清导致谁都不敢改"。
二十七、JunoYi 后续还可以继续优化的方向
目前 JunoYi 已经有比较清晰的模块结构,但后续还可以继续增强。
1. 更标准的 auto-configuration
部分 framework 模块可以进一步使用 Spring Boot 标准自动配置方式,例如:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
让模块更符合 Spring Boot Starter 规范。
2. 更细的 starter
现在有一个大的:
junoyi-framework-boot-starter
未来可以继续拆出:
junoyi-framework-security-starter
junoyi-framework-redis-starter
junoyi-framework-file-starter
junoyi-framework-wechat-starter
这样使用者可以更精细地选择。
3. 模块依赖检查
可以引入 ArchUnit 或 Spring Modulith 的模块验证能力,约束:
framework 不能依赖 module
module 之间不能随意循环依赖
server 只做装配
4. 示例模块
可以新增:
junoyi-module-demo
专门展示如何基于 JunoYi framework 开发一个独立业务模块。
5. 代码生成器适配模块结构
未来代码生成器可以直接生成到:
junoyi-module-xxx
而不是生成到一个大模块里。
二十八、总结:framework、module、server 是 JunoYi 的长期骨架
JunoYi 之所以拆成:
junoyi-framework
junoyi-module
junoyi-server
不是为了让项目目录显得复杂,而是为了让复杂度有地方可去。
我的设计理解是:
framework 负责沉淀能力。
module 负责承载业务。
server 负责组装运行。
dependencies 负责统一版本。
这种结构带来的核心收益是:
- 基础能力和业务能力分离。
- 模块职责更清楚。
- 依赖方向更可控。
- 业务模块更容易扩展。
- 框架能力更容易复用。
- 后续做 starter、脚手架、多租户、微服务都更自然。
- 开源使用者更容易理解和二次开发。
如果说普通单体项目追求的是"先跑起来",那么 JunoYi 现在追求的是:
跑起来之后,还能长期演进、持续复用、稳定扩展。
这就是我把 Spring Boot 项目拆成 framework、module、server 的原因。
参考资料
- Apache Maven 官方文档:多模块项目由 Maven Reactor 负责收集模块、按依赖关系排序并构建。
https://maven.apache.org/guides/mini/guide-multiple-modules.html - Spring Boot 官方文档:创建自定义 Auto-configuration 与 Starter。官方说明 starter 可以作为一个依赖入口,提供使用某项能力所需的典型依赖集合。
https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html - Spring Modulith 官方项目页:强调在 Spring Boot 应用中构建结构良好的应用模块,并支持模块验证、模块级测试、观察和文档生成。
https://spring.io/projects/spring-modulith/ - RuoYi-Plus-UniApp 后端项目结构文档:其后端也区分启动入口、通用模块、业务模块、扩展模块。
https://www.ruoyi.plus/backend/project-structure - RuoYi-Vue-Plus GitHub 项目说明:强调后端结构解耦、扩展包形式、易于扩展。
https://github.com/bin1031/RuoYi-Vue-Plus