在 Java 9 里对 IntegerCache 进行修改?

五年前,我在 Hungarian 上发表了一篇关于“怎样在 JDK 中修改 IntegerCahe”的文章。侵入 Java 运行时是非常有必要的,而且有显而易见的好处。当你在编写侵入代码时,你会对反射的工作机制以及 Integer 类的实现过程有一个更加深入的理解。

Integer 类有一个私有嵌套类——IntegerCache,包含 int 值从 -127 到 128 的 Integer 对象。当代码要把一个 int 数据类型封装成 Integer 对象,并且 int 值在范围 -127~128 内时,Java 运行时环境使用缓存而不是重新创建一个 Integer 对象。这是为了加快代码优化的速度,要记住:编程中的 int 数据类型在很多情况下是在规定的范围内的(类似数组的索引)。

这样做的副作用是,在很多时候,使用等号运算符来比较两个整数对象可能会工作,只要整数值在该范围内。这通常发生在单元测试期间。 在操作模式下,当某些值大于 128 时,此代码执行失败。

使用反射对 IntegerCache 做修改也可能导致奇怪的副作用,而且这会对整个 JVM 产生影响。如果一个 servlet 重新定义了小整数的缓存值,那么在同一个 JVM 上的相同 Tomcat 中运行的所有其他 servlet 都将受到影响。

现在我已经上手了 Java 9 的早期公开版本,我想到可以用新版的 Java 来做相同的侵入操作。在开始用 Java 9 侵入之前,我们先用 Java 8 做一次。

import java.lang.reflect.Field;
import java.util.Random;

public class Entropy {
 public static void main(String[] args)
    throws Exception {
    
        // Extract the IntegerCache through reflection
        //获取类
        Class < ? > clazz = Class.forName(
            "java.lang.Integer$IntegerCache");

        //获取cache成员变量
        Field field = clazz.getDeclaredField("cache");
        field.setAccessible(true);
        Integer[] cache = (Integer[]) field.get(clazz);
        
        // Rewrite the Integer cache
        for (int i = 0; i < cache.length; i++) {
            cache[i] = new Integer(
                new Random().nextInt(cache.length));
        }
        
        // Prove randomness
        for (int i = 0; i < 10; i++) {
            System.out.println((Integer) i);
        }
    }


}

上面的代码通过反射获取了 IntegerCache,然后把随机数赋值给 cache 成员变量。真调皮!

我们在 Java 9 中也能执行这些代码。但是,不要高兴得太早。当人们试图在 Java 9 中进行违规操作时,后果会更严重。

Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
  Unable to make field static final java.lang.Integer[]
  java.lang.Integer$IntegerCache.cache
  accessible: module java.base does not "opens java.lang" to unnamed module @1bc6a36e

会抛出一个 Java 8 中没有的异常。说是"对象不可访问",因为 java.base 模块(每个 java 程序都会自动导入的 JDK 运行时的一部分)不会将模块打开(侵入)到未知的模块。当我们试图设置该成员变量(cache)为“可访问的”的时候,就会抛出这个异常。

在 Java 8 中轻易就能访问到的对象在 Java 9 中变得不可访问了,因为 Java 9 中模块系统会对对象进行保护。代码只有在类处于同一模块下,或为了所有模块或本模块能进行反射性访问,模块打开包时,才能访问成员变量、方法以及其他内容。这些都是在"module-info.java"模块定义文件里配置的:

module myModule {
    exports com.javax0.module.demo;
    opens com.javax0.module.demo;
}

“java.base”模块不会为了编程人员开放它本身,尤其是不会为了未知的模块——也就是我们自己运行的代码。如果我们为了自己的代码创建一个模块,并且对它进行命名,然后错误信息就会包括我们模块的名字(而不是说未知的模块)。

那我们是不是可以用 Java 9 本身有的方法来达到我们的目的呢?在“java.lang.reflect.Module”模块有一个"addOpens"方法可以做到。

这样做可以吗?

这样做是不行的,对于我们这些想这样做的人来说是个坏消息。如果包已经被模块通过上面的方法打开了,那么我们只能将模块下的包打开到别的模块。这样的话,模块可以传递给其他模块,虽然 Java 9 不对外开放的内容我们还是无法访问到 ,但是通过反射模块已经可以访问到一些包了。

但是同时,对于我们来说这也是一个好消息。不像 Java 8,Java 9 的安全性比较好,不容易被攻击。至少这个小漏洞被堵住了。看起来 Java 好像开始变成专业级别的,而不再是一个小玩物了(不好意思,开个玩笑)。在不久的将来,我们可以从 RPG 和 COBOL 迁移一些重要的程序到 Java 上来。



你可能感兴趣的:(嵌套类,IntegerCahe,副作用,缓存值,随机数)