从单体到模块化:我的 Spring Boot 项目为什么拆成 framework、module、server?

一、前言

在开发 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 项目拆成 frameworkmoduleserver,这种 Maven 模块设计到底解决了什么问题,以及它和主流开源框架的结构有什么相似之处。

二、先说结论:这不是为了"看起来高级",而是为了控制复杂度

很多项目一开始都是这样的:

复制代码
src/main/java
└── com.xxx
    ├── controller
    ├── service
    ├── mapper
    ├── domain
    ├── config
    ├── utils
    ├── security
    ├── redis
    ├── excel
    ├── file
    └── ...

早期这样写没有问题,甚至开发速度很快。

但当项目逐渐加入下面这些能力时,单体目录就开始变得吃力:

  • 登录认证
  • 权限控制
  • 字段级权限
  • 数据权限
  • Redis 缓存
  • 文件存储
  • Excel 导入导出
  • API 文档
  • XSS 防护
  • SQL 注入防护
  • 多平台登录
  • 微信登录
  • 企业微信能力
  • 系统管理
  • 用户、角色、菜单、部门、字典、参数配置
  • 操作日志
  • 登录日志
  • 在线会话
  • 前后端分离接口

如果这些内容全部堆在一个应用模块里,项目会慢慢出现几个问题:

  1. 基础能力和业务代码混在一起。
  2. 改一个工具类,可能影响一堆业务。
  3. 业务模块无法单独拆卸或复用。
  4. 新人不知道哪些代码是框架能力,哪些代码是具体业务。
  5. 后续想做脚手架、代码生成、多租户、微服务版本,会越来越困难。

所以 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>

这种设计的好处是:

  1. 版本集中管理,升级有入口。
  2. 内部模块版本统一,避免各模块版本不一致。
  3. 第三方依赖冲突更容易排查。
  4. 开源使用者阅读 POM 时更清楚。
  5. 后续发布 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

这样做有几个好处:

  1. framework 可以保持通用性。
  2. system 可以独立演进。
  3. 以后如果有人不想用 JunoYi 默认的系统管理模块,可以替换或裁剪。
  4. 后续新增 mall、workflow、crm、tenant 等模块时,可以和 system 平级。
  5. 权限底座可以复用,但权限业务可以变化。

一个简单判断标准是:

如果它离开 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 做的是装配工作,而不是业务实现。

它主要负责:

  1. 引入需要启用的 framework 能力。
  2. 引入需要启用的业务模块。
  3. 提供启动类。
  4. 放置应用配置文件。
  5. 完成最终打包。

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 统一装配

模块化单体的好处是:

  1. 保留单体部署简单的优势。
  2. 避免微服务过早引入分布式复杂度。
  3. 代码边界比普通单体清晰。
  4. 后续如果真的需要拆服务,已有模块边界可以作为拆分依据。
  5. 更适合开源框架和企业脚手架。

我不认为所有项目都应该一开始上微服务。很多中后台系统,最合适的起点就是模块化单体。


十二、对标其他开源框架: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,把所有基础能力都塞进去?

原因是:框架层内部也需要继续分治。

如果全部塞进一个模块,会出现几个问题:

  1. Redis、Excel、文件、微信、企业微信等依赖全部被迫引入。
  2. 不使用某个能力,也要承担它的依赖体积和配置复杂度。
  3. 某个基础能力变更时,影响范围不清楚。
  4. 模块职责不清晰。
  5. 后续做 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:

提供用户登录流程和业务接口。


二十四、为什么这种结构适合开源框架

开源框架和公司内部项目不一样。

公司内部项目只要满足当前业务就行,开源框架要考虑更多:

  1. 别人能不能看懂。
  2. 别人能不能裁剪。
  3. 别人能不能二次开发。
  4. 别人能不能只使用一部分能力。
  5. 项目能不能长期维护。
  6. 后续版本升级能不能保持兼容。
  7. 新模块加入时会不会破坏老结构。

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

二十六、这种设计不是没有成本

模块化不是免费的。

它会带来一些额外成本:

  1. POM 数量增加。
  2. 新增模块时需要考虑依赖关系。
  3. 包扫描范围需要设计。
  4. 模块之间不能随便互相调用。
  5. 初学者理解成本略高。

但我认为,对 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 负责统一版本。

这种结构带来的核心收益是:

  1. 基础能力和业务能力分离。
  2. 模块职责更清楚。
  3. 依赖方向更可控。
  4. 业务模块更容易扩展。
  5. 框架能力更容易复用。
  6. 后续做 starter、脚手架、多租户、微服务都更自然。
  7. 开源使用者更容易理解和二次开发。

如果说普通单体项目追求的是"先跑起来",那么 JunoYi 现在追求的是:

跑起来之后,还能长期演进、持续复用、稳定扩展。

这就是我把 Spring Boot 项目拆成 framework、module、server 的原因。


参考资料

  1. Apache Maven 官方文档:多模块项目由 Maven Reactor 负责收集模块、按依赖关系排序并构建。
    https://maven.apache.org/guides/mini/guide-multiple-modules.html
  2. Spring Boot 官方文档:创建自定义 Auto-configuration 与 Starter。官方说明 starter 可以作为一个依赖入口,提供使用某项能力所需的典型依赖集合。
    https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html
  3. Spring Modulith 官方项目页:强调在 Spring Boot 应用中构建结构良好的应用模块,并支持模块验证、模块级测试、观察和文档生成。
    https://spring.io/projects/spring-modulith/
  4. RuoYi-Plus-UniApp 后端项目结构文档:其后端也区分启动入口、通用模块、业务模块、扩展模块。
    https://www.ruoyi.plus/backend/project-structure
  5. RuoYi-Vue-Plus GitHub 项目说明:强调后端结构解耦、扩展包形式、易于扩展。
    https://github.com/bin1031/RuoYi-Vue-Plus
相关推荐
回家路上绕了弯2 小时前
Codex 与 ZCode 有什么区别?从开发工作流看 AI 编程工具怎么选
后端
Java内核笔记2 小时前
Spring Boot 4 可观测性源码剖析:OpenTelemetry 全链路打通日志、指标、追踪
java·后端
Wang's Blog2 小时前
Java框架快速入门: Spring Security+OAuth2之跨域处理
java·开发语言·spring
学渣超2 小时前
从一次早高峰数据库告警说起:你真的理解缓存该如何落地应用吗?
redis·后端·架构
我是大猴子2 小时前
MyBatis‑Plus & MyBatis‑Flex 区别
java·服务器·数据库
中趴菜2 小时前
接口返回的JSON为什么有反斜杠
后端
Bs_MoneyMagnet2 小时前
基于springboot+vue的在线音乐管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
小番茄程序猿2 小时前
Agent 工程化实测:p95 从 836ms 降到 12ms,而真正的收获是发现瓶颈根本不在 Agent 这层
后端