JSON parse error: Unexpected character (‘%‘ (code 37)): expected a valid value (number, String, arr

本文详细探讨了Spring MVC框架中不同HTTP请求方式下数据的处理方法,包括GET和POST请求如何与控制器方法交互,以及@RequestBody注解的作用。通过具体的实践案例,分析了不同请求头类型对数据解析的影响。

1 复现过程 POST  + @RequestBody + json格式的数据,不指定数据格式

MappingJackson2HttpMessageConverter 读取 application/x-www-form-urlencoded;charset=UTF-8 就会报异常:JSON parse error: Unexpected character ('%' (code 37)): expected a valid value (number, String, array, object, 'true', 'false' or 'null')

项目配置的参数转换器配置如下:指定MappingJackson2HttpMessageConverter 解析 application/x-www-form-urlencoded 数据

debug 可以定位到源码:

org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#readJavaType

最终会调用com.fasterxml.jackson.databind.ObjectMapper#readValue(java.io.InputStream, com.fasterxml.jackson.databind.JavaType)

其中 InputStream 内容为 %7B%22keyWord%22%3A%22xx%22%2C%22offset%22%3A0%2C%22size%22%3A10%7D=

解码后是KV形式:{"keyWord":"xx","offset":0,"size":10}=

2 根本原因

POST请求传入了json格式的数据,但是却没有指定数据格式(默认) -H "Content-Type:application/x-www-form-urlencoded;charset=UTF-8",引导jackson来解析kv格式的表单数据!

这就超级**了! 正确做法是指定 content-type: application/json如下图(项目中指定MappingJackson2HttpMessageConverter 解析 application/x-www-form-urlencoded 数据)

 

 

 

3  GET 请求 + @RequestParam 

参数 curl -X GET '127.0.0.1:8080/add/employee?nameee=12&empId=0' 从注解中获取参数名如,KV

org.springframework.web.bind.annotation.RequestParam

4  GET 请求 + @RequestBody + 不指定content-type 也可以传输json数据

输入:curl -X GET --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list'

@RequestMapping(value = "/list", method = RequestMethod.GET)
public SearchRequest getHabitTitleMatchList(@RequestBody SearchRequest searchRequest,HttpServletRequest httpServletRequest) {

      return searchRequest;

}

输出:{"keyWord":"xx","offset":0,"size":10,"sort":0,"order":0}% 符合预期

   GET 不指定content-type 默认 application/x-www-form-urlencoded 

   先去根据表单---->如果没有--->再去找body(前提项目中指定MappingJackson2HttpMessageConverter 解析 application/x-www-form-urlencoded 数据 )

匹配后还是用jackson可以解析http协议传输的data。如果指定content-type:application/json,当然更合理的顺利执行。

 

 

  • 结论 :对于GET请求只要,能匹配到MappingJackson2HttpMessageConverter 并且传输 data body为json格式就能,解析出符合预期的入参。

5  POST/GET 无 RequestBody注解并且非简单类型相当于给参数加了@ModelAttribut

A curl -X GET --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1111111'

{"keyWord":"1111111","offset":0,"size":100,"sort":0,"order":0}% 符合预期从表单找数据

B curl -X GET -H 'content-type:application/json' --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1111111'

// {"keyWord":"1111111","offset":0,"size":100,"sort":0,"order":0}% 符合预期 指定content-type 无效,依然从表单找数据

C curl -X POST -H 'content-type:application/json' '127.0.0.1:8080/list?keyWord=1233'

{"keyWord":"1233","offset":0,"size":100,"sort":0,"order":0}% 有/无@ModelAttribute 符合预期从表单找数据

D curl -X POST -H 'content-type:application/json' --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1233'

{"keyWord":"1233","offset":0,"size":100,"sort":0,"order":0}% 有/无@ModelAttribute 符合预期从表单找数据

E curl -X POST -H 'content-type:application/json' '127.0.0.1:8080/list?keyWord=1233'

{"keyWord":"1233","offset":0,"size":100,"sort":0,"order":0}% 无 @ModelAttribute 符合预期从表单找数据

解释:

(SearchRequest searchRequest)非简单类型 和@ModelAttribute 类似 方法签名决定了解析器-->ModelAttributeMethodProcessor

原因:construction---> Bean property binding and validation -->

org.springframework.web.method.annotation.ModelAttributeMethodProcessor#bindRequestParameters--->构造参数键值对

org.springframework.beans.MutablePropertyValues#MutablePropertyValues(java.util.Map)--->coyote 封装了请求参数
  • 结论:GET请求参数直接使用@ModelAttribute 接受参数就OK了

6 POST/GET 有RequestBody注解 POST 必须指定 content-type:application/json

经由:

@RequestMapping(value = "/list", method = RequestMethod.POST/GET)

public SearchRequest getHabitTitleMatchList(@RequestBody SearchRequest searchRequest) {// RequestBody 方法签名决定了解析器jackson

return searchRequest;

}

A curl -X GET --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1111111'

// 无异常 但是不符合预期 {"keyWord":"1111111","offset":0,"size":100,"sort":0,"order":0}% GET提交表单-->提交解析url中参数异常(因为GET 先去根据表单---->如果没有--->再去找body)(前提 jackson 支持类型中有 application/x-www-form-urlencoded )

B curl -X GET -H 'content-type:application/json' --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1111111'

// 无异常{"keyWord":"xx","offset":0,"size":10,"sort":0,"order":0}% GET 根据content直接去找body

C curl -X GET -H 'content-type:application/json' '127.0.0.1:8080/list?keyWord=1111111' 指定了content-type 必须要body(前提 jackson 支持类型中有 application/x-www-form-urlencoded )

// Required request body is missing: public com.xiaodaka.rec.face.aspectj.SearchRequest

D curl -X GET --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list'

// 无异常 {"keyWord":"xx","offset":0,"size":10,"sort":0,"order":0}% GET 先去根据表单---->如果没有--->再去找body(前提 jackson 支持类型中有 application/x-www-form-urlencoded )

curl -X POST -H 'content-type:application/json' --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1233' jackson-->解析body

// 无异常

curl -X POST --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1233'

// com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'keyWord' 表单-->提交解析url中参数异常

curl -X POST --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list'

// nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('%' (code 37)) 表单-->skip(提交解析url中参数无)--->解析body异常
  • 结论:一定要指定content-type 即使知道使用默认的content-type,GET 请求url不带参数但是带了body也是可以jackson解析的

7 其他情况列举

curl -X POST -H 'content-type:application/json' --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1233'

{"keyWord":"1233","offset":0,"size":100,"sort":0,"order":0}%

curl -X POST -H 'content-type:application/json' --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list'

{"keyWord":null,"offset":0,"size":100,"sort":0,"order":0}%

改为(@RequestBody SearchRequest searchRequest) 后:

curl -X POST -H 'content-type:application/json' --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list'

{"keyWord":"xx","offset":0,"size":10,"sort":0,"order":0}%

curl -X POST -H 'content-type:application/json' --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1233'

{"keyWord":"xx","offset":0,"size":10,"sort":0,"order":0}%

// 取消类型 application/x-www-form-urlencoded

curl -X POST --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1233'

{"timestamp":"2020-11-21","status":415,"error":"Unsupported Media Type","message":"Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported","path":"/list"}%

// 打开类型 application/x-www-form-urlencoded

curl -X POST --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list?keyWord=1233'

{"timestamp":"2020-11-21","status":400,"error":"Bad Request","message":"JSON parse error: Unrecognized token 'keyWord': was expecting ('true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'keyWord': was expecting ('true', 'false' or 'null')\n at [Source: (ByteArrayInputStream); line: 1, column: 9]","path":"/list"}%

curl -X POST --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list' 但是(controller和curl method)改为GET就行了!!!

{"timestamp":"2020-11-21","status":400,"error":"Bad Request","message":"JSON parse error: Unexpected character ('%' (code 37)): expected a valid value (number, String, array, object, 'true', 'false' or 'null'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('%' (code 37)): expected a valid value (number, String, array, object, 'true', 'false' or 'null')\n at [Source: (ByteArrayInputStream); line: 1, column: 2]","path":"/list"}%

curl -X GET --data '{"keyWord":"xx","offset":0,"size":10}' '127.0.0.1:8080/list'

{"keyWord":"xx","offset":0,"size":10,"sort":0,"order":0}% // 符合预期

GET 协议一起传输 POST 放到body中??? POST 必须指定content-type; GET 提交 --data数据不是body传输,但是可以用jackson解析

 

解析:classdef JSON < handle % v = JSON.parse(jsonString) converts a JSON string to a MATLAB value. % % This started out as a recursive descent parser, but JSON is so simple % that most of the parser collapsed out. % % In the service of speed, simplicity, and laziness, this code is NOT a % validator. Its purpose is to convert correct JSON strings to MATLAB % values. It does not reject all malformed JSON. % Copyright 2013 The MathWorks, Inc. properties (Access = private) json % the string index % position in the string end methods (Access = private) function this = JSON(JSONstring) this.json = JSONstring; this.index = 1; end function value = getValue(this) % get the next value in the string token = this.getNextToken; if token == '{' value = this.getObject; elseif token == '[' value = this.getArray; else value = token; end end function number = getNumber(this) first = this.index; last = first; ch = charAt(this,first); if(ch == '-') last = last + 1; ch = charAt(this,last); end while isDigit(ch) last = last + 1; ch = charAt(this,last); end if(ch == '.') last = last + 1; ch = charAt(this,last); while isDigit(ch) last = last + 1; ch = charAt(this,last); end end if ch == 'e' || ch == 'E' last = last + 1; ch = charAt(this,last); if ismember(ch,'+-') last = last + 1; ch = charAt(this,last); end while isDigit(ch) last = last + 1; ch = charAt(this,last); end end % pull out the string str = this.json(first:(last-1)); number = str2double(str); % move past it this.index = last; % helper functions function char = charAt(this,position) if(position > length(this.json)) char = 0; else char = this.json(position); end end function tf = isDigit(aChar) tf = aChar > 47 && aChar < 58; end end function string = getString(this) first = this.index + 1; last = first; str = this.json; ch = str(last); while ch ~= '"' if(ch == '\\') %#ok<STCMP> We KNOW both are single chars last = last + 2; else last = last + 1; end ch = str(last); end % get the string without it's quotes string = str(first:(last-1)); this.index = last + 1; % skip the trailing " end function array = getArray(this) % an array is [ value, ... ] array = {}; value = this.getValue; while ~strcmp(value,']') % got a value array{end+1} = value; %#ok<AGROW> final size is unknowable % followed by a comma or a "]" value = this.getValue; if value == ',' value = this.getValue; elseif value == ']' continue else error('JSON parser requires commas between array elements'); end end % Arrays of all numbers are turned into numeric arrays fcn = @(x) isnumeric(x) && ~isscalar(x); if all(cellfun(fcn,array)) array = [array{:}]; end end function obj = getObject(this) % an object is { string : value, ... } obj = struct; value = this.getValue; while ~strcmp(value,'}') fieldname = value; % make sure its a valid structure field name fieldname = strrep(fieldname,':','_'); fieldname = strrep(fieldname,'-','_'); % check for colon value = this.getValue; if value ~= ':' error('JSON parser requires colons between object names and values'); end % get the value value = this.getValue; obj.(fieldname) = value; value = this.getValue; if value == ',' value = this.getValue; elseif value == '}' continue else error('JSON parser requires commas between object elements'); end end end function token = getNextToken(this) % get whatever is next in the string % skip whitespace ch = this.json(this.index); while isWhitespace(ch) this.index = this.index + 1; ch = this.json(this.index); end % is it a special character? if isSpecial(ch) token = ch; this.index = this.index + 1; return end % is it one of the three keywords? switch(ch) case 't' match(this,'true'); token = true; return; case 'f' match(this,'false'); token = false; return; case 'n' match(this,'null'); token = []; return; end % is it a string? if(ch == '"') token = this.getString; return; end % well, then it better be a number token = this.getNumber; function match(this,str) % find and consume exactly str at the current location of error n = length(str); range = this.index:(this.index + n - 1); found = this.json(range); if strcmp(str,found) this.index = this.index + n; else error('The JSON parser expected "%s" but found %s',str,found) end end function tf = isWhitespace(aChar) % space, carrage return, linefeed, horizontal tab tf = aChar == 32 || aChar == 10 || aChar == 13 || aChar == 9; end function tf = isSpecial(aChar) % the special characters in the JSON "language" tf = aChar == '{' || aChar == '}' || aChar == '['|| aChar == ']'|| aChar == ':' || aChar == ','; end end end methods(Static) % This is the one method you should call from outside the file. % JSON.parse(string)... that should be familiar to Javascrpt % programmers function value = parse(JSONstring) jsonObject = JSON(JSONstring); value = jsonObject.getValue; end end end
08-07
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

自驱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值