我们需要将一个已经长期运行的Jdk 1.6的项目升级到Jdk 1.8,大约花了两周时间完成,遇到了不少问题,特别记录一下。
虽说Java一向努力向下兼容,但是对于一个已经在线上正常运行的项目,贸然升级还是有很大风险的,所以我们提供了一个名为java8
的开关。如果打开,则全部使用Java8来编译、测试、部署,如果出错,则关掉它。等经过一段时间的测试后发现比较稳定时,再去掉这个开关,直接使用Java8。
这个任务分成了以下几个部分:
java8
,将jdk1.6和1.8完全分离sourceCompatibility
和targetCompatibility
_java8
后缀下面依次详述各部分的做法,以及遇到的问题。
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版本提供两个值。
从上面可以看到我们升级了哪些库。其中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.jar
和gradle-wrapper.properties
文件。
sourceCompatibility
和targetCompatibility
之前这两个值都是1.6
,现在在java8
开关打开的情况下,应该改成1.8
。这样以后才好使用jdk1.8中提供的新特性。
升级完版本后,很快发现了一些编译和测试错误。
升级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);
}
@Autowired
的行为发生了难以追踪的变化这个问题在另一篇文章中已有详细说明。主要是某个类中使用@Autowired
声明了两个拥有相同类型的字段,而没有指定名字,大约如下:
@Autowired
private User user1;
@Autowired
private User user2;
在spring3.x下,所有测试都正常。而在spring4.x下,部分测试正常,部分报“找不到合适的bean”这样的错误。加上@Qualifier("bean-id")
这样的 显式声明后,一切正常。
这个在另一篇博客中将会记录。大意是说,在3.x中,如果用户以put
方式请求了一个json类型资源,但是body为空,它将被设为正常。spring将会以正常方式处理所有逻辑,并且最后返回200
但是在4.x中,这样的请求被设视为不正常。spring直接返回400
,根本不会执行我们后面的逻辑。
我感觉3.x中的行为更像是一个bug,但是我们却不能贸然的修复,因为到后来这不仅仅是一个简单的修改一下http code的问题,更多的是要搞清楚当前其它系统到底是怎么使用的,以及最初的需求是什么。
经过几天的讨论以及尝试之后,我们发现这个测试实际上没有反应真实的情况。因为当用户put
过来的body为空时,我们的会把这个请求转给上游系统,并且得到一个500
错误转发给用户。
在这种情况下,我们判断其它系统不会发送一个空请求,最后删了这个测试用例。
从这里可以看出,最始时写下的一个看起来无害的,但没有反应真实需求的测试用例,会给后来的维护带来多大的麻烦。
最开始我们发现,当升级到jdk1.8.05时,测试不能正常执行,总会报一些奇怪的错误。后来在build.gradle
中的test
任务中,加入了针对JVM的选项-noverify
才能执行。
不过后来同事说,这似乎是一个jdk1.8中某些小版本的bug,使用其它的版本,就不需要加-noverify
了。
我们尝试使用某些声明已经解决了这个bug的jdk小版本,但是发现还是报错。经过反复排查,还是找不到原因,可能是gradle或者依赖的某些库有问题,但无法定位。
所以我们还是暂时加上了这个参数,等以后弄清楚了再去掉它。
_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。而它们生成的版本号总是变大的,就可以完美的解决这个问题。
这里要涉及到部署了。有一件事非常重要,但一开始我们没有注意到:我们生成的rpm包,也必须依赖于正确的jdk版本
通过检查build.gradle
,发现我们已经处理好了,在打rpm包时,会选择正确的jdk版本传进去。
最开始依赖的是java-1.8.0-openjdk
,后来改成了java-1.8.0-oracle
部署之后,我们很快又发现一个新的问题:虽然我们依赖的jdk1.8 java-1.8.0-openjdk
正常安装了,但是运行的时候,还是用的java1.6,导致无法启动。
后来发现,我们要把它改成java-1.8.0-oracle
,它安装后会自动更新系统的java
命令,指向新版本,从而解决了这个问题。
把程序部署到产品后,又发现了一个问题,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'
后正常。
最后还发现了一个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
与其他组同事讨论之后,发现我们项目中使用的某些库,还依赖于旧版本的ASM。经过检查,我们项目中一共依赖了三种不同版本的ASM:
由于我们显式地声明了ASM 5.x
,所以其它的库都使用了ASM 5.x。但是由于不同的ASM之间并不是完全兼容,所以这是一个潜在的风险。
所以我们打算把它们都升级到支持5.x的版本:
org.eclipse.jetty.aggregate:jetty-all-server:8.1.7.v20120910@jar
=> org.eclipse.jetty.aggregate:jetty-all:9.2.7.v20150116
com.jayway.restassured:rest-assured:1.8.1
=> com.jayway.restassured:rest-assured:2.4.0
后来又发现一个问题,我们使用jetty时,直接使用了组合版,如jetty-all-server
或者jetty-all
我在stackoverflow上提了一个问题,Jetty的Committer建议我们不要使用这种组合包,而是需要哪个就用哪个。
所以我们最后去掉了jetty-all
的依赖,而是使用实际依赖的小包: http://stackoverflow.com/questions/28101846/where-is-jetty-all-server-for-9-x