看似无害的代码如何搞垮系统

简介: 编程就像魔法。最近遇到一个诡异的问题:添加一段看似无害的简单代码后,系统原有功能不可用了。## 复现演示jdk 8 可使用如下演示代码复现这个问题。 `TaskCenter` 是一个任务框架,可添加多个任务,随后框架将执行这些任务。 `First` 任务是新增代码,看起来简单无害,且看不出对原有任务 `Count` 有何影响,但添加 `First` 任务后,其自身执行正常,原本正常的 `C

编程就像魔法。最近遇到一个诡异的问题:添加一段看似无害的简单代码后,系统原有功能不可用了。

复现演示

jdk 8 可使用如下演示代码复现这个问题。 TaskCenter 是一个任务框架,可添加多个任务,随后框架将执行这些任务。 First 任务是新增代码,看起来简单无害,且看不出对原有任务 Count 有何影响,但添加 First 任务后,其自身执行正常,原本正常的 Count 任务却不可用了。

public class TaskCenter {

    static class Task {
        final Function<Object[], Object> action;
        final Object[] data;

        Task(Function<Object[], Object> action, Object... data) {
            this.action = action;
            this.data = data;
        }
    }

    static Function<Object[], Object> First = (Object[] data) -> {
        for (Object item : data) {
            if (item != null) {
                return item;
            }
        }
        return null;
    };

    static Function<Object[], Object> Count = (Object[] data) -> {
        final Predicate<Object> nonNull = new Predicate<Object>() {
            public boolean test(Object item) {
                return item != null;
            }
        };
        return Arrays.stream(data).filter(nonNull).count();
    };

    public static void main(String[] args) throws Exception {
        final ArrayList<Task> taskList = new ArrayList<>();

        taskList.add(new Task(First, null, First, "hello"));  // <- 新增代码
        taskList.add(new Task(Count, "hello", null, "world"));
        taskList.add(new Task(Count, "try", "again"));

        for (Task task : taskList) {
            final Object result = task.action.apply(task.data);
            postProcess(result);
        }
        System.out.println("Success");
    }

    // 框架层面的附加检查处理
    static void postProcess(final Object result) throws Exception {
        if (result != null && result.getClass().getClassLoader() != null) {
            ClassLoader parent = result.getClass().getClassLoader().getParent();
            if (parent != null) {
                final Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
                addURL.setAccessible(true);
                addURL.invoke(parent, result.getClass().getProtectionDomain().getCodeSource().getLocation());
            }
        }
    }

}

使用 jdk 8 执行如上代码,可看到 Count 任务在创建内部类 nonNull 对象时报错如下:

Exception in thread "main" java.lang.IllegalAccessError: tried to access class TaskCenter$1 from class TaskCenter
    at TaskCenter.lambda$static$1(TaskCenter.java:33)
    at TaskCenter.main(TaskCenter.java:49)

原因分析

从报错信息看,是 TaskCenter 不能访问 Count 任务方法的内部类 TaskCenter$1

在加载此内部类的地方打断点,仔细比较 TaskCenterTaskCenter$1 各项属性的变化,发现正常情况下两个类都是由系统类加载器加载的,而报错时 TaskCenter$1 变成了由系统类加载器的父加载器加载,导致 IllegalAccessError

新增代码 First 任务找不到任何可疑点,问题在哪里呢?在框架执行任务时产生的副作用上,演示代码中体现的非常直接。真实案例中,框架维护了多个 ClassLoader ,偶然情况下可能将本来由一个 ClassLoader 加载的程序库意外提升到其父 ClassLoader 上。

以演示代码为例,创建任务对象时,任务类由系统类加载器加载,但任务方法用到的内部类,会延迟到任务方法执行时才会加载,此时程序库已被提升到父加载器,导致内部类被父加载器加载,出现不一致。

为什么使用 jdk 8 演示?原问题与 jdk 版本无关,但 jdk 8 内置的 ClassLoader 更方便修改,更方便使用简单演示复现问题。

顺带提一下,演示代码如果将 Count 任务下的内部类改成 lambda 表达式,即

        final Predicate<Object> nonNull = Objects::nonNull;

就不会出现这个问题,这是因为 lambda 表达式进行了特殊优化,与传统的内部类加载不一样。

问题解决

查到问题为:延迟加载内部类时类加载器不一致。如使解决呢?

办法一,修改创建 Count 任务对象的代码,如同时创建 nonNull 对象避免延迟加载等。但这显然是治标不治本,不可取。谁知道哪里还有这种运行时触发的延迟加载呢,如何保证往后的新增代码不引入类似的延迟加载呢?这是 Java 的常规用法,没有可靠的办法来避免,也不应该这样避免。

办法二,分析框架为什么会有这样的副作用,改动框架避免这样的副作用。看起来是要治根,但框架本身涉及面太大,可能没法改,既使改了反而可能会引起更多不可预料的问题。从工作量和后果来看都不太现实。

办法三,将新增代码移动到一个新的程序库,以尽量避免新增代码影响现有代码,拆分细化风险面。类似办法一,也是个治标不治本的办法,只能说风险更可控一些。目前暂采取了此办法,未来还得从长计议。

办法四,引入更合理的新框架,新增代码转而使用新框架。

相关文章
|
1月前
|
设计模式 算法 程序员
程序员为何需要反复修改Bug?探寻代码编写中的挑战与现实
作为开发者,我们在日常开发过程中,往往会遇到反复修改bug的情况,而且不能一次性把代码写的完美无瑕,其实开发项目是一项复杂而富有挑战性的任务,即使经验丰富的程序员也难以在一次性编写完美无瑕地完成代码,我个人觉得一次性写好代码是不可能完成的事情。虽然在设计之初已经尽力思考全面,并在实际操作中力求精确,但程序员仍然需要花费大量时间和精力来调试和修复Bug。那么本文就来分享程序员需要反复修改Bug的原因,以及在开发中所面临的复杂性与挑战。
35 1
程序员为何需要反复修改Bug?探寻代码编写中的挑战与现实
|
26天前
|
算法 程序员
为何程序员在编写程序时难以一次性将所有代码完美无瑕地完成,而是需要经历反复修改Bug的过程?
为何程序员在编写程序时难以一次性将所有代码完美无瑕地完成,而是需要经历反复修改Bug的过程?
18 7
|
5月前
思考:如何写出让同事难以维护的代码?(2)
思考:如何写出让同事难以维护的代码?
29 0
思考:如何写出让同事难以维护的代码?(2)
|
5月前
|
程序员
思考:如何写出让同事难以维护的代码?(1)
思考:如何写出让同事难以维护的代码?(1)
40 0
思考:如何写出让同事难以维护的代码?(1)
|
2月前
|
数据可视化 测试技术
测试范围不清晰该咋办?
测试范围不清晰该咋办?
|
5月前
|
API 计算机视觉
思考:如何写出让同事难以维护的代码?(4)
思考:如何写出让同事难以维护的代码?
33 0
思考:如何写出让同事难以维护的代码?(4)
|
5月前
思考:如何写出让同事难以维护的代码?(3)
思考:如何写出让同事难以维护的代码?
27 0
思考:如何写出让同事难以维护的代码?(3)
|
7月前
|
安全 测试技术
不会写测试用例咋办?牢记这5点,你也能写出高逼格案例
不会写测试用例咋办?牢记这5点,你也能写出高逼格案例
|
11月前
|
程序员 API 计算机视觉
思考:如何写出让同事难以维护的代码?doge
本文从【程序命名&注释】【数据类型&类&对象】【控制执行流程】和【程序/结构设计】四个方面梳理了一些真实案例,相信通过这些案例你能迅速get技能:如何写出让同事难以维护的代码doge。
8891 0
|
设计模式 测试技术
重构·改善既有代码的设计.02之代码的“坏味道”
之前在《重构·改善既有代码的设计.01》中初步了解了重构的基本前提,基础原则等入门知识。今天我们继续第二更......
163 1
重构·改善既有代码的设计.02之代码的“坏味道”