工作多年,身边 pom 写的溜的极少

我相信许多人不喜欢maven的原因是没有掌握好该掌握的部分,看到那大堆的xml,总有种无法下手的感觉。而且在使用中时老是遇到问题,却很难在短时间内找到答案。总觉得它太笨重了

不钻研,无论用gradle还是什么,遇到问题都一样的

maven设计了一套复杂的体系,如坐标、依赖、生命周期、插件等

环环相扣,如果不把这一套东西都搞明白,是没法下手的

一套相对封闭但又有特色的体系,虽然内部配合很好,但对于外界就没那么友好了。在花时间了解、适应、认同它之前,会经常撞墙的


ant与maven是两个极端

ant很灵活,但没有统一的流程,需要写很多的代码,且每个人都有自己的一套

maven相反,定义了严谨、繁琐的流程,考虑了很多,但具体到某一个任务的时候,又不够灵活。要么搜现成插件,要么自己写,不论哪种方式,都会让人觉得束手束脚,感觉被maven设计出来的锁链绑着。一想到随便来点什么都要上插件,让人压力巨大

thoughtworks文章提到,对于构建工具,在plugin的层面上抽象,还是不够灵活

还是需要一种能语言层面上抽象的工具。比如buildr,比如gradle

而gradle正好填上了这两个极端的中间。它利用groovy提供的dsl,写起来要比xml舒服很多,而且可以直接如果函数调用般调用ant提供的工具,还可以直接写groovy(Java)代码。相比ant/maven,人们反映使用它的感觉要舒服多了


一些吐槽

  • 运行慢

mvn clean test

  • 日志乱

难以阅读。每次出点问题想从日志里找线索,都是一场折磨

  • 文档难

遇到问题想从maven官网上找点资料,感觉很难。满屏幕都是文字,为什么看不到自己需要的?

  • 插件烦

P大的一点事也要加插件!

比如,想建个跟src平级的integationTest目录,也要下插件!一堆配置!

  • 心情差

每次编辑POM、看到POM、甚至想到POM,都感觉压力巨大,直接影响写代码心情

比较

dependencies {
    compile("group-a:artifact-a:1.0") {
        exclude group: 'group-c', module: 'excluded-artifact'
    }
    runtime "group-a:artifact-b:1.0@bar"
}
<project>
  <dependencies>
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-a</artifactId>
      <version>1.0</version>
      <exclusions>
        <exclusion>
          <groupId>group-c</groupId>
          <artifactId>excluded-artifact</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>group-a</groupId>
      <artifactId>artifact-b</artifactId>
      <version>1.0</version>
      <type>bar</type>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

Maven这,如果不借助编辑器的高亮,基本上没法看啊,眼都瞎了

所以idea 里的maven-helper 是必备的,eclipse 里的maven插件就不提了,

我无法相像idea里维护公司较复杂项目的话,不用这插件的POM会长成啥样

Maven三大死穴

  • 不灵活

尽管Maven本身已经包罗万象拥有了很多功能,但想要增加点自定义的编译时干的事情很麻烦

必须另外建立一个Maven插件项目,然后用Java写一坨插件代码,最后再到另一个项目中去引用那个插件项目,把插件的执行注入到构建的某个步骤中

哪怕想干的事情只是简单的一句 echo yyy=$XXX > xxx.ini,都得写个插件

  • 依赖的库难以修改

Maven中央仓库使用方式是下载二进制包,太不给力,如果你发现别人的东西有bug要修复,那你改了之后那个依赖的库就有两个版本,一个是官方版本,一个是你的修改版本,你就得hack你本地的Maven库缓存让他用你的修改版本,一旦官方版本更新,你就又要蛋疼了

ps:jitpack,但没发现本地部署版

  • 太慢,除了依赖于网速要下载大量东西以外,本身执行速度也很慢

死穴需要有更好的设计来解决

1、配置文件格式换成某种图灵完备语言,用这种语言本身作为DSL即可

buildr、sbt配置文件格式分别是JRuby、Scalabuildr

sbt中如果想定制编译时操作,直接用那种语言写就得了

2、dvcs已经解决得很好

了仓库里面要放源码,方便用户自己hack。发现别人写的代码有问题,自己改好了还得装孙子求着别人合并回主干

遇到Github上的项目就不用装孙子,自己Fork出来改改改改改,改好了给你丢个Pull Request,你爱接受不接受!!

3、分为两部分解决网速问题

改用git或mercurial协议之后就可以大大改善本身执行速度慢,因为Java加载速度太慢,加载类时的文件IO极为可观,再加上Java虚拟机JIT还有些开销

尽管对一个长久运行的进程来说启动慢一点没有关系,但对于构建管理这种一个进程只存活几秒钟的应用来说,启动慢就不能忍了

其实如果要用JVM语言做构建系统,不应该每次执行都启动一个Java进程,而应该做成守护进程,每次执行只启动一个客户端来戳一下守护进程

守护进程和客户端进程之间通讯协议用ssh就好了嘛。 守护进程用Scala或者Java编写,客户端就不用写了,用openssh就行。人家openssh是C写的,启动速度超快

对这糟糕设计的解读,则因人而异

在我看来,Maven处处违背提供机制,而不是策略的设计准则,藐视用户智商,把用户人为划分成插件开发者和普通用户,才是导致错误设计的内在原因

这种错误是路线错误,Maven选择了迎合白痴而得罪黑客,必将被世人唾弃

遵守Java规范的JVM都很难实现快速加载,由.class文件的文件格式所决定如果需要快速加载,要么得改用守护进程,要么就得换掉.class文件格式

例如Android上的Dalvik虚拟机就选了后一条路

但Java社区普遍存在一种对向后兼容性的偏执,所以他们为了避免打破二进制兼容型,恐怕永远不会打字节码格式的主意

例一些深坑

  • spring boot

通常默认是package=jar

main()直接启动,spring-boot-starter-web内嵌embed-tomcat,间接依赖了javax.servlet-api

要是同时想打出 war 包,就得调整

  • lombok

lombok打出的java-source也会带上lombok annotation,造成其他人debug时源码定位不准确

  • javadoc

jdk 8 javadoc 不规范,直接就compile error

加的 ignore lint flag,jdk7 又不认

  • nexus index

fuck gfw

repo.maven.apache.org

repo1.maven.org

repo2.maven.org

http://repo1.maven.org/maven2/.index/nexus-maven-repository-index.properties http://repo1.maven.org/maven2/.index/nexus-maven-repository-index.gz

网上都是

  <mirror>
  <id>nexus-aliyun</id>
  <mirrorOf>central</mirrorOf>
  <name>Nexus aliyun</name>
  <url>http://maven.aliyun.com/nexus/content/groups/public</url>
  </mirror>

且标榜可用

但其实

http://maven.aliyun.com/nexus/content/repositories/central/.index/nexus-maven-repository-index.properties 才可以访问

  <mirror>
  <id>nexus-aliyun</id>
  <mirrorOf>central</mirrorOf>
  <name>Nexus aliyun</name>
  <url>http://maven.aliyun.com/nexus/content/repositories/central</url>
  </mirror>

basic

Maven本质是插件框架,核心不执行具体构建任务,所有任务交给插件完成

编译源码由maven-compiler-plugin完成

每个任务对应了一个插件目标(goal),每个插件有一个或多个目标

maven-compiler-plugin的compile目标编译src/main/java/源码,testCompile目标编译src/test/java/源码

两种方式调用Maven插件目标

1) 插件目标与生命周期阶段(lifecycle phase)绑定

命令行只输入生命周期阶段

mvn compile

maven 默认将maven-compiler-plugin的compile目标与 compile生命周期阶段绑定

根据绑定关系调用maven-compiler-plugin的compile目标

2) 直接在指定要执行的插件目标

mvn archetype:generate 调用maven-archetype-plugin的generate目标

这种带冒号的调用方式与生命周期无关

常见插件

http://www.infoq.com/cn/news/2011/04/xxb-maven-7-plugin

  • archetype

mvn archetype:generate -DarchetypeCatalog=internal

  • maven-dependency-plugin

idea maven helper 插件足够了

  • maven-assembly-plugin

  • maven-enforcer-plugin

  • maven-release-plugin

目标通常直接在命令行调用,因为版本发布显然不是日常构建生命周期的一部分

  • maven-resources-plugin

相当重要,有些扩展名,如密钥不能被过滤

  • maven-surefire-plugin

历史原因,名字不是maven-test-plugin,跳过、排除某些测试等,就要配置了

  • versions-maven-plugin
mvn versions:display-dependency-updates
mvn versions:display-plugin-updates

package

Maven 对项目打包时,要了解 3 个plugin

pluginfunction
maven-jar-pluginmaven 默认打包插件,创建 project jar
maven-shade-plugin打可执行包,executable(fat) jar
maven-assembly-plugin支持定制化打包方式,例如 apache 项目的打包方式

maven-assembly-plugin

jar-with-dependencies

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <configuration>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
    </configuration>
</plugin>

这种打出的是fat jar(依赖的jar包全部解压成class文件后,再与自己的代码打成一个jar包)

需要 mvn assembly:assembly

加上execution,将 maven 某个生命周期与插件的目标相互绑定

 
<executions>
 <execution>
     <id>assembly</id>
     <phase>package</phase>
     <goals>
         <goal>single</goal>
     </goals>
 </execution>
</executions>
      

就只要 mvn package


自定义打包

提供 打包配置文件

文件元素

  • id

release

添加到生成文件名称的后缀符。 ${artifactId}-${id}

  • formats
  • dependencySets
  • fileSets
  • files
    <configuration>
        <descriptors>
            <descriptor>src/main/resources/assembly/release.xml</descriptor>
        </descriptors>
    </configuration>

可执行

configuration 里加 archive/manifest/mainClass

<configuration>
     <archive>
         <manifest>
             <mainClass>com.ddatsh.App</mainClass>
         </manifest>
     </archive>
 </configuration>

import scope

2.0.9后提供的功能

之前版本用parent实现,但每个POM只能有一个parent POM

现在有了import,就可以随意导入另一个POM中定义的依赖,不需要继承

如果有版本冲突,是按全部导入后的情况来计算的

maven nearest definition

各种构建工具都有自己的确定最终使用版本的方式

Gradle默认采用最大值,不论是谁声明的,直接取最大值作为最终采用的版本

Maven 是nearest definition方式

我们自己显式声明的依赖称为第一层依赖,它们自己的依赖叫第二层,还有第三层,更多层

发现版本冲突的时候,优先采用层数小的,忽略后面的

比如 A -> B -> C 1.8和A -> C 1.7,maven用C 1.7,因为它比较近,层数小

但是如果冲突出现在同一层,A -> C 1.8和B -> C 1.7,maven 2.0.8前,没有办法决定使用哪一个,2.0.9才确定会使用它先遇到的那个

maven这种还需要数层的方式,造成的麻烦:

一旦升级了某个依赖后发现它必须使用一个更加新的版本比如C 1.7时,发现比它层数小的某个依赖,使用了C 1.6,那最后maven用C 1.6 !

这时就必须显式声明对C 1.7的依赖。但这会造成理解上的误会,因为通常我们声明的依赖只应该是我们代码中直接依赖的库