Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2015-01-14) 将一个Jdk1.6项目升级到Jdk1.8

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

我们需要将一个已经长期运行的Jdk 1.6的项目升级到Jdk 1.8,大约花了两周时间完成,遇到了不少问题,特别记录一下。

虽说Java一向努力向下兼容,但是对于一个已经在线上正常运行的项目,贸然升级还是有很大风险的,所以我们提供了一个名为java8的开关。如果打开,则全部使用Java8来编译、测试、部署,如果出错,则关掉它。等经过一段时间的测试后发现比较稳定时,再去掉这个开关,直接使用Java8。

这个任务分成了以下几个部分:

  1. 提供一个开关java8,将jdk1.6和1.8完全分离
  2. 升级jdk版本,以及其它必须升级的库
  3. 升级sourceCompatibilitytargetCompatibility
  4. 修复Java代码中的编译错误,以及失败的测试
  5. 修改打出来的rpm包,让它加上_java8后缀
  6. 修改rpm包依赖,让它使用jdk1.8
  7. 确保部署后的机器,使用正确的jdk版本来运行
  8. 升级部署机上的相关库
  9. 修正spring4.x导致的warning
  10. 升级到jetty 9.x

下面依次详述各部分的做法,以及遇到的问题。

(1). 提供一个开关java8,将jdk1.6和1.8完全分离

我们的项目使用gradle来构建,我们在build.gradle中,使用这样的方式来支持java8开关

ext.javaPackageName = "java-1.6.0-sun"
ext.JAVA_VERSION = "1.6"
ext.SPRING_VERSION = "3.2.2.RELEASE"
ext.APACHE_HTTP_CLIENT_VERSION = '4.1.3'
ext.ASM_VERSION = "4.0"

if (hasProperty("java8")) {
  ext.buildNumber = ext.buildNumber + "_java8"
  ext.javaPackageName = "java-1.8.0-oracle"
  ext.JAVA_VERSION = "1.8"
  ext.SPRING_VERSION = "4.1.3.RELEASE"
  ext.APACHE_HTTP_CLIENT_VERSION = '4.3.6'
  ext.ASM_VERSION = "5.0.3"
}

当我们需要打开java8开关时,我们在调用./gradlew命令时,需要传入-Pjava8命令,如:

./gradlew test -Pjava8

当然此时也要保证当前使用的java版本为jdk1.8。

在做这一步时,要确定把所有需要变化的值抽出来,然后针对两个jdk版本提供两个值。

(2). 升级jdk版本,以及其它必须升级的库

从上面可以看到我们升级了哪些库。其中jdk的升级是必须的,其它的库有很多是因为使用了ASM这样的字节码工具(如gradle, spring等),必须升级成新的ASM版本才能支持jdk1.8。

还有一些是运行时报一些奇怪的错,可能对Java8支持不好,比如Apache http client。

另外,gradle本身也升级了,我们修改了build.gradle中的wrapper任务:

    task wrapper(type: Wrapper) {
        gradleVersion = '1.10'
    }

将它从1.9升级到1.10,之后不要忘了运行./gradlew wrapper命令,生成新的gradle-wrapper.jargradle-wrapper.properties文件。

(3). 升级sourceCompatibilitytargetCompatibility

之前这两个值都是1.6,现在在java8开关打开的情况下,应该改成1.8。这样以后才好使用jdk1.8中提供的新特性。

(4). 修复Java代码中的编译错误,以及失败的测试

升级完版本后,很快发现了一些编译和测试错误。

新版本中的接口增加了方法

升级Spring后(由3.x到4.x),发现有一些类编译不过了,比如我们在代码中实现了接口org.springframework.cache.Cache,而它在4.x中增加了两个方法。

我们也必须实现这两个方法。因为它们从未使用过,所以直接在内部抛出UnsupportedOperationException,并加上合适的注释:

    // We have to implement method `putIfAbsent` because we upgraded spring to new version(4.x)
    // and the interface we depend on is changed with two new methods:
    // - get(Object key, Class<T> type)
    // - putIfAbsent(Object key, Object value)
    // we are not using it in the code, so just make it throw a UnsupportedOperationException

    // @Override
    // Why we comment the `@Override` annotation here:
    //   Next 2 methods are in the `Cache` interface in spring 4.x, but not in Spring 3.x,
    //   but for now, we have to support Java1.6 & spring 3.x, so we need to comment it in order to make it compilable with Spring 3.x
    // Note: If we remove the "java8" toggle and just run it with Java8, we should uncomment it.
    public <T> T get(Object key, Class<T> type) {
        throw new UnsupportedOperationException();
    }

    // @Override (see the comment of previous method why comment it)
    public ValueWrapper putIfAbsent(Object key, Object value) {
        throw new UnsupportedOperationException();
    }

这里有个地方要注意:我们把@Override给注释掉了。这是因为在jdk1.6下,因为接口中没有这两个方法,所以当我们加上@Override后,它会有编译错误。注释之后在两个版本下都运行正常,但是当我们以后只使用java8时,最好再把它弄回来。

针对这两个方法,我们也需要提供相应的测试,因为我们预期它们的行为就是抛出抛出异常。

@Test(expected = UnsupportedOperationException.class)
public void shouldNotSupportedMethodPutIfAbsentSinceWeAreNotUsingItButHaveToImplementIt() {
    cache.putIfAbsent(null, null);
}

@Test(expected = UnsupportedOperationException.class)
public void shouldNotSupportedMethodGetByKeyAndTypeSinceWeAreNotUsingItButHaveToImplementIt() {
    cache.get(null, null);
}

Spring针对@Autowired的行为发生了难以追踪的变化

这个问题在另一篇文章中已有详细说明。主要是某个类中使用@Autowired声明了两个拥有相同类型的字段,而没有指定名字,大约如下:

@Autowired
private User user1;
@Autowired
private User user2;

在spring3.x下,所有测试都正常。而在spring4.x下,部分测试正常,部分报“找不到合适的bean”这样的错误。加上@Qualifier("bean-id")这样的 显式声明后,一切正常。

Spring-MVC对于json类型的空body,行为发生了变化

这个在另一篇博客中将会记录。大意是说,在3.x中,如果用户以put方式请求了一个json类型资源,但是body为空,它将被设为正常。spring将会以正常方式处理所有逻辑,并且最后返回200

但是在4.x中,这样的请求被设视为不正常。spring直接返回400,根本不会执行我们后面的逻辑。

我感觉3.x中的行为更像是一个bug,但是我们却不能贸然的修复,因为到后来这不仅仅是一个简单的修改一下http code的问题,更多的是要搞清楚当前其它系统到底是怎么使用的,以及最初的需求是什么。

经过几天的讨论以及尝试之后,我们发现这个测试实际上没有反应真实的情况。因为当用户put过来的body为空时,我们的会把这个请求转给上游系统,并且得到一个500错误转发给用户。

在这种情况下,我们判断其它系统不会发送一个空请求,最后删了这个测试用例。

从这里可以看出,最始时写下的一个看起来无害的,但没有反应真实需求的测试用例,会给后来的维护带来多大的麻烦。

-noverify加还是不加

最开始我们发现,当升级到jdk1.8.05时,测试不能正常执行,总会报一些奇怪的错误。后来在build.gradle中的test任务中,加入了针对JVM的选项-noverify才能执行。

不过后来同事说,这似乎是一个jdk1.8中某些小版本的bug,使用其它的版本,就不需要加-noverify了。

我们尝试使用某些声明已经解决了这个bug的jdk小版本,但是发现还是报错。经过反复排查,还是找不到原因,可能是gradle或者依赖的某些库有问题,但无法定位。

所以我们还是暂时加上了这个参数,等以后弄清楚了再去掉它。

(5). 修改打出来的rpm包,让它加上_java8后缀

为了区分使用jdk1.8还是1.6编译的,我们将在jdk1.8下,打出来的包后面加上_java8后缀,方便维护。

在这里遇到了一个开始没有考虑到的问题:我们以前正常运行的build pipeline,已经打出来很多包了,比如:mylib-2-110.rpm, mylib-2-111.rpm

而我们为了保证当前pipeline正常运行,所以新开了一条pipeline,结果build number从0开始了,很快我们就发现它打出来的包形如:mylib-2-3_java8.rpm

这给我们带来了很多麻烦,因为在服务器上安装时,比如使用:

yum install mylib

它会默认对包名进行排序,找最新的包,所以我们的mylib-2-3_java8.rpm永远都找不到。当然我们可以手动调大这个build number,但这就意味着我需要时刻关注这两条pipeline,保证它们每一次产生的rpm包都是“最新的”

还有一种方案,我们可以在jdk1.8这边,把base version改成3,这样我们的包就变成mylib-3-3_java8.rpm。虽然现在可以找到了,但这意味着jdk1.6那边,永远拿不到最新的包了,因为它的base version是2

通过各种权衡,因为我们现在的修改已经比较稳定了,没有必要再维护两个pipeline,所以我们最终决定把这两个pipeline合并在一起,通过开启或者关闭某些任务,来控制使用jdk1.6还是1.8。而它们生成的版本号总是变大的,就可以完美的解决这个问题。

(6). 修改rpm包依赖,让它使用jdk1.8

这里要涉及到部署了。有一件事非常重要,但一开始我们没有注意到:我们生成的rpm包,也必须依赖于正确的jdk版本

通过检查build.gradle,发现我们已经处理好了,在打rpm包时,会选择正确的jdk版本传进去。

最开始依赖的是java-1.8.0-openjdk,后来改成了java-1.8.0-oracle

(7). 确保部署后的机器,使用正确的jdk版本来运行

部署之后,我们很快又发现一个新的问题:虽然我们依赖的jdk1.8 java-1.8.0-openjdk正常安装了,但是运行的时候,还是用的java1.6,导致无法启动。

后来发现,我们要把它改成java-1.8.0-oracle,它安装后会自动更新系统的java命令,指向新版本,从而解决了这个问题。

(8). 升级部署机上的相关库

把程序部署到产品后,又发现了一个问题,newrelic无法正常启动,报了一个错:

Java 8 JRE detected but not enabled. Not starting the New Relic Agent.

错误原因及解决方法在这里:https://discuss.newrelic.com/t/enable-java-se-8-compatibility/3574,只要升级一下版本即可。

我们当前使用的newrelic版本为

NEWRELIC = 'com.newrelic.agent.java:newrelic-agent:3.9.0'

该版本不支持jdk1.8,运行时出错。升级为:

NEWRELIC = 'com.newrelic.agent.java:newrelic-agent:3.12.1'

后正常。

(9). 修正spring4.x导致的warning

最后还发现了一个warning,在程序启动的时候,会报

Requested bean is currently in creation: Is there an unresolvable circular reference?

这样的warning。经过反复排查后,发现这是一个非常奇怪的问题,最后的解决办法是使用spring4中提供的一些更推荐的类MethodInvokingBean替换掉spring3.x中的MethodInvokingFactoryBean

相关代码参看这里: https://github.com/freewind/spring-circle-reference

(10). 升级到jetty 9.x

ASM依赖

与其他组同事讨论之后,发现我们项目中使用的某些库,还依赖于旧版本的ASM。经过检查,我们项目中一共依赖了三种不同版本的ASM:

由于我们显式地声明了ASM 5.x,所以其它的库都使用了ASM 5.x。但是由于不同的ASM之间并不是完全兼容,所以这是一个潜在的风险。

所以我们打算把它们都升级到支持5.x的版本:

Jetty-All

后来又发现一个问题,我们使用jetty时,直接使用了组合版,如jetty-all-server或者jetty-all

我在stackoverflow上提了一个问题,Jetty的Committer建议我们不要使用这种组合包,而是需要哪个就用哪个。

所以我们最后去掉了jetty-all的依赖,而是使用实际依赖的小包: http://stackoverflow.com/questions/28101846/where-is-jetty-all-server-for-9-x

comments powered by Disqus