内容目录
在介绍Java编译器(javac)的常量引用替换之前,我先说一个我所经历的与之相关的真实案例。
真实的惨案
还记得,是在三年前,有位技术同事负责维护一个老系统。
老系统中有一个常量类(Const.java)用来存储所有的常量配置数据,包括用于请求第三方平台的 APP_ID
和 APP_KEY
(意即账号和密钥),另一个实现该功能的工具类(ThirdPartyClient.java))需要依赖这两个配置数据。
大致的伪代码如下:
public class Const {
/** 请求第三方平台所需的 APP_ID */
public static final String APP_ID = "theAppId";
/** 请求第三方平台所需的 APP_KEY */
public static final String APP_KEY = "thePassword";
}
public class ThirdPartyClient {
public void postRequest(String args) {
String text = Const.APP_ID + Const.APP_KEY + args;
// handle the text
// ……
}
}
由于正式环境和测试环境需要使用不同的账号和密钥,因此每次在打包发布前,都需要将测试环境的配置修改为正式环境的配置,然后编译 => 打包 => 部署到正式环境。
有一次,那是一个月黑风高的夜晚……该同事忘记修改配置,就直接将程序部署到了正式环境。部署之后,当然很快就发现了故障……所幸发现得还算及时,而且由于是在晚上凌晨,因此也没造成什么重大损失。
该同事开始全力挽救,希望能尽快解决掉该问题。
修改好了配置后,由于将程序代码重新完整编译、打包、部署上传需要花费比较多的时间,救急如救火,同事就打算不重新打包上传,而是直接将Const.java重新编译后的字节码文件(Const.class)上传并覆盖原文件,然后重启。
问题来了,奇怪的是:重启之后,Web 应用仍然使用的是 旧的配置(也就是用于 测试环境 的配置)?!
经过反复验证,同事确认上传的字节码文件为修改后的字节码文件,甚至又重试了几次,但问题依旧。TA 通过 debug 查看输出,也证明这两个配置已经是正式环境的配置了。
于是,紧急情况下,同事开始寻求支援。
我听了同事的描述后,建议同事将修正后重新编译的 ThirdPartyClient.class 上传并替换掉原文件,然后重启试试。
不出意外,Web 应用程序恢复正常,使用的配置当然也是正式环境的了。
同事百思不得其解。
Java编译器(javac)在编译Java源代码时,会读取并分析源代码,并进行一些优化工作。实际上,出现这个问题的根本原因就是 Java 编译器的优化特性之一 —— 常量引用替换!
“罪魁祸首”——常量引用替换
当Java编译器编译源代码时,如果发现某处代码引用了「常量」(同时使用static
和final
两个关键字来修饰),且该常量为字面值形式的原始数据类型或字符串,Java编译器会将此处的常量引用优化为常量值的「内联」(inline)。
就拿上面的例子来说,ThirdPartyClient.java中的代码将被优化为如下等价代码:
public class ThirdPartyClient {
public void postRequest(String args) {
// Const.APP_ID 的引用 被替换成了该常量在编译时的字符串字面值
// Const.APP_KEY 的引用 被替换成了该常量在编译时的字符串字面值
String text = "theAppId" + "thePassword" + args;
// handle the text
// ……
}
}
在Java中,常量在初始化赋值后是不能再被改变的,因此Java编译器就会针对常量进行优化,从而在运行时避免变量引用的调用开销。
当然,它也有可能带来「坏处」,本文中提到的「惨案」即为一例。
大家了解了Java编译器的这个优化特性之后,自然也就能够轻松避免这样的问题。
小结
- Java编译器会在编译源代码时,也会进行优化,会将代码中的常量引用替换为对应的字面值。
- 并不是所有的常量引用都会被替换,该常量必须是原始数据类型或字符串类型,常量的值必须是字面值(例如:
1
、true
、"Hello"
)或在编译期间能够直接计算出结果的常量表达式(例如:2 * 2
,5 + 1
,"Hello" + "World"
)。
注意
- 在应用部署的过程中,尽量不要通过类似的方式来修改配置参数!这种重复的必要性操作是很容易疏忽的,一旦疏忽,就可能给生产环境带来重大损失。我们应该编写程序代码或使用第三方工具,确保依赖于运行环境的参数配置能够自动化智能化的切换,而无需在每次部署时都提心吊胆。
- 上面示例代码中的"theAppId" + "thePassword"实际上也会被 Java 编译器优化合并为一个字符串字面值"theAppIdthePassword"。
1 条评论
right!(期待加上emoji)
撰写评论