Maven属性替换与多模块配置继承机制详解

文章目录

1. Maven属性替换

1.1Maven 构建生命周期中的资源过滤

Maven 在构建过程中会经历多个阶段,其中资源处理阶段 (process-resources)负责将 src/main/resources 目录下的文件复制到 target/classes 目录,并可以选择性地对其中的占位符进行替换。

复制代码
validate → initialize → generate-sources → process-sources 
→ generate-resources → process-resources → compile → ...
                              ↑
                        资源过滤发生在这里

1.2Maven Properties 替换机制

1.2.1 配置流程

第一步:在 POM 中定义 Properties

根 pom.xml:

xml 复制代码
<properties>
    <revision>1.0.0</revision>
    <java.version>17</java.version>
    <spring-boot.version>3.2.0</spring-boot.version>
</properties>

<profiles>
    <profile>
        <id>dev</id>
        <properties>
            <profiles.active>dev</profiles.active>
            <config.server>192.168.1.100:8848</config.server>
            <config.username>admin</config.username>
            <config.password>password123</config.password>
        </properties>
        <activation>
            <!-- 默认环境 -->
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile id="prod">
         <id>dev</id>
        <properties>
            <profiles.active>prod</profiles.active>
            <config.server>10.0.0.100:8848</config.server>
        </properties>
    </profile>
</profiles>
第二步:在 application.yml 中使用占位符
yaml 复制代码
spring:
  profiles:
    active: @profiles.active@
  cloud:
    config:
      server-addr: @config.server@
      username: @config.username@
      password: @config.password@
第三步:启用资源过滤

pom.xml 的 build 配置:

xml 复制代码
<build>
    <resources>
        <!-- 不进行过滤的资源 -->
        <resource>
            <directory>src/main/resources</directory>
            <filtering>false</filtering>
        </resource>
        
        <!-- 需要进行属性替换的资源 -->
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>application*.yml</include>
                <include>application*.properties</include>
                <include>bootstrap*.yml</include>
                <include>logback*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>
第四步:执行构建
bash 复制代码
# 使用 dev 环境(默认)
mvn clean package

# 或显式指定环境
mvn clean package -P dev
mvn clean package -P prod

2. 替换过程示例

构建前(源代码):

yaml 复制代码
spring:
  profiles:
    active: @profiles.active@
  cloud:
    config:
      server-addr: @config.server@

构建后(target/classes/application.yml):

yaml 复制代码
spring:
  profiles:
    active: dev
  cloud:
    config:
      server-addr: 192.168.1.100:8848

2.属性查找与继承规则

2.1.多模块项目中的属性继承

复制代码
my-project (根模块)
├── my-modules (父模块)
│   ├── module-a (子模块)
│   ├── module-b (子模块)
│   └── module-c (子模块)
├── module-auth (子模块)
└── module-gateway (子模块)

2.2属性查找优先级(从高到低)

复制代码
┌─────────────────────────────────────────┐
│ 1. 当前模块 POM 的 <properties>         │ ← 最高优先级
├─────────────────────────────────────────┤
│ 2. 当前模块激活的 Profile 中的           │
│    <properties>                         │
├─────────────────────────────────────────┤
│ 3. 父模块 POM 的 <properties>           │ ← 通过继承
├─────────────────────────────────────────┤
│ 4. 父模块激活的 Profile 中的             │
│    <properties>                         │
├─────────────────────────────────────────┤
│ 5. 祖父模块(根 POM)的 <properties>    │ ← 最终 fallback
├─────────────────────────────────────────┤
│ 6. 根 POM 激活的 Profile 中的            │
│    <properties>                         │
└─────────────────────────────────────────┘

2.3实际案例:module-a 模块

场景: module-a/src/main/resources/application.yml 中有 @profiles.active@

查找路径:

复制代码
第1步: module-a/pom.xml
       ↓ 
       没有定义 <profiles.active>
       
第2步: my-modules/pom.xml (父模块)
       ↓
       没有定义 <profiles.active>
       
第3步: my-project/pom.xml (根模块/祖父模块) ✅
       ↓
       在 <profile id="dev"> 中找到
       → <profiles.active>dev</profiles.active>

2.4属性覆盖示例

子模块可以覆盖父模块的属性:

xml 复制代码
<!-- 根 pom.xml -->
<properties>
    <custom.property>root-value</custom.property>
</properties>

<!-- 子模块 pom.xml -->
<properties>
    <!-- 覆盖父模块的值 -->
    <custom.property>child-value</custom.property>
</properties>

结果: 子模块及其资源文件中 @custom.property@ 会被替换为 child-value

3.两种占位符的区别

3.1对比表(!!!)

特性 Maven 占位符 @xxx@ Spring 占位符 ${xxx}
处理时机 Maven 构建时 Spring Boot 运行时
数据来源 POM 文件的 <properties> application.yml、环境变量、系统属性等
是否可动态修改 ❌ 打包后固定 ✅ 可通过启动参数修改
典型用途 环境配置、版本号 运行时配置、外部化配置
处理工具 Maven Resource Plugin Spring PropertyPlaceholderConfigurer

3.2代码示例

yaml 复制代码
server:
  port: 8080

spring:
  application:
    name: my-application
  
  # ✅ Maven 占位符 - 构建时确定,不同环境打不同的包
  profiles:
    active: @profiles.active@
  
  cloud:
    config:
      # ✅ Maven 占位符 - 配置中心地址在构建时确定
      server-addr: @config.server@
      username: @config.username@
      password: @config.password@

---

spring:
  datasource:
    dynamic:
      # ✅ Spring 占位符 - 可以从其他配置文件或环境变量获取(获取不到时,使用默认值master)
      primary: ${datasource.primary:master}
      
      datasource:
        master:
          type: ${spring.datasource.type}
          url: ${datasource.master.url}
          username: ${datasource.master.username}
          password: ${datasource.master.password}

3.2重要说明

Maven 占位符不会从 application.yml 中获取值!

错误理解: "如果 POM 中找不到,就从 yml 中找"

正确理解:

  • Maven 构建时只从 POM 的 properties 中取值
  • 如果整个继承链都找不到,Maven 会:
    • 保留占位符原样(默认行为)
    • 或者构建失败(如果配置了严格模式)
  • 绝不会从 application.yml 中补充值

3.2实际案例分析

案例 1:多环境配置(!!!)

项目结构
复制代码
my-project/
├── pom.xml (根 POM,定义了 dev/prod 两个 profile)
├── module-auth/
│   └── src/main/resources/application.yml
└── module-service/
    └── src/main/resources/application.yml
根 POM 配置
xml 复制代码
<profiles>
    <profile id="dev">
        <properties>
            <profiles.active>dev</profiles.active>
            <config.server>192.168.1.100:8848</config.server>
            <logstash.address>192.168.1.100:4560</logstash.address>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile id="prod">
        <properties>
            <profiles.active>prod</profiles.active>
            <config.server>10.0.0.100:8848</config.server>
            <logstash.address>10.0.0.100:4560</logstash.address>
        </properties>
    </profile>
</profiles>
子模块 application.yml
yaml 复制代码
spring:
  profiles:
    active: @profiles.active@
  cloud:
    config:
      server-addr: @config.server@

logging:
  logstash:
    address: @logstash.address@
构建不同环境的包
bash 复制代码
# 构建开发环境(默认)
mvn clean package
# 结果:profiles.active=dev, config.server=192.168.1.100:8848

# 构建生产环境
mvn clean package -P prod
# 结果:profiles.active=prod, config.server=10.0.0.100:8848

案例 2:版本统一管理

根 POM 定义版本
xml 复制代码
<properties>
    <revision>1.0.0</revision>
    <spring-boot.version>3.2.0</spring-boot.version>
    <mybatis-plus.version>3.5.5</mybatis-plus.version>
</properties>
子模块引用
xml 复制代码
<parent>
    <groupId>com.example</groupId>
    <artifactId>my-project</artifactId>
    <version>${revision}</version>
</parent>

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>${mybatis-plus.version}</version>
    </dependency>
</dependencies>

优势: 所有模块的版本在根 POM 统一管理,升级只需修改一处。

4.常见问题与解决方案

问题 1:占位符未被替换

现象: 打包后的 yml 文件中仍然是 @profiles.active@

可能原因:

  1. 未启用资源过滤(<filtering>false</filtering>
  2. POM 中没有定义对应的 property
  3. 使用了错误的 profile

解决方案:

xml 复制代码
<!-- 确保启用了过滤 -->
<resource>
    <directory>src/main/resources</directory>
    <includes>
        <include>application*.yml</include>
    </includes>
    <filtering>true</filtering>
</resource>

<!-- 确保定义了属性 -->
<properties>
    <profiles.active>dev</profiles.active>
</properties>

问题 2:多模块构建失败

现象: 子模块找不到父模块定义的 BOM 依赖

错误信息:

复制代码
Could not find artifact com.example:my-common-bom:pom:${revision}

原因: 内部 BOM 依赖无法从远程仓库下载

解决方案: 必须从根目录构建

bash 复制代码
# ✅ 正确:从根目录构建
cd my-project
mvn clean install

# ❌ 错误:直接从子模块构建
cd my-project/module-service
mvn clean install

问题 3:Profile 未激活

现象: 使用了默认的 property 值,而不是 profile 中的值

检查方法:

bash 复制代码
# 查看当前激活的 profile
mvn help:active-profiles

# 显式激活 profile
mvn clean package -P dev

问题 4:Spring 占位符解析失败

现象: 启动时报错 Could not resolve placeholder 'xxx'

原因: Spring 占位符 ${xxx} 在运行时无法找到对应的值

解决方案:

yaml 复制代码
# 方式1:提供默认值
spring:
  datasource:
    type: ${datasource.type:com.zaxxer.hikari.HikariDataSource}

# 方式2:在 application.yml 中定义
datasource:
  type: com.zaxxer.hikari.HikariDataSource

# 方式3:通过启动参数传入
java -jar app.jar --datasource.type=com.zaxxer.hikari.HikariDataSource

问题 5:IDE 中显示占位符错误

现象: IDEA 中 yml 文件的 @xxx@ 显示红色警告

原因: IDE 不理解 Maven 占位符语法

解决方案:

  1. 忽略警告(不影响构建)
  2. 安装 Maven Helper 插件
  3. 使用 Maven 视图刷新项目

5.最佳实践

5.1合理使用两种占位符

yaml 复制代码
# ✅ 推荐:构建时确定的配置用 Maven 占位符
spring:
  profiles:
    active: @profiles.active@
  cloud:
    config:
      server-addr: @config.server@

# ✅ 推荐:运行时可能变化的配置用 Spring 占位符
spring:
  datasource:
    url: ${DB_URL:jdbc:mysql://localhost:3306/mydb}
    username: ${DB_USERNAME:root}
    password: ${DB_PASSWORD:password}

5.2.集中管理版本号

根 POM:

xml 复制代码
<properties>
    <!-- 项目版本 -->
    <revision>1.0.0</revision>
    
    <!-- 框架版本 -->
    <spring-boot.version>3.2.0</spring-boot.version>
    <spring-cloud.version>2023.0.0</spring-cloud.version>
    
    <!-- 第三方库版本 -->
    <mybatis-plus.version>3.5.5</mybatis-plus.version>
    <redisson.version>3.35.0</redisson.version>
</properties>

5.3.使用 Flatten Maven Plugin

作用: 解决 CI/CD 中 ${revision} 占位符的问题

xml 复制代码
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>flatten-maven-plugin</artifactId>
    <version>1.3.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>

5. 4 Profile 命名规范

xml 复制代码
<profiles>
    <!-- 开发环境 -->
    <profile>
        <id>dev</id>
        <properties>
            <profiles.active>dev</profiles.active>
        </properties>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    
    <!-- 测试环境 -->
    <profile>
        <id>test</id>
        <properties>
            <profiles.active>test</profiles.active>
        </properties>
    </profile>
    
    <!-- 预发布环境 -->
    <profile>
        <id>staging</id>
        <properties>
            <profiles.active>staging</profiles.active>
        </properties>
    </profile>
    
    <!-- 生产环境 -->
    <profile id="prod">
        <id>prod</id>
        <properties>
            <profiles.active>prod</profiles.active>
        </properties>
    </profile>
</profiles>

5.5敏感信息管理

❌ 不要硬编码在 POM 中:

xml 复制代码
<properties>
    <db.password>123456</db.password> <!-- 危险! -->
</properties>

✅ 推荐做法:

方式1:使用 Maven settings.xml

xml 复制代码
<!-- ~/.m2/settings.xml -->
<servers>
    <server>
        <id>db-config</id>
        <username>root</username>
        <password>${env.DB_PASSWORD}</password>
    </server>
</servers>

方式2:使用环境变量

bash 复制代码
export DB_PASSWORD=secure_password
mvn clean package

方式3:使用 Jasypt 加密

yaml 复制代码
spring:
  datasource:
    password: ENC(encrypted_value_here)

5.6资源过滤性能优化

只过滤需要的文件:

xml 复制代码
<resources>
    <!-- 大部分资源不过滤(提高构建速度) -->
    <resource>
        <directory>src/main/resources</directory>
        <filtering>false</filtering>
        <excludes>
            <exclude>application*.yml</exclude>
            <exclude>bootstrap*.yml</exclude>
        </excludes>
    </resource>
    
    <!-- 只过滤配置文件 -->
    <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
        <includes>
            <include>application*.yml</include>
            <include>bootstrap*.yml</include>
        </includes>
    </resource>
</resources>

5.7使用 BOM 管理依赖

根 POM:

xml 复制代码
<dependencyManagement>
    <dependencies>
        <!-- 自定义 BOM -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>my-common-bom</artifactId>
            <version>${revision}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

子模块(无需指定版本):

xml 复制代码
<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>my-common-core</artifactId>
        <!-- 版本由 BOM 管理 -->
    </dependency>
</dependencies>

6.附录:常用命令速查

6.1Maven 构建命令

bash 复制代码
# 清理并编译
mvn clean compile

# 清理并打包
mvn clean package

# 清理并安装到本地仓库
mvn clean install

# 跳过测试打包
mvn clean package -DskipTests

# 指定 profile
mvn clean package -P prod

# 查看激活的 profile
mvn help:active-profiles

# 查看有效 POM
mvn help:effective-pom

# 查看属性值
mvn help:evaluate -Dexpression=project.version

6.2调试技巧

bash 复制代码
# 详细输出(查看属性替换过程)
mvn clean package -X

# 只构建特定模块
mvn clean install -pl module-service -am

# 从根目录构建所有模块
mvn clean install

7.总结

7.1核心要点

  1. Maven 属性替换发生在构建时,不是运行时
  2. 属性通过继承机制传递,优先级从子模块到父模块再到根模块
  3. @xxx@${xxx} 有本质区别,前者是 Maven 处理,后者是 Spring 处理
  4. 多模块项目必须从根目录构建,确保内部依赖正确解析
  5. 合理设计 Profile 和 Properties,实现灵活的多环境配置

7.2知识图谱

复制代码
Maven 配置体系
├── Properties 定义
│   ├── 全局 Properties
│   └── Profile-specific Properties
├── 资源过滤
│   ├── filtering=true/false
│   ├── includes/excludes
│   └── 占位符替换 (@xxx@)
├── Profile 管理
│   ├── activation (自动激活)
│   ├── manual activation (-P)
│   └── environment-based
└── 多模块继承
    ├── parent-child 关系
    ├── 属性继承
    └── 依赖管理 (BOM)

Spring Boot 配置
├── application.yml
├── Property Placeholder (${xxx})
├── Environment Variables
├── Command-line Arguments
└── External Configuration