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

简介: 编程就像魔法。最近遇到一个诡异的问题:添加一段看似无害的简单代码后,系统原有功能不可用了。## 复现演示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 的常规用法,没有可靠的办法来避免,也不应该这样避免。

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

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

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

相关文章
|
6月前
|
设计模式 算法 程序员
程序员为何需要反复修改Bug?探寻代码编写中的挑战与现实
作为开发者,我们在日常开发过程中,往往会遇到反复修改bug的情况,而且不能一次性把代码写的完美无瑕,其实开发项目是一项复杂而富有挑战性的任务,即使经验丰富的程序员也难以在一次性编写完美无瑕地完成代码,我个人觉得一次性写好代码是不可能完成的事情。虽然在设计之初已经尽力思考全面,并在实际操作中力求精确,但程序员仍然需要花费大量时间和精力来调试和修复Bug。那么本文就来分享程序员需要反复修改Bug的原因,以及在开发中所面临的复杂性与挑战。
170 1
程序员为何需要反复修改Bug?探寻代码编写中的挑战与现实
|
30天前
|
数据可视化 数据挖掘 BI
没办法用Trello?其实有更聪明的替代方案!
在快节奏的工作环境中,Trello作为一款广受好评的项目管理和任务协作工具,凭借其直观的看板界面赢得了全球用户的青睐。然而,由于访问受限、数据安全和本土化资源不足等问题,Trello在国内的实际使用面临诸多挑战。为此,板栗看板(Banli)应运而生,作为一款专为国内市场开发的工具,板栗看板不仅在功能上媲美Trello,还在访问稳定性、自定义选项、智能提醒、数据分析和权限管理等方面进行了优化,特别适合中国团队和企业的实际需求。
40 0
|
5月前
|
存储 缓存 NoSQL
不扒瞎,这个程序让我从150s优化到了5s
在优化一个业务开发组的生产问题时,发现销售管理系统查询数据延迟高达2-3分钟。问题根源在于,程序在for循环中频繁读取Redis大KEY数据,导致性能下降。解决方案是采用本地缓存HutoolCache,将耗时降至毫秒级别。此外,还对RedisTemplate配置进行了研究,Jackson2JsonRedisSerializer在序列化时包括了所有字段,即使字段值为null,增加了数据体积。通过对ObjectMapper的调整,仅序列化非空字段,可以显著提升redis读取性能。本文同时还提醒我们在使用Redis时要注意大对象缓存,强调了正确使用和配置缓存以及避免大对象存储的重要性。
65 5
思考:如何写出让同事难以维护的代码?(2)
思考:如何写出让同事难以维护的代码?
57 0
思考:如何写出让同事难以维护的代码?(2)
|
程序员
思考:如何写出让同事难以维护的代码?(1)
思考:如何写出让同事难以维护的代码?(1)
73 0
思考:如何写出让同事难以维护的代码?(1)
|
6月前
|
算法 程序员
为何程序员在编写程序时难以一次性将所有代码完美无瑕地完成,而是需要经历反复修改Bug的过程?
为何程序员在编写程序时难以一次性将所有代码完美无瑕地完成,而是需要经历反复修改Bug的过程?
65 7
|
6月前
|
数据可视化 测试技术
测试范围不清晰该咋办?
测试范围不清晰该咋办?
|
API 计算机视觉
思考:如何写出让同事难以维护的代码?(4)
思考:如何写出让同事难以维护的代码?
71 0
思考:如何写出让同事难以维护的代码?(4)
思考:如何写出让同事难以维护的代码?(3)
思考:如何写出让同事难以维护的代码?
51 0
思考:如何写出让同事难以维护的代码?(3)
|
设计模式 测试技术
重构·改善既有代码的设计.02之代码的“坏味道”
之前在《重构·改善既有代码的设计.01》中初步了解了重构的基本前提,基础原则等入门知识。今天我们继续第二更......
206 1
重构·改善既有代码的设计.02之代码的“坏味道”