Skip to content

MyBatis动态SQL设计与Spring多环境配置体系实战:基于smart-scaffold项目的深度解析

作者: 必码 | bima.cc


前言

在企业级Java开发中,数据持久层和多环境配置管理是两个永恒的核心命题。MyBatis作为国内使用最广泛的ORM框架之一,其动态SQL能力直接决定了数据访问层的灵活性与可维护性;而Spring Boot的多环境配置体系则关系到项目从开发、测试到生产部署的全生命周期管理。这两个看似独立的技术领域,在实际项目中往往交织在一起,共同构成了项目基础设施的基石。

然而,在实际开发中,我们经常遇到以下痛点:

数据持久层痛点: 大量重复的SQL编写工作,条件查询的动态拼接逻辑散落在各处,缺乏统一的查询模式;MyBatis Generator生成的代码千篇一律,难以满足业务定制需求;resultMap映射冗余,Entity与DTO之间的转换逻辑混乱;分页查询的实现方式五花八门,缺乏统一规范。

配置管理痛点: 开发、测试、生产环境的配置文件相互拷贝,稍有遗漏就可能导致线上事故;敏感信息如数据库密码、API密钥直接写在配置文件中,存在安全隐患;不同环境之间的配置差异缺乏清晰的分层策略,维护成本高;Maven资源过滤配置不当,导致打包后的配置文件内容异常。

工程化痛点: Mapper XML文件与Java接口分离存放,增加了文件管理的复杂度;多数据源场景下,配置和代码的组织方式缺乏统一标准;项目从单体向微服务演进时,配置体系需要同步重构,但往往缺乏前瞻性设计。

smart-scaffold-springboot项目作为必码(bima.cc)开源的脚手架工程,针对上述痛点提供了一套完整的解决方案。本文将基于该项目源码,从MyBatis动态SQL设计、Spring多环境配置体系、数据库设计规范三个维度进行深度解析,帮助读者理解企业级项目中数据持久层与配置管理的最佳实践。

本文的代码示例均基于项目源码提取并简化为教学示例,旨在展示核心设计思路,而非暴露完整源码。读者可以在理解设计原理的基础上,结合自身项目需求进行灵活应用。


一、MyBatis动态SQL设计

1.1 Base_Where SQL片段的设计哲学

在企业级项目中,数据库查询条件往往是动态的——用户可能只填了部分搜索条件,也可能同时指定了多个筛选维度。如果为每种查询组合都编写一条SQL,不仅工作量巨大,而且维护成本极高。MyBatis的<sql>片段和<if>标签为我们提供了一种优雅的解决方案:将通用的查询条件抽取为可复用的SQL片段,通过动态标签实现按需拼接。

smart-scaffold项目中,每个Mapper XML都定义了一个名为Base_Where的SQL片段,它承担着"通用查询条件容器"的角色。这种设计模式的核心思想是:将所有可能的查询条件集中管理,通过<if>标签的null判断实现条件的动态拼接,最终在多个查询语句中通过<include>标签复用。

以下是Base_Where SQL片段的教学示例:

xml
<!-- 教学示例:Base_Where SQL片段 -->
<sql id="Base_Where">
    <!-- 主键精确匹配 -->
    <if test="id != null">
        and id = #{id, jdbcType=BIGINT}
    </if>

    <!-- 时间范围查询:创建时间 -->
    <if test="timeCreateBegin != null and timeCreateEnd != null">
        and time_create &gt;= #{timeCreateBegin, jdbcType=TIMESTAMP}
        and time_create &lt;= #{timeCreateEnd, jdbcType=TIMESTAMP}
    </if>

    <!-- 时间范围查询:更新时间 -->
    <if test="timeUpdateBegin != null and timeUpdateEnd != null">
        and time_update &gt;= #{timeUpdateBegin, jdbcType=TIMESTAMP}
        and time_update &lt;= #{timeUpdateEnd, jdbcType=TIMESTAMP}
    </if>

    <!-- 外键精确匹配 -->
    <if test="userId != null">
        and user_id = #{userId, jdbcType=BIGINT}
    </if>

    <!-- 字符串模糊查询 -->
    <if test="modelName != null and modelName != ''">
        <bind name="likeModelName" value="'%' + _parameter.modelName + '%'" />
        and model_name like #{likeModelName, jdbcType=VARCHAR}
    </if>

    <!-- 布尔状态匹配 -->
    <if test="isDefault != null">
        and is_default = #{isDefault, jdbcType=TINYINT}
    </if>
    <if test="status != null">
        and status = #{status, jdbcType=TINYINT}
    </if>
</sql>

1.1.1 <if>动态条件拼接的原理

<if>标签是MyBatis动态SQL中最基础也最常用的标签。它的test属性接受一个OGNL表达式,当表达式结果为true时,标签内的SQL片段才会被拼接到最终语句中。

在上面的示例中,test="id != null"表示只有当传入的查询参数中id不为null时,才会拼接and id = #{id}条件。这种设计的好处在于:查询条件完全由前端传入的参数决定,后端无需为不同的查询组合编写不同的SQL语句。

需要注意的是,<if>标签的test表达式支持OGNL语法,这意味着我们可以使用andor等逻辑运算符来组合多个条件。例如timeCreateBegin != null and timeCreateEnd != null就表示只有当时间范围的起始和结束值都不为null时,才拼接时间范围条件。这种"双null检查"的设计确保了时间范围查询的完整性——如果用户只传了起始时间或只传了结束时间,条件不会被拼接,避免了不完整的范围查询。

1.1.2 时间范围查询的设计细节

时间范围查询是业务系统中最常见的查询场景之一。在Base_Where片段中,时间范围查询采用了"双字段"设计模式:查询DTO中定义了timeCreateBegintimeCreateEnd两个Date类型字段,分别表示时间范围的起止点。

java
// 教学示例:查询DTO中的时间范围字段
public class ModelQueryDTO extends PageDTO implements Serializable {
    private Date timeCreateBegin;  // 创建时间起始
    private Date timeCreateEnd;    // 创建时间截止
    private Date timeUpdateBegin;  // 更新时间起始
    private Date timeUpdateEnd;    // 更新时间截止
    // ... 其他查询字段
}

这种设计的优势在于语义清晰:字段名直接表达了"起始"和"截止"的含义,便于前端对接和后续维护。同时,双字段设计天然支持"不传则不限制"的查询模式——当timeCreateBegintimeCreateEnd都为null时,时间范围条件不会被拼接到SQL中。

在XML层面,时间范围查询使用了&gt;=&lt;=操作符(XML中需要对大于号和小于号进行转义),配合jdbcType=TIMESTAMP类型声明,确保了时间比较的精确性。对于MySQL数据库,datetime类型字段的范围查询可以利用索引,前提是查询条件的顺序与索引列的顺序一致。

1.1.3 <bind>标签实现模糊查询

模糊查询是另一个高频查询场景。在传统的做法中,我们通常在Java代码中手动拼接%keyword%,然后将拼接后的字符串传入Mapper。但这种方式存在SQL注入的风险,且代码不够优雅。

smart-scaffold项目采用了<bind>标签来实现模糊查询的拼接:

xml
<!-- 教学示例:使用bind标签拼接模糊查询 -->
<if test="modelName != null and modelName != ''">
    <bind name="likeModelName" value="'%' + _parameter.modelName + '%'" />
    and model_name like #{likeModelName, jdbcType=VARCHAR}
</if>

<bind>标签的工作原理是:在OGNL上下文中创建一个新的变量,该变量的值由value属性中的OGNL表达式计算得出。在上述示例中,likeModelName变量被赋值为'%' + modelName + '%',然后在后续的like条件中通过#{likeModelName}引用这个变量。

这种设计的关键优势在于:模糊查询的通配符拼接完全在MyBatis层面完成,Java代码中传入的modelName保持原始值不变。#{}预编译参数的方式有效防止了SQL注入,而<bind>标签则确保了通配符的正确拼接。

需要注意的是,<if>标签的条件判断中除了modelName != null之外,还增加了modelName != ''的判断。这是因为在实际业务中,空字符串往往被视为"未填写",与null的处理逻辑一致。如果只判断null而不判断空字符串,当前端传入空字符串时,SQL会变成model_name like '%%',这虽然不会报错,但会匹配所有记录,显然不是预期的行为。

1.1.4 Base_Where片段的复用策略

Base_Where片段的真正价值在于复用。在smart-scaffold项目中,同一个Base_Where片段被selectBycountByuniqueBy三个查询语句共同引用:

xml
<!-- 教学示例:Base_Where片段的多处复用 -->
<!-- 列表查询 -->
<select id="selectBy" resultMap="ResultMapWithDTO">
    select <include refid="Base_Column_List" />
    from user_model where 1=1
    <include refid="Base_Where" />
    order by id desc
</select>

<!-- 计数查询 -->
<select id="countBy" resultType="java.lang.Integer">
    select count(1) from user_model where 1=1
    <include refid="Base_Where" />
</select>

<!-- 唯一查询 -->
<select id="uniqueBy" resultMap="ResultMapWithDTO">
    select <include refid="Base_Column_List" />
    from user_model where 1=1
    <include refid="Base_Where" />
    limit 1
</select>

这种"一处定义,多处复用"的设计模式带来了显著的好处:当需要新增查询条件时,只需修改Base_Where片段即可,所有引用该片段的查询语句都会自动生效。这避免了在多个SQL语句中重复修改相同条件的麻烦,大大降低了维护成本。

where 1=1的写法是动态SQL中的经典技巧。由于Base_Where片段中的每个条件都以and开头,如果第一个条件前面没有1=1,SQL语法就会出错(where and id = ?)。通过添加where 1=1,确保了无论哪个条件被拼接,SQL语法始终正确。虽然1=1对性能的影响微乎其微(现代数据库优化器会自动忽略它),但一些追求极致的团队也可以使用MyBatis的<where>标签来替代这种写法。

1.2 selectBy通用查询的设计模式

selectBy是smart-scaffold项目中最核心的通用查询方法。它不仅仅是一个简单的查询语句,更是一个集成了动态排序、分页控制和条件过滤的完整查询引擎。理解selectBy的设计模式,对于掌握整个项目的数据访问层架构至关重要。

1.2.1 动态排序字段的实现

在业务系统中,列表查询通常需要支持用户自定义排序。例如,用户可能希望按创建时间降序查看记录,也可能希望按名称升序排列。smart-scaffold项目通过<choose>标签实现了动态排序字段的选择:

xml
<!-- 教学示例:动态排序字段 -->
<select id="selectBy" parameterType="com.example.dto.ModelQueryDTO"
        resultMap="ResultMapWithDTO">
    select
    <include refid="Base_Column_List" />
    from user_model where 1=1
    <include refid="Base_Where" />
    order by
    <choose>
        <when test="fields == 'id'">id</when>
        <otherwise>id</otherwise>
    </choose>
    <choose>
        <when test="order == 'asc'">asc</when>
        <when test="order == 'desc'">desc</when>
        <otherwise>desc</otherwise>
    </choose>
</select>

<choose>标签类似于Java中的switch-case语句。它包含多个<when>子标签和一个可选的<otherwise>标签,从上到下依次判断每个<when>的条件,一旦某个条件为true,就使用该分支的内容,后续的<when>不再判断。如果所有<when>条件都不满足,则使用<otherwise>的内容。

在排序字段的选择中,fields参数来自PageDTO基类。当fields的值为'id'时,排序字段为id;否则(<otherwise>),默认也使用id排序。这种设计在当前示例中看似冗余(因为两个分支结果相同),但它的真正价值在于扩展性——当需要支持更多排序字段时,只需添加更多的<when>分支即可:

xml
<!-- 教学示例:扩展多字段排序 -->
<choose>
    <when test="fields == 'id'">id</when>
    <when test="fields == 'timeCreate'">time_create</when>
    <when test="fields == 'modelName'">model_name</when>
    <otherwise>id</otherwise>
</choose>

1.2.2 排序方向的安全控制

排序方向的控制同样使用了<choose>标签:

xml
<!-- 教学示例:排序方向控制 -->
<choose>
    <when test="order == 'asc'">asc</when>
    <when test="order == 'desc'">desc</when>
    <otherwise>desc</otherwise>
</choose>

这里的设计亮点在于"白名单"式的安全控制:只允许ascdesc两个值通过,其他任何值都会被替换为默认的desc。这是一种防御性编程的体现——如果直接将用户传入的排序方向拼接到SQL中(例如order by id ${order}),恶意用户可能传入SQL片段,导致SQL注入攻击。通过<choose>标签的白名单机制,彻底杜绝了这种风险。

PageDTO基类中定义了排序方向的默认值:

java
// 教学示例:PageDTO中的排序默认值
public class PageDTO implements Serializable {
    /** 查询字段,默认只查询id字段 */
    private String fields = "id";
    /** 排序方式,默认降序 */
    private String order = "desc";
    // ... 分页相关字段
}

默认降序(desc)的选择是经过考虑的:在大多数业务场景中,最新创建的记录通常排在前面,而自增主键id的大小与创建时间正相关,因此按id降序排列可以近似实现"最新优先"的效果。

1.2.3 分页控制的实现机制

smart-scaffold项目采用了"内存分页"策略,即通过SQL的LIMIT子句在数据库层面实现分页:

xml
<!-- 教学示例:条件分页 -->
<if test="isPage">limit #{start}, #{end}</if>

isPage是一个布尔类型的标志位,用于控制是否启用分页。当isPage为true时,SQL会追加limit #{start}, #{end}子句;当isPage为false时,则查询所有符合条件的记录。

startend的计算逻辑封装在PageDTO中:

java
// 教学示例:PageDTO中的分页计算
public class PageDTO implements Serializable {
    private Boolean isPage = false;  // 是否分页,默认不分页
    private Integer page = 1;        // 当前页码,默认第1页
    private Integer pageSize = 20;   // 每页大小,默认20条

    /** 获取起始位置(偏移量) */
    public Integer getStart() {
        return (page - 1) * pageSize;
    }

    /** 获取每页大小(limit的第二个参数) */
    public Integer getEnd() {
        return pageSize;
    }
}

这里有一个容易混淆的命名:getStart()返回的是偏移量(offset),getEnd()返回的是每页大小(limit size)。在MySQL的LIMIT offset, size语法中,第一个参数是偏移量(从0开始),第二个参数是返回的记录数。因此,当用户查询第1页(page=1)时,start = (1-1) * 20 = 0end = 20,对应的SQL为LIMIT 0, 20,即从第1条记录开始返回20条。

在Service层的selectPageBy方法中,isPage会被自动设置为true:

java
// 教学示例:Service层自动设置分页标志
public PageEntity<T> selectPageBy(Q queryDTO) {
    queryDTO.setIsPage(true);  // 启用分页
    handleQueryParam(queryDTO);
    PageEntity<T> pageEntity = new PageEntity<>(
        handleQueryResult(mapper.selectBy(queryDTO)),
        mapper.countBy(queryDTO)
    );
    pageEntity.setPage(queryDTO.getPage());
    pageEntity.setPageSize(queryDTO.getPageSize());
    return pageEntity;
}

这种设计的巧妙之处在于:同一个selectBy方法既可以用于分页查询(isPage=true),也可以用于全量查询(isPage=false)。在Service层中,selectPageBy方法设置isPage=true并调用countBy获取总数;而selectBy方法则设置isPage=false,只返回符合条件的列表。这种"一套SQL,两种用法"的设计避免了代码重复。

1.2.4 countBy与uniqueBy的协作

除了selectBy之外,smart-scaffold还定义了countByuniqueBy两个辅助查询方法,它们与selectBy共享同一个Base_Where片段:

xml
<!-- 教学示例:countBy计数查询 -->
<select id="countBy" parameterType="com.example.dto.ModelQueryDTO"
        resultType="java.lang.Integer">
    select count(1) from user_model where 1=1
    <include refid="Base_Where" />
</select>

<!-- 教学示例:uniqueBy唯一查询 -->
<select id="uniqueBy" parameterType="com.example.dto.ModelQueryDTO"
        resultMap="ResultMapWithDTO">
    select <include refid="Base_Column_List" />
    from user_model where 1=1
    <include refid="Base_Where" />
    limit 1
</select>

countBy用于获取符合条件的记录总数,配合selectBy实现分页查询的总页数计算。uniqueBy则用于获取符合条件的唯一一条记录,通过limit 1确保即使有多条匹配也只返回第一条。

这三个方法共同构成了一个完整的查询体系:selectBy负责列表查询(可选分页),countBy负责计数,uniqueBy负责单条查询。它们共享相同的查询条件逻辑(Base_Where),确保了查询结果的一致性。

1.3 resultMap继承与Entity-DTO映射

在MyBatis中,resultMap是数据库列与Java对象属性之间的映射桥梁。随着项目规模的增长,一个表可能对应多种映射需求——例如,Entity映射用于数据持久化操作,DTO映射用于业务层传输。如果为每种映射需求都定义一个完整的resultMap,不仅代码冗余,而且维护成本高。

smart-scaffold项目通过resultMapextends属性实现了映射关系的继承复用。

1.3.1 BaseResultMap的定义

BaseResultMap是每个Mapper XML中的基础映射定义,它建立了数据库列与Entity实体类属性之间的对应关系:

xml
<!-- 教学示例:BaseResultMap基础映射 -->
<resultMap id="BaseResultMap" type="com.example.entity.UserModel">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="time_create" jdbcType="TIMESTAMP" property="timeCreate" />
    <result column="time_update" jdbcType="TIMESTAMP" property="timeUpdate" />
    <result column="admin_id" jdbcType="VARCHAR" property="adminId" />
    <result column="admin_name" jdbcType="VARCHAR" property="adminName" />
    <result column="user_id" jdbcType="BIGINT" property="userId" />
    <result column="api_type" jdbcType="VARCHAR" property="apiType" />
    <result column="config_name" jdbcType="VARCHAR" property="configName" />
    <result column="model_name" jdbcType="VARCHAR" property="modelName" />
    <result column="is_default" jdbcType="BIT" property="isDefault" />
    <result column="status" jdbcType="BIT" property="status" />
</resultMap>

BaseResultMap遵循了MyBatis Generator的命名约定:id元素用于标识主键列,result元素用于标识普通列。每个映射都显式指定了jdbcType,这是一种良好的实践——虽然MyBatis可以自动推断类型,但显式声明可以避免某些边界情况下的类型转换错误。

1.3.2 ResultMapWithDTO的继承设计

在BaseResultMap的基础上,项目定义了一个扩展的resultMap,专门用于DTO映射:

xml
<!-- 教学示例:继承BaseResultMap的DTO映射 -->
<resultMap id="ResultMapWithDTO" extends="BaseResultMap"
           type="com.example.dto.UserModelDTO" />

这行看似简单的配置,背后蕴含着精心的设计思考。extends="BaseResultMap"表示ResultMapWithDTO继承了BaseResultMap的所有列映射关系,而type属性则指向了DTO类。由于DTO类继承了Entity类,属性完全一致,因此不需要额外的映射配置。

Java层面的继承关系如下:

java
// 教学示例:Entity与DTO的继承关系
// Entity基类
public class UserModel extends ModelPO implements Serializable {
    private Long id;
    private Date timeCreate;
    private Date timeUpdate;
    private String adminId;
    private String adminName;
    private Long userId;
    private Boolean isDefault;
    private Boolean status;
}

// DTO继承Entity
public class UserModelDTO extends UserModel {
    // 继承UserModel的所有属性
    // 可在此扩展业务所需的额外字段
}

这种"Entity-DTO继承"的设计模式在中小型项目中非常实用。它的核心思想是:DTO作为Entity的子类,天然拥有Entity的所有属性,因此resultMap可以直接复用。当业务需要扩展字段时,只需在DTO中添加新属性,并在ResultMapWithDTO中补充映射即可,不影响BaseResultMap的定义。

1.3.3 多层继承的Entity设计

在smart-scaffold项目中,Entity类本身也采用了继承设计。以UserModel为例,它继承了ModelPO基类:

java
// 教学示例:ModelPO基类封装AI模型通用属性
public class ModelPO implements Serializable {
    private String apiType;         // AI提供商类型
    private String configName;      // 配置名称
    private String apiKey;          // API密钥
    private String baseUrl;         // API基础URL
    private String modelName;       // 模型名称
    private BigDecimal temperature; // 温度参数
    private Integer maxTokens;      // 最大Token数
    private String embeddingModel;  // 嵌入模型名称
    private String remark;          // 备注信息
}

ModelPO封装了AI模型配置的通用属性,这些属性在所有与AI模型相关的Entity中都会用到。通过继承ModelPO,UserModel自动拥有了这些属性,避免了重复定义。这种设计在涉及多张表共享相同字段组的场景中特别有价值。

继承链路为:ModelPO -> UserModel -> UserModelDTO。每一层都有明确的职责:

  • ModelPO:封装跨表的通用字段组(AI模型配置属性)
  • UserModel:定义user_model表特有的字段(id、时间、用户关联、状态等)
  • UserModelDTO:继承UserModel,用于业务层数据传输

1.3.4 BaseMapper泛型接口的统一抽象

在Java接口层面,smart-scaffold项目定义了一个泛型的BaseMapper接口,统一了所有Mapper的基本操作:

java
// 教学示例:BaseMapper泛型接口
public interface BaseMapper<T, Q extends PageDTO> {
    Integer insertSelective(T record);
    Integer deleteByPrimaryKey(Long id);
    T selectByPrimaryKey(Long id);
    Integer updateByPrimaryKeySelective(T record);
    Integer updateByPrimaryKey(T record);
    List<T> selectBy(Q queryDTO);
    Integer countBy(Q queryDTO);
    T uniqueBy(Q queryDTO);
}

BaseMapper定义了两个泛型参数:T表示数据传输对象类型(通常是DTO),Q表示查询条件类型(通常是QueryDTO,继承自PageDTO)。这种泛型设计使得具体的Mapper接口可以非常简洁:

java
// 教学示例:具体Mapper接口只需继承BaseMapper
@Mapper
public interface UserModelMapper extends BaseMapper<UserModelDTO, UserModelQueryDTO> {
    // 所有通用方法已在BaseMapper中定义
    // 如需自定义方法,可在此添加
}

这种"接口继承+泛型约束"的设计模式,使得每个业务Mapper接口只需一行代码即可获得完整的CRUD能力。BaseMapper中定义的selectByPrimaryKeyselectBycountByuniqueBy等方法与Mapper XML中的SQL语句一一对应,通过MyBatis的自动绑定机制实现关联。

1.4 MyBatis Generator集成与代码生成策略

MyBatis Generator(MBG)是MyBatis官方提供的代码生成工具,能够根据数据库表结构自动生成Entity、Mapper接口和Mapper XML文件。smart-scaffold项目集成了MBG,并对其生成策略进行了定制化配置。

1.4.1 generatorConfig.xml配置解析

generatorConfig.xml是MBG的核心配置文件,它定义了数据库连接信息、生成策略和输出路径:

xml
<!-- 教学示例:generatorConfig.xml核心配置 -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC
    "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
    "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!-- 指定数据库驱动JAR路径 -->
    <classPathEntry location="${user.home}/.m2/repository/mysql/mysql-connector-java/8.0.21/mysql-connector-java-8.0.21.jar" />

    <context id="MySql" targetRuntime="MyBatis3">
        <!-- 注释生成配置:抑制所有自动生成的注释 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true" />
        </commentGenerator>

        <!-- 数据库连接配置 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
            connectionURL="jdbc:mysql://192.168.1.30:3306/smart_scaffold_1"
            userId="root"
            password="your_password" />

        <!-- Entity生成配置 -->
        <javaModelGenerator targetPackage="com.example.dao.entity.db1"
            targetProject="smart-scaffold-dao">
            <property name="enableSubPackages" value="false" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <!-- Mapper XML生成配置 -->
        <sqlMapGenerator targetPackage="mapper"
            targetProject="smart-scaffold-dao/src/main/resources">
            <property name="enableSubPackages" value="false" />
        </sqlMapGenerator>

        <!-- Mapper接口生成配置 -->
        <javaClientGenerator targetPackage="com.example.dao.mapper.db1"
            targetProject="smart-scaffold-dao" type="XMLMAPPER">
            <property name="enableSubPackages" value="false" />
        </javaClientGenerator>

        <!-- 表生成配置 -->
        <table tableName="user_model" domainObjectName="UserModel"
            enableCountByExample="false"
            enableUpdateByExample="false"
            enableDeleteByExample="false"
            enableSelectByExample="false"
            selectByExampleQueryId="false">
            <property name="useActualColumnNames" value="false" />
        </table>
    </context>
</generatorConfiguration>

1.4.2 生成策略的关键配置项

suppressAllComments=true: 这个配置抑制了MBG自动生成的所有注释。虽然注释在一般场景下是有益的,但MBG生成的注释通常包含时间戳和警告信息,对于版本控制不太友好。项目选择抑制默认注释,转而使用自定义的JavaDoc注释。

trimStrings=true: 这个配置会对字符串类型的getter方法进行自动trim处理。当数据库中存储的字符串值包含前后空格时,Entity的getter方法会自动去除这些空格。这在处理用户输入数据时特别有用,可以有效避免因多余空格导致的数据不一致问题。

useActualColumnNames=false: 这个配置表示不使用数据库的实际列名作为Entity属性名,而是使用MBG的驼峰转换规则。例如,数据库列time_create会被转换为Java属性timeCreateapi_type会被转换为apiType。这种命名转换符合Java的命名规范,使代码更加自然。

Example相关配置全部关闭: enableCountByExampleenableUpdateByExampleenableDeleteByExampleenableSelectByExampleselectByExampleQueryId这五个属性全部设置为false。这意味着MBG不会生成Example相关的类和方法。smart-scaffold项目选择了自定义的QueryDTO + Base_Where方案来替代MBG的Example机制,这种选择基于以下考虑:

  • Example类的API较为复杂,学习成本高
  • 自定义QueryDTO可以更灵活地控制查询条件
  • Base_Where片段提供了更好的SQL可控性和可读性

1.4.3 生成后的定制化改造

MBG生成的代码只是起点,smart-scaffold项目在生成代码的基础上进行了大量的定制化改造:

第一,添加自定义SQL片段。 在MBG生成的BaseResultMap和Base_Column_List之后,项目添加了自定义的Base_Where片段、ResultMapWithDTO映射、selectBy/countBy/uniqueBy查询语句。这些自定义内容被标注在<!-- 自定义,以下 --><!-- 自定义,以上 -->注释之间,与MBG生成的代码形成清晰的分界。

第二,修改resultMap的type。 MBG生成的BaseResultMap的type指向Entity类,而项目将selectByPrimaryKey等查询语句的resultMap修改为ResultMapWithDTO,使查询结果直接映射为DTO对象。

第三,改造Mapper接口。 MBG生成的Mapper接口只包含基本的CRUD方法,项目将其改造为继承BaseMapper泛型接口,并添加了selectBy、countBy、uniqueBy等自定义方法的声明。

这种"先生成,再改造"的工作流程兼顾了效率和灵活性:MBG负责生成基础代码(建表语句到Java代码的映射),开发者则在此基础上添加业务定制逻辑。

1.4.4 多数据源场景下的生成策略

smart-scaffold项目采用了多数据源架构,不同的数据库对应不同的Entity和Mapper。MBG的配置通过targetPackage的包路径来区分数据源:

db1数据源:
  Entity包路径:com.example.dao.entity.db1
  Mapper包路径:com.example.dao.mapper.db1

db2数据源:
  Entity包路径:com.example.dao.entity.db2
  Mapper包路径:com.example.dao.mapper.db2

Mapper XML则通过目录结构来区分:

resources/
  mapper/
    db1/
      UserModelMapper.xml
    db2/
      DepartmentInfoMapper.xml

当需要为db2数据源生成代码时,只需修改generatorConfig.xml中的数据库连接URL、targetPackage和table配置,然后重新运行MBG即可。每个数据源的生成配置相互独立,互不影响。


二、Spring多环境配置体系

2.1 Spring Profile配置机制

Spring Boot的Profile机制是多环境配置管理的核心。通过定义多个以application-{profile}.yml命名的配置文件,并在主配置文件中通过spring.profiles.active指定激活的Profile,可以实现不同环境配置的灵活切换。

smart-scaffold项目定义了四个配置文件,分别对应主配置和三个环境配置:

application.yml          # 主配置文件(共享配置)
application-dev.yml      # 开发环境配置
application-qa.yml       # 测试环境配置
application-prd.yml      # 生产环境配置

2.1.1 application.yml主配置文件

主配置文件是所有环境共享的配置基础,它定义了不随环境变化的通用配置项:

yaml
# 教学示例:application.yml主配置
project:
  name: smart-scaffold-springboot

spring:
  profiles:
    active: dev  # 默认使用开发环境

  # 数据源配置(共享部分:驱动和凭据)
  datasource:
    db1:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: ${DB_PASSWORD:default_password}
    db2:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: ${DB_PASSWORD:default_password}

  # 日志配置
  logging:
    level:
      org.springframework.ai: info
      cc.bima.scaffold.service.rocketmq: debug

  # Spring AI 向量存储配置
  ai:
    vectorstore:
      chroma:
        tenant-name: default_tenant
        database-name: default_database

  # Kafka 配置(共享部分:序列化器)
  kafka:
    consumer:
      group-id: smart-scaffold-kafka-group
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

  # RabbitMQ 配置(共享部分:端口和凭据)
  rabbitmq:
    port: 5672
    username: admin
    password: admin

# RocketMQ 配置(共享部分)
rocketmq:
  producer:
    group: smart-scaffold-rocketmq-group

# AI 模型配置(共享部分)
bima:
  ai:
    default-api-type: OLLAMA
    ollama:
      embedding-url: http://192.168.1.99:11434
      embedding-name: nomic-embed-text
    openai:
      base-url: https://api.openai.com
      model-name: gpt-4o
      embedding-model: text-embedding-3-small
      temperature: 0.7
      max-tokens: 4096
    compatible-openai:
      base-url: ""
      model-name: ""
      embedding-model: ""
      api-key: ""
      temperature: 0.7
      max-tokens: 4096

  # OAuth2 配置(共享部分)
  oauth2:
    client-id: oauth2-clientId-bima-web
    client-secret: oauth2-clientSecret-bima-web
    client-url: http://smart-scaffold.bima.cc:8080
    callback-url: ${bima.oauth2.client-url}/callback
    cas:
      authorize-url: https://cas.bima.cc:8443/cas/oauth2.0/authorize
      token-url: https://cas.bima.cc:8443/cas/oauth2.0/accessToken
      profile-url: https://cas.bima.cc:8443/cas/oauth2.0/profile

主配置文件的设计遵循了一个核心原则:将不随环境变化的配置放在主配置中,将随环境变化的配置放在环境配置中。这种分层策略确保了配置的最小化重复和最大化的可维护性。

2.1.2 application-dev.yml开发环境配置

开发环境配置主要定义了各中间件的连接地址,这些地址通常指向开发服务器或本地服务:

yaml
# 教学示例:application-dev.yml开发环境
server:
  port: 8080

spring:
  datasource:
    db1:
      jdbc-url: jdbc:mysql://192.168.1.30:3306/smart_scaffold_1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
    db2:
      jdbc-url: jdbc:mysql://192.168.1.30:3306/smart_scaffold_2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8

  ai:
    vectorstore:
      chroma:
        base-url: http://192.168.1.30:8000

  elasticsearch:
    uris: http://192.168.1.30:9200

  data:
    mongodb:
      uri: mongodb://root:password@192.168.1.30:27017/test_db?authSource=admin
    redis:
      host: 192.168.1.30
      port: 6379

  kafka:
    bootstrap-servers: 192.168.1.30:9092

  rabbitmq:
    host: 192.168.1.30

rocketmq:
  name-server: 192.168.1.30:9876

bima:
  ai:
    ollama:
      model-url: http://192.168.1.99:11434
      model-name: qwen2.5:7b-instruct-q4_k_m
      api-key: ""
      temperature: 0.7
      max-tokens: 4096

开发环境配置的特点是:所有中间件的地址都指向同一个内网IP(192.168.1.30),这是一个典型的开发环境部署模式——所有中间件服务部署在同一台或同一网段的服务器上,便于开发和调试。Ollama模型服务则指向另一台开发机(192.168.1.99),这是因为AI模型推理通常需要GPU资源,与普通中间件分开部署。

2.1.3 application-qa.yml测试环境配置

测试环境配置与开发环境配置在结构上基本一致,主要差异在于端口号:

yaml
# 教学示例:application-qa.yml测试环境
server:
  port: 8081

spring:
  datasource:
    db1:
      jdbc-url: jdbc:mysql://192.168.1.30:3306/smart_scaffold_1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
    db2:
      jdbc-url: jdbc:mysql://192.168.1.30:3306/smart_scaffold_2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
  # ... 其他中间件配置与dev环境相同

测试环境使用8081端口,与开发环境的8080端口错开,使得两个环境可以同时运行在同一台服务器上。在实际项目中,测试环境通常会使用独立的数据库实例,以避免测试数据污染开发数据。但在smart-scaffold项目中,由于是脚手架演示项目,测试环境和开发环境暂时共享了相同的中间件地址。

2.1.4 application-prd.yml生产环境配置

生产环境配置是最关键的配置文件,它与开发/测试环境有显著差异:

yaml
# 教学示例:application-prd.yml生产环境
server:
  port: 8082

spring:
  datasource:
    db1:
      jdbc-url: jdbc:mysql://prd-db:3306/smart_scaffold_1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
    db2:
      jdbc-url: jdbc:mysql://prd-db:3306/smart_scaffold_2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8

  logging:
    level:
      org.springframework.ai: warn
      cc.bima.scaffold.service.rocketmq: info

  ai:
    vectorstore:
      chroma:
        base-url: http://prd-chroma:8000

  elasticsearch:
    uris: http://prd-elasticsearch:9200

  data:
    mongodb:
      uri: mongodb://root:password@prd-mongodb:27017/test_db?authSource=admin
    redis:
      host: prd-redis
      port: 6379

  kafka:
    bootstrap-servers: prd-kafka:9092

  rabbitmq:
    host: prd-rabbitmq

rocketmq:
  name-server: prd-rocketmq:9876

bima:
  ai:
    ollama:
      model-url: http://prd-ollama:11434
      model-name: qwen2.5:7b-instruct-q4_k_m
      api-key: ""
      temperature: 0.7
      max-tokens: 4096

生产环境配置的几个关键特征:

使用主机名替代IP地址。 所有中间件的地址从IP地址(如192.168.1.30)变为主机名(如prd-db、prd-redis)。这是一种最佳实践——主机名通过DNS或Docker网络解析,具有更好的可移植性。当中间件服务迁移到新的服务器时,只需修改DNS记录或Docker网络配置,无需修改应用配置。

日志级别调整。 生产环境的日志级别从info/debug调整为warn/info,减少了日志输出量,降低了磁盘IO压力。这是生产环境日志管理的基本原则——只记录重要的警告和错误信息。

独立端口。 生产环境使用8082端口,与开发(8080)和测试(8081)环境区分,便于在同一台机器上进行多环境部署(虽然在生产环境中通常不会这样做)。

2.1.5 Profile切换的多种方式

Spring Boot提供了多种Profile切换方式,适用于不同的部署场景:

方式一:配置文件指定(推荐用于默认环境)。 在application.yml中设置spring.profiles.active: dev,这是最简单的方式,适用于本地开发阶段。

方式二:命令行参数(推荐用于生产部署)。 启动时通过命令行参数指定:

bash
java -jar smart-scaffold-web.jar --spring.profiles.active=prd

这种方式不修改配置文件,适用于CI/CD流水线中的自动化部署。

方式三:环境变量(推荐用于容器化部署)。 通过设置操作系统环境变量:

bash
export SPRING_PROFILES_ACTIVE=prd
java -jar smart-scaffold-web.jar

在Docker/Kubernetes环境中,可以通过环境变量注入的方式设置Profile。

方式四:JVM系统属性。 通过JVM启动参数指定:

bash
java -Dspring.profiles.active=prd -jar smart-scaffold-web.jar

smart-scaffold项目默认使用dev环境,这在开发阶段非常方便——开发者无需额外配置即可启动项目。当项目部署到测试或生产环境时,通过命令行参数或环境变量切换到对应的Profile。

2.2 配置分层策略

配置分层是多环境配置体系的核心设计思想。smart-scaffold项目将配置项按照"是否随环境变化"这一标准,划分为"共享配置"和"环境配置"两个层次。这种分层策略不仅减少了配置的重复,更重要的是建立了一套清晰的配置管理规范。

2.2.1 数据源配置的分层

数据源配置是分层策略的典型应用场景。smart-scaffold项目将数据源配置拆分为两部分:

共享配置(application.yml):

yaml
spring:
  datasource:
    db1:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: ${DB_PASSWORD:default_password}
    db2:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: ${DB_PASSWORD:default_password}

环境配置(application-{env}.yml):

yaml
spring:
  datasource:
    db1:
      jdbc-url: jdbc:mysql://192.168.1.30:3306/smart_scaffold_1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
    db2:
      jdbc-url: jdbc:mysql://192.168.1.30:3306/smart_scaffold_2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8

这种分层的逻辑是:

  • 驱动类名(driver-class-name) 在所有环境中都相同,放在主配置中
  • 用户名和密码(username/password) 在所有环境中都相同(使用同一套凭据),放在主配置中
  • JDBC URL(jdbc-url) 包含数据库主机地址,随环境变化,放在环境配置中

密码字段使用了${DB_PASSWORD:default_password}的占位符语法,支持通过环境变量覆盖。冒号后面的值是默认值,当环境变量DB_PASSWORD未设置时使用默认值。这种设计既保持了配置文件的完整性,又为外部化密码管理提供了入口。

2.2.2 中间件配置的分层

中间件配置的分层更加清晰——所有中间件的连接地址都随环境变化,因此全部放在环境配置中;而与地址无关的通用配置(如序列化器、消费者组ID等)则放在主配置中。

Kafka配置的分层示例:

主配置(共享部分):

yaml
spring:
  kafka:
    consumer:
      group-id: smart-scaffold-kafka-group
      auto-offset-reset: earliest
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    producer:
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer

环境配置(随环境变化的部分):

yaml
# dev环境
spring:
  kafka:
    bootstrap-servers: 192.168.1.30:9092

# prd环境
spring:
  kafka:
    bootstrap-servers: prd-kafka:9092

消费者组ID(group-id)在所有环境中保持不变,因为它是应用级别的标识,不随部署环境变化。而bootstrap-servers(Kafka集群地址)则随环境变化,因此放在环境配置中。

同样的分层逻辑也适用于RabbitMQ、Redis、MongoDB、Elasticsearch等其他中间件。RabbitMQ的端口(5672)和凭据放在主配置中,host地址放在环境配置中;Redis的端口(6379)放在环境配置中(虽然端口号不变,但与host地址放在一起更便于整体管理)。

2.2.3 AI模型配置的分层

AI模型配置的分层体现了业务特性的考虑:

主配置(共享部分):

yaml
bima:
  ai:
    default-api-type: OLLAMA
    ollama:
      embedding-url: http://192.168.1.99:11434
      embedding-name: nomic-embed-text
    openai:
      base-url: https://api.openai.com
      model-name: gpt-4o
      embedding-model: text-embedding-3-small
      temperature: 0.7
      max-tokens: 4096

环境配置(随环境变化的部分):

yaml
bima:
  ai:
    ollama:
      model-url: http://192.168.1.99:11434
      model-name: qwen2.5:7b-instruct-q4_k_m
      temperature: 0.7
      max-tokens: 4096

主配置中定义了默认的API类型(OLLAMA)和各提供商的通用配置。OpenAI的配置(base-url、model-name等)在所有环境中都相同(因为调用的是同一个OpenAI服务),因此放在主配置中。Ollama的模型URL和模型名称则可能因环境而异(开发环境可能使用本地GPU服务器,生产环境可能使用专用的推理服务器),因此放在环境配置中。

2.2.4 OAuth2配置的分层

OAuth2配置是典型的"全量共享"配置——在所有环境中都使用同一套OAuth2参数:

yaml
bima:
  oauth2:
    client-id: oauth2-clientId-bima-web
    client-secret: oauth2-clientSecret-bima-web
    client-url: http://smart-scaffold.bima.cc:8080
    callback-url: ${bima.oauth2.client-url}/callback
    cas:
      authorize-url: https://cas.bima.cc:8443/cas/oauth2.0/authorize
      token-url: https://cas.bima.cc:8443/cas/oauth2.0/accessToken
      profile-url: https://cas.bima.cc:8443/cas/oauth2.0/profile

OAuth2的client-id和client-secret在所有环境中保持一致,因为它们是应用在CAS服务器上注册的身份标识。CAS服务器的地址(cas.bima.cc)也是固定的,不随部署环境变化。

callback-url使用了Spring的属性引用语法${bima.oauth2.client-url}/callback,它引用了同一配置文件中的client-url属性。这种设计确保了callback-url始终与client-url保持同步——当client-url变化时,callback-url会自动更新。

2.3 Maven资源过滤机制

Maven资源过滤(Resource Filtering)是连接Maven构建过程与Spring配置体系的重要桥梁。通过在pom.xml中配置资源过滤,可以在构建时动态替换配置文件中的占位符变量,实现构建级别的配置定制。

2.3.1 filtering=true的作用与原理

当Maven的<filtering>设置为true时,Maven在处理资源文件时会扫描文件内容,将${variable}格式的占位符替换为对应的值。这些值可以来自pom.xml中的<properties><profiles>配置,也可以来自系统属性和环境变量。

smart-scaffold项目的dao模块pom.xml中配置了资源过滤:

xml
<!-- 教学示例:dao模块的Maven资源配置 -->
<build>
    <resources>
        <!-- Java源码目录中的XML文件(Mapper XML),禁用过滤 -->
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
        <!-- resources目录下的所有文件,启用过滤 -->
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>

这里定义了两个资源目录,分别采用了不同的过滤策略:

第一个resource: 指向src/main/java目录,包含**/*.xml文件(即Mapper XML文件),filtering=false。这意味着Mapper XML文件会被原样复制到输出目录,不进行任何占位符替换。这是正确的做法——Mapper XML中包含MyBatis的#{}${}语法,如果启用过滤,Maven会错误地将这些MyBatis占位符当作Maven变量进行替换,导致SQL语句损坏。

第二个resource: 指向src/main/resources目录,包含所有文件,filtering=true。这意味着resources目录下的配置文件(如application.yml、generatorConfig.xml等)在构建时会进行占位符替换。

2.3.2 web模块的精细化资源过滤

web模块的pom.xml采用了更精细化的资源过滤策略:

xml
<!-- 教学示例:web模块的精细化资源配置 -->
<build>
    <resources>
        <!-- 配置文件,启用过滤 -->
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>*.yml</include>
                <include>*.properties</include>
                <include>templates/**/*</include>
            </includes>
            <filtering>true</filtering>
        </resource>
        <!-- 静态资源,禁用过滤 -->
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>static/**/*</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

web模块将src/main/resources目录按照文件类型进行了细分:

  • yml和properties文件:启用过滤,支持占位符替换
  • templates目录(Thymeleaf模板):启用过滤,支持在模板中使用Maven变量
  • static目录(静态资源如JS、CSS、图片):禁用过滤,原样复制

静态资源禁用过滤的原因是:二进制文件(如图片)中可能偶然包含${字符序列,如果启用过滤,Maven会尝试替换这些字符,导致文件损坏。即使对于文本类型的静态资源(JS、CSS),通常也不需要占位符替换,禁用过滤可以提高构建速度。

2.3.3 Mapper XML与Java同目录存放

smart-scaffold项目采用了一种非传统的Mapper XML存放方式——将Mapper XML文件与Java接口放在同一目录下,而不是放在resources目录中:

smart-scaffold-dao/
  src/main/java/
    cc/bima/scaffold/dao/
      mapper/
        db1/
          UserModelMapper.java    # Mapper接口
          UserModelMapper.xml     # Mapper XML(与接口同目录)
        db2/
          DepartmentInfoMapper.java
          DepartmentInfoMapper.xml
  src/main/resources/
    generatorConfig.xml           # MBG配置文件
    mapper/                       # MBG生成的XML输出目录
      db1/
        UserModelMapper.xml
      db2/
        DepartmentInfoMapper.xml

这种"同目录存放"的方式需要配合Maven的资源过滤配置才能正常工作。在pom.xml中,第一个resource配置将src/main/java目录下的XML文件包含进来,并设置filtering=false。这样,Maven在编译时会将这些XML文件复制到class输出目录中,MyBatis就可以通过类路径加载它们。

同目录存放的优势在于:

  • 导航便捷: 在IDE中,Mapper接口和对应的XML文件在同一目录下,切换非常方便
  • 包路径一致: XML的namespace与Java接口的全限定名天然对应,不需要手动维护
  • 重构友好: 当使用IDE的重构功能移动或重命名Mapper接口时,XML文件可以同步移动

需要注意的是,这种方式要求在Spring Boot的配置中正确设置Mapper XML的扫描路径。smart-scaffold项目通过MyBatis的mapper-locations配置项来指定XML文件的加载路径。

2.3.4 资源过滤与Spring Profile的协同

Maven资源过滤与Spring Profile是两个不同层次的配置机制,它们可以协同工作:

  • Maven资源过滤在构建时生效,将${variable}替换为具体值,生成最终的配置文件
  • Spring Profile在运行时生效,根据激活的Profile加载对应的配置文件

两者的协同体现在:Maven资源过滤可以用于注入构建时的信息(如项目版本号、构建时间等),而Spring Profile则用于管理运行时的环境差异。在smart-scaffold项目中,Maven资源过滤主要用于确保配置文件中的占位符(如${DB_PASSWORD})在构建时被正确处理,而Spring Profile则负责在不同环境中加载不同的配置。


三、数据库设计

3.1 smart-scaffold.sql建表脚本解析

数据库设计是整个数据持久层的根基。smart-scaffold项目提供了完整的建表脚本,定义了项目的核心数据结构。通过分析这些脚本,我们可以理解项目的数据模型设计理念和字段规范。

3.1.1 user_model表设计

user_model表是smart-scaffold项目的核心业务表,用于存储用户的AI大模型配置信息:

sql
-- 教学示例:user_model建表语句
CREATE TABLE `user_model` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '模型ID,自增主键',
  `time_create` datetime NOT NULL COMMENT '创建时间',
  `time_update` datetime NOT NULL COMMENT '更新时间',
  `admin_id` varchar(20) DEFAULT NULL COMMENT '操作人ID',
  `admin_name` varchar(20) DEFAULT NULL COMMENT '操作人',
  `user_id` bigint NOT NULL COMMENT '用户ID (0表示系统配置)',
  `api_type` varchar(50) NOT NULL COMMENT 'AI提供商类型: OPENAI, OLLAMA等',
  `config_name` varchar(100) NOT NULL COMMENT '配置名称',
  `api_key` varchar(500) DEFAULT NULL COMMENT 'API密钥 (AES-256加密存储)',
  `base_url` varchar(500) DEFAULT NULL COMMENT 'API基础URL',
  `model_name` varchar(100) DEFAULT NULL COMMENT '模型名称',
  `temperature` decimal(3,2) DEFAULT '0.70' COMMENT '温度参数 (0.0-2.0)',
  `max_tokens` int DEFAULT '4096' COMMENT '最大Token数',
  `embedding_model` varchar(100) DEFAULT NULL COMMENT '嵌入模型名称',
  `is_default` tinyint(1) DEFAULT '0' COMMENT '是否为默认配置',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态: 1-禁用, 0-启用',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  COMMENT='用户大模型配置表';

字段设计分析:

主键设计: 使用bigint unsigned NOT NULL AUTO_INCREMENT作为主键,这是MySQL中最常用的自增主键方案。unsigned限定为非负整数, effectively 将主键范围扩大了一倍。自增主键的优势在于插入性能好、占用空间小、对B+树索引友好。

时间审计字段: time_createtime_update是标准的审计字段,分别记录记录的创建时间和最后更新时间。两个字段都设置为NOT NULL,确保每条记录都有明确的时间戳。在实际应用中,这两个字段通常由应用层在插入和更新时自动设置,而非依赖数据库的默认值或触发器。

操作人字段: admin_idadmin_name记录了最后操作该记录的管理员信息。这种"冗余存储"的设计在审计场景中很常见——即使管理员的信息在其他表中发生了变化,历史操作记录仍然可以追溯到当时的操作人。

业务字段: api_type定义了AI提供商类型(如OPENAI、OLLAMA),config_name是用户自定义的配置名称,api_key存储API密钥(注释中标注了AES-256加密存储),base_url是API的基础URL,model_name是具体的模型名称。这些字段共同描述了一个完整的AI模型配置。

参数字段: temperature使用decimal(3,2)类型,精度为小数点后两位,取值范围0.00到9.99,满足温度参数的需求(通常在0.0到2.0之间)。max_tokens使用int类型,默认值4096,表示单次请求的最大Token数。

状态字段: is_default标记是否为用户的默认配置(每个用户只能有一个默认配置),status标记配置的启用/禁用状态。注意status的值定义:0表示启用,1表示禁用,这是一种"非标准"的约定(通常0表示禁用,1表示启用),项目选择了相反的定义。

字符集选择: 使用utf8mb4字符集和utf8mb4_unicode_ci排序规则。utf8mb4是MySQL中真正的UTF-8编码,支持4字节的Unicode字符(包括emoji),而MySQL的utf8只支持3字节字符。unicode_ci排序规则提供了不区分大小写的比较,适用于大多数业务场景。

3.1.2 department_info表设计

department_info表用于存储部门信息,是一个典型的树形结构数据表:

sql
-- 教学示例:department_info建表语句
CREATE TABLE `department_info` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '部门ID,自增主键',
  `time_create` datetime NOT NULL COMMENT '创建时间',
  `time_update` datetime NOT NULL COMMENT '更新时间',
  `admin_id` varchar(20) DEFAULT NULL COMMENT '操作人ID',
  `admin_name` varchar(20) DEFAULT NULL COMMENT '操作人',
  `up_id` bigint unsigned DEFAULT NULL COMMENT '上级部门ID',
  `name` varchar(20) DEFAULT NULL COMMENT '部门名称',
  `remark` longtext COMMENT '部门备注',
  `freeze` tinyint NOT NULL COMMENT '是否冻结,0正常,1冻结',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

树形结构设计: up_id字段存储上级部门的ID,通过这种"邻接表"模式实现树形结构。邻接表是最简单的树形结构存储方案,每个节点只记录其父节点的ID。查询某节点的所有子节点需要递归查询,MySQL 8.0提供了CTE(Common Table Expression)语法来简化递归查询。

字段对比: 与user_model表相比,department_info表的结构更简单,但共享了相同的基础字段模式(id、time_create、time_update、admin_id、admin_name)。这种"基础字段标准化"的设计使得Base_Where片段可以在不同表的Mapper XML中保持一致的结构。

remark字段类型差异: department_info表的remark字段使用了longtext类型,而user_model表使用了varchar(500)。这反映了不同业务场景的需求差异——部门备注可能需要存储大量文本内容,而模型配置的备注通常较短。

字符集差异: department_info表使用了utf8mb4_0900_ai_ci排序规则,而user_model表使用了utf8mb4_unicode_ci0900_ai_ci是MySQL 8.0新增的排序规则,基于Unicode 9.0标准,性能更好。在实际项目中,建议统一使用同一种排序规则。

freeze字段: 使用tinyint NOT NULL(没有默认值),这意味着在插入记录时必须显式指定freeze的值。与user_model表的status字段(有默认值)不同,这种设计强制要求开发者明确指定部门的冻结状态。

3.1.3 索引策略分析

在当前的建表脚本中,两张表都只定义了主键索引,没有额外的二级索引。这种设计在数据量较小的场景下是合理的,但随着数据量的增长,可能需要添加以下索引:

user_model表的推荐索引:

sql
-- 教学示例:user_model表推荐索引
-- 用户维度查询索引(按用户查询其所有模型配置)
CREATE INDEX idx_user_id ON user_model(user_id);

-- 默认配置查询索引(快速定位用户的默认配置)
CREATE INDEX idx_user_default ON user_model(user_id, is_default);

-- API类型查询索引(按提供商类型筛选)
CREATE INDEX idx_api_type ON user_model(api_type);

-- 时间范围查询索引(支持按创建时间排序和范围查询)
CREATE INDEX idx_time_create ON user_model(time_create);

department_info表的推荐索引:

sql
-- 教学示例:department_info表推荐索引
-- 上级部门查询索引(查询某部门的所有子部门)
CREATE INDEX idx_up_id ON department_info(up_id);

-- 部门名称查询索引(支持按名称搜索)
CREATE INDEX idx_name ON department_info(name);

-- 冻结状态查询索引(筛选正常/冻结的部门)
CREATE INDEX idx_freeze ON department_info(freeze);

索引的设计需要根据实际的查询模式来决定。smart-scaffold项目的Base_Where片段中涉及的查询条件(如user_id、is_default、status、up_id、name、freeze等)都应该考虑建立对应的索引。但索引并非越多越好——每个索引都会增加写入开销和存储空间,需要在查询性能和写入性能之间取得平衡。

3.1.4 建表脚本的多数据库管理

smart-scaffold项目采用了多数据库架构,user_model表存储在smart_scaffold_1数据库中,department_info表存储在smart_scaffold_2数据库中。建表脚本将两个数据库的DDL合并在同一个SQL文件中,通过注释分隔:

sql
-- 教学示例:多数据库DDL组织方式
-- Database: smart_scaffold_1
CREATE TABLE `user_model` (
    -- ... 字段定义
);

-- Database: smart_scaffold_2
CREATE TABLE `department_info` (
    -- ... 字段定义
);

在实际执行时,需要分别连接到对应的数据库执行相应的DDL语句。这种组织方式虽然简单,但在数据库数量较多时可能不够清晰。更规范的做法是将每个数据库的DDL放在单独的文件中,或者使用数据库迁移工具(如Flyway、Liquibase)来管理数据库版本。

3.2 多数据源架构设计

smart-scaffold项目采用了多数据源架构,通过Spring Boot的配置机制实现了两个MySQL数据源的并行管理。这种架构在需要隔离不同业务数据的场景中非常常见。

3.2.1 数据源配置结构

多数据源配置在application.yml中的组织方式:

yaml
# 教学示例:多数据源配置
spring:
  datasource:
    db1:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: ${DB_PASSWORD:default_password}
    db2:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: ${DB_PASSWORD:default_password}

在环境配置中,每个数据源有独立的JDBC URL:

yaml
# 教学示例:环境配置中的多数据源URL
spring:
  datasource:
    db1:
      jdbc-url: jdbc:mysql://192.168.1.30:3306/smart_scaffold_1?...
    db2:
      jdbc-url: jdbc:mysql://192.168.1.30:3306/smart_scaffold_2?...

注意这里使用的是jdbc-url而非url。在Spring Boot的多数据源配置中,当使用自定义的DataSource Bean时,需要使用jdbc-url属性名(而非Spring Boot自动配置使用的url),否则可能导致配置无法正确绑定。

3.2.2 包路径与数据源的映射关系

smart-scaffold项目通过包路径来区分不同数据源的Mapper和Entity:

db1数据源(smart_scaffold_1数据库):
  Entity: cc.bima.scaffold.dao.entity.db1.*
  Mapper: cc.bima.scaffold.dao.mapper.db1.*
  DTO: cc.bima.scaffold.dao.dto.db1.*

db2数据源(smart_scaffold_2数据库):
  Entity: cc.bima.scaffold.dao.entity.db2.*
  Mapper: cc.bima.scaffold.dao.mapper.db2.*
  DTO: cc.bima.scaffold.dao.dto.db2.*

db1db2后缀清晰地标识了每个类所属的数据源。这种命名约定虽然简单,但在实际开发中非常有效——开发者只需看包名就能知道一个类操作的是哪个数据库。

对应的Mapper XML也通过目录结构进行了区分:

resources/mapper/
  db1/UserModelMapper.xml
  db2/DepartmentInfoMapper.xml

3.2.3 Base_Where片段在不同表中的适配

虽然Base_Where片段在不同表的Mapper XML中结构相似,但具体的查询条件会根据表的字段进行调整。以下是两个表的Base_Where对比:

user_model表的Base_Where:

  • 支持id精确查询
  • 支持timeCreate和timeUpdate时间范围查询
  • 支持userId精确查询
  • 支持modelName模糊查询
  • 支持isDefault和status布尔查询

department_info表的Base_Where:

  • 支持id精确查询
  • 支持timeCreate和timeUpdate时间范围查询
  • 支持upId精确查询(上级部门)
  • 支持name模糊查询(部门名称)
  • 支持freeze布尔查询(冻结状态)

两张表的Base_Where共享了相同的基础模式(id查询、时间范围查询),但业务条件根据表结构进行了定制。这种"统一模式,定制内容"的设计保证了代码风格的一致性,同时满足了不同表的查询需求。


四、BaseService通用服务层设计

4.1 泛型 BaseService 的架构价值

在smart-scaffold项目中,BaseService是与BaseMapper配套的通用服务层基类。它通过泛型参数约束和模板方法模式,为所有业务Service提供了统一的CRUD操作实现。

java
// 教学示例:BaseService泛型抽象类
public abstract class BaseService<M extends BaseMapper<T, Q>, T, Q extends PageDTO> {

    @Autowired
    protected M mapper;

    /** 通过主键查询 */
    public T get(Long id) {
        return handleQueryResult(mapper.selectByPrimaryKey(id), null);
    }

    /** 通过主键删除 */
    public void remove(Long id) {
        mapper.deleteByPrimaryKey(id);
    }

    /** 分页查询 */
    public PageEntity<T> selectPageBy(Q queryDTO) {
        queryDTO.setIsPage(true);
        handleQueryParam(queryDTO);
        PageEntity<T> pageEntity = new PageEntity<>(
            handleQueryResult(mapper.selectBy(queryDTO)),
            mapper.countBy(queryDTO)
        );
        pageEntity.setPage(queryDTO.getPage());
        pageEntity.setPageSize(queryDTO.getPageSize());
        return pageEntity;
    }

    /** 列表查询 */
    public List<T> selectBy(Q queryDTO) {
        queryDTO.setIsPage(false);
        return handleQueryResult(mapper.selectBy(queryDTO));
    }

    /** 唯一查询 */
    public T uniqueBy(Q queryDTO) {
        queryDTO.setIsPage(false);
        handleQueryParam(queryDTO);
        return handleQueryResult(mapper.uniqueBy(queryDTO));
    }

    /** 查询前的入参处理(子类可重写) */
    public Q handleQueryParam(Q queryDTO) {
        return queryDTO;
    }

    /** 查询后的结果处理(子类可重写) */
    public T handleQueryResult(T dto) {
        return dto;
    }

    /** 新增数据校验(子类可重写) */
    public BaseResult<?> checkSaveInput(T dto) {
        return BaseResult.success();
    }

    /** 删除数据校验(子类可重写) */
    public BaseResult<?> checkRemove(Long id) {
        return BaseResult.success();
    }
}

BaseService定义了三个泛型参数:

  • M:Mapper类型,必须继承BaseMapper
  • T:数据传输对象类型(DTO)
  • Q:查询条件类型,必须继承PageDTO

这种泛型设计使得具体的Service类可以非常简洁。例如,UserModelService只需继承BaseService并指定泛型参数,即可获得完整的CRUD能力,同时保留了通过重写handleQueryParamhandleQueryResult等方法进行定制的能力。

4.2 模板方法模式的应用

BaseService是模板方法模式(Template Method Pattern)的经典应用。模板方法模式的核心思想是:在基类中定义算法的骨架,将某些步骤延迟到子类中实现。

在BaseService中,selectPageBy方法就是模板方法:

java
// 教学示例:模板方法模式
public PageEntity<T> selectPageBy(Q queryDTO) {
    // 步骤1:设置分页标志(固定逻辑)
    queryDTO.setIsPage(true);
    // 步骤2:查询前的入参处理(可由子类定制)
    handleQueryParam(queryDTO);
    // 步骤3:执行查询和计数(固定逻辑)
    PageEntity<T> pageEntity = new PageEntity<>(
        handleQueryResult(mapper.selectBy(queryDTO)),  // 步骤3a:结果处理可定制
        mapper.countBy(queryDTO)
    );
    // 步骤4:设置分页信息(固定逻辑)
    pageEntity.setPage(queryDTO.getPage());
    pageEntity.setPageSize(queryDTO.getPageSize());
    return pageEntity;
}

handleQueryParamhandleQueryResult是"钩子方法",它们在基类中提供了默认实现(直接返回原值),子类可以根据业务需要重写这些方法。例如:

java
// 教学示例:子类重写钩子方法
public class UserModelService extends BaseService<UserModelMapper, UserModelDTO, UserModelQueryDTO> {

    @Override
    public UserModelQueryDTO handleQueryParam(UserModelQueryDTO queryDTO) {
        // 在查询前对参数进行预处理
        if (queryDTO.getModelName() != null) {
            queryDTO.setModelName(queryDTO.getModelName().trim());
        }
        return queryDTO;
    }

    @Override
    public UserModelDTO handleQueryResult(UserModelDTO dto, Boolean isList) {
        // 在查询后对结果进行后处理
        if (dto != null && dto.getApiKey() != null) {
            // 脱敏处理:隐藏API密钥的中间部分
            dto.setApiKey(maskApiKey(dto.getApiKey()));
        }
        return dto;
    }
}

4.3 PageEntity分页结果封装

PageEntity是分页查询的统一返回类型,它封装了分页查询的全部信息:

java
// 教学示例:PageEntity分页结果
public class PageEntity<T> implements Serializable {
    private List<T> list;       // 数据列表
    private Integer page;       // 当前页码
    private Integer pageSize;   // 每页大小
    private Integer count;      // 总记录数
}

PageEntity的设计遵循了"信息完整"原则——前端不仅需要数据列表,还需要当前页码、每页大小和总记录数来渲染分页组件。总记录数(count)通过单独的countBy查询获取,而不是从列表大小推算,这确保了分页信息的准确性。

4.4 查询参数校验机制

BaseService提供了checkSaveInputcheckRemove两个校验方法,用于在新增和删除操作前进行参数校验:

java
// 教学示例:参数校验的子类实现
public class UserModelService extends BaseService<UserModelMapper, UserModelDTO, UserModelQueryDTO> {

    @Override
    public BaseResult<?> checkSaveInput(UserModelDTO dto) {
        if (dto.getConfigName() == null || dto.getConfigName().isEmpty()) {
            return BaseResult.fail("配置名称不能为空");
        }
        if (dto.getApiType() == null || dto.getApiType().isEmpty()) {
            return BaseResult.fail("API类型不能为空");
        }
        return BaseResult.success();
    }

    @Override
    public BaseResult<?> checkRemove(Long id) {
        // 检查是否为系统默认配置,禁止删除
        UserModelDTO existing = mapper.selectByPrimaryKey(id);
        if (existing != null && existing.getIsDefault()) {
            return BaseResult.fail("默认配置不允许删除");
        }
        return BaseResult.success();
    }
}

这种校验机制将业务规则校验集中在Service层,避免了校验逻辑散落在Controller层的各个方法中。通过继承和多态,每个业务Service可以定义自己的校验规则,而调用方(通常是Controller)只需调用统一的校验方法即可。


五、项目工程化架构

5.1 Maven多模块工程结构

smart-scaffold-springboot项目采用了Maven多模块架构,将项目划分为四个子模块:

smart-scaffold-springboot/          # 父POM
  ├── smart-scaffold-common/        # 公共模块(Base类、工具类、常量)
  ├── smart-scaffold-dao/           # 数据访问模块(Entity、Mapper、DTO)
  ├── smart-scaffold-service/       # 业务逻辑模块(Service实现)
  └── smart-scaffold-web/           # Web模块(Controller、配置文件、启动类)

模块依赖关系:

  • common:无外部依赖,被所有其他模块依赖
  • dao:依赖common,包含MyBatis相关依赖
  • service:依赖dao,实现业务逻辑
  • web:依赖service,包含Controller和Spring Boot启动类

这种分层架构遵循了"依赖倒置"原则——上层模块依赖下层模块,下层模块不知道上层模块的存在。common模块作为最底层的模块,定义了BaseMapper、BaseService、PageDTO等基础类,被所有模块共享。

5.2 父POM的统一管理

父POM负责统一管理项目的版本和构建配置:

xml
<!-- 教学示例:父POM核心配置 -->
<project>
    <groupId>cc.bima.scaffold</groupId>
    <artifactId>smart-scaffold-springboot</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.12</version>
    </parent>

    <modules>
        <module>smart-scaffold-common</module>
        <module>smart-scaffold-dao</module>
        <module>smart-scaffold-service</module>
        <module>smart-scaffold-web</module>
    </modules>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>

父POM继承了spring-boot-starter-parent,获得了Spring Boot的依赖管理和插件管理能力。java.version设置为17,与Spring Boot 3.x的最低要求一致。project.build.sourceEncoding设置为UTF-8,确保编译时使用正确的字符编码。

5.3 各模块的职责与依赖

smart-scaffold-common模块:

  • 定义BaseMapper、BaseService、PageDTO、PageEntity等基础类
  • 定义ModelPO等业务基类
  • 定义Constants常量类
  • 不依赖任何业务模块,保持纯粹的通用性

smart-scaffold-dao模块:

  • 依赖common模块
  • 包含MyBatis相关依赖(mybatis-spring-boot-starter、mysql-connector-java、druid)
  • 存放Entity、DTO、QueryDTO、Mapper接口和Mapper XML
  • 配置MyBatis Generator
  • 配置Maven资源过滤(Mapper XML的filtering=false)

smart-scaffold-service模块:

  • 依赖dao模块
  • 存放业务Service实现类
  • 实现具体的业务逻辑和校验规则

smart-scaffold-web模块:

  • 依赖service模块
  • 包含Spring Boot启动类
  • 存放Controller类
  • 存放所有配置文件(application.yml等)
  • 包含WebFlux、Thymeleaf等Web相关依赖
  • 配置spring-boot-maven-plugin打包插件

5.4 构建与打包流程

smart-scaffold-web模块的pom.xml配置了Spring Boot打包插件:

xml
<!-- 教学示例:Spring Boot打包插件配置 -->
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <mainClass>cc.bima.scaffold.ScaffoldWebApplication</mainClass>
        <includeSystemScope>true</includeSystemScope>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
</plugin>

repackage目标将普通的JAR包重新打包为可执行的Spring Boot JAR(Fat JAR),其中包含了所有依赖的第三方库。includeSystemScope设置为true,确保system scope的依赖也被包含在最终的JAR中。

构建流程为:在父目录执行mvn clean package,Maven会按照模块依赖顺序依次编译打包,最终在web模块的target目录下生成可执行的JAR文件。


六、生产环境配置管理建议

6.1 配置中心集成

随着项目规模的增长和微服务架构的引入,传统的文件式配置管理逐渐暴露出局限性:配置变更需要重新打包和部署,无法实现动态刷新;多台服务器之间的配置一致性难以保证;配置的版本管理和审计追踪缺乏工具支持。

针对这些问题,建议在生产环境中引入配置中心。目前主流的配置中心方案包括:

Spring Cloud Config: Spring生态原生支持的配置中心,与Spring Boot无缝集成。它将配置存储在Git、SVN或本地文件系统中,通过HTTP API提供服务端配置。配合Spring Cloud Bus可以实现配置的动态刷新。

Nacos Config: 阿里巴巴开源的配置中心,同时支持配置管理和服务发现。Nacos提供了Web控制台,配置变更直观可见,支持灰度发布和配置回滚。与Spring Boot的集成通过nacos-config-spring-boot-starter实现。

Apollo: 携程开源的分布式配置中心,支持多环境、多集群的配置管理。Apollo提供了丰富的权限控制和审计功能,适合大型企业级应用。

在smart-scaffold项目中集成配置中心的建议步骤:

yaml
# 教学示例:Nacos Config集成配置
spring:
  cloud:
    nacos:
      config:
        server-addr: ${NACOS_ADDR:localhost:8848}
        namespace: ${NACOS_NAMESPACE:dev}
        group: SMART_SCAFFOLD_GROUP
        file-extension: yml
        shared-configs:
          - data-id: common.yml
            group: SHARED_GROUP
            refresh: true

通过配置中心,可以将application-prd.yml中的环境相关配置迁移到配置中心管理,实现配置的集中化、动态化和可审计化。

6.2 敏感信息加密

在smart-scaffold项目的当前配置中,数据库密码、MongoDB密码、API密钥等敏感信息以明文形式存储在配置文件中。虽然在开发环境中这是可以接受的,但在生产环境中,这种做法存在严重的安全风险。

方案一:JASYPT加密。 Jasypt(Java Simplified Encryption)是一个简单的Java加密库,可以与Spring Boot无缝集成。使用Jasypt,配置文件中的敏感信息可以被加密存储:

yaml
# 教学示例:JASYPT加密后的配置
spring:
  datasource:
    db1:
      password: ENC(G6N718UuyPE5bHyWKyuLQSm02auQPUtm)

加密密钥通过启动参数传入:

bash
java -jar app.jar -Djasypt.encryptor.password=your_secret_key

方案二:Spring Cloud Vault。 HashiCorp Vault是一个专业的密钥管理工具,Spring Cloud Vault提供了与Spring Boot的集成。敏感信息存储在Vault中,应用启动时从Vault获取:

yaml
# 教学示例:Vault集成配置
spring:
  cloud:
    vault:
      uri: https://vault.bima.cc:8200
      token: ${VAULT_TOKEN}
      kv:
        enabled: true
        backend: secret
        application-name: smart-scaffold

方案三:Kubernetes Secrets。 如果项目部署在Kubernetes上,可以使用Kubernetes Secrets来管理敏感信息。Secrets通过环境变量或Volume挂载的方式传递给应用:

yaml
# 教学示例:Kubernetes Secret引用
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: smart-scaffold-secrets
        key: db-password

6.3 配置验证机制

配置错误是导致生产事故的常见原因之一。为了在应用启动阶段尽早发现配置问题,建议实现配置验证机制。

Spring Boot的@ConfigurationProperties验证: 通过JSR-303注解对配置属性进行校验:

java
// 教学示例:配置属性验证
@ConfigurationProperties(prefix = "bima.ai")
@Validated
public class AiProperties {

    @NotBlank(message = "默认API类型不能为空")
    private String defaultApiType;

    @DecimalMin(value = "0.0", message = "温度参数不能小于0")
    @DecimalMax(value = "2.0", message = "温度参数不能大于2.0")
    private BigDecimal temperature;

    @Min(value = 1, message = "最大Token数不能小于1")
    private Integer maxTokens;
}

自定义配置健康检查: 实现Spring Boot的HealthIndicator,在应用启动时检查关键配置的可用性:

java
// 教学示例:数据源连接健康检查
@Component
public class DataSourceHealthIndicator implements HealthIndicator {

    @Autowired
    private DataSource db1DataSource;

    @Override
    public Health health() {
        try (Connection conn = db1DataSource.getConnection()) {
            if (conn.isValid(5)) {
                return Health.up().withDetail("database", "db1").build();
            }
        } catch (SQLException e) {
            return Health.down()
                .withDetail("error", e.getMessage())
                .withException(e)
                .build();
        }
        return Health.down().withDetail("database", "db1").build();
    }
}

启动时的配置断言: 在应用启动时通过@PostConstructApplicationRunner进行关键配置的断言检查:

java
// 教学示例:启动时配置断言
@Component
public class ConfigAssertRunner implements ApplicationRunner {

    @Value("${spring.profiles.active}")
    private String activeProfile;

    @Override
    public void run(ApplicationArguments args) {
        // 生产环境必须使用非默认密码
        if ("prd".equals(activeProfile)) {
            assertConfig("数据库密码不能使用默认值",
                !"${DB_PASSWORD:default_password}".equals(getDbPassword()));
        }
    }
}

6.4 配置变更管理流程

在生产环境中,配置变更应该遵循严格的流程:

变更申请: 配置变更需要提交变更申请,说明变更内容、影响范围和回滚方案。

变更审批: 配置变更需要经过技术负责人审批,敏感配置的变更需要安全团队审批。

变更执行: 通过配置中心或自动化工具执行配置变更,避免手动修改配置文件。

变更验证: 变更后需要验证应用是否正常启动,关键功能是否正常工作。

变更记录: 所有配置变更需要记录在案,包括变更人、变更时间、变更内容和变更原因。

如果使用了配置中心(如Nacos),这些流程可以通过配置中心的版本管理和权限控制功能来实现。如果暂时没有配置中心,也可以通过Git管理配置文件,利用Git的版本控制和代码审查机制来规范配置变更。

6.5 配置文档化建议

良好的配置文档是团队协作的基础。建议为每个配置项编写文档,包含以下信息:

  • 配置项名称: 完整的配置路径
  • 配置项说明: 配置项的用途和含义
  • 默认值: 未配置时的默认值
  • 可选值: 枚举类型的可选值列表
  • 环境差异: 不同环境中的配置差异说明
  • 注意事项: 配置不当可能导致的问题

smart-scaffold项目的配置文件中已经通过注释对主要配置项进行了说明,这是一个良好的实践。建议在此基础上,为团队维护一份独立的配置参考文档,方便新成员快速了解项目的配置体系。


七、MyBatis动态SQL进阶技巧

7.1 动态SQL的性能考量

虽然MyBatis的动态SQL极大地方便了开发,但在使用时也需要注意性能问题。

条件判断的顺序优化: 在Base_Where片段中,<if>标签的判断顺序会影响SQL的执行计划。建议将选择性高的条件(即能过滤更多记录的条件)放在前面。例如,如果user_id条件通常能将结果集缩小到个位数,而status条件只能过滤掉一半的记录,那么user_id应该放在前面。

避免全表扫描: 当Base_Where片段中的所有<if>条件都不满足时,SQL会变成select * from table where 1=1,这等同于全表扫描。在数据量较大的表中,应该考虑添加一个默认条件(如status = 0表示只查询启用状态的记录),避免意外返回大量数据。

索引利用: 确保Base_Where片段中涉及的条件字段都有对应的索引。特别是模糊查询(LIKE '%keyword%')无法利用普通B+树索引,如果模糊查询是高频操作,可以考虑使用全文索引。

7.2 SQL注入防护

虽然MyBatis的#{}预编译参数机制有效防止了SQL注入,但在某些场景下仍需注意:

${}的使用风险: ${}是字符串直接替换,不会进行预编译。在smart-scaffold项目中,排序字段和排序方向通过<choose>白名单机制控制,避免了直接使用${}的风险。如果确实需要使用${}(例如动态表名),务必在Java代码中进行严格的白名单校验。

<bind>标签的安全性: <bind>标签中的表达式在OGNL上下文中执行,虽然它不直接拼接SQL,但如果表达式中引用了不可信的用户输入,可能导致OGNL表达式注入。建议<bind>标签中只引用经过校验的参数。

模糊查询的LIKE注入: 使用<bind>标签拼接%keyword%时,如果keyword中包含%_等LIKE通配符,可能影响查询结果。建议在拼接前对这些特殊字符进行转义:

xml
<!-- 教学示例:LIKE通配符转义 -->
<if test="name != null and name != ''">
    <bind name="likeName" value="'%' + _parameter.name.replace('%', '\\%').replace('_', '\\_') + '%'" />
    and name like #{likeName} escape '\\'
</if>

7.3 批量操作优化

在需要插入或更新大量数据时,逐条操作效率较低。MyBatis提供了批量操作的支持:

xml
<!-- 教学示例:批量插入 -->
<insert id="batchInsert" parameterType="java.util.List">
    insert into user_model (time_create, time_update, admin_id, admin_name,
        user_id, api_type, config_name, model_name)
    values
    <foreach collection="list" item="item" separator=",">
        (#{item.timeCreate}, #{item.timeUpdate}, #{item.adminId},
         #{item.adminName}, #{item.userId}, #{item.apiType},
         #{item.configName}, #{item.modelName})
    </foreach>
</insert>

<foreach>标签用于遍历集合,生成批量操作的SQL语句。separator=","指定了每个元素之间的分隔符。需要注意的是,批量插入的SQL语句长度受MySQL的max_allowed_packet参数限制,如果批量数据量过大,需要分批执行。

7.4 动态SQL的调试技巧

动态SQL的调试是MyBatis开发中的常见痛点。以下是一些实用的调试技巧:

开启SQL日志: 在application.yml中配置MyBatis的SQL日志输出:

yaml
# 教学示例:开启MyBatis SQL日志
logging:
  level:
    cc.bima.scaffold.dao.mapper: debug

设置Mapper接口所在包的日志级别为debug,MyBatis会输出完整的SQL语句(包括参数绑定后的最终SQL)和参数值。

使用MyBatis日志插件: MyBatis提供了多种日志插件的集成(SLF4J、Log4j2、Commons Logging等),可以根据项目使用的日志框架选择合适的插件。

IDE的MyBatis插件: IntelliJ IDEA提供了MyBatisX等插件,可以在Mapper接口方法和XML语句之间快速跳转,并支持SQL语句的语法高亮和自动补全。


八、多环境配置最佳实践总结

8.1 配置分层原则

通过分析smart-scaffold项目的配置体系,我们可以总结出以下配置分层原则:

原则一:共享优先。 将所有环境共享的配置放在主配置文件中,减少重复。判断一个配置是否应该放在主配置中的标准是:在项目的整个生命周期中,这个配置的值是否可能因环境而异。如果不会,就放在主配置中。

原则二:环境隔离。 将随环境变化的配置放在对应的环境配置文件中。环境配置文件应该只包含与主配置不同的部分,而不是完整的配置副本。

原则三:敏感分离。 敏感信息(密码、密钥等)不应直接写在配置文件中,应通过环境变量、配置中心或加密机制进行管理。

原则四:命名规范。 配置项的命名应该具有自描述性,使用层级结构组织相关配置。smart-scaffold项目使用了bima.aibima.oauth2等自定义命名空间,与Spring的默认命名空间区分开来。

8.2 环境配置的一致性保障

在多环境配置体系中,保持各环境配置的一致性是一个重要的挑战。以下是一些保障一致性的方法:

配置模板: 使用模板引擎(如Maven资源过滤)生成各环境的配置文件,确保配置结构的一致性。

配置对比工具: 定期对比各环境配置文件的差异,确保差异只存在于预期的配置项上。

CI/CD配置检查: 在CI/CD流水线中添加配置检查步骤,验证配置文件的完整性和正确性。

配置文档: 维护一份配置差异对照表,明确记录各环境之间的配置差异及其原因。

8.3 配置与代码的解耦

良好的配置体系应该实现配置与代码的解耦。smart-scaffold项目在这方面做得比较好:

  • 数据库连接信息通过配置文件管理,代码中不硬编码
  • 中间件地址通过配置文件管理,切换环境只需修改配置
  • 业务参数(如分页大小、默认排序方式)通过PageDTO的常量管理

进一步解耦的建议:

  • 将更多的业务规则参数化(如默认温度值、最大Token数等)
  • 使用@ConfigurationProperties替代@Value,实现类型安全的配置绑定
  • 将配置分组管理,每个配置类负责一个功能域的配置

8.4 从单体到微服务的配置演进

smart-scaffold项目同时提供了SpringBoot单体版、SpringCloud微服务版和Dubbo微服务版三个架构版本。随着架构从单体向微服务演进,配置体系也需要相应调整:

单体阶段: 配置文件随应用打包,通过Profile切换环境。这是smart-scaffold-springboot项目当前的方案。

微服务初期: 各微服务独立管理配置,但共享相同的配置模板。可以通过Git子模块或Maven父POM统一管理配置模板。

微服务成熟期: 引入配置中心,实现配置的集中管理和动态刷新。各微服务从配置中心拉取配置,不再本地存储环境相关配置。

云原生阶段: 配置与容器编排平台深度集成,通过ConfigMap和Secret管理配置,实现配置与部署的完全解耦。

smart-scaffold项目的配置体系已经为微服务演进做好了准备——清晰的配置分层和命名规范使得配置迁移到配置中心的成本很低。


总结与展望

本文基于smart-scaffold-springboot项目源码,从MyBatis动态SQL设计、Spring多环境配置体系、数据库设计规范、通用服务层架构、项目工程化实践、生产环境配置管理建议等多个维度进行了深度解析。

在MyBatis动态SQL设计方面,项目通过Base_Where SQL片段实现了查询条件的统一管理和多 处复用;通过<choose>标签实现了安全的动态排序;通过<bind>标签实现了安全的模糊查询;通过resultMap继承实现了Entity到DTO的映射复用;通过BaseMapper泛型接口统一了数据访问层的API规范。

在多环境配置体系方面,项目通过Spring Profile机制实现了dev/qa/prd三环境的配置隔离;通过配置分层策略将共享配置和环境配置清晰分离;通过Maven资源过滤实现了构建时的配置定制;通过Mapper XML与Java同目录存放提升了开发体验。

在工程化架构方面,项目通过Maven多模块实现了代码的分层组织;通过BaseService泛型基类实现了服务层的统一抽象;通过模板方法模式预留了业务定制的扩展点。

展望未来,随着项目向微服务架构演进和云原生部署方式的普及,配置体系还有进一步优化的空间:引入配置中心实现配置的集中管理和动态刷新、集成密钥管理服务实现敏感信息的安全存储、采用数据库迁移工具实现数据库版本的自动化管理、引入配置验证机制在应用启动时尽早发现配置问题。

smart-scaffold项目的设计理念和实现方案,为中小型Spring Boot项目的数据持久层和配置管理提供了一套经过实践验证的最佳实践。读者可以在理解这些设计原理的基础上,结合自身项目的实际需求进行灵活应用和扩展。


版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。

本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。

文档内容提取自项目源码与配置文件,如需获取完整项目代码,请访问 bima.cc