01 ruoyi-vue-pro框架架构剖析

ruoyi-vue-pro框架架构剖析

本文深入剖析ruoyi-vue-pro框架的Maven多模块工程设计、模块职责划分、依赖管理以及自动装配机制,结合企业级实践讲解核心原理。

1.1 Maven多模块工程结构设计

为什么需要多模块工程?

在传统的单体应用中,所有代码都在一个工程里,随着业务复杂度增加会出现:

  • 编译慢:修改一个类需要重新编译整个项目
  • 职责不清:业务代码、框架代码、工具类混在一起
  • 无法复用:其他项目想用某个模块,只能复制代码
  • 团队协作难:多人修改同一个工程,冲突频繁

多模块工程解决方案: 将一个大工程拆分成多个Maven模块,每个模块独立编译、独立打包,通过Maven依赖关系组织在一起。

典型的企业级多模块结构

以电商业务系统为例,展示ruoyi-vue-pro的典型模块划分:

复制代码
demo-platform/                        # 根目录(聚合工程)
├── pom.xml                          # 父POM(版本管理中心)
├── demo-framework/                  # 框架层(技术组件封装)
│   ├── demo-spring-boot-starter-web/
│   ├── demo-spring-boot-starter-mybatis/
│   └── demo-spring-boot-starter-redis/
├── demo-module-system/              # 系统模块(用户、角色、菜单)
│   ├── demo-module-system-api/
│   └── demo-module-system-biz/
├── demo-module-order/               # 订单模块
│   ├── demo-module-order-api/       # 订单API(接口定义)
│   └── demo-module-order-biz/       # 订单业务实现
├── demo-module-payment/             # 支付模块
│   ├── demo-module-payment-api/     # 支付API
│   └── demo-module-payment-biz/     # 支付业务实现
└── demo-server/                     # 应用启动模块
    └── src/main/resources/
        └── application.yml

三层架构分层原则

ruoyi-vue-pro采用三层分层架构,每层职责明确:

1. framework层(框架层)

  • 职责:封装技术组件,提供开箱即用的能力
  • 包含:MyBatis配置、Redis配置、Web配置、统一异常处理等
  • 特点:与业务无关,可跨项目复用
  • 原理:通过Spring Boot的AutoConfiguration机制实现自动装配

2. module层(业务层)

  • 职责:实现具体业务逻辑,按业务域拆分
  • 包含:订单模块、支付模块、商品模块等
  • 特点:每个模块内部又分api和biz子模块
  • 原理:通过Maven依赖管理模块间调用关系

3. server层(启动层)

  • 职责:整合所有模块,提供应用启动入口
  • 包含:启动类、配置文件、打包配置
  • 特点:只有这一层最终打成可执行jar包
  • 原理:依赖所有biz模块,Spring Boot自动扫描并装配

父POM配置详解

父POM是整个工程的版本控制中心,所有子模块都继承它:

xml 复制代码
<!-- demo-platform/pom.xml -->
<groupId>com.company</groupId>
<artifactId>demo-platform</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>  <!-- 注意:父POM必须是pom类型 -->

<!-- 聚合所有子模块 -->
<modules>
    <module>demo-framework</module>
    <module>demo-module-system</module>
    <module>demo-module-order</module>
    <module>demo-module-payment</module>
    <module>demo-server</module>
</modules>

关键点说明:

配置项 作用 必要性
<packaging>pom</packaging> 声明这是聚合工程,不产生jar包 必须
<modules> 聚合子模块,执行mvn install时会依次构建 必须
<parent> 子模块通过parent继承父POM的配置 子模块必须

为什么要这样设计?

  • 统一构建 :执行mvn clean install一次性编译所有模块
  • 版本统一:所有子模块的版本号统一由父POM管理
  • 依赖继承:子模块自动继承父POM的依赖管理配置

模块分层的核心价值

维度 单体应用 多模块工程
编译速度 修改一处需要全量编译(耗时长) 只编译修改的模块(快速)
代码复用 复制粘贴代码 Maven依赖引入
团队协作 代码冲突频繁 模块独立开发,减少冲突
职责划分 代码混乱,难以维护 职责清晰,易于维护
持续集成 全量打包部署 只部署变更模块

1.2 模块职责划分(api/biz分离)

为什么要api/biz分离?

在微服务架构或模块化开发中,经常遇到跨模块调用的场景:

  • 订单模块需要调用支付模块查询支付状态
  • 商品模块需要调用库存模块扣减库存
  • 用户模块需要调用权限模块验证权限

不分离的问题:

xml 复制代码
<!-- 订单模块依赖整个支付模块 -->
<dependency>
    <groupId>com.company</groupId>
    <artifactId>demo-module-payment</artifactId>  <!-- 依赖整个biz模块 -->
</dependency>

这会导致:

  1. 依赖臃肿:订单模块只需要支付查询接口,却引入了支付模块的所有实现类、Mapper、配置等
  2. 循环依赖风险:支付模块如果也依赖订单模块,就会形成循环依赖,Maven无法构建
  3. 启动冲突:两个模块都扫描到对方的@Service、@Mapper,导致Bean重复定义

api/biz分离方案:

xml 复制代码
<!-- 订单模块只依赖支付的api -->
<dependency>
    <groupId>com.company</groupId>
    <artifactId>demo-module-payment-api</artifactId>  <!-- 只依赖接口定义 -->
</dependency>

api模块:接口契约层

api模块是模块对外暴露的接口规范,只包含接口定义,不包含任何实现。

标准目录结构:

java 复制代码
demo-module-order-api/
├── OrderApi.java                    // 核心API接口
├── dto/                             // 数据传输对象
│   ├── OrderCreateReqDTO.java       // 创建订单请求DTO
│   ├── OrderRespDTO.java            // 订单响应DTO
│   └── OrderQueryReqDTO.java        // 查询订单请求DTO
├── enums/                           // 枚举定义
│   ├── OrderStatusEnum.java         // 订单状态枚举
│   └── OrderTypeEnum.java           // 订单类型枚举
└── constants/                       // 常量定义
    └── OrderConstants.java          // 订单相关常量

核心API接口示例:

java 复制代码
package com.company.module.order.api;

import com.company.module.order.api.dto.*;
import java.util.Collection;
import java.util.List;

/**
 * 订单API接口
 * 
 * <p>设计原则:
 * 1. 只定义接口方法,不包含任何实现逻辑
 * 2. 参数和返回值都使用DTO对象,不暴露DO(数据库实体)
 * 3. 方法命名清晰,见名知义
 * 4. 添加必要的JavaDoc注释
 */
public interface OrderApi {
    
    /**
     * 根据订单号查询订单
     * 
     * @param orderNo 订单号
     * @return 订单信息,不存在返回null
     */
    OrderRespDTO getByOrderNo(String orderNo);
    
    /**
     * 创建订单
     * 
     * @param reqDTO 创建订单请求参数
     * @return 订单ID
     */
    Long createOrder(OrderCreateReqDTO reqDTO);
    
    /**
     * 批量查询订单
     * 
     * @param ids 订单ID集合
     * @return 订单列表
     */
    List<OrderRespDTO> listByIds(Collection<Long> ids);
    
    /**
     * 验证订单是否存在
     * 
     * @param orderId 订单ID
     * @return true-存在 false-不存在
     */
    Boolean validateOrderExists(Long orderId);
}

DTO对象设计:

java 复制代码
package com.company.module.order.api.dto;

import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 订单响应DTO
 * 
 * <p>关键点:
 * 1. 实现Serializable接口,支持序列化(RPC调用需要)
 * 2. 只包含必要字段,不暴露敏感信息
 * 3. 使用包装类型(Integer而非int),避免null值问题
 */
@Data
public class OrderRespDTO implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /** 订单ID */
    private Long id;
    
    /** 订单号 */
    private String orderNo;
    
    /** 用户ID */
    private Long userId;
    
    /** 订单金额 */
    private BigDecimal amount;
    
    /** 订单状态 1-待支付 2-已支付 3-已取消 */
    private Integer status;
    
    /** 创建时间 */
    private LocalDateTime createTime;
}

枚举类设计:

java 复制代码
package com.company.module.order.api.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 订单状态枚举
 * 
 * <p>为什么要定义枚举?
 * 1. 类型安全:避免使用魔法值(1、2、3)
 * 2. 自文档化:枚举名称清晰表达含义
 * 3. IDE提示:编码时有智能提示
 * 4. 统一管理:状态值统一维护,避免重复定义
 */
@Getter
@AllArgsConstructor
public enum OrderStatusEnum {
    
    WAIT_PAY(1, "待支付"),
    PAID(2, "已支付"),
    CANCELLED(3, "已取消"),
    COMPLETED(4, "已完成"),
    REFUNDED(5, "已退款");
    
    /** 状态值 */
    private final Integer status;
    
    /** 状态描述 */
    private final String desc;
    
    /**
     * 根据状态值获取枚举
     */
    public static OrderStatusEnum valueOf(Integer status) {
        for (OrderStatusEnum e : values()) {
            if (e.getStatus().equals(status)) {
                return e;
            }
        }
        return null;
    }
}

biz模块:业务实现层

biz模块是具体业务逻辑的实现,包含完整的MVC三层架构。

标准目录结构:

java 复制代码
demo-module-order-biz/
├── controller/                      // 控制层(HTTP接口)
│   ├── admin/                       // 后台管理接口
│   │   └── OrderController.java
│   └── app/                         // 前台用户接口
│       └── AppOrderController.java
├── service/                         // 服务层(业务逻辑)
│   ├── OrderService.java            // 服务接口
│   └── impl/
│       └── OrderServiceImpl.java    // 服务实现
├── dal/                             // 数据访问层(Data Access Layer)
│   ├── dataobject/                  // 数据库实体对象(DO)
│   │   └── OrderDO.java
│   └── mapper/                      // MyBatis Mapper接口
│       └── OrderMapper.java
├── convert/                         // 对象转换器
│   └── OrderConvert.java            // DTO/DO/VO之间转换
└── api/                             // API接口实现
    └── OrderApiImpl.java            // 实现api模块定义的接口

Controller层示例:

java 复制代码
package com.company.module.order.biz.controller.admin;

import com.company.framework.common.pojo.CommonResult;
import com.company.framework.common.pojo.PageResult;
import com.company.module.order.biz.controller.admin.vo.*;
import com.company.module.order.biz.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * 订单管理Controller
 * 
 * <p>职责:
 * 1. 接收HTTP请求,参数校验
 * 2. 调用Service处理业务
 * 3. 返回统一格式响应
 */
@Tag(name = "管理后台 - 订单管理")
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
@Validated
public class OrderController {
    
    private final OrderService orderService;
    
    @GetMapping("/get")
    @Operation(summary = "查询订单详情")
    public CommonResult<OrderRespVO> getOrder(@RequestParam("id") Long id) {
        OrderDO order = orderService.getById(id);
        return CommonResult.success(OrderConvert.INSTANCE.convert(order));
    }
    
    @PostMapping("/create")
    @Operation(summary = "创建订单")
    public CommonResult<Long> createOrder(@Valid @RequestBody OrderCreateReqVO reqVO) {
        return CommonResult.success(orderService.createOrder(reqVO));
    }
    
    @GetMapping("/page")
    @Operation(summary = "分页查询订单列表")
    public CommonResult<PageResult<OrderRespVO>> getOrderPage(@Valid OrderPageReqVO pageVO) {
        PageResult<OrderDO> pageResult = orderService.getOrderPage(pageVO);
        return CommonResult.success(OrderConvert.INSTANCE.convertPage(pageResult));
    }
}

Service层实现:

java 复制代码
package com.company.module.order.biz.service.impl;

import com.company.module.order.biz.dal.dataobject.OrderDO;
import com.company.module.order.biz.dal.mapper.OrderMapper;
import com.company.module.order.biz.service.OrderService;
import com.company.module.payment.api.PaymentApi;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 订单Service实现类
 * 
 * <p>职责:
 * 1. 实现业务逻辑
 * 2. 调用Mapper操作数据库
 * 3. 调用其他模块的API
 * 4. 事务管理
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    
    private final OrderMapper orderMapper;
    
    // 注入支付模块的API(通过api依赖实现跨模块调用)
    private final PaymentApi paymentApi;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createOrder(OrderCreateReqVO reqVO) {
        // 1. 构建订单DO对象
        OrderDO order = OrderConvert.INSTANCE.convert(reqVO);
        order.setOrderNo(generateOrderNo());
        order.setStatus(OrderStatusEnum.WAIT_PAY.getStatus());
        
        // 2. 插入数据库
        orderMapper.insert(order);
        log.info("[创建订单] 订单号={}", order.getOrderNo());
        
        // 3. 调用支付模块创建支付单(跨模块调用)
        paymentApi.createPayment(order.getId(), order.getAmount());
        
        return order.getId();
    }
    
    @Override
    public OrderDO getByOrderNo(String orderNo) {
        return orderMapper.selectByOrderNo(orderNo);
    }
    
    /**
     * 生成订单号
     * 规则:yyyyMMddHHmmss + 6位随机数
     */
    private String generateOrderNo() {
        String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        String random = String.format("%06d", ThreadLocalRandom.current().nextInt(1000000));
        return timestamp + random;
    }
}

API接口实现类:

java 复制代码
package com.company.module.order.biz.api;

import com.company.module.order.api.OrderApi;
import com.company.module.order.api.dto.*;
import com.company.module.order.biz.convert.OrderConvert;
import com.company.module.order.biz.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.List;

/**
 * 订单API实现类
 * 
 * <p>关键点:
 * 1. 实现api模块定义的接口
 * 2. 供其他模块通过Spring依赖注入调用
 * 3. 如果是微服务架构,通过Feign远程调用
 * 4. 返回值必须是DTO对象,不能返回DO
 */
@Service
@RequiredArgsConstructor
public class OrderApiImpl implements OrderApi {
    
    private final OrderService orderService;
    
    @Override
    public OrderRespDTO getByOrderNo(String orderNo) {
        OrderDO order = orderService.getByOrderNo(orderNo);
        // 将DO转换为DTO(不暴露数据库字段)
        return OrderConvert.INSTANCE.convertToApi(order);
    }
    
    @Override
    public Long createOrder(OrderCreateReqDTO reqDTO) {
        return orderService.createOrderByApi(reqDTO);
    }
    
    @Override
    public List<OrderRespDTO> listByIds(Collection<Long> ids) {
        List<OrderDO> orders = orderService.listByIds(ids);
        return OrderConvert.INSTANCE.convertListToApi(orders);
    }
    
    @Override
    public Boolean validateOrderExists(Long orderId) {
        return orderService.getById(orderId) != null;
    }
}

api/biz分离的核心价值

1. 依赖解耦

复制代码
支付模块 ───依赖──> 订单api模块
                    ↓ 实现
                 订单biz模块
  • 支付模块只依赖订单的api,不依赖biz
  • 订单的biz内部实现如何变化,支付模块都不受影响
  • 减小依赖传递范围,避免引入不必要的jar包

2. 接口稳定性

  • api接口一旦发布,尽量保持向后兼容
  • biz内部可以重构、优化,只要api不变即可
  • 类似于Java的interface和implements的关系

3. 支持RPC调用

java 复制代码
// 单体应用:直接注入
@Autowired
private OrderApi orderApi;  // Spring注入本地实现类

// 微服务:通过Feign远程调用
@FeignClient(name = "order-service")
public interface OrderApi {
    // 接口定义完全一致
}

4. 版本管理清晰

  • api版本升级影响面可控
  • 可以同时维护多个api版本
  • 平滑过渡,逐步迁移

对比总结:

维度 不分离(单一模块) api/biz分离
依赖复杂度 依赖整个模块,臃肿 只依赖轻量api,简洁
循环依赖 容易出现 几乎不会出现
启动冲突 Bean重复定义 不会冲突
接口稳定性 实现变动影响调用方 api稳定,biz可任意调整
远程调用 不支持 天然支持Feign/Dubbo

1.3 父子POM继承与版本管理

Maven依赖管理的痛点

在企业级项目中,通常会有几十个甚至上百个依赖,如果不统一管理会出现:

1. 版本冲突问题

xml 复制代码
<!-- 订单模块 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.70</version>  <!-- 使用1.2.70版本 -->
</dependency>

<!-- 支付模块 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>  <!-- 使用1.2.83版本 -->
</dependency>

后果:最终打包时Maven会选择其中一个版本,可能导致某个模块运行异常。

2. 升级困难

  • 要升级Spring Boot版本,需要修改所有子模块的pom.xml
  • 容易遗漏某个模块,导致版本不一致

3. 重复定义

  • 每个模块都要写相同的依赖配置
  • 代码冗余,维护成本高

Maven依赖管理机制

Maven提供了两个核心机制解决版本管理问题:

1. 继承(Inheritance)

  • 子POM通过<parent>继承父POM的配置
  • 子POM自动获得父POM的所有配置(dependencyManagement、properties等)

2. 依赖管理(Dependency Management)

  • 父POM通过<dependencyManagement>声明依赖版本
  • 子POM引用依赖时无需指定版本号,自动使用父POM声明的版本

父POM完整配置详解

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    
    <!-- ==================== 基本信息 ==================== -->
    <groupId>com.company</groupId>
    <artifactId>demo-platform</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>  <!-- 父POM必须是pom类型 -->
    <name>Demo Platform</name>
    
    <!-- ==================== 聚合子模块 ==================== -->
    <modules>
        <module>demo-framework</module>
        <module>demo-module-system</module>
        <module>demo-module-order</module>
        <module>demo-module-payment</module>
        <module>demo-server</module>
    </modules>
    
    <!-- ==================== 统一版本管理 ==================== -->
    <properties>
        <!-- 核心版本 -->
        <revision>1.0.0</revision>  <!-- 使用变量统一内部模块版本 -->
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        
        <!-- Spring Boot版本 -->
        <spring-boot.version>2.7.18</spring-boot.version>
        
        <!-- 数据库相关 -->
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
        <druid.version>1.2.20</druid.version>
        <mysql.version>8.0.33</mysql.version>
        
        <!-- 缓存相关 -->
        <redisson.version>3.25.2</redisson.version>
        
        <!-- 工具类 -->
        <hutool.version>5.8.23</hutool.version>
        <guava.version>32.1.3-jre</guava.version>
        <lombok.version>1.18.30</lombok.version>
        
        <!-- 文件处理 -->
        <easyexcel.version>3.3.3</easyexcel.version>
        <poi.version>5.2.5</poi.version>
        
        <!-- 其他 -->
        <swagger.version>2.2.19</swagger.version>
        <transmittable.version>2.14.5</transmittable.version>
    </properties>
    
    <!-- ==================== 依赖版本管理(核心) ==================== -->
    <dependencyManagement>
        <dependencies>
            <!-- ========== Spring Boot BOM(物料清单) ========== -->
            <!-- 
                type=pom: 这是一个聚合POM,包含所有Spring Boot相关依赖的版本
                scope=import: 导入BOM中定义的依赖版本
                作用:一次性引入Spring Boot全家桶的版本管理
             -->
            <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.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            
            <!-- ========== Redis相关 ========== -->
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson-spring-boot-starter</artifactId>
                <version>${redisson.version}</version>
            </dependency>
            
            <!-- ========== 工具类 ========== -->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>${hutool.version}</version>
            </dependency>
            
            <!-- ========== 内部模块版本管理 ========== -->
            <!-- framework模块 -->
            <dependency>
                <groupId>com.company</groupId>
                <artifactId>demo-spring-boot-starter-mybatis</artifactId>
                <version>${revision}</version>
            </dependency>
            <dependency>
                <groupId>com.company</groupId>
                <artifactId>demo-spring-boot-starter-redis</artifactId>
                <version>${revision}</version>
            </dependency>
            
            <!-- 业务模块api -->
            <dependency>
                <groupId>com.company</groupId>
                <artifactId>demo-module-order-api</artifactId>
                <version>${revision}</version>
            </dependency>
            <dependency>
                <groupId>com.company</groupId>
                <artifactId>demo-module-payment-api</artifactId>
                <version>${revision}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <!-- ==================== 构建配置 ==================== -->
    <build>
        <plugins>
            <!-- Maven编译插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
            </plugin>
            
            <!-- Flatten插件:处理${revision}变量 -->
            <!-- 
                问题:Maven install时,${revision}变量不会被替换
                解决:使用flatten插件,生成.flattened-pom.xml,将变量替换为实际值
             -->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>flatten-maven-plugin</artifactId>
                <version>1.5.0</version>
                <configuration>
                    <updatePomFile>true</updatePomFile>
                    <flattenMode>resolveCiFriendliesOnly</flattenMode>
                </configuration>
                <executions>
                    <execution>
                        <id>flatten</id>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>flatten</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>flatten.clean</id>
                        <phase>clean</phase>
                        <goals>
                            <goal>clean</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

关键配置说明

1. <packaging>pom</packaging>

xml 复制代码
<packaging>pom</packaging>
  • 作用:声明这是一个聚合工程,不会产生jar包
  • 必要性:父POM必须是pom类型,否则Maven会报错
  • 对比 :子模块使用<packaging>jar</packaging>

2. <modules>:聚合模块

xml 复制代码
<modules>
    <module>demo-framework</module>
    <module>demo-module-order</module>
</modules>
  • 作用:聚合所有子模块,一键构建
  • 效果 :执行mvn clean install时,按依赖顺序依次构建各模块
  • 顺序:Maven自动分析依赖关系,确定构建顺序

3. <properties>:版本集中管理

xml 复制代码
<properties>
    <revision>1.0.0</revision>
    <spring-boot.version>2.7.18</spring-boot.version>
</properties>
  • 好处:只需修改一处,所有模块生效
  • 命名规范 :使用xxx.version格式
  • ${revision}变量:用于统一内部模块版本,需配合flatten插件使用

4. <dependencyManagement>:依赖版本锁定

xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

核心原理:

  • dependencyManagement只声明版本,不会实际引入依赖
  • 子模块引用时不需要指定版本号,自动使用父POM声明的版本
  • 如果子模块显式指定版本,会覆盖父POM的版本

对比:

配置项 <dependencies> <dependencyManagement>
作用域 当前POM和所有子POM 仅声明版本,不实际引入
是否引入依赖 是,会实际引入jar包 否,只声明版本
子模块是否继承 是,子模块会自动获得依赖 否,子模块需主动引用
使用场景 所有子模块都需要的公共依赖 版本管理,避免版本冲突

子模块POM配置

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    
    <!-- ==================== 继承父POM ==================== -->
    <parent>
        <groupId>com.company</groupId>
        <artifactId>demo-platform</artifactId>
        <version>1.0.0</version>
        <relativePath>../../pom.xml</relativePath>  <!-- 指定父POM路径 -->
    </parent>
    
    <!-- ==================== 当前模块信息 ==================== -->
    <!-- groupId和version继承自父POM,无需重复定义 -->
    <artifactId>demo-module-order-biz</artifactId>
    <packaging>jar</packaging>
    <name>Demo Order Business Module</name>
    
    <!-- ==================== 依赖声明 ==================== -->
    <dependencies>
        <!-- ========== 依赖自己的api模块 ========== -->
        <dependency>
            <groupId>com.company</groupId>
            <artifactId>demo-module-order-api</artifactId>
            <!-- 版本号继承自父POM的dependencyManagement,无需指定 -->
        </dependency>
        
        <!-- ========== 依赖其他模块api ========== -->
        <dependency>
            <groupId>com.company</groupId>
            <artifactId>demo-module-payment-api</artifactId>
            <!-- 版本号继承自父POM -->
        </dependency>
        
        <!-- ========== 依赖framework ========== -->
        <dependency>
            <groupId>com.company</groupId>
            <artifactId>demo-spring-boot-starter-mybatis</artifactId>
            <!-- 版本号继承自父POM -->
        </dependency>
        
        <!-- ========== Spring Boot依赖 ========== -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- 版本号来自父POM导入的spring-boot-dependencies -->
        </dependency>
        
        <!-- ========== 数据库依赖 ========== -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <!-- 版本号继承自父POM -->
        </dependency>
        
        <!-- ========== 工具类 ========== -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <!-- 版本号继承自父POM -->
        </dependency>
        
        <!-- ========== Lombok(编译期依赖) ========== -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>  <!-- 只在编译期有效,运行时不需要 -->
        </dependency>
    </dependencies>
</project>

版本管理最佳实践

1. 使用${revision}统一内部模块版本

xml 复制代码
<!-- 父POM -->
<properties>
    <revision>1.0.0</revision>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.company</groupId>
            <artifactId>demo-module-order-api</artifactId>
            <version>${revision}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

好处:

  • 升级版本时只需修改一处
  • 避免内部模块版本不一致
  • 配合flatten插件,发布时自动替换变量

2. 使用BOM(Bill of Materials)管理第三方依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>${spring-boot.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

原理:

  • Spring Boot提供了一个BOM,包含所有组件的版本号
  • 使用scope=import导入BOM中的版本管理
  • 子模块引用Spring Boot组件时无需指定版本

3. 使用<optional>true</optional>避免依赖传递

xml 复制代码
<dependency>
    <groupId>com.company</groupId>
    <artifactId>demo-module-third-api</artifactId>
    <optional>true</optional>  <!-- 不传递给依赖方 -->
</dependency>

场景:

  • A模块依赖B模块(optional=true)
  • C模块依赖A模块
  • 结果:C模块不会自动获得B模块的依赖

4. 使用<exclusions>排除冲突依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

场景:

  • EasyExcel内部依赖slf4j 1.7.x
  • 项目已经统一使用slf4j 2.0.x
  • 解决:排除EasyExcel传递的slf4j,使用项目统一版本

依赖冲突解决原则

Maven使用最短路径优先最先声明优先原则解决版本冲突:

1. 最短路径优先

复制代码
项目 -> A 1.0 -> C 2.0  (路径长度=2)
项目 -> B 1.0 -> D 1.0 -> C 1.0  (路径长度=3)
结果:使用C 2.0(路径更短)

2. 最先声明优先

复制代码
项目 -> A 1.0 -> C 2.0  (路径长度=2)
项目 -> B 1.0 -> C 1.0  (路径长度=2)
结果:使用C 2.0(A先声明)

解决方案:

  • 查看依赖树mvn dependency:tree
  • 显式声明版本:在dependencyManagement中锁定版本
  • 排除冲突依赖 :使用<exclusions>排除不需要的版本

1.4 模块间依赖关系梳理

依赖层级与调用链路

在多模块架构中,理清模块间的依赖关系至关重要,否则容易陷入循环依赖的泥潭。

完整的依赖层级图:

复制代码
┌─────────────────────────────────────────────────────────┐
│                   demo-server (启动层)                   │
│  职责:应用启动、整合所有模块、打包部署                    │
│  依赖:所有biz模块                                        │
└─────────────────────────────────────────────────────────┘
                            ↓ 依赖
┌──────────────────┬──────────────────┬──────────────────┐
│  order-biz       │  payment-biz     │  system-biz      │  (业务实现层)
│  订单业务实现     │  支付业务实现     │  系统业务实现     │
└──────────────────┴──────────────────┴──────────────────┘
        ↓ 依赖           ↓ 依赖            ↓ 依赖
┌──────────────────┬──────────────────┬──────────────────┐
│  order-api       │  payment-api     │  system-api      │  (API接口层)
│  订单接口定义     │  支付接口定义     │  系统接口定义     │
└──────────────────┴──────────────────┴──────────────────┘
                            ↓ 依赖
┌─────────────────────────────────────────────────────────┐
│              demo-framework (框架层)                     │
│  mybatis-starter, redis-starter, web-starter            │
│  职责:技术组件封装、自动装配                              │
└─────────────────────────────────────────────────────────┘
                            ↓ 依赖
┌─────────────────────────────────────────────────────────┐
│          Spring Boot + 第三方依赖                         │
│  MyBatis-Plus、Redisson、Hutool等                        │
└─────────────────────────────────────────────────────────┘

模块依赖的三大原则

原则一:严禁循环依赖

循环依赖会导致Maven构建失败,出现类似错误:

复制代码
[ERROR] The projects in the reactor contain a cyclic reference

常见循环依赖场景:

复制代码
❌ 错误示例:
order-biz 依赖 payment-api  →  payment-biz 依赖 order-api  →  形成环

解决方案:

  • 单向依赖:A依赖B,B不能依赖A
  • 抽取公共api:如果两个模块互相需要,抽取公共接口到独立的api模块
  • 事件驱动:通过消息队列解耦,避免直接依赖
xml 复制代码
<!-- ✅ 正确做法:订单模块只依赖支付api -->
<dependency>
    <groupId>com.company</groupId>
    <artifactId>demo-module-payment-api</artifactId>
</dependency>

<!-- ❌ 绝不能再依赖支付的biz -->
<!-- <dependency>
    <groupId>com.company</groupId>
    <artifactId>demo-module-payment-biz</artifactId>
</dependency> -->

原则二:最小化依赖

模块应该只依赖必要的内容,避免引入过多无关的jar包。

api模块依赖配置示例:

xml 复制代码
<!-- demo-module-order-api/pom.xml -->
<dependencies>
    <!-- ✅ 只需要基础工具类 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
    <!-- ✅ 需要序列化 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
    </dependency>
    
    <!-- ❌ 不应该依赖实现相关的组件 -->
    <!-- <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency> -->
    
    <!-- ❌ 不应该依赖Spring Web -->
    <!-- <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency> -->
</dependencies>

为什么api模块要保持轻量?

  1. 减少依赖传递:其他模块依赖api时,不会带入不必要的jar包
  2. 降低冲突概率:依赖越少,版本冲突概率越低
  3. 提升构建速度:依赖少,Maven构建更快
  4. 明确职责边界:api就是接口定义,不应该有业务实现

原则三:控制依赖传递

Maven的依赖是传递性的:

复制代码
A依赖B,B依赖C  →  A自动获得C的依赖(传递依赖)

传递依赖的问题:

  • 引入不需要的jar包,增大应用体积
  • 可能产生版本冲突
  • 依赖关系复杂,难以排查问题

解决方案:

1. 使用<optional>true</optional>

xml 复制代码
<!-- framework模块可能依赖某些可选组件 -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <optional>true</optional>  <!-- 标记为可选依赖 -->
</dependency>

效果:

  • 业务模块依赖framework时,不会自动获得caffeine依赖
  • 如果业务模块需要使用caffeine,需要显式声明依赖

2. 使用<exclusions>排除冲突

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.3</version>
    <exclusions>
        <!-- 排除EasyExcel自带的slf4j,使用项目统一版本 -->
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
        <!-- 排除旧版本的POI -->
        <exclusion>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- 显式声明统一版本 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.5</version>
</dependency>

实战案例:订单业务依赖支付API

需求场景: 订单创建后,需要调用支付模块创建支付单。

依赖配置:

xml 复制代码
<!-- demo-module-order-biz/pom.xml -->
<dependencies>
    <!-- 1. 依赖自己的api -->
    <dependency>
        <groupId>com.company</groupId>
        <artifactId>demo-module-order-api</artifactId>
    </dependency>
    
    <!-- 2. 依赖支付模块api(跨模块调用) -->
    <dependency>
        <groupId>com.company</groupId>
        <artifactId>demo-module-payment-api</artifactId>
    </dependency>
    
    <!-- 3. 依赖框架starter -->
    <dependency>
        <groupId>com.company</groupId>
        <artifactId>demo-spring-boot-starter-mybatis</artifactId>
    </dependency>
    
    <dependency>
        <groupId>com.company</groupId>
        <artifactId>demo-spring-boot-starter-redis</artifactId>
    </dependency>
    
    <!-- 4. Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

业务代码:

java 复制代码
package com.company.module.order.biz.service.impl;

import com.company.module.order.api.dto.*;
import com.company.module.payment.api.PaymentApi;
import com.company.module.payment.api.dto.PaymentCreateReqDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    
    private final OrderMapper orderMapper;
    
    // 注入支付模块的API(通过依赖支付api实现跨模块调用)
    private final PaymentApi paymentApi;
    
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createOrder(OrderCreateReqVO reqVO) {
        // 1. 创建订单
        OrderDO order = buildOrder(reqVO);
        orderMapper.insert(order);
        log.info("[创建订单] 订单号={}, 金额={}", order.getOrderNo(), order.getAmount());
        
        // 2. 调用支付模块创建支付单(跨模块调用)
        PaymentCreateReqDTO paymentReq = new PaymentCreateReqDTO();
        paymentReq.setOrderId(order.getId());
        paymentReq.setOrderNo(order.getOrderNo());
        paymentReq.setAmount(order.getAmount());
        
        Long paymentId = paymentApi.createPayment(paymentReq);
        log.info("[创建支付单] 支付ID={}", paymentId);
        
        return order.getId();
    }
}

调用链路分析:

  1. Controller调用Service:同模块内部调用
  2. OrderService调用PaymentApi:跨模块调用(通过依赖payment-api)
  3. Spring容器自动装配:PaymentApiImpl作为PaymentApi的实现类被注入
  4. 最终调用PaymentService:PaymentApiImpl内部调用PaymentService

1.5 starter自动装配机制

什么是Spring Boot Starter?

Spring Boot Starter是开箱即用的依赖包 ,遵循约定优于配置的理念。

传统Spring配置的痛点:

xml 复制代码
<!-- 需要手动配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="url" value="jdbc:mysql://localhost:3306/db"/>
    <property name="username" value="root"/>
    <property name="password" value="123456"/>
</bean>

<!-- 需要手动配置SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
</bean>

<!-- 需要手动配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

Spring Boot Starter的优势:

xml 复制代码
<!-- 只需引入starter依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

<!-- 配置文件中简单配置 -->
<!-- application.yml -->
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db
    username: root
    password: 123456

自动装配的魔法:

  • Spring Boot启动时自动扫描jar包中的META-INF/spring.factories
  • 加载配置类,自动创建Bean
  • 开发者只需引入依赖,无需手动配置

自定义Starter结构设计

以MyBatis-Plus Starter为例,展示自定义starter的标准结构:

复制代码
demo-spring-boot-starter-mybatis/
├── pom.xml
└── src/main/
    ├── java/com/company/framework/mybatis/
    │   ├── config/
    │   │   └── MybatisAutoConfiguration.java         # 自动配置类(核心)
    │   ├── core/
    │   │   ├── TableYearContext.java                 # 分表上下文(ThreadLocal)
    │   │   └── TableYearInterceptor.java             # 分表拦截器
    │   └── properties/
    │       └── MybatisProperties.java                # 配置属性类
    └── resources/
        └── META-INF/
            └── spring.factories                       # SPI配置文件(关键)

核心组件详解

1. 自动配置类(AutoConfiguration)

java 复制代码
package com.company.framework.mybatis.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.*;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

/**
 * MyBatis-Plus自动配置类
 * 
 * <p>关键注解说明:
 * - @AutoConfiguration: 标记为自动配置类(Spring Boot 2.7+新注解,替代@Configuration)
 * - @ConditionalOnClass: 当classpath存在指定类时才加载(判断依赖是否存在)
 * - @ConditionalOnMissingBean: 当Spring容器中不存在指定Bean时才创建(避免重复)
 * - @EnableConfigurationProperties: 启用配置属性类
 */
@Slf4j
@AutoConfiguration  // Spring Boot 2.7+新注解
@ConditionalOnClass(MybatisPlusInterceptor.class)  // 只有引入了MyBatis-Plus依赖才加载
@EnableConfigurationProperties(MybatisProperties.class)  // 启用配置属性绑定
public class MybatisAutoConfiguration {
    
    /**
     * 配置MyBatis-Plus拦截器
     * 
     * <p>核心拦截器包括:
     * 1. 动态表名拦截器(支持按年分表)
     * 2. 分页拦截器(自动分页)
     */
    @Bean
    @ConditionalOnMissingBean  // 如果用户自定义了拦截器,就不创建默认的
    public MybatisPlusInterceptor mybatisPlusInterceptor(MybatisProperties properties) {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 1. 动态表名拦截器(必须放第一个)
        if (properties.getDynamicTable().getEnabled()) {
            interceptor.addInnerInterceptor(dynamicTableNameInterceptor());
            log.info("[MyBatis-Plus] 动态表名拦截器已加载");
        }
        
        // 2. 分页拦截器
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        log.info("[MyBatis-Plus] 分页拦截器已加载");
        
        return interceptor;
    }
    
    /**
     * 动态表名拦截器
     * 实现按年份分表:backend_order_2024, backend_order_2025
     */
    @Bean
    @ConditionalOnProperty(prefix = "mybatis-plus.dynamic-table", name = "enabled", 
                           havingValue = "true", matchIfMissing = true)
    public DynamicTableNameInnerInterceptor dynamicTableNameInterceptor() {
        DynamicTableNameInnerInterceptor interceptor = new DynamicTableNameInnerInterceptor();
        
        // 设置动态表名处理器(核心逻辑)
        interceptor.setTableNameHandler((sql, tableName) -> {
            // 从ThreadLocal获取年份
            Integer year = TableYearContext.getYear();
            
            // 判断是否需要分表(这里简化处理,实际应该配置化)
            if (needPartition(tableName)) {
                // 如果没有设置年份,默认使用当前年份
                int targetYear = (year != null) ? year : LocalDate.now().getYear();
                String newTableName = tableName + "_" + targetYear;
                log.debug("[动态表名] {} -> {}", tableName, newTableName);
                return newTableName;
            }
            
            return tableName;  // 不需要分表,返回原表名
        });
        
        return interceptor;
    }
    
    /**
     * 判断表是否需要分表
     * 实际项目中应该通过配置文件配置需要分表的表名
     */
    private boolean needPartition(String tableName) {
        // 示例:订单表、支付表需要分表
        return tableName.equals("backend_order") 
            || tableName.equals("backend_payment")
            || tableName.equals("backend_bill");
    }
}

关键注解详解:

注解 作用 场景
@AutoConfiguration 标记自动配置类 替代@Configuration
@ConditionalOnClass 类存在时才加载 判断依赖是否引入
@ConditionalOnMissingBean Bean不存在时才创建 允许用户自定义覆盖
@ConditionalOnProperty 配置存在且匹配时才加载 根据配置开关功能
@EnableConfigurationProperties 启用配置属性绑定 读取application.yml配置

2. 配置属性类(Properties)

java 复制代码
package com.company.framework.mybatis.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * MyBatis配置属性
 * 
 * <p>绑定application.yml中的配置:
 * mybatis-plus:
 *   dynamic-table:
 *     enabled: true
 */
@Data
@ConfigurationProperties(prefix = "mybatis-plus")
public class MybatisProperties {
    
    /** 动态表名配置 */
    private DynamicTableConfig dynamicTable = new DynamicTableConfig();
    
    @Data
    public static class DynamicTableConfig {
        /** 是否启用动态表名 */
        private Boolean enabled = true;
        
        /** 需要分表的表名列表 */
        private List<String> tables = Arrays.asList("backend_order", "backend_payment");
    }
}

3. ThreadLocal分表上下文

java 复制代码
package com.company.framework.mybatis.core;

import lombok.extern.slf4j.Slf4j;
import java.util.function.Supplier;

/**
 * 分表年份上下文
 * 
 * <p>核心原理:
 * 1. 使用ThreadLocal存储当前线程的年份信息
 * 2. 在Service方法中设置年份,Mapper执行时自动读取
 * 3. finally块中清除年份,防止内存泄漏
 */
@Slf4j
public class TableYearContext {
    
    /** 线程变量:存储年份 */
    private static final ThreadLocal<Integer> YEAR_CONTEXT = new ThreadLocal<>();
    
    /**
     * 设置年份
     * 
     * @param year 年份(如2024)
     */
    public static void setYear(Integer year) {
        YEAR_CONTEXT.set(year);
        log.debug("[TableYearContext] 设置年份: {}", year);
    }
    
    /**
     * 获取年份
     * 
     * @return 年份,未设置则返回null
     */
    public static Integer getYear() {
        return YEAR_CONTEXT.get();
    }
    
    /**
     * 清除年份
     * 重要:防止ThreadLocal内存泄漏
     */
    public static void clear() {
        YEAR_CONTEXT.remove();
        log.debug("[TableYearContext] 清除年份");
    }
    
    /**
     * 在指定年份中执行查询操作(有返回值)
     * 
     * <p>使用示例:
     * OrderDO order = TableYearContext.callInYear(2024, () -> {
     *     return orderMapper.selectById(id);
     * });
     * 
     * @param year 年份
     * @param supplier 查询逻辑
     * @return 查询结果
     */
    public static <T> T callInYear(Integer year, Supplier<T> supplier) {
        try {
            setYear(year);  // 设置年份
            return supplier.get();  // 执行查询
        } finally {
            clear();  // 清除年份(重要!)
        }
    }
    
    /**
     * 在指定年份中执行更新操作(无返回值)
     * 
     * <p>使用示例:
     * TableYearContext.runInYear(2024, () -> {
     *     orderMapper.updateById(order);
     * });
     * 
     * @param year 年份
     * @param runnable 更新逻辑
     */
    public static void runInYear(Integer year, Runnable runnable) {
        try {
            setYear(year);  // 设置年份
            runnable.run();  // 执行更新
        } finally {
            clear();  // 清除年份(重要!)
        }
    }
}

ThreadLocal原理说明:

java 复制代码
// 线程A执行
TableYearContext.setYear(2024);
// ThreadLocal内部:Thread-A -> 2024

// 线程B执行
TableYearContext.setYear(2025);
// ThreadLocal内部:Thread-A -> 2024, Thread-B -> 2025

// 每个线程都有自己独立的年份值,互不干扰

为什么要在finally中clear()?

  1. 防止内存泄漏:ThreadLocal变量不清除,线程池复用线程时会残留旧值
  2. 避免数据错乱:下次使用该线程时,可能读取到上次的年份
  3. 最佳实践:所有ThreadLocal使用都应该在finally中清除

4. SPI配置文件(spring.factories)

properties 复制代码
# demo-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories

# 自动配置类(Spring Boot会自动扫描并加载)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.company.framework.mybatis.config.MybatisAutoConfiguration

spring.factories详解:

  • 位置 :必须在META-INF/spring.factories
  • 格式接口全限定名=实现类全限定名1,实现类全限定名2
  • 换行 :使用\换行,等号后不能有空格
  • 原理 :Spring Boot启动时通过SpringFactoriesLoader加载所有jar包的spring.factories

常见错误:

properties 复制代码
# ❌ 等号后有空格(错误)
org.springframework.boot.autoconfigure.EnableAutoConfiguration= \
  com.company.framework.mybatis.config.MybatisAutoConfiguration

# ✅ 等号后无空格(正确)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.company.framework.mybatis.config.MybatisAutoConfiguration

Starter使用示例

业务模块只需引入依赖,无需任何配置:

1. pom.xml引入依赖

xml 复制代码
<dependency>
    <groupId>com.company</groupId>
    <artifactId>demo-spring-boot-starter-mybatis</artifactId>
</dependency>

2. application.yml配置(可选)

yaml 复制代码
mybatis-plus:
  dynamic-table:
    enabled: true  # 启用动态表名
    tables:        # 需要分表的表
      - backend_order
      - backend_payment

3. 业务代码直接使用

java 复制代码
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    
    private final OrderMapper orderMapper;
    
    /**
     * 查询2024年的订单
     * SQL自动路由到:backend_order_2024
     */
    public OrderDO getOrderIn2024(Long id) {
        return TableYearContext.callInYear(2024, () -> {
            return orderMapper.selectById(id);
        });
    }
    
    /**
     * 更新2025年的订单
     * SQL自动路由到:backend_order_2025
     */
    public void updateOrderIn2025(OrderDO order) {
        TableYearContext.runInYear(2025, () -> {
            orderMapper.updateById(order);
        });
    }
    
    /**
     * 跨年查询:查询2024和2025两年的订单
     */
    public List<OrderDO> getOrdersCrossYear(Long userId) {
        List<OrderDO> result = new ArrayList<>();
        
        // 查询2024年
        List<OrderDO> orders2024 = TableYearContext.callInYear(2024, () -> {
            return orderMapper.selectList(new LambdaQueryWrapper<OrderDO>()
                .eq(OrderDO::getUserId, userId));
        });
        result.addAll(orders2024);
        
        // 查询2025年
        List<OrderDO> orders2025 = TableYearContext.callInYear(2025, () -> {
            return orderMapper.selectList(new LambdaQueryWrapper<OrderDO>()
                .eq(OrderDO::getUserId, userId));
        });
        result.addAll(orders2025);
        
        return result;
    }
}

自动装配流程总结

复制代码
1. Spring Boot启动
   ↓
2. SpringFactoriesLoader扫描所有jar包的META-INF/spring.factories
   ↓
3. 加载EnableAutoConfiguration对应的配置类
   ↓
4. 根据@Conditional条件判断是否加载
   ↓
5. 创建Bean并注入Spring容器
   ↓
6. 业务代码通过@Autowired使用

核心优势:

  • 开箱即用:引入依赖即可使用,无需手动配置
  • 约定优于配置:提供合理的默认配置
  • 可覆盖:用户可以通过配置或自定义Bean覆盖默认行为
  • 模块化:技术组件独立封装,业务模块只需关注业务逻辑

总结

ruoyi-vue-pro框架通过Maven多模块 + api/biz分离 + 自动装配构建了一套完整的企业级开发体系:

核心架构设计

层次 职责 关键技术 核心价值
框架层 技术组件封装 Spring Boot AutoConfiguration 开箱即用、统一技术栈
API层 接口契约定义 DTO、枚举、常量 依赖解耦、接口稳定
BIZ层 业务逻辑实现 Controller/Service/Mapper 职责清晰、易于维护
启动层 应用启动入口 Spring Boot Application 一键启动、整合模块

关键技术要点

1. Maven多模块管理

  • <packaging>pom</packaging>:父POM必须是pom类型
  • <dependencyManagement>:统一版本管理,子模块无需指定版本
  • ${revision}变量:统一内部模块版本号
  • flatten-maven-plugin:处理变量占位符

2. api/biz分离设计

  • api模块:只定义接口、DTO、枚举,不包含实现
  • biz模块:完整的MVC三层架构,实现业务逻辑
  • 跨模块调用:通过依赖api实现,避免循环依赖
  • RPC友好:api接口天然支持Feign/Dubbo远程调用

3. 依赖管理原则

  • 严禁循环依赖:A依赖B,B不能依赖A
  • 最小化依赖:api模块保持轻量,只引入必要依赖
  • 控制传递 :使用<optional><exclusions>控制依赖传递

4. 自动装配机制

  • META-INF/spring.factories:SPI配置文件
  • @AutoConfiguration:标记自动配置类
  • @ConditionalOnXxx:条件装配,按需加载
  • @EnableConfigurationProperties:绑定配置文件

实战应用场景

这套架构在企业级系统中广泛应用:

  • 电商系统:订单模块、支付模块、库存模块、商品模块
  • 金融系统:账户模块、交易模块、风控模块、清算模块
  • SaaS平台:租户模块、权限模块、审计模块、报表模块

通过模块化设计,实现了高内聚、低耦合、可复用、易维护的目标,为企业级应用开发奠定了坚实基础。


面试题精选

Q1: 为什么要使用Maven多模块结构?相比单体应用有什么优势?

参考答案:

单体应用的痛点:

  1. 编译慢:修改一个类需要重新编译整个项目,大型项目编译可能需要几分钟
  2. 职责不清:业务代码、框架代码、工具类混在一起,难以维护
  3. 无法复用:其他项目想用某个功能,只能复制代码
  4. 团队协作难:多人修改同一个工程,代码冲突频繁
  5. 部署耦合:任何小改动都需要重新部署整个应用

多模块结构的优势:

  1. 模块隔离:按业务域拆分模块,职责清晰,降低耦合
  2. 编译提速:只编译修改的模块,大幅提升编译速度
  3. 代码复用:通过Maven依赖引入,避免代码重复
  4. 团队协作:不同团队负责不同模块,减少代码冲突
  5. 持续集成:只部署变更模块,加快部署速度
  6. 版本管理:统一管理依赖版本,避免版本冲突

实际案例:

某电商系统有订单、支付、库存、商品四个核心模块。使用多模块后:

  • 订单团队修改订单模块,只需编译订单模块(10秒),无需编译整个项目(5分钟)
  • 库存模块升级,只部署库存服务,不影响订单、支付等模块
  • 新项目需要支付功能,直接引入支付模块依赖即可

Q2: api/biz分离的核心原理是什么?为什么能避免循环依赖?

参考答案:

核心原理:接口与实现分离

api模块定义接口契约(interface),biz模块提供具体实现(implementation)。这是经典的**依赖倒置原则(DIP)**的应用。

依赖关系:

复制代码
订单biz ---依赖---> 支付api (只依赖接口)
                      ↑
                   实现关系
                      ↑
                   支付biz

为什么能避免循环依赖?

  1. 单向依赖:订单biz只依赖支付api,不依赖支付biz
  2. 实现隔离:支付biz的任何变化,只要api接口不变,订单biz都不受影响
  3. 接口稳定:api接口一旦发布,保持向后兼容,biz内部可任意重构

反例(会产生循环依赖):

复制代码
订单biz ---依赖---> 支付biz
  ↑                    ↓
  └────────依赖────────┘
Maven构建失败:cyclic reference detected

其他好处:

  • 轻量依赖:api模块只包含DTO、枚举,体积小,依赖少
  • RPC友好:微服务架构下,api可以作为Feign Client定义
  • 版本管理:api版本独立升级,平滑过渡

Q3: <dependencies><dependencyManagement>有什么区别?什么时候用哪个?

参考答案:

核心区别:

维度 <dependencies> <dependencyManagement>
是否实际引入依赖 是,会下载jar包到classpath 否,只声明版本号
子模块是否继承 是,子模块自动获得依赖 否,子模块需主动引用
是否需要指定版本 是(在父POM中)
作用域 当前POM和所有子POM 仅声明版本,不实际引入

使用场景:

1. <dependencies>:所有子模块都需要的公共依赖

xml 复制代码
<!-- 父POM -->
<dependencies>
    <!-- 所有子模块都需要Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>
  • 效果:所有子模块自动获得Lombok依赖,无需重复声明

2. <dependencyManagement>:统一版本管理

xml 复制代码
<!-- 父POM -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- 子POM -->
<dependencies>
    <!-- 只需声明groupId和artifactId,版本继承自父POM -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
</dependencies>
  • 效果:版本统一管理,子模块无需指定版本号

最佳实践:

  • 父POM的<dependencies>:只放所有子模块都需要的依赖(如Lombok、SLF4J)
  • 父POM的<dependencyManagement>:声明所有可能用到的依赖版本
  • 子POM的<dependencies>:引用需要的依赖,无需指定版本

Q4: Spring Boot的自动装配原理是什么?@AutoConfigurationspring.factories是如何工作的?

参考答案:

自动装配流程:

复制代码
1. Spring Boot应用启动
   @SpringBootApplication
   └─ @EnableAutoConfiguration(核心注解)
      ↓
2. AutoConfigurationImportSelector生效
   扫描classpath下所有jar包的META-INF/spring.factories
      ↓
3. 加载spring.factories中的配置类
   org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
   com.company.framework.mybatis.config.MybatisAutoConfiguration
      ↓
4. 根据@Conditional条件判断是否加载
   @ConditionalOnClass:类存在时才加载
   @ConditionalOnMissingBean:Bean不存在时才创建
   @ConditionalOnProperty:配置匹配时才加载
      ↓
5. 创建Bean并注入Spring容器
   配置类中@Bean方法返回的对象注册到容器
      ↓
6. 业务代码通过@Autowired使用

关键组件详解:

1. spring.factories

properties 复制代码
# 位置:META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.company.framework.mybatis.config.MybatisAutoConfiguration,\
com.company.framework.redis.config.RedisAutoConfiguration
  • 作用:SPI(Service Provider Interface)机制,声明自动配置类
  • 加载时机 :Spring Boot启动时通过SpringFactoriesLoader加载
  • 注意事项 :等号后不能有空格,使用\换行

2. @AutoConfiguration

java 复制代码
@AutoConfiguration
@ConditionalOnClass(MybatisPlusInterceptor.class)
public class MybatisAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 创建拦截器
    }
}
  • 作用:标记自动配置类(Spring Boot 2.7+新注解,替代@Configuration)
  • 条件装配:配合@Conditional注解,按需加载

3. 常用Conditional注解

注解 作用 示例
@ConditionalOnClass classpath存在指定类时加载 @ConditionalOnClass(SqlSessionFactory.class)
@ConditionalOnMissingBean 容器中不存在指定Bean时创建 @ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty 配置存在且匹配时加载 @ConditionalOnProperty(prefix="mybatis", name="enabled")
@ConditionalOnBean 容器中存在指定Bean时加载 @ConditionalOnBean(DataSource.class)

为什么这样设计?

  • 开箱即用:引入starter依赖即可使用,无需手动配置
  • 约定优于配置:提供合理的默认配置,减少配置工作量
  • 可覆盖:用户可以通过自定义Bean或配置覆盖默认行为
  • 按需加载:通过条件判断,只加载需要的组件

Q5: ThreadLocal在分表场景中是如何使用的?为什么要在finally中清除?

参考答案:

ThreadLocal原理:

ThreadLocal为每个线程提供独立的变量副本,不同线程之间互不干扰。

java 复制代码
// 线程A执行
TableYearContext.setYear(2024);  // Thread-A -> 2024

// 线程B执行  
TableYearContext.setYear(2025);  // Thread-B -> 2025

// 每个线程都有自己独立的年份值

在分表场景中的应用:

java 复制代码
public class TableYearContext {
    private static final ThreadLocal<Integer> YEAR_CONTEXT = new ThreadLocal<>();
    
    public static <T> T callInYear(Integer year, Supplier<T> supplier) {
        try {
            YEAR_CONTEXT.set(year);  // 设置年份
            return supplier.get();   // 执行查询(Mapper会读取这个年份)
        } finally {
            YEAR_CONTEXT.remove();   // 清除年份(重要!)
        }
    }
}

调用链路:

复制代码
1. Service调用:callInYear(2024, () -> mapper.selectById(id))
   ↓
2. 设置ThreadLocal:YEAR_CONTEXT.set(2024)
   ↓
3. 执行Mapper:selectById(id)
   ↓
4. MyBatis拦截器:读取YEAR_CONTEXT.get() = 2024
   ↓
5. 动态替换表名:backend_order -> backend_order_2024
   ↓
6. finally清除:YEAR_CONTEXT.remove()

为什么必须在finally中清除?

1. 防止内存泄漏

java 复制代码
// 假设没有清除
public void method1() {
    TableYearContext.setYear(2024);
    // 执行查询...
    // 忘记清除
}

// 线程池复用线程
public void method2() {
    // 这个方法没有设置年份,但ThreadLocal还残留2024
    Integer year = TableYearContext.getYear();  // 返回2024(错误!)
}

问题:

  • Tomcat线程池默认200个线程,线程复用
  • ThreadLocal变量没有清除,会一直存在于线程中
  • 下次使用该线程时,读取到上次的旧值

2. 避免数据错乱

java 复制代码
// 场景:处理2024年订单
TableYearContext.setYear(2024);
orderMapper.selectById(1);  // 查询 backend_order_2024

// 线程池复用,处理默认年份订单(期望当前年份2025)
// 但ThreadLocal残留2024,导致查询了错误的表
orderMapper.selectById(2);  // 错误地查询了 backend_order_2024

3. ThreadLocal内存泄漏原理

复制代码
Thread -> ThreadLocalMap -> Entry(ThreadLocal, Value)
                             ↓
                        弱引用ThreadLocal,强引用Value
  • Entry的Key:弱引用ThreadLocal,GC时会被回收
  • Entry的Value:强引用,只有手动remove()才会释放
  • 泄漏原因:ThreadLocal对象被回收,但Value还在,且无法访问

正确使用模式:

java 复制代码
public static <T> T callInYear(Integer year, Supplier<T> supplier) {
    try {
        setYear(year);
        return supplier.get();
    } finally {
        clear();  // 无论是否异常,都要清除
    }
}

最佳实践:

  1. 所有ThreadLocal使用都要在finally中清除
  2. 使用工具类封装:避免每次都手动set/remove
  3. 线程池场景尤其重要:线程复用会放大问题
  4. 可以用InheritableThreadLocal:父子线程传递,但仍需清除

Q6: Maven依赖冲突是如何解决的?如何排查依赖冲突问题?

参考答案:

Maven依赖冲突解决机制:

1. 最短路径优先(Nearest Definition)

复制代码
项目 -> A 1.0 -> C 2.0  (路径长度=2)
项目 -> B 1.0 -> D 1.0 -> C 1.0  (路径长度=3)

结果:使用 C 2.0(路径更短)

2. 最先声明优先(First Declaration)

复制代码
项目 -> A 1.0 -> C 2.0  (路径长度=2,先声明)
项目 -> B 1.0 -> C 1.0  (路径长度=2,后声明)

结果:使用 C 2.0(A先声明)

排查依赖冲突的方法:

方法1:查看依赖树

bash 复制代码
# 查看完整依赖树
mvn dependency:tree

# 查看指定依赖的冲突
mvn dependency:tree -Dverbose -Dincludes=com.alibaba:fastjson

# 输出示例
[INFO] +- com.alibaba:easyexcel:jar:3.3.3:compile
[INFO] |  +- com.alibaba:fastjson:jar:1.2.70:compile
[INFO] +- com.company:demo-common:jar:1.0.0:compile
[INFO] |  \- com.alibaba:fastjson:jar:1.2.83:compile (version managed from 1.2.70)

方法2:使用IDEA的依赖分析

  • 打开pom.xml
  • 右键 -> Diagrams -> Show Dependencies
  • 可视化展示依赖关系,红线表示冲突

方法3:Maven Helper插件

  • IDEA安装插件:Maven Helper
  • 打开pom.xml
  • 点击底部"Dependency Analyzer"
  • 红色表示冲突,右键可直接Exclude

解决依赖冲突的方法:

方法1:显式声明版本(推荐)

xml 复制代码
<!-- 父POM的dependencyManagement中锁定版本 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>
</dependencyManagement>

方法2:排除冲突依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <exclusions>
        <!-- 排除EasyExcel自带的fastjson -->
        <exclusion>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- 显式引入统一版本 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>

方法3:调整依赖顺序

xml 复制代码
<!-- 将期望的版本放在前面(利用最先声明优先原则) -->
<dependencies>
    <!-- 先声明统一版本 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
    
    <!-- 后声明其他依赖 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
    </dependency>
</dependencies>

常见冲突案例:

  1. SLF4J冲突:多个框架带不同版本的slf4j-api
  2. Jackson冲突:Spring Boot和其他库的Jackson版本不一致
  3. Netty冲突:Redisson、RocketMQ等都依赖Netty
  4. Guava冲突:版本跨度大,API不兼容

最佳实践:

  1. 使用BOM统一版本:引入Spring Boot Dependencies、Spring Cloud Dependencies
  2. 父POM锁定核心依赖版本:在dependencyManagement中声明
  3. 定期排查依赖冲突:CI/CD中增加依赖检查
  4. 升级依赖要谨慎:先在测试环境验证

Q7: 如何设计一个高质量的Starter?有哪些关键要素?

参考答案:

Starter设计的关键要素:

1. 清晰的目录结构

复制代码
demo-spring-boot-starter-xxx/
├── pom.xml
└── src/main/
    ├── java/
    │   └── com/company/framework/xxx/
    │       ├── config/                    # 配置包
    │       │   └── XxxAutoConfiguration.java
    │       ├── properties/                # 属性包
    │       │   └── XxxProperties.java
    │       ├── core/                      # 核心功能包
    │       └── support/                   # 支持工具包
    └── resources/
        └── META-INF/
            ├── spring.factories           # SPI配置(必须)
            └── additional-spring-configuration-metadata.json  # 配置元数据(可选)

2. 规范的命名约定

复制代码
# 官方Starter
spring-boot-starter-{name}
例如:spring-boot-starter-web

# 第三方Starter
{name}-spring-boot-starter
例如:mybatis-spring-boot-starter

3. 完善的AutoConfiguration

java 复制代码
@AutoConfiguration
@ConditionalOnClass(Xxx.class)  // 判断依赖是否存在
@EnableConfigurationProperties(XxxProperties.class)  // 绑定配置
public class XxxAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean  // 允许用户自定义覆盖
    public XxxService xxxService(XxxProperties properties) {
        return new XxxServiceImpl(properties);
    }
    
    @Bean
    @ConditionalOnProperty(prefix = "xxx", name = "enabled", 
                           havingValue = "true", matchIfMissing = true)
    public XxxInterceptor xxxInterceptor() {
        return new XxxInterceptor();
    }
}

4. 灵活的配置属性

java 复制代码
@Data
@ConfigurationProperties(prefix = "xxx")
public class XxxProperties {
    
    /** 是否启用(提供开关) */
    private Boolean enabled = true;
    
    /** 超时时间(提供默认值) */
    private Duration timeout = Duration.ofSeconds(30);
    
    /** 自定义配置(嵌套对象) */
    private CustomConfig custom = new CustomConfig();
    
    @Data
    public static class CustomConfig {
        private String host = "localhost";
        private Integer port = 8080;
    }
}

5. 智能的条件装配

java 复制代码
// 根据类是否存在
@ConditionalOnClass(Redis.class)

// 根据Bean是否存在
@ConditionalOnBean(DataSource.class)
@ConditionalOnMissingBean(RedisTemplate.class)

// 根据配置是否存在
@ConditionalOnProperty(prefix = "xxx", name = "enabled")

// 根据表达式
@ConditionalOnExpression("${xxx.enabled:true} and ${xxx.mode} == 'advanced'")

// 组合条件
@ConditionalOnClass(Xxx.class)
@ConditionalOnProperty(prefix = "xxx", name = "enabled", havingValue = "true")

6. 配置元数据文件(提升IDE体验)

json 复制代码
{
  "groups": [
    {
      "name": "xxx",
      "type": "com.company.framework.xxx.properties.XxxProperties",
      "description": "XXX configuration properties."
    }
  ],
  "properties": [
    {
      "name": "xxx.enabled",
      "type": "java.lang.Boolean",
      "description": "Whether to enable XXX.",
      "defaultValue": true
    },
    {
      "name": "xxx.timeout",
      "type": "java.time.Duration",
      "description": "Timeout for XXX operations.",
      "defaultValue": "30s"
    }
  ],
  "hints": [
    {
      "name": "xxx.mode",
      "values": [
        {"value": "simple"},
        {"value": "advanced"}
      ]
    }
  ]
}

效果: 在application.yml中编写配置时,IDE会提供智能提示和文档说明。

7. 完善的文档和示例

markdown 复制代码
# XXX Starter使用文档

## 快速开始

### 1. 引入依赖
```xml
<dependency>
    <groupId>com.company</groupId>
    <artifactId>xxx-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

2. 配置参数

yaml 复制代码
xxx:
  enabled: true
  timeout: 30s
  custom:
    host: localhost
    port: 8080

3. 使用示例

java 复制代码
@Autowired
private XxxService xxxService;

public void demo() {
    xxxService.doSomething();
}

设计原则总结:

  1. 开箱即用:引入依赖即可使用,提供合理默认值
  2. 约定优于配置:大部分场景使用默认配置即可
  3. 可覆盖:允许用户自定义Bean或配置覆盖
  4. 按需加载:通过条件注解,只加载必要的组件
  5. 向后兼容:配置变更要考虑向后兼容性
  6. 文档完善:提供清晰的使用文档和示例代码

下一篇预告: 《Spring Boot企业级配置详解》- 深入解析多环境配置、属性绑定、配置加密等实战技巧。

相关推荐
恋猫de小郭29 分钟前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端