8.5. 使用Maven Dependency插件进行优化

在大型的项目中,随着依赖数目的增加,一些额外的依赖会悄悄进入项目的POM中。而当依赖改变的时候,你又常常会留下一些不再被使用的依赖,而又有时候,你会忘记显示声明你需要的类库依赖。由于Maven 2.x会在编译范围引入传递性依赖,你的项目可能编译没问题,但在产品阶段不能运行。考虑这种情况,当一个项目使用一些被广泛使用的类库如Jakarta Commons Beanutils。你没有显式的声明对Beanutils的依赖,你的项目依赖于一个项目如Hibernate,而后者有对Beanutils的传递性依赖。你的项目可能编译成功并很好的运行,但当你将Hibernate升级到一个新版本,而它不再依赖于Beantuils,你就会遇到编译和运行错误了,这种情况直到项目不能编译才能显现。同时,由于你没有显式的列出依赖的版本,Maven不能帮你解析可能出现的版本冲突问题。

一个好的经验方法是,总是为你代码引用的类显式声明依赖。如果你要引入Commons Beanutils类,你应该声明一个对于Commons Beanutils的直接依赖。幸运的是,通过字节码分析,Maven Dependency插件能够帮助你发现对于依赖的直接引用。使用我们之前优化过的新的POM,让我们看一下是否会有错误突然出现。

$ mvn dependency:analyze
[INFO] Scanning for projects...
[INFO] Reactor build order:
[INFO]   Chapter 8 Simple Parent Project
[INFO]   Chapter 8 Simple Object Model
[INFO]   Chapter 8 Simple Weather API
[INFO]   Chapter 8 Simple Persistence API
[INFO]   Chapter 8 Simple Command Line Tool
[INFO]   Chapter 8 Simple Web Application
[INFO]   Chapter 8 Parent Project
[INFO] Searching repository for plugin with prefix: 'dependency'.

...

[INFO] ------------------------------------------------------------------------
[INFO] Building Chapter 8 Simple Object Model
[INFO]    task-segment: [dependency:analyze]
[INFO] ------------------------------------------------------------------------
[INFO] Preparing dependency:analyze
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [dependency:analyze]
[WARNING] Used undeclared dependencies found:
[WARNING]    javax.persistence:persistence-api:jar:1.0:compile
[WARNING] Unused declared dependencies found:
[WARNING]    org.hibernate:hibernate-annotations:jar:3.3.0.ga:compile
[WARNING]    org.hibernate:hibernate:jar:3.2.5.ga:compile
[WARNING]    junit:junit:jar:3.8.1:test

...

[INFO] ------------------------------------------------------------------------
[INFO] Building Chapter 8 Simple Web Application
[INFO]    task-segment: [dependency:analyze]
[INFO] ------------------------------------------------------------------------
[INFO] Preparing dependency:analyze
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] No sources to compile
[INFO] [dependency:analyze]
[WARNING] Used undeclared dependencies found:
[WARNING]    org.sonatype.mavenbook.ch08:simple-model:jar:1.0:compile
[WARNING] Unused declared dependencies found:
[WARNING]    org.apache.velocity:velocity:jar:1.5:compile
[WARNING]    javax.servlet:jstl:jar:1.1.2:compile
[WARNING]    taglibs:standard:jar:1.1.2:compile
[WARNING]    junit:junit:jar:3.8.1:test

在上面截取的输出中,我们能看到运行dependency:analyze目标的结果。该目标分析这个项目,查看是否有直接依赖,或者一些引用了但不是直接声明的依赖。在simple-model项目中,dependency插件指出有一个对于javax.persistence:persistence-api的“使用了但为未声明的依赖”。为了进一步调查,到simple-model目录下运行dependency:tree目标,该目标会列出项目中所有的直接和传递性依赖。

$ mvn dependency:tree
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'dependency'.
[INFO] ------------------------------------------------------------------------
[INFO] Building Chapter 8 Simple Object Model
[INFO]    task-segment: [dependency:tree]
[INFO] ------------------------------------------------------------------------
[INFO] [dependency:tree]
[INFO] org.sonatype.mavenbook.ch08:simple-model:jar:1.0
[INFO] +- org.hibernate:hibernate-annotations:jar:3.3.0.ga:compile
[INFO] |  \- javax.persistence:persistence-api:jar:1.0:compile
[INFO] +- org.hibernate:hibernate:jar:3.2.5.ga:compile
[INFO] |  +- net.sf.ehcache:ehcache:jar:1.2.3:compile
[INFO] |  +- commons-logging:commons-logging:jar:1.0.4:compile
[INFO] |  +- asm:asm-attrs:jar:1.5.3:compile
[INFO] |  +- dom4j:dom4j:jar:1.6.1:compile
[INFO] |  +- antlr:antlr:jar:2.7.6:compile
[INFO] |  +- cglib:cglib:jar:2.1_3:compile
[INFO] |  +- asm:asm:jar:1.5.3:compile
[INFO] |  \- commons-collections:commons-collections:jar:2.1.1:compile
[INFO] \- junit:junit:jar:3.8.1:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------

从上面的输出我们可以看到persistence-api依赖来自于hibernate。对该模块源码的粗略扫描会展现很多的javax.persistence的import语句,这让我们确信直接引用了这个依赖。简单的修复手段是添加对这个依赖的直接引用。该例中,我们将依赖版本放到simple-parentdependencyManagement片段中,因为这个依赖链接到Hibernate,而Hiberate的版本是在这里声明的。最终你会想要升级的项目的HIbernate版本,在Hibernate依赖旁边列出persistence-api依赖的版本能让你在将来升级父POM中的Hibernate版本的时候,更明显的看到两者关联。

如果你查看simple-web模块的dependency:analyze输出,你会看到这里我们也需要添加对simple-model的直接依赖。simple-webapp中的代码直接引用simple-model中的模型对象,而现在simple-model是通过simple-persist的传递性依赖暴露给simple-webapp的。既然兄弟依赖共享同样的versiongroupId,因此这个依赖可以在simple-webapppom.xml中用org.sonatype.mavenbook0.5定义。

Maven Dependency插件是如何发现这些问题的呢?dependency:analyze如何知道什么类和依赖是你项目的字节码直接引用的?Dependency插件使用ObjectWeb ASM工具包来分析字节码。Dependency插件使用ASM来遍历当前项目中的所有类,构建一个所有其它被引用的类的列表。之后它遍历所有的依赖,直接依赖和传递性依赖,然后标记所有在直接依赖中发现的类。任何没有在直接依赖中找到的类会在传递性依赖中被发现,然后,“使用的,但未声明的依赖”列表就产生了。

相反的,未使用的,但声明的依赖列表就相对比较难验证了,而且该列表没有“使用的,但未声明的依赖”有用。一种情况,一些依赖只在运行时或测试时使用,它们不会在字节码中被发现。你能在输出中很明显的看到它们的存在,例如,JUnit就在这个列表中,但是它是需要的,因为它被用来做单元测试。你也会在simple-web模块中注意到Velocity和Servlet API依赖出现在这个列表中,它们也是需要的,因为,虽然项目的类中没有任何对这些依赖的直接引用,但在运行的时候它们是必要的。

小心移除那些未使用,但声明的依赖,除非你拥有很好的测试覆盖率,否则你很可能引入了一个运行时错误。字节码优化还会突然出现更险恶的问题,例如,编译器可以合法的替换常量的值,优化并移除引用。此时移除依赖会造成编译错误,但是工具却分析显示该依赖未被使用。将来的Maven Dependency插件版本会提供个更好的技术来检测或忽略这些类型的问题。

你应该定期的使用dependency:analyze工具来检测你项目中的这些普遍错误。你可以配置它,当一些条件被发现的时候,让构建失败,它也能够用来生成报告。