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企业级配置详解》- 深入解析多环境配置、属性绑定、配置加密等实战技巧。

相关推荐
华仔啊14 小时前
JavaScript 如何准确判断数据类型?5 种方法深度对比
前端·javascript
毕设十刻15 小时前
基于Vue的迅读网上书城22f4d(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
程序员小寒15 小时前
从一道前端面试题,谈 JS 对象存储特点和运算符执行顺序
开发语言·前端·javascript·面试
七夜zippoe15 小时前
事件驱动架构:构建高并发松耦合系统的Python实战
开发语言·python·架构·eda·事件驱动
爱健身的小刘同学15 小时前
Vue 3 + Leaflet 地图可视化
前端·javascript·vue.js
神秘的猪头16 小时前
Ajax 数据请求:从零开始掌握异步通信
前端·javascript
稀饭5216 小时前
用changeset来管理你的npm包版本
前端·npm
TeamDev16 小时前
基于 Angular UI 的 C# 桌面应用
前端·后端·angular.js
Komorebi゛16 小时前
【CSS】斜角流光样式
前端·css