optional
本章承接"exclusions",深入讲解 Maven 的可选依赖机制。理解
optional,是设计"可插拔功能模块"、避免强制传递重型依赖的核心工具,也是实现多数据库支持、多缓存策略等灵活架构的关键。
核心机制
标记为 optional=true 的依赖不会传递到依赖该项目的其他项目中。它只在当前项目的编译和测试阶段可见,对下游项目不可见。
这句话的潜台词是:optional 是单向玻璃——你能看见它、使用它,但依赖你的人看不见它、也不会继承它。
optional 是 <dependency> 的一个布尔属性,默认 false。当设为 true 时:
- 当前项目:编译 ✅、测试 ✅、运行 ✅(如果当前项目自身使用)
- 下游项目:不可见 ❌、不传递 ❌、不进入下游的 classpath ❌
这与 provided 的区别:
| 维度 | optional=true | provided |
|---|---|---|
| 当前项目编译 | ✅ 可见 | ✅ 可见 |
| 当前项目测试 | ✅ 可见 | ✅ 可见 |
| 当前项目运行 | ✅ 可用 | ❌ 由外部提供 |
| 下游项目传递 | ❌ 不传递 | ❌ 不传递 |
| 下游项目可见 | ❌ 不可见 | ❌ 不可见 |
| 打包产物 | ✅ 包含 | ❌ 不包含 |
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"——这是多模块项目依赖治理的核心工具。