package jnpf.bizcommon.audit.service; import com.alibaba.fastjson.JSONException; import jnpf.util.JsonUtil; import java.lang.reflect.Method; import java.util.Map; /** * 层 0 负缓存的**失败分类**探针(C1 二轮评审 I-3 的证据脚本)。 * *
为什么必须实测而不能推:{@code AuditFormRegistry.deterministicParseFailure} 决定 * 「这次解析失败要不要进负缓存」。判错的代价不对称——把**瞬时**失败判成确定性,该表单 * 从此静默不被审计直到重启(缓存是进程级、无过期的);反过来只是多查一次库。 * L061 ① 记的就是这个坑,而它当时正是**推**出来的黑名单被实证推翻的。 * *
本探针喂各种畸形输入给 {@code JsonUtil.getJsonToList}(层 0 反查主表用的正是它), * 打印真实异常链,再让 {@code deterministicParseFailure} 对它做判定,逐条断言。 * *
退出码:0 = 全绿 / 1 = 有断言失败。**不连库、不起容器**,秒级。 */ public final class AuditL0ParseFailureProbe { private static int passed; private static int failed; public static void main(String[] args) throws Exception { Method decide = AuditFormRegistry.class .getDeclaredMethod("deterministicParseFailure", Throwable.class); decide.setAccessible(true); System.out.println("=== 层0 解析失败分类探针(fastjson 实测链形 + 白名单判定)==="); // ① 各类语法错误:应抛 JSONException → 确定性 → 可缓存 expect(decide, "截断的 JSON", "[{\"table\":\"lims_x\"", true); expect(decide, "顶层非数组", "{\"table\":\"lims_x\"}", true); expect(decide, "元素非对象", "[1,2,3]", true); expect(decide, "空串以外的纯垃圾", "not json at all", true); // ② 畸形 Unicode 转义:**实测抛裸的 NumberFormatException、不被包装成 JSONException** // (codex 独立复核发现,本探针独立复核其发现)。它同样是文本的固有属性 → 确定性。 expect(decide, "畸形 Unicode 转义 \\uZZZZ", "[{\"table\":\"\\uZZZZ\"}]", true); // ③ 瞬时故障:**绝不能**判成确定性 assertDecide(decide, "StackOverflowError(深层嵌套的真实形态)", new StackOverflowError(), false); assertDecide(decide, "OutOfMemoryError", new OutOfMemoryError(), false); assertDecide(decide, "线程中断", new InterruptedException(), false); assertDecide(decide, "包了一层的 SOE(池线程形态)", new RuntimeException(new StackOverflowError()), false); // ④ 自环 cause 链不能把遍历卡死(L061 的写法要求)。 // 注意**不能**用 `e.initCause(e)` 构造——JDK 直接抛 IllegalArgumentException: // Self-causation not permitted(本探针第一版就这么写的,当场炸了)。 // 真实的自环只可能来自覆盖了 getCause() 的自定义异常,所以照那个形态造。 assertDecide(decide, "自环 cause 链不死循环(覆盖 getCause 的形态)", new SelfCausedException(), false); System.out.println(); System.out.printf("=== 层0 失败分类断言:%d 通过 / %d 失败 ===%n", passed, failed); if (failed > 0) { System.exit(1); } } /** 真喂一段畸形 JSON,打印它实际抛的异常链,再断言白名单的判定。 */ private static void expect(Method decide, String label, String json, boolean deterministic) { Throwable caught = null; try { JsonUtil.getJsonToList(json, Map.class); } catch (Throwable t) { caught = t; } if (caught == null) { // 没抛异常也是有效信息(说明 fastjson 宽容地接受了它),不算失败,但要打印出来 System.out.printf(" [INFO] %s → 未抛异常(fastjson 接受了这段输入)%n", label); return; } System.out.printf(" 链形 %-28s → %s%n", label, chain(caught)); assertDecide(decide, label, caught, deterministic); } private static void assertDecide(Method decide, String label, Throwable t, boolean expected) { boolean actual; try { actual = (Boolean) decide.invoke(null, t); } catch (Exception e) { System.out.printf(" [FAIL] %s → 判定方法自身抛异常:%s%n", label, e); failed++; return; } if (actual == expected) { System.out.printf(" [PASS] %s → %s%n", label, expected ? "确定性(可负缓存)" : "瞬时(不缓存)"); passed++; } else { System.out.printf(" [FAIL] %s → 判为%s,应为%s%s%n", label, actual ? "确定性" : "瞬时", expected ? "确定性" : "瞬时", expected ? "" : " ← 危险方向:瞬时失败被永久负缓存 = 该表单静默不再被审计"); failed++; } } private static String chain(Throwable t) { StringBuilder sb = new StringBuilder(); for (Throwable c = t; c != null; c = (c.getCause() == c ? null : c.getCause())) { if (sb.length() > 0) { sb.append(" ← "); } sb.append(c.getClass().getSimpleName()); if (c instanceof JSONException || c instanceof NumberFormatException) { sb.append("*"); // * = 命中白名单的那一环 } } return sb.toString(); } /** 唯一能造出自环 cause 的形态:覆盖 getCause() 返回自己。 */ private static final class SelfCausedException extends RuntimeException { @Override public synchronized Throwable getCause() { return this; } } private AuditL0ParseFailureProbe() { } }