Maven简单入门(二)

关于分模块开发与依赖管理

Posted by YD Blog on November 9, 2022

Maven简单入门(二)

分模块开发的意义

使用Java技术开发的工程项目,无论是数据处理系统还是Web网站,随着项目的不断发展,需求的不断细化与添加,工程项目中的代码越来越多,包结构也越来越复杂这时候工程的进展就会遇到各种问题:

  1. 不同方面的代码之间相互耦合,这时候一系统出现问题很难定位到问题的出现原因,即使定位到问题也很难修正问题,可能在修正问题的时候引入更多的问题。
  2. 多方面的代码集中在一个整体结构中,新入的开发者很难对整体项目有直观的感受,增加了新手介入开发的成本,需要有一个熟悉整个项目的开发者维护整个项目的结构(通常在项目较大且开发时间较长时这是很难做到的)。
  3. 开发者对自己或者他人负责的代码边界很模糊,这是复杂项目中最容易遇到的,导致的结果就是开发者很容易修改了他人负责的代码且代码负责人还不知道,责任追踪很麻烦。

将一个复杂项目拆分成多个模块是解决上述问题的一个重要方法,多模块的划分可以降低代码之间的耦合性(从类级别的耦合提升到jar包级别的耦合),每个模块都可以是自解释的(通过模块名或者模块文档),模块还规范了代码边界的划分,开发者很容易通过模块确定自己所负责的内容。

所有用Maven管理的真实的项目都应该是分模块的,每个模块都对应着一个pom.xml。它们之间通过继承和聚合(也称作多模块,multi-module)相互关联。那么,为什么要这么做呢?我们明明在开发一个项目,划分模块后,导入Eclipse变成了N个项目,这会带来复杂度,给开发带来不便。

为了解释原因,假设有这样一个项目,很常见的Java Web应用。在这个应用中,我们分了几层:

  • Dao层负责数据库交互,封装了Hibernate交互的类。
  • Service层处理业务逻辑,放一些Service接口和实现相关的Bean。
  • Web层负责与客户端交互,主要有一些Structs的Action类。

对应的,在一个项目中,我们会看到一些包名:

  • org.myorg.app.dao
  • org.myorg.app.service
  • org.myorg.app.web
  • org.myorg.app.util

这样整个项目的框架就清晰了,但随着项目的进行,你可能会遇到如下问题:

  1. 这个应用可能需要有一个前台和一个后台管理端(web或者swing),你发现大部分dao,一些service,和大部分util是在两个应用中可。这样的问题,你一周内遇到了好几次。
  2. pom.xml中的依赖列表越来越长以重用的,但是,由于目前只有一个项目(WAR),你不得不新建一个项目依赖这个WAR,这变得非常的恶心,因为在Maven中配置对WAR的依赖远不如依赖JAR那样简单明了,而且你根本不需要org.myorg.app.web。有人修改了dao,提交到svn并且不小心导致build失败了,你在编写service的代码,发现编译不过,只能等那人把dao修复了,你才能继续进行,很多人都在修改,到后来你根本就不清楚哪个依赖是谁需要的,渐渐的,很多不必要的依赖被引入。甚至出现了一个依赖有多个版本存在。
  3. build整个项目的时间越来越长,尽管你只是一直在web层工作,但你不得不build整个项目。
  4. 某个模块,比如util,你只想让一些经验丰富的人来维护,可是,现在这种情况,每个开发者都能修改,这导致关键模块的代码质量不能达到你的要求。

我们会发现,其实这里实际上没有遵守一个设计模式原则:“高内聚,低耦合”。虽然我们通过包名划分了层次,并且你还会说,这些包的依赖都是单向的,没有包的环依赖。这很好,但还不够,因为就构建层次来说,所有东西都被耦合在一起了。因此我们需要使用Maven划分模块。

用项目层次的划分替代包层次的划分能给我们带来如下好处:

  1. 方便重用,如果你有一个新的swing项目需要用到app-dao和app-service,添加对它们的依赖即可,你不再需要去依赖一个WAR。而有些模块,如app-util,完全可以渐渐进化成公司的一份基础工具类库,供所有项目使用。这是模块化最重要的一个目的。
  2. 由于你现在划分了模块,每个模块的配置都在各自的pom.xml里,不用再到一个混乱的纷繁复杂的总的POM中寻找自己的配置。
  3. 果你只是在app-dao上工作,你不再需要build整个项目,只要在app-dao目录运行mvn命令进行build即可,这样可以节省时间,尤其是当项目越来越复杂,build越来越耗时后。
  4. 某些模块,如app-util被所有人依赖,但你不想给所有人修改,现在你完全可以从这个项目结构出来,做成另外一个项目,svn只给特定的人访问,但仍提供jar给别人使用。
  5. 多模块的Maven项目结构支持一些Maven的更有趣的特性(如DepencencyManagement),这留作以后讨论。

分模块开发与设计

分模块开发的基本步骤如下:

  1. 创建maven模块
  2. 书写模块代码
  3. 通过Maven指令安装模块到本地仓库(install指令)

关于模块拆分:

  1. 划分模块的方式 模块划分主要是根据程序的职责单一性和耦合性,如果是项目建立初期就使用多模块来规划整个项目,那么职责单一性原则应该是首要考虑的也就是通常意义上的按照层次划分,如果是从一个已经十分复杂的项目开始拆分那么在划分模块的时候就不仅仅是考虑职责单一了,职责单一会造成大量的子模块产生导致pom文件臃肿且不容易识别,如果将耦合性考虑进去就应该将关系较为紧密的模块合并降低模块的数量提高实用性。
  2. 公有依赖的抽象 parent中的依赖配置主要是一些公有的依赖,例如log,apache commons,spring等,怎么界定一个依赖是否属于公有呢?一般情况下如果这个依赖被超过2/3的子模块所依赖就可以将其认定为公有依赖,另外也可以将依赖的某些配置(例如版本号)以parent中pom的属性的形式加以声明,这样在升级某些依赖的时候就只改动一处就行了(很类似C语言中的#define)
  3. 模块的存在与消亡 模块的划分并不是一成不变的,模块的存在就是为了方便维护和提高生产效率,如果某些模块不合理并且影响了开发效率,那么这些模块就需要再好好斟酌一下。一般情况下这种情况出现不是因为模块过于庞大就是因为模块的碎片化,对于前者需要拆分更多模块以提高复用和去除冗余,后者则需要酌情合并一些耦合性较高的模块。
  4. 子模块的子模块 这种情况的出现往往就意味着这个项目本身就应该被分成多个项目,多个项目之间也可以继承同一个parent的pom.xml这主要是为了方便统一构建多个项目。所以子模块的子模块应该避免出现。

其中关于继承等的问题会在后面详细说明。

依赖管理

依赖传递

  • 依赖具有传递性
    • 直接依赖:在当前项目中荣国依赖配置建立的依赖关系
    • 间接依赖:被资源的资源如果依赖其它资源,当前项目间接依赖其他资源
  • 依赖传递冲突问题:当依赖中直接或间接出现同一依赖项时会出现依赖冲突
    • 路径优先:当依赖中出现相同资源时,层级越深,优先级越低,层级越浅,优先级越高
    • 声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖配置顺序靠后的
    • 特殊优先:当配置了相同资源的不同版本,后配置的覆盖先配置的

可选依赖与排除依赖

  • 可选依赖是隐藏当前工程所依赖的资源,隐藏后对应的依赖资源将不具有依赖传递性
    • 具体操作为:在需要隐藏的依赖项中添加<optional>true</optional>
  • 排除依赖是隐藏当前资源对应的依赖关系
    • 具体操作为:在资源的依赖项中加入<exclusions>标签,在其中通过多个<exclusion>标签标明此资源中需要排除的依赖项
  • 可选依赖与排除依赖的区别
    • 排除依赖是控制当前项目是否使用其直接依赖传递下来的接间依赖
    • 可选依赖是控制当前项目的依赖是否向下传递
    • 可选依赖的优先级高于排除依赖
    • 若对于同一个间接依赖同时使用排除依赖和可选依赖进行设置,那么可选依赖的取值必须为 false,否则排除依赖无法生效

可以将可选依赖理解为将自己项目的依赖项设置成不透明,排除依赖则是将别人提供的资源的依赖标注为不需要。