Grails的核心依赖必须保证项目中版本一致!

好久没写语录式大标题了,实在是今天玩Grails受害很深很深。

Grails因为本身是通过ivy来管理依赖的,虽然后续的版本和maven的集成不断加深,但是本身还是通过ivy来解析pom.xml。也就是说最后的活还是ivy干的。如果你的项目比较大,选择了通过maven来管理项目,而你的依赖中又牵扯了很多其他的依赖,那么你危险了。

:: UNRESOLVED DEPENDENCIES ::

:: log4j#log4j;1.2.16: configuration not found in log4j#log4j;1.2.16: ‘master’. It was required from …;1.7.0 runtime

对maven用户这是一条多么发指的报错!(比如这个

它的原因其实是你的dependency中有!=1.2.16版本你的log4j,比如1.2.12,即使你在pom里将它添加到了exclusion中也没有用,必须要保持版本的一致!至于你用什么办法让他们保持一致,还是干脆放弃用maven管理你的grails项目:It’s up to you.

同样的问题还出现在其他核心依赖上,例如commons-pool。

jip 0.2 released

As you may know, jip is a dependency management tool for Jython/Java development. It resolves and downloads Java packages from maven-compatible repositories. jip also follows some best practices, encouraging you to use a portable and standalone environment (virtualenv) for your Jython development.

It has been four months since the initial release. In version 0.2, I made following changes for you:

  • Improved console output format
  • Correct scope dependency management inheritance
  • Snapshot management, alpha
  • Environment independent configuration
  • Bug fixes

You can find the typical usage doc on github:
https://github.com/sunng87/jip

jip-0.2 has been published to pypi so you can install or upgrade it with:
easy_install -U jip

Please do remember to use it inside virtualenv!

包管理与路径管理

现在几乎每一种语言都有一些依赖管理工具,或者是中央的包仓库。比如这些:

  • Java: maven, ivy, gradle
  • Ruby: gems
  • Python: easy_install, pip
  • Clojure: leinigen
  • Groovy: maven, grape, gradle
  • Hashkell: cabal
  • PHP: pear
  • Nodejs: npm

这些工具在管理包和路径时都会采用各不相同的策略,有的是通过自身实现,有的是借助语言平台本身的特点。

Java

其中最注明的Maven,它的方式是在POM文件中定义你的依赖,Maven会在本地仓库中维护这些依赖。Maven的本地仓库默认是在$HOME/.m2/repository目录下,是用户独立的,当然要下载一个依赖也不需要root权限。而在通过Maven运行Java项目时,Maven插件会自动管理classpath,你并不需要把这些依赖从本地仓库里拷贝出来而单独维护一个所谓lib目录(这样也不好管理)。这是Maven的方式,目前看来,这也是最经典的一种方式。除了maven本身的特性以外,这也和Java的classpath机制有关。

最近势头很猛的Java构建工具gradle,方式也与Maven类似,它会把下载的依赖存放在$HOME/.gradle/cache目录里,并自动管理classpath。

同样是Java,与maven相对应的是ant+ivy。ivy可以为你管理依赖,但是ivy不会帮你管理classpath。ivy的包管理,是以project为scope的,你需要维护一个lib目录来存放这些下载的包,再通过传统的ant的方式去管理classpath,从而使项目可以进行编译和运行。

在Java世界里,还有一个特别的工具叫做grape,它是专门用于groovy的轻量级的依赖解决方案。grape是以脚本为scope,在需要依赖的脚本中通过@Grab声明依赖,grape工具可以从maven仓库中下载依赖到$HOME/.groovy/grapes中,并把相关的依赖加入groovy的classpath。除此以外,grape还有一个命令行工具帮助你手动下载依赖到本地仓库。grape的内部是基于ivy的,不过它的方式比ant要自动化很多。

在Java世界里还有一个特例,是Clojure的依赖管理工具leiningen。leiningen本身也比较简单,它的方式与ivy相同,会解析project.clj文件中定义的依赖关系,并下载到当前的工程目录下的lib中。lein是鼓励通过uberjar的方式把依赖统统打包的,所以它并没有classpath的管理功能。

总体来说,Java世界的工具和Java是相似的,其最大特点就是System independent,安装包不需要root权限,每次的运行都需要管理classpath。作为开发人员,classpath中有哪些可以访问的类库是可以控制的,这也使Java程序的移植性得到良好的控制和管理。

Python

与Java不同,Python通常作为系统分发的一部分,他的包管理和PATH管理要相对混乱一些。通常我们有两种方式来安装一个Python的软件包:

  • sudo apt-get install python-redis
  • sudo easy_install redis

一种是通过系统的包管理工具(如apt-get)从系统的软件仓库里安装,一种是通过python自己的包管理工具(如easy_install / pip)从Python Cheese Shop中下载安装。这两种安装方式有什么不同呢。以Ubuntu为例,通过apt-get安装的python包通常会被放在 /usr/share/pyshared 或 /usr/lib/python2.6/dist-packages 中,相对应的,由easy_install安装的Python包,则存放在 /usr/local/lib/python2.6/dist-packages 中。Python启动后可以通过查看sys.path来了解当前的path情况。

除了安装到系统目录,easy_install可以通过 –user 选项来把软件包安装到用户目录 $HOME/.local/lib/python2.6/site-packages。不过无论是系统级别还是用户级别,python都很难在启动时管理Path,即任何时候python都可以访问安装在系统中的所有软件包。这导致了混乱的情况,导致编写的python软件难以进行依赖管理和移植(即使没有定义在setup.py中,很多依赖还是可以访问的)。

由此virtualenv营运而生,virtualenv帮助你创建一个独立的python运行环境。激活这个小环境之后,easy_install/pip仅仅安装软件到小环境,python仅能访问环境内部的site-packages,这样整个环境中的依赖关系就非常清楚,也保障了程序的移植性。这样,就将原本系统scope的python包管理级别改进为项目级别。我之前写的jip也是将依赖下载或拷贝到virtualenv的小环境中,并且修改jython的启动脚本修改PYTHONPATH的设置,保证Java依赖对Jython的透明可访问。

Nodejs

nodejs是一个新兴的生态系统,一个包管理工具对其也是必不可少。npm是目前整个社区都比较认可的工具。

不过目前npm并不好用。npm默认会把自己安装到 node安装前缀的目录,比如node安装时你选择了默认前缀/usr/local,那么npm会把自己安装到/usr/local/lib/node里。这个目录是系统级别的,所以需要root权限,而npm本身又不鼓励用户用root权限来安装软件包(安全问题)。所以作者说希望用户把/usr/local/lib/node权限授予用户,或者把node安装到用户目录里。这两种方式其实都不太优雅。

Ruby的gems在这方面最符合unix哲学,即用户知道自己在做什么。如果用户以root权限运行gem install,gem会把软件包安装到系统目录中对所有用户可用,而如果以普通用户权限运行,则安装到用户目录 $HOME/.gem 中仅当前用户可见。

nodejs在加载软件包时,会在require.paths中的几个目录里查找,前两个都是用户目录,所以npm也并非一定要把包安装到系统目录里去。虽然现在可以用过修改.npmrc文件在修改npm的默认行为,不过在这个CoC的时代,显然太繁琐了。

Best Pratice

总结一下,包管理和路径管理的最佳实践应该是:语言平台有CoC的路径机制,包管理器有基于环境变量、用户权限的判断执行合适行为。

LM Tooling, 2010

阿哈,大家都开始总结2010年了,我也想找个角度来总结一下今年。三句不理老本行,还是聊软件开发周边的事情。Lifecycle Management Tooling,软件生命周期管理的工具和我一年的感受。

一月份的时候我还有大把的业余时间,在做一个叫做Yan的验证码服务。这个项目从我刚一到盛大工作就开始了,不过因为没有特别的需求,最后变成了自己的开源项目。这个项目用Maven做dependency management,用hg做版本控制,代码放在bitbucket上。它对Maven的使用还很基本,是一个Single Module的小项目,因为服务本身已经实现了插件机制,本来我准备把它拆成Multimodule的Maven工程来着,不过后来因为比较忙,这件事就放掉了。那时我已经开始尝试用hg进行分支管理、每个release打tag,甚至还有一段时间会有一些两个分支的并行开发。这些基本上是实验性质的尝试,不过和后来相比也算是做的比较规范的了。

三月份我注册了github,顺带就开始用git了。到目前为止,我对hg和git的区别还没有特别的感受,选择的主要依据其实bitbucket和github。相对social一些的项目,github是当仁不让的选择。bitbucket上人少比较清净,一些incubator项目放在上面,烂尾了也不至于太丢脸。扯远了,哈哈。

之后我们开始做一个规模比较大的项目,我们使用Maven,使用Nexus,使用SVN。项目规模比较大,我们有数个Multimodule的Maven项目,遇到的第一个问题就是集成的成本非常大。最初几周,我都是在自己的机器上做集成,Windows,你懂的,又不太方便做自动化,结果整个过程非常痛苦。到了四月份项目还没有全部完成,已经不能忍了,于是我引入了Hudson。我们为每一个项目创建单独的build task,unstable的构建会有邮件通知开发人员。一方面把我从每天反复的手动集成打包中解脱出来,另一方面也促进了大家写高质量的UnitTest,因为一旦有测试不通过就会有一个虚构的200000000@snda.com的邮箱给大家发邮件,以至于最后几乎所有人都把这个地址拉黑了。

写真正能够Automation的单元测试其实很不简单,尤其是DAL层。理想的solution是用内存数据库来做单元测试的数据源,不过在实际开发里,因为一些SQL会依赖特定的数据库,所以这个方案并不太理想。如果有事务的话,Spring的TestCase可以设置自动回滚,可是互联网项目,你也懂的,连一致性都搞成最终一致了,事务这么重的东西我们是不用的。

我们遇到的另一个问题是,多个独立的Maven项目,在做集成时,会存在依赖的冲突。这类问题一旦出现,找原因的代价还是比较大的。这方面,老大有一个Best Practice,定义一个统一的、专门管理版本的pom,通过dependency management的import来使用,你可以看这里来了解。

我们suffer的另一个问题是SVN的分支管理,到了项目的后期,功能越来越多,也出现了两个分支的并行开发,回滚的情况也出现了。我们基本上可以保证每次发布都有tag,但是分支的管理实在是非常痛苦。我现在的认识是,两方面原因。第一,SVN的分支成本还是比较高的,远程整个目录拷贝一下,本地需要指向新的地址。第二,项目的计划不明确,根本没有办法去估计新的分支、新的版本。两个原因结合起来说,其实分支管理不完全是一个版本控制系统层面的问题,和整个项目规划都有一定的联系。

到了后期,我们对Hudson的使用也有了一些最佳实践,结合ssh/scp的插件,task的trigger机制,我们有了自动化的部署,你可以在这里了解。不过我们的自动化最后倒在公司强制的动态密码验证机制上,每次登录需要使用动态密码,这样一切最后又都回归了手动。

除了在公司的项目,在9月份我又做了一个实验性的项目,叫做Bason,他可以在编译时生成JavaBean到Mongodb BSON代码。也是用Maven管理,不过值得一提的是,我申请了Sonatype提供的OSS Repository,这样可以把项目都deploy到公共的服务器上,使用会非常方便。还值得一提的是,OSS Repository的管理者是校友@Juvenxu

不过今年Maven并不是Java生命周期管理的唯一工具,还有一些工具也开始进入视线,当然我说的不是ant+ivy。前段时间发布0.9的Gradle是比较受关注的,它又回归了ant的task oriented ,结合Groovy的DSL和插件机制提供更高的灵活性。另外,断断续续学了一年的clojure,clojure的生态系统里已经有了一个叫做leinigen的生命周期管理工具,在clojure的世界里,它非常好用。

另外Python的世界,我逐渐开始强制自己用virtualenv来创建一个更可控,更具有移植性的python环境。随着年底开始的一个小项目,我应该对python项目的生命周期管理有更深的认识。

年底到了SAP,开始在NetWeaver平台上做Abap开发。NetWeaver是一个复杂的平台,它的Scope也涵盖了生命周期管理。在NetWeaver中,开发一个非local的程序,需要首先创建一个Change Request,之后所有的变更,最后都会保存到这个ChangeRequest上,有些类似传统的分支。当开发完成后,开发人员把change request release掉,这部分代码会自动被transport到Test系统或者其他下游系统上。实际开发里,一个Change Request可能对应一个Ticket,一个特性。这样的平台上,开发人员需要额外操心的事情,非常少。

Hudson tips

Hudson的项目有三种状态,分别是failed, Success, Unstable。当单元测试未能通过时,Hudson不会fail掉整个build而是设置为ubstable,并且继续执行post build scripts和actions。这就为集成的版本带来了一些不可知因素。取消这个设置,可以通过在maven options中添加一个 -Dmaven.test.failure.ignore=false。或者在全局设置,manage hudson -> configure hudson -> Global MAVEN_OPTS。这个方法来自:http://stackoverflow.com/questions/1004540/fail-hudson-build-on-single-unit-test-failure

此外,我们借助hudson来自动完成开发环境的部署。从hudson的插件列表中安装ssh plugin和scp plugin。对打包项目新增一个post build action,使用SCP插件把打包生成的压缩文件上传到开发机上。本想同时配置一个build script,在开发环境机器上执行一个自动化部署脚本,但是使用发现ssh plugin的操作居然先于scp操作,而且这个顺序无法配置!不过不要紧,新建一个freestyle项目,项目只利用ssh插件运行远程脚本,再在打包项目配置中新增一个post build action -> build other project,填写前者的项目名,使之成为打包项目的downstream project即可。

The post is brought to you by lekhonee v0.7