十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Fastjson JSON.parseObject非法字符报错原因与排查方法

Fastjson JSON.parseObject非法字符报错原因与排查方法 遇到过JSON.parseObject报“非法字符”这类问题的朋友应该都知道那种感觉代码看着哪儿都对字符串打印出来也人模人样可程序一跑就是com.alibaba.fastjson.JSONException: illegal character甚至连行号列号都给得模棱两可让人一头雾水。这个报错在 Java 后端开发里出现频率极高尤其是涉及接口对接、配置文件读取、第三方回调数据解析的时候几乎每个项目周期里都要撞上几次。我最早被这个报错折磨是在一个对接物流网关的项目里对方推送的 JSON 里带了个不可见的控制字符我整整排查了一个下午最后用十六进制打开报文才发现问题。那次之后我学乖了凡是 JSON 解析报错第一反应不再是盯着代码逻辑看而是先确认“这串字符串到底干不干净”。这篇文章我就把这类问题的常见成因、排查思路和处理方案完整梳理一遍里面包含了我这些年实际踩坑换来的经验希望能帮你少走几步弯路。1. 非法字符报错到底是什么问题1.1 报错现象与触发场景Fastjson 的JSON.parseObject方法说白了就是把一段 JSON 格式的字符串转换成 Java 对象。它在解析的时候会严格按照 JSON 规范去扫描每一个字符——遇到引号、冒号、花括号、方括号这些结构符号就走对应逻辑遇到普通字符就归类为字符串内容。这个过程中只要出现任何一个它认为“不该出现”的字符立刻抛出JSONException错误信息里通常会带上illegal character字样。触发这个报错的场景很常见但表现形式各不相同。我整理了一下基本集中在下面几类接口返回的 JSON 字符串被截断或拼接错误末尾多了个引号、缺少右花括号JSON 字符串中的某个字符串值里嵌套了未转义的双引号报文里混入了 BOM 头、换行符、制表符等不可见控制字符编码问题导致中文字符变成乱码进而产生非法字节序列从文件、数据库或配置中心读取的 JSON 中混杂了注释、特殊符号无论是哪一类本质都是同一个问题你给parseObject的输入并不是一个严格合法的 JSON 文本。1.2 为什么 Fastjson 对字符如此敏感JSON 的语法规则其实非常简单只有六种结构字符{、}、[、]、:、,用来表示字符串的双引号以及用来转义的\。除此之外的字符出现在结构位置就会被判定为非法出现在字符串值内部也有严格的转义要求。Fastjson 的字符扫描逻辑对这方面非常严格。它不是像某些宽松的解析器那样“能猜到意图就继续往下走”而是发现一处不合规就立刻终止解析。这种设计的初衷是避免畸形 JSON 给程序带来不可预知的副作用但对开发者来说报错信息有时候确实不够直观——只说“非法字符”却不告诉你这个字符到底是什么、在哪个位置。我记得有一次排查一个报错错误信息指向某一行可我数了几遍都觉得那一行完全正常。后来把整个 JSON 字符串转成 char 数组逐个打印 ASCII 码才发现问题根本不在报错指向的那一行而是前面某个字符串值里有个肉眼根本看不见的\u0000。从那以后我养成了一个习惯只要parseObject报非法字符先怀疑“看不见的东西”再看看得见的东西。1.3 复现一个最典型的非法字符场景光说不练没什么意思我先构造一个最简单的复现场景。假设你从某个旧系统接口拿到一段 JSON里面某个字段值是用户昵称用户昵称里恰好带了一个没转义的双引号String jsonStr {\name\:\张三\大师\,\age\:30}; JSONObject obj JSON.parseObject(jsonStr);这段代码跑起来必报illegal character。为什么因为JSON.parseObject在扫描时遇到了字符串值里的第二个它认为这个引号标志着字符串结束了结果发现引号后面跟的是“大师”而不是结构字符逗号或右花括号于是判定这里出现了非法字符。这种场景在手工拼接 JSON 时特别容易出现。我见过不少老项目里还是用字符串拼接的方式构建 JSON一不留神就会漏掉转义。但要注意的是它并不是parseObject独有的问题换Gson、Jackson一样会报错只是报错信息不一样。所以遇到这个问题时先从 JSON 本身的合法性找原因而不是急着换解析库。2. 深入解析 parseObject 的字符处理机制2.1 从字符串到对象的完整解析链路要彻底理解这个报错得先弄明白parseObject内部大致做了什么。这个方法的输入是一个字符串输出是一个 Java 对象中间的解析过程可以简单分成三步。第一步是词法分析。Fastjson 会把字符串从头到尾扫一遍把每一个字符分类结构字符、普通字符、转义序列、空白符。这个阶段如果遇到不合规的字符组合就会抛出我们说的非法字符异常。第二步是语法分析。在词法分析的基础上Fastjson 会尝试按照 JSON 语法规则把这些词法单元组织成树状结构遇到{就认为开始一个对象遇到[就认为开始一个数组遇到:就认为前面是键后面是值。第三步是对象绑定。语法树构建完成后Fastjson 会根据你传入的目标类型可能是JSONObject也可能是某个自定义 POJO把树里的数据一一映射到 Java 对象的字段上。这一步也有可能出现类型不匹配的异常比如字符串里存的是abc但目标字段是int类型。非法字符报错基本都发生在第一步词法分析阶段。这也解释了为什么有时候你只是多了一个换行符、多了一个空格它都会给你报出来——因为空格和换行在 JSON 规范里只允许出现在结构字符之间出现在字符串内部必须写成\n、\t这样的转义形式或者干脆原样保留但前提是不影响语法判断。2.2 转义字符在 JSON 解析中的角色JSON 规范里定义了一组标准的转义字符\、\\、\/、\b、\f、\n、\r、\t、\uXXXX其中 XXXX 是四位十六进制数。这些转义序列在字符串内部是有特殊含义的解析器看到反斜杠会把它后面的字符按照转义规则解读。我遇到过不少初学者在这一步犯迷糊在 Java 字符串里写{\name\:\张三\}看着是转义了但转义的结果是什么Java 编译器先把\解释成一个普通双引号字符所以运行时传给parseObject的字符串内容其实是{name:张三}。如果这时候你在字符串值内部再插一个就得写成\但 Java 层面对应的代码是\\。这层嵌套关系搞不清楚非常容易出问题。这里我给你一个非常实用的排查技巧如果代码里写的是字符串拼接构造 JSON先在代码里输出一下最终的 JSON 字符串内容而不是凭肉眼猜。很多时候最后拼出来的字符串跟你想象的根本不是一回事。String jsonStr {\name\:\张三\\\大师\,\age\:30}; System.out.println(jsonStr);上面这段代码输出的内容是{name:张三\大师,age:30}这才是合法 JSON。如果你看到的输出是{name:张三大师,age:30}那基本可以确定问题出在转义层级上。2.3 为什么空白字符也会成为非法字符JSON 规范对空白字符是有明确定义的空格0x20、水平制表符0x09、换行0x0A、回车0x0D这四个字符允许出现在 JSON 文本的任意结构之间但不允许出现在字符串内部除非以转义形式出现。如果你把一个真正的换行符直接放进了字符串值里而没有转义解析器就会认为这个字符串已经结束了换行符之后的任何内容都属于意外的非法字符。这个问题在处理多行文本、日志内容、地址信息时特别常见。比如一个用户的备注信息里包含一段换行这段数据存入数据库时被保留成了真实换行符后端读取出来拼进 JSON 字符串时没有做处理parseObject直接就炸了。规避方案有两个层面。第一是在构建方处理构建 JSON 时对字符串值里的特殊字符统一做转义不要偷懒直接拼接。第二是在解析方处理如果无法控制上游数据可以先把字符串里的控制字符替换掉再做解析。比如str.replaceAll([\\x00-\\x1F], )这类方式但要注意这属于兜底方案不能根治问题。3. 非法字符报错的五大典型成因与定位方法3.1 字符串截断与拼接错误这类问题在分布式系统里特别常见。比如上游服务返回超长 JSON中间被网关或者网络框架截断或者分页查询时把 JSON 字符串做了分片拼接某一帧丢了内容。这种场景下报错信息通常指向前半段末尾或者后半段开头规律性不强但都有一个共同点JSON 的括号配不平。我建议遇到这种情况先做一个快速校验数一下字符串里的{和}、[和]数量是否配对。不需要写什么复杂代码一个小工具方法就够了public static boolean isJsonStructureBalanced(String json) { // 简单校验忽略字符串内部的括号统计结构括号是否配对 boolean inString false; int braceCount 0; int bracketCount 0; for (int i 0; i json.length(); i) { char c json.charAt(i); if (c (i 0 || json.charAt(i - 1) ! \\)) { inString !inString; } else if (!inString) { if (c {) braceCount; if (c }) braceCount--; if (c [) bracketCount; if (c ]) bracketCount--; } } return braceCount 0 bracketCount 0; }这个方法的逻辑不算严谨但胜在快速。如果返回 false直接去排查上游数据的完整性而不是在解析代码里找问题。3.2 字符串值内嵌未转义引号这是所有原因里最容易理解也最高频触发的一个。用户输入、数据库内容、第三方接口返回值里都可能带有双引号字符比如昵称李大锤、评论内容他说你好。这些数据被拼进 JSON 时没有转义解析器就会在半路判断字符串结束然后被后面的内容打个措手不及。定位这类问题有一个非常高效的手段把报错位置附近的字符片段打印出来重点看引号前一个字符是什么。如果前一个字符不是反斜杠那基本就是漏了转义。String jsonStr {\content\:\他说\你好\\}; // 打印出问题的附近片段 int idx 10; // 根据报错信息调整 System.out.println(jsonStr.substring(Math.max(0, idx - 20), Math.min(jsonStr.length(), idx 20)));打印出来的片段如果是他说你好一眼就能看出引号没有转义。这类问题修改方式也简单构建 JSON 的环节用JSON.toJSONString()而不是手动拼字符串如果必须手动拼就把值里的引号先替换成\。3.3 BOM 头与不可见控制字符BOMByte Order Mark是 UTF-8 编码文件开头的几个特殊字节EF BB BF用于标识文件编码。很多 Windows 下的编辑器保存文件时会自动加上 BOM这些字节读入内存后变成字符串开头的\uFEFF字符。这个字符是不可见的但 Fastjson 的解析器看到它就会直接判定为非法字符。这种问题在读取配置文件、读取 JSON 文件时特别常见。我的经验是只要parseObject报的非法字符位置在字符串开头附近第一个想到的就是 BOM 头。处理方式也很成熟先判断字符串开头有没有\uFEFF有就去掉再做解析。String jsonStr readFileContent(config.json); if (jsonStr.startsWith(\uFEFF)) { jsonStr jsonStr.substring(1); } JSONObject obj JSON.parseObject(jsonStr);还有一种类似的不可见字符是\u0000空字符它出现在数据迁移、二进制转文本的场景里相对多一些。这种字符在日志打印时完全看不出来只有转成十六进制才能现出原形。我建议排查工具里准备一段十六进制转换的代码随时能查public static String toHexString(String str) { StringBuilder sb new StringBuilder(); for (char c : str.toCharArray()) { sb.append(String.format(\\u%04x, (int) c)); } return sb.toString(); }3.4 编码不一致产生的乱码编码问题引发的非法字符报错有一个明显特征报错信息里夹杂着锟斤拷、这类典型乱码字符。这些乱码本质上是因为数据在编码转换过程中发生了字节丢失或错误映射到了 Fastjson 解析器眼里就成了不符合 UTF-8 规范的字节序列。我在一个 Spring Boot 项目里遇到过这样的情况接口接收的请求头里带了Content-Type: application/json; charsetGBK但项目全局配置的是 UTF-8两边一冲突中文全部变成乱码。后来在过滤器里统一了编码处理才算根治。遇到这类问题优先排查三个环节数据库连接字符串里有没有指定characterEncoding、HTTP 请求响应的Content-Type里字符集是什么、文件读取时用的字符集和文件实际编码是否一致。三个环节只要有一个不一致数据链路长一点就会出现编码错乱。3.5 数据源中混入注释或特殊符号JSON 规范本身不支持注释但很多开发者写 JSON 配置文件时会手贱加//或者/* */。跟运维同事对接的时候也经常看到他们把 JSON 文件里加上这种“友情提示”前端 JavaScript 里能解析后端 Java 的 Fastjson 一读就直接报非法字符。还有一种情况是数据源里混入了特殊符号比如 JSON 里某个值是 URLURL 本身带了或者中文参数看起来没问题但如果你没有把它当作字符串值包在双引号里解析器就会在或者:出现的位置报错。这类问题的排查思路比较统一把 JSON 字符串原样输出到文件里用支持 JSON 语法高亮的编辑器打开看。如果有任何一行文字是灰色的、没有语法高亮那十有八九就是混入了不该出现的内容。把那些内容删掉再跑解析基本就正常了。4. 系统化排查路径与手工定位技巧4.1 从报错信息中获取有效线索Fastjson 的非法字符报错信息虽然有时候不够直观但仔细看还是有规律可循的。常见的几种报错信息分别对应不同的问题类型报错信息关键词通常对应原因排查方向illegal character结构位置出现意外字符锁定位置查看附近字符unclosed string字符串缺少闭合引号检查字符串值是否漏了引号not close json textJSON 结构不完整检查括号配对与数据完整性syntax error语法层面不符合规范检查是否有注释或多余逗号我这里想多说一句关于“位置”的解读。Fastjson 报错信息里不一定每个版本都会带上明确的字符偏移量就算带上了也可能因为编码问题偏移不准。所以不要把宝都押在报错信息本身需要通过打印、十六进制转换这些手段逐步逼近问题点。4.2 二分定位法快速锁定问题片段如果你面对的是一段非常长的 JSON 字符串靠肉眼从头到尾找问题位置是不现实的。我分享一个自己常用的“二分定位法”效率非常高。第一步把parseObject换成JSON.parse让它把 JSON 解析成 Object 类型。第二步把原 JSON 对半切开注意别从字符串值中间切分别尝试解析前后两段。哪一段报错问题就在哪一段。然后继续对那一段对半切不断缩小范围直到锁定最小的问题片段。这个方法本质上利用的是 Fastjson 解析的“就近报错”特性——它会在遇到第一个非法字符时停下来所以你只需要不断二分逼近就能找到那个字符的确切位置。我实测下来一段几千字符的 JSON四五次二分就能定位到问题点。4.3 利用 IDE 调试器检查解析前字符串IDE 调试器是排查这类问题的强大工具。在调用parseObject的那一行打断点程序停住后把jsonStr变量添加到 Watch 窗口选择“View as JSON”或者类似的选项。如果 IDE 能够以 JSON 格式展示并标出语法错误问题位置基本一眼就能看到。有的 IDE 还支持在调试器里直接输入表达式你可以选中字符串变量右键执行new String(jsonStr.getBytes(UTF-8), UTF-8)之类的转换操作明确当前变量的实际内容。这种方式比打日志更直接因为调试器里不会像控制台一样做一些字符层面的隐藏处理。4.4 第三方在线工具的辅助与局限遇到实在定位不了的问题把 JSON 片段贴到第三方在线校验工具比如 JSONLint里通常能给出比 Fastjson 更友好的错误提示。这个手段我偶尔也会用但有几个注意点要提醒一下第一不能把敏感数据贴到在线工具。这属于基本的安全素养公司内部接口报错、包含真实用户信息的 JSON一律不能外传。第二在线工具对 JSON 标准可能比 Fastjson 更严格或更宽松在线工具校验通过不代表 Fastjson 不会报错反之亦然。所以在线工具更适合用来辅助理解问题不能作为最终裁决。我个人的使用方式是这样的先把 JSON 里的敏感字段替换成xxx这样的占位值再贴到在线工具校验如果在线工具也报错就顺着它提示的位置回头查如果在线工具不报错就回到本地用二分定位法继续排查。5. 彻底解决与防患于未然的工程化手段5.1 编码层面统一入口处理与其每次都靠手工排查不如在工程层面把它堵住。我后来接手的老项目里凡是涉及外部数据解析的地方都会先经过一个统一的数据清洗方法。这个方法做的事很简单移除 BOM 头、把非法的控制字符剔除、确保字符串首尾没有多余空白。public static String sanitizeJsonInput(String raw) { if (raw null) { return null; } // 移除 BOM String cleaned raw; if (cleaned.startsWith(\uFEFF)) { cleaned cleaned.substring(1); } // 控制字符替换为空格排除 JSON 结构中合法的空白 StringBuilder sb new StringBuilder(); for (char c : cleaned.toCharArray()) { if (c 0x20 c ! \t c ! \n c ! \r) { sb.append( ); } else { sb.append(c); } } return sb.toString().trim(); }这样写并不是万能的——它处理不了字符串值里嵌着未转义引号的问题但能把 BOM、空字符这类“看不见的坑”统一排掉让后续解析的关注点集中在真正有逻辑问题的数据上。5.2 构建层面禁止手工拼接 JSON字符串拼接构造 JSON 是非法字符报错的第一大来源。我的建议非常明确项目里禁止手工拼接 JSON统一走序列化工具。Fastjson 自带的JSON.toJSONString()、Jackson 的ObjectMapper.writeValueAsString()都是成熟方案它们会正确地处理字符串值里的特殊字符转义出现非法字符的概率会极大降低。如果因为历史原因必须维持字符串拼接的写法那至少要把需要嵌入 JSON 的每一个字符串值统一经过一个转义方法public static String escapeForJson(String value) { if (value null) { return ; } StringBuilder sb new StringBuilder(); for (char c : value.toCharArray()) { switch (c) { case : sb.append(\\\); break; case \\: sb.append(\\\\); break; case \n: sb.append(\\n); break; case \r: sb.append(\\r); break; case \t: sb.append(\\t); break; default: sb.append(c); } } return sb.toString(); }这个方法和 Java 标准库的做法思路一致但可控性更强。把它当作“兜底工具”用就好最好的选择仍然是走序列化器。5.3 数据链路层面明确编码规范跨系统数据交互时编码问题往往是非法字符报错的深层根源。我的实操经验是交互文档里必须明确写明所有文本数据统一使用 UTF-8 编码并且后端代码里对每个接收数据的入口做编码校验和统一转换。具体来说有四个节点值得排查HTTP 请求的Content-Type头、HTTP 响应的Content-Type头、数据库连接串里的characterEncodingutf8、文件读写的InputStreamReader指定的字符集。这四个节点只要一个不一致数据流转到某个环节就可能变成乱码。5.4 防御层面解析失败时的兜底策略不论你做了多少前置处理生产环境里总会有你预料不到的“脏数据”飘过来。我的习惯是在所有解析外部数据的入口加一层防御解析失败先记录原始数据注意脱敏再尝试走备用字段或默认值不直接让异常打到上层。public static JSONObject parseQuietly(String jsonStr) { try { return JSON.parseObject(jsonStr); } catch (Exception e) { // 记录原始数据方便事后排查注意敏感信息脱敏 log.error(JSON解析失败, 原文(前200字符): {}, truncate(jsonStr, 200), e); // 尝试兜底策略这里可以根据业务自行决定 return null; } }这个做法的价值不仅在于防止线上故障扩散更重要的是让你有了排查依据。很多非法字符问题是偶发性的没有日志你就完全无从下手。有了原始数据日志后续定位就变成了例行公事。6. 常见问题速查表与实战经验总结6.1 几类典型场景的问答速查我在实战中见过不少重复度很高的问题场景这里整理成一个速查表方便你遇到问题时快速对照。具体现象可能原因优先排查动作字符串开头就报非法字符BOM 头或不可见字符检查是否以\uFEFF开头中文字符变成乱码后报错编码不一致检查链路各节点字符集对象内某个字段值报错字段值内有未转义双引号打印该字段附近的字符片段很长 JSON 在末尾位置报错数据被截断检查括号配对、数据完整性同一段 JSON 时好时坏字符串拼接逻辑问题检查构建端的转义处理从文件读取的 JSON 必报错文件编码问题或 BOM用十六进制查看文件头部6.2 我在实际项目中的一次完整排查记录之前有个定时任务每天凌晨从某个第三方数据源拉取用户行为数据存到本地再解析入库。上线两周都很正常突然某一天凌晨开始任务一直在重试日志里全是JSON.parseObject非法字符报错。一开始我以为是上游数据格式变了后来比对发现别的字段都正常唯独某一个字段的值里多了一个0x1F控制字符。那个字段是用户搜索关键词某位用户提交的搜索词里包含了一个“单元分隔符”键盘上几乎不可能打出来但通过某些输入法或复制操作可以混入。这个控制字符在日志里完全隐形我用十六进制打印才看到它。当时就意识到一个问题非法字符不一定来自程序逻辑错误也有可能是业务数据本身“脏”。那次之后我调整了两处代码一是在解析前统一过滤控制字符参考上文sanitizeJsonInput二是解析失败时记录原始数据的十六进制片段方便快速定位。这两处改动上线后类似的偶发问题就没再困扰过我们。6.3 几个值得长期坚持的编码习惯除了特定的排查和修复手段我更想分享几个能从根本上降低此类报错发生频率的编码习惯。第一个习惯拿到外部 JSON 字符串先打印、后解析、再写逻辑。打印是有成本的但比起排查线上故障的成本打印这点开销可以忽略。很多问题在打印输出的一瞬间就能发现端倪。第二个习惯构造 JSON 永远用工具方法。Fastjson、Jackson、Gson 都提供了成熟的序列化方法手动拼字符串看着省事实际上是给自己埋雷。第三个习惯不要忽略非空判断和类型校验。很多非法字符报错发生之前其实已经有一个空指针或者类型转换问题出现过。但因为日志被吞了问题一路传导到 JSON 解析才爆发看起来像是 JSON 的锅实际上源头在更早的地方。养成在关键节点打日志的习惯排查链路会轻松很多。第四个习惯解析外部数据前做脱敏日志。这不是为了排查而是为了安全合规。如果日志里记录的是完整用户信息一旦日志泄露就是事故。脱敏后再记录既不影响排查又能守住底线。6.4 兜底方案换个解析器值不值得网上很多人一遇到 Fastjson 报非法字符第一反应是“换 Jackson 吧”。我在实际项目里换过一次后来发现其实没必要。Fastjson 对非法字符的敏感度和 Jackson 相比确实更严格一些但换解析器只解决“报错方式不同”的问题解决不了数据本身不合法的问题。如果 JSON 字符串本身就是脏的换什么解析器都会在某个环节暴雷只是暴雷时间早晚的问题。我个人的建议是优先解决数据问题而不是换工具。把数据弄干净了用哪个解析器都舒坦数据不干净换解析器只是把定时炸弹往后挪了挪。当然如果你的项目本来就在 Fastjson 和 Jackson 之间做技术选型那么 Jackson 在社区活跃度、对 JSON 规范的支持等方面确实有优势这是选型层面的考虑和排障是两回事。最后再分享一个小技巧遇到定位困难的非法字符问题试着把 JSON 字符串写入.json文件用带语法高亮的编辑器打开那一瞬间往往比你想的任何调试手段都直观。我很多次排查的突破口就是靠编辑器的高亮把问题区域“照”出来的。记住一个原则——报错信息不是终点而是起点真正有价值的信息永远在那串字符串本身里。
返回列表