乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 介绍与核心概念

    • Maven是什么
    • 约定优于配置
  • 第2章 安装与配置

    • 安装与验证
    • settings.xml
    • 本地仓库与镜像
  • 第3章 POM与项目坐标

    • POM
    • GAV坐标
    • packaging
  • 第4章 标准目录布局

    • 标准目录布局
  • 第5章 依赖机制

    • dependencies
    • scope
    • 依赖传递
    • 依赖冲突与调解
    • exclusions
    • optional
    • dependencyManagement
  • 第6章 仓库

    • 仓库体系
    • 本地仓库
    • 远程仓库与镜像
    • 私服
  • 第7章 构建生命周期

    • 生命周期概述
    • clean 生命周期
    • default 生命周期
    • site 生命周期
    • 生命周期与插件绑定
  • 第8章 插件

    • 插件概述
    • maven-compiler-plugin
    • maven-surefire-plugin
    • maven-war-plugin
  • 第9章 继承与聚合

    • parent继承
    • 聚合
    • BOM
    • properties
  • 第10章 属性与资源过滤

    • 资源过滤
    • Profile
  • 第11章 常用命令

    • mvn compile
    • mvn test
    • mvn package
    • mvn clean
    • mvn install
    • mvn dependency:tree
  • 第12章 常见问题与最佳实践

    • 依赖冲突排查
    • 最佳实践

optional

本章承接"exclusions",深入讲解 Maven 的可选依赖机制。理解 optional,是设计"可插拔功能模块"、避免强制传递重型依赖的核心工具,也是实现多数据库支持、多缓存策略等灵活架构的关键。


核心机制

标记为 optional=true 的依赖不会传递到依赖该项目的其他项目中。它只在当前项目的编译和测试阶段可见,对下游项目不可见。

这句话的潜台词是:optional 是单向玻璃——你能看见它、使用它,但依赖你的人看不见它、也不会继承它。

optional 是 <dependency> 的一个布尔属性,默认 false。当设为 true 时:

  • 当前项目:编译 ✅、测试 ✅、运行 ✅(如果当前项目自身使用)
  • 下游项目:不可见 ❌、不传递 ❌、不进入下游的 classpath ❌

这与 provided 的区别:

维度optional=trueprovided
当前项目编译✅ 可见✅ 可见
当前项目测试✅ 可见✅ 可见
当前项目运行✅ 可用❌ 由外部提供
下游项目传递❌ 不传递❌ 不传递
下游项目可见❌ 不可见❌ 不可见
打包产物✅ 包含❌ 不包含

optional 的核心用途是:当前项目需要这个库来实现某个功能,但不强制下游项目也使用这个功能。

典型场景:多数据库支持

飞翔科技开发了一个通用 DAO 框架 feixiang-dao,支持 MySQL、PostgreSQL、Oracle 三种数据库。框架的代码里通过接口抽象了数据库操作,但每种数据库需要对应的 JDBC 驱动来运行测试。

如果 feixiang-dao 把三个驱动都声明为普通依赖:

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.5.0</version>
    </dependency>
    <dependency>
        <groupId>com.oracle.database.jdbc</groupId>
        <artifactId>ojdbc11</artifactId>
        <version>21.7.0.0</version>
    </dependency>
</dependencies>

任何依赖 feixiang-dao 的项目都会被迫继承这三个驱动,打包体积增加 20MB+,且可能出现驱动类冲突。

正确的做法是将三个驱动标记为 optional:

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.5.0</version>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.oracle.database.jdbc</groupId>
        <artifactId>ojdbc11</artifactId>
        <version>21.7.0.0</version>
        <optional>true</optional>
    </dependency>
</dependencies>

下游项目 employee-system 使用 MySQL,只需声明自己需要的驱动:

<dependencies>
    <dependency>
        <groupId>com.feixiang</groupId>
        <artifactId>feixiang-dao</artifactId>
        <version>1.0.0</version>
    </dependency>
    <!-- 只引入自己需要的驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>
</dependencies>

生活类比:汽车的"可选配置"

想象你买了一辆乐途牌汽车(依赖 feixiang-dao):

  • optional=false(标配):无论你需不需要,车上都装了天窗、导航、真皮座椅。你付的钱包含了这些成本,而且它们占用了车内空间(打包体积)。
  • optional=true(选配):基础车型只提供底盘和发动机。天窗、导航、真皮座椅是可选包。你需要导航,就单独选装导航包;你不需要天窗,就不付这笔钱,也不占这个空间。

Maven 的 optional 就是这套"选配系统"——基础功能默认提供,扩展功能按需加载。


图示

optional=true/false 的传递差异对比

上图展示了 optional 的核心差异。当 optional=false(默认)时,框架项目的所有数据库驱动强制传递给下游,无论下游是否需要。当 optional=true 时,驱动被"截断"在框架层,下游项目根据自身需求自行声明需要的驱动,实现了真正的按需加载。


完整示例

场景

飞翔科技的架构师白歌设计了一个通用缓存框架 feixiang-cache,支持 Redis、Caffeine(本地缓存)和 Ehcache 三种实现。框架提供统一的 CacheManager 接口,但每种实现需要对应的客户端库。白歌希望:

  • 框架自身能编译和测试所有实现
  • 下游项目只引入自己需要的缓存客户端,不被迫继承全部

CTO 大翔同意这个设计,后端小崔负责实现。

操作前:optional=false 的灾难

小崔最初的 feixiang-cache/pom.xml:

<dependencies>
    <!-- Redis 客户端 -->
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
        <version>6.2.0.RELEASE</version>
    </dependency>
    <!-- 本地缓存 Caffeine -->
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
        <version>3.1.1</version>
    </dependency>
    <!-- Ehcache -->
    <dependency>
        <groupId>org.ehcache</groupId>
        <artifactId>ehcache</artifactId>
        <version>3.10.0</version>
    </dependency>
</dependencies>

下游项目 employee-system 只需要 Redis:

<dependencies>
    <dependency>
        <groupId>com.feixiang</groupId>
        <artifactId>feixiang-cache</artifactId>
        <version>1.0.0</version>
    </dependency>
</dependencies>

执行依赖树:

cd employee-system
mvn dependency:tree

输出:

com.feixiang:employee-system:jar:1.0.0
\- com.feixiang:feixiang-cache:jar:1.0.0:compile
   +- io.lettuce:lettuce-core:jar:6.2.0.RELEASE:compile
   +- com.github.ben-manes.caffeine:caffeine:jar:3.1.1:compile
   \- org.ehcache:ehcache:jar:3.10.0:compile

问题:

  • employee-system 的 WAR 里被迫包含 caffeine 和 ehcache,增加 5MB+ 体积
  • 启动时,Spring 可能自动扫描到 ehcache 的类,误触发不需要的缓存配置
  • 如果另一个依赖也传递了不同版本的 ehcache,可能引发版本冲突

操作后:optional=true 的精确控制

白歌指导小崔修改 feixiang-cache/pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.feixiang</groupId>
    <artifactId>feixiang-cache</artifactId>
    <version>1.0.0</version>

    <dependencies>
        <!-- Redis 客户端:可选 -->
        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
            <version>6.2.0.RELEASE</version>
            <optional>true</optional>
        </dependency>

        <!-- Caffeine 本地缓存:可选 -->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>3.1.1</version>
            <optional>true</optional>
        </dependency>

        <!-- Ehcache:可选 -->
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>3.10.0</version>
            <optional>true</optional>
        </dependency>

        <!-- 测试时使用所有实现 -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

下游项目 employee-system 按需声明:

<dependencies>
    <dependency>
        <groupId>com.feixiang</groupId>
        <artifactId>feixiang-cache</artifactId>
        <version>1.0.0</version>
    </dependency>
    <!-- 只引入需要的 Redis 客户端 -->
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
        <version>6.2.0.RELEASE</version>
    </dependency>
</dependencies>

再次执行依赖树:

cd employee-system
mvn dependency:tree

输出:

com.feixiang:employee-system:jar:1.0.0
+- com.feixiang:feixiang-cache:jar:1.0.0:compile
\- io.lettuce:lettuce-core:jar:6.2.0.RELEASE:compile

变化分析:

  • feixiang-cache 自身编译时,三个缓存客户端都在 classpath 中,所有实现类都能编译通过
  • feixiang-cache 的测试代码可以测试 Redis、Caffeine、Ehcache 三种实现
  • employee-system 只看到了 lettuce-core,caffeine 和 ehcache 被 optional 截断
  • WAR 体积减小,启动时无多余缓存框架的干扰
  • 如果另一个项目 payroll-service 想用 Caffeine,只需自行声明 caffeine 依赖,互不干扰

易错点与常见问题

误区一:optional 等于"当前项目不需要"

错误认知:"我把 lettuce-core 标记为 optional=true,feixiang-cache 编译时就不需要它了。"

纠正:optional=true 不影响当前项目。feixiang-cache 的编译、测试、运行阶段仍然可以正常使用 lettuce-core。optional 只影响下游项目——依赖 feixiang-cache 的人不会自动继承 lettuce-core。

误区二:optional 和 provided 可以互换

错误认知:"optional 和 provided 都是'不传递',用哪个都一样。"

纠正:两者有本质区别:

  • provided:运行时由外部容器提供,当前项目打包时不包含(如 Servlet API)
  • optional:运行时由当前项目或下游项目自行提供,当前项目打包时包含(如果当前项目自身使用)

如果你把 mysql-connector 标记为 provided,feixiang-cache 打包时不会包含它,运行时如果环境没提供就会报错。如果你标记为 optional,feixiang-cache 打包时会包含它(如果框架自身测试用到),但下游项目不会被迫继承。

误区三:optional 可以替代 exclusions

错误认知:"我有两个传递依赖冲突,把其中一个标记为 optional 就能解决。"

纠正:optional 是你向外传递时的开关(我依赖 A,但我不让下游知道 A),exclusions 是你向内接收时的剪刀(我依赖 B,但我不接受 B 传递的 C)。两者解决的问题方向相反,不能互换。冲突调解通常需要 exclusions 或 dependencyManagement,而不是 optional。


小结

optional 是 Maven 的"选配开关",用于标记"当前项目需要,但下游项目不一定需要"的依赖。典型场景包括多数据库支持、多缓存策略、多消息队列实现等可插拔架构。optional=true 截断传递链,让下游项目按需自行声明,避免强制继承和打包膨胀。

本章与全局的关系:本章讲解了"如何标记依赖为可选,阻止向下传递"。下一章"dependencyManagement"将讲解"如何在父 POM 中统一声明版本,让子模块省略 version"——这是多模块项目依赖治理的核心工具。

上一页
exclusions
下一页
dependencyManagement