查看原文
其他

java -version 的秘密

闪客 低并发编程 2022-12-05

当我们执行 java -version 命令时,通常会看到如下信息。

java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode

当然,这是 oracle jdk 8u201 版本的输出结果。


如果我们是用的是 openJDK 构建出的 jdk 来看,它会是这样的。

openjdk version "1.8.0-internal"
OpenJDK Runtime Environment (build 1.8.0-internal-root_2022_06_08_12_25-b00)
OpenJDK 64-Bit Server VM (build 25.71-b00, mixed mode

当然,这是在没有修改源码且没有增加多余 configure 参数构建出的结果。

 

今天我想改一改这个输出信息,让它成为我自己拥有的 jdk,应该怎么办呢?

 

首先,我们下载 openjdk 8u312 版本的源码,可以从 openjdk 官网下载,也可以从一个更开发者友好的 adoptopenjdk 网站下载。


https://github.com/AdoptOpenJDK/openjdk8-upstream-binaries

 

下载好源码后,我们一顿操作猛如虎,配置、编译、打断点、运行调试,进入了 java -version 命令所执行到的关键源码位置。

 

 

可以看出,它最终执行了 sun/misc/Version 类的 print 方法,我们打开这个类。

public class Version {
    private static final String launcher_name = 
        "openjdk";
 private static final String java_version = 
        "1.8.0-internal-debug";
    private static final String java_runtime_name = 
        "OpenJDK Runtime Environment";
    private static final String java_profile_name = 
        "";
    private static final String java_runtime_version = 
        "1.8.0-internal-debug-root_2022_06_09_16_41-b00";

    public static void print() {
        print(System.err);
    }

    public static void print(PrintStream ps) {
      ...
        /* First line: platform version. */
        ps.println(launcher_name + " version \"" + java_version + "\"");
  ...
        /* Second line: runtime version (ie, libraries). */
        ps.print(java_runtime_name + " (build " + java_runtime_version);
  ...
        /* Third line: JVM information. */
        String java_vm_name    = System.getProperty("java.vm.name");
        String java_vm_version = System.getProperty("java.vm.version");
        String java_vm_info    = System.getProperty("java.vm.info");
        ps.println(java_vm_name + " (build " + java_vm_version + ", " + java_vm_info + ")");
    }

这是段 Java 代码,所以非常好理解,print 函数里面就是简单粗暴地输出了三行文字。

 

而这些输出信息,是通过一个个变量拼接的,变量的定义就在上方,比如 launcher_namejava_version 等等。

 

那看来我们只需要修改这个文件的 print 函数,或者修改上面定义的变量的值,就可以做到修改输出信息了。

 

但是呢,这个 Version 文件并不在 openJDK 源码里,而在 openJDK 源码构建出的产物 build 文件夹里。

 

也就是说,这个 Version 文件是构建过程中生成的,想要修改这个 Version 文件,就得找到这个 Version 文件是怎么构造出来的。

 

实际上,Version 文件是由 Version.java.template 文件生成的,这个文件是在源码里。

// jdk/src/share/classes/sun/misc/Version.java.template
public class Version {
    private static final String launcher_name =
        "@@launcher_name@@";
    private static final String java_version =
        "@@java_version@@";
    private static final String java_runtime_name =
        "@@java_runtime_name@@";
    private static final String java_profile_name =
        "@@java_profile_name@@";
    private static final String java_runtime_version =
        "@@java_runtime_version@@";

    public static void print(PrintStream ps) {
        ...
        /* First line: platform version. */
        ps.println(launcher_name + " version \"" + java_version + "\"");
        ...
        /* Second line: runtime version (ie, libraries). */
        ps.print(java_runtime_name + " (build " + java_runtime_version);
        ...
        /* Third line: JVM information. */
        String java_vm_name    = System.getProperty("java.vm.name");
        String java_vm_version = System.getProperty("java.vm.version");
        String java_vm_info    = System.getProperty("java.vm.info");
        ps.println(java_vm_name + " (build " + java_vm_version + ", " +
                   java_vm_info + ")");
    }

可以看出,这个文件是个模板文件,与刚刚生成的 Version 文件只差了上面那些常量的值,是通过 @@XXX@@ 这种占位符替换做到的。

 

那我们接下来就应该寻找,这些占位符变量的值,分别是什么,就知道应该如何修改它了。

 

探索后发现,Version.java.template 这个文件里的 @@XXX@@ 占位符,会由 GensrcMisc.gmk 文件进行替换。

// jdk/make/gensrc/GensrcMisc.gmk
################################################################################
# Install the launcher name, release version string, full version
# string and the runtime name into the Version.java file.
# To be printed by java -version

$
(JDK_OUTPUTDIR)/gensrc/sun/misc/Version.java \
$(PROFILE_VERSION_JAVA_TARGETS): \
    $(JDK_TOPDIR)/src/share/classes/sun/misc/Version.java.template
 $(MKDIR) -p $(@D)
 $(RM) $@ $@.tmp
 $(ECHO) Generating sun/misc/Version.java $(call profile_version_name, $@)
 $(SED) -e 's/@@launcher_name@@/$(LAUNCHER_NAME)/g' \
     -e 's/@@java_version@@/$(RELEASE)/g' \
     -e 's/@@java_runtime_version@@/$(FULL_VERSION)/g' \
     -e 's/@@java_runtime_name@@/$(RUNTIME_NAME)/g' \
     -e 's/@@java_profile_name@@/$(call profile_version_name, $@)/g' \
     $< > $@.tmp
 $(MV) $@.tmp $@

可以看出,这里用了 shell 脚本中的 sed 命令做占位符的替换,比如,将 @@java_version@@ 的值替换为 $(RELEASE)

 

很可惜,$(RELEASE) 也是个 shell 脚本的变量,我们还得寻找这部分变量的来源。

 

再经过一番探索,我们发现,这些变量会在 spec.gmk.in 以及 version-numbers 中定义,比如

// common/autoconf/spec.gmk.in
JDK_VERSION:=@JDK_VERSION@
BUILD_VARIANT_RELEASE:=@BUILD_VARIANT_RELEASE@
MILESTONE:=@MILESTONE@

ifeq ($(MILESTONE), fcs)
  RELEASE=$(JDK_VERSION)$(BUILD_VARIANT_RELEASE)
else
  RELEASE=$(JDK_VERSION)-$(MILESTONE)$(BUILD_VARIANT_RELEASE)
endif

ifneq ($(USER_RELEASE_SUFFIX), )
  FULL_VERSION=$(RELEASE)-$(USER_RELEASE_SUFFIX)-$(JDK_BUILD_NUMBER)
else
  FULL_VERSION=$(RELEASE)-$(JDK_BUILD_NUMBER)
endif
JRE_RELEASE_VERSION:=$(FULL_VERSION

很是可惜,这里面仍然是变量替换,我们继续往下寻找。

 

JDK_VERSION 这个变量来说,找到它在 generated-configure.sh 中有这样的赋值方式。

// common/autoconf/generated-configure.sh
...
# Check whether --with-update-version was given.
JDK_UPDATE_VERSION="$with_update_version"
...
if test "x$JDK_UPDATE_VERSION" != x; then
  JDK_VERSION="${JDK_MAJOR_VERSION}.${JDK_MINOR_VERSION}.${JDK_MICRO_VERSION}_${JDK_UPDATE_VERSION}"
else
  JDK_VERSION="${JDK_MAJOR_VERSION}.${JDK_MINOR_VERSION}.${JDK_MICRO_VERSION}"
f

这里的 --with-update-version 就是我们在编译 openJDK 时,./configure 传入的参数。

 

好了,我们现在终于找到根了!

 

也就是说,假如我们在 configure 的时候传入了

--with-update-version=XXX

这样的参数:


那么 JDK_UPDATE_VERSION 将会等于 XXX

进而导致 JDK_VERSION = 1.8.0_XXX

进而导致 RELEASE = 1.8.0_XXX-internal

进而导致 @@java_version@@ = 1.8.0_XXX-internal

进而导致 java -version 打印出的第一行字符串为

openjdk version "1.8.0_XXX-internal"

而不再是

openjdk version "1.8.0-internal"

 

其他内容同理可知,我们直接说结论。

 

我们重新编译 openJDK,加入编译参数。

./configure \
--with-milestone=fcs \
--with-update-version=312 \
--with-build-number=b00

make al

在得出的产物中,执行 java -version,将会得到如下信息。

openjdk version "1.8.0_312"
OpenJDK Runtime Environment (build 1.8.0_312-b07)
OpenJDK 64-Bit Server VM (build 25.71-b00, mixed mode

这和一开始的默认版本,就不一样了!

openjdk version "1.8.0-internal"
OpenJDK Runtime Environment (build 1.8.0-internal-root_2022_06_08_12_25-b00)
OpenJDK 64-Bit Server VM (build 25.71-b00, mixed mode

OK!我们这就成功得到了一个我们自己定制的 openJDK 发行版!

 

不过先别高兴得太早,我们来看几个业界知名的企业 JDK 发行版的 java -verison 信息。

 

腾讯 Kona 的是这样的。

openjdk version "1.8.0_322"
OpenJDK Runtime Environment (Tencent Kona 8.0.9) (build 1.8.0_322-b1)
OpenJDK 64-Bit Server VM (Tencent Kona 8.0.9) (build 25.322-b1, mixed mode, sharing

阿里 Dragonwell 的是这样的。

openjdk version "1.8.0_322"
OpenJDK Runtime Environment (Alibaba Dragonwell 8.10.11) (build 1.8.0_322-b01)
OpenJDK 64-Bit Server VM (Alibaba Dragonwell 8.10.11) (build 25.322-b01, mixed mode

华为 bisheng 的是这样的。

openjdk version "1.8.0_322"
OpenJDK Runtime Environment BiSheng (build 1.8.0_322-b06)
OpenJDK 64-Bit Server VM BiSheng (build 25.322-b06, mixed mode

我们可以看出,它们不但修改了我们刚刚所说的那几个配置参数,还在第二行的中间,加入了自己独特的 JDK 名称,很是酷炫。

 

那他们是怎么做的呢?

 

别急,你都知道了 Version.java.template 文件可以生成最终的 Version 文件里的 print 方法,那你直接在方法里,打印第二行的中间写死一个字符串,不就行了么?

 

当然可以,你的 JDK 你做主,但是,我们还是来看看他们是怎么优雅地修改源码的。

 

以腾讯 Kona 为例,它分别修改了如下地方的源码。

 

一、改造了 Version.java.template 里的 print 代码,但是没有写死字符串,而是像其他常量一样,留了个占位符。

// https://github.com/Tencent/TencentKona-8/blob/master/jdk/src/share/classes/sun/misc/Version.java.template
public class Version {
    ...
    private static final String java_distro_name =
         "@@java_distro_name@@";
    
    private static final String java_distro_version =
         "@@java_distro_version@@";

    public static void print(PrintStream ps) {
        ...
        /* First line: platform version. */
        ...
        /* Second line: runtime version (ie, libraries). */
        ps.print(java_runtime_name +
            " (" + java_distro_name + " " + java_distro_version + ")" +
            " (build " + java_runtime_version);...
        /* Third line: JVM information. */
        ...
    }
}


二、改造了 GensrcMisc.gmk 文件,将这些占位符同样用 sed 命令替换。

// https://github.com/Tencent/TencentKona-8/blob/master/jdk/make/gensrc/GensrcMisc.gmk
$(JDK_OUTPUTDIR)/gensrc/sun/misc/Version.java \
 $(PROFILE_VERSION_JAVA_TARGETS): \
     $(JDK_TOPDIR)/src/share/classes/sun/misc/Version.java.template
  $(MKDIR) -p $(@D)
  $(RM) $@ $@.tmp
  $(ECHO) Generating sun/misc/Version.java $(call profile_version_name, $@)
  $(SED) -e 's/@@launcher_name@@/$(LAUNCHER_NAME)/g' \
      -e 's/@@java_version@@/$(RELEASE)/g' \
      -e 's/@@java_runtime_version@@/$(FULL_VERSION)/g' \
      -e 's/@@java_runtime_name@@/$(RUNTIME_NAME)/g' \
      -e 's/@@java_profile_name@@/$(call profile_version_name, $@)/g' \
      -e 's/@@java_distro_name@@/$(DISTRO_NAME)/g' \
      -e 's/@@java_distro_version@@/$(DISTRO_VERSION)/g' \

      $< > $@.tmp
  $(MV) $@.tmp $@


三、改造了 spec.gmk.in 文件,在里面做了变量的定义。

// https://github.com/Tencent/TencentKona-8/blob/master/common/autoconf/spec.gmk.in
...
# Include TencentJDK version information
DISTRO_NAME=Tencent Kona
...
DISTRO_VERSION=8.0.9

所以最终,这里的 Tencent Kona 8.0.9 就显示在了 java -version 信息里了。

 

其实本质上,就是改变最终 print 方法的代码而已。

 

好了,关于如何搞出一套自己的 openJDK 发行版并做好 java -version 这个表面工作,你学废了么?

​​

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存