对x-www-form-urlencode类型的encode和decode问题
记录一下开发过程中遇到的一个问题。
问题场景
使用feign调用另一服务b时,在feign-client包里跑单测能调用成功,在另一项目a引入该feign-client时使用同样的参数调用失败。content-type为application/x-www-form-urlencode POST请求
问题原因
入参中有一个String,数据是jsonArray,包含","和":",在打印请求的参数发现,feign-client包里对参数encode之后,“,” 和“:"不变,而项目a调用feign-client对参数encode会把“,” 和“:"encode成%2C和%3A,导致服务b decode失败。
后来debug对比两次的不同点,发现关键点在于feign中生成的RequestTemplate不同;一步一步调试发现,feign-client包中 feign-core版本是10.2.3,项目a的feign-core版本是9.5.1,两者在生成RequestTemplate中底层对参数encode的方法不同,低版本使用的JDK1.8的URLEncode,高版本使用的feign里的UriUtils.encodeReserved。
feign.template.UriUtils.encodeReserved对参数编码时,会将参数列表中key-value的value分割为byte数组,然后依次对每个byte进行encode,根据isAllowed方法判断是否需要encode,pctEncode(b, encoded)方法是真正去encode的地方。下面的代码可以看到UriUtils.encodeReserved保留了字母数字逗号冒号等字符。而java.net.URLEncode的encode方法不会保留逗号冒号等字符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
private static String encodeChunk(String value, FragmentType type, Charset charset) { byte [] data = value.getBytes(charset); ByteArrayOutputStream encoded = new ByteArrayOutputStream(); // 依次对每个byte编码 for ( byte b : data) { // 对于一些字符不进行编码 if (type.isAllowed(b)) { encoded.write(b); } else { /* percent encode the byte */ pctEncode(b, encoded); } } return new String(encoded.toByteArray()); } boolean isAllowed( int c) { return this .isPchar(c) || (c == '/' ); } protected boolean isPchar( int c) { return this .isUnreserved(c) || this .isSubDelimiter(c) || c == ':' || c == '@' ; } protected boolean isUnreserved( int c) { return this .isAlpha(c) || this .isDigit(c) || c == '-' || c == '.' || c == '_' || c == '~' ; } protected boolean isAlpha( int c) { return (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' ); } protected boolean isDigit( int c) { return (c >= '0' && c <= '9' ); } protected boolean isSubDelimiter( int c) { return (c == '!' ) || (c == '$' ) || (c == '&' ) || (c == '\'' ) || (c == '(' ) || (c == ')' ) || (c == '*' ) || (c == '+' ) || (c == ',' ) || (c == ';' ) || (c == '=' ); } |
至于为什么服务b对URLEncode编码的参数解析不了,还待探索,因为我没看服务b的decode代码,不知道服务b是怎么解析的。
由于服务b已经对多方提供,不能让他们适应低版本去增加解决方案(事实上他们也不想动代码),所以只能从发起方来解决问题。
可能的解决办法(没来得及尝试)
1、版本升级,将项目a的feign-core版本升级到10.2.3,问题能解决(已尝试),但是项目a中已经使用低版本的feign与多个服务交互,虽然理论上feign会向下兼容,但是我不敢轻易升级版本,而且版本号跨度还挺大,风险太大 = =。
2、将高版本的encode方法提取出来,手动配置到feign.encode中
3、加一个interceptor,将低版本encode的template再特殊decode一次,保持和高版本的一致 (失败,template属性是unModifiable)
4、看能否让项目a调用b服务时使用高版本feign-core ,其他feign仍然使用低版本
5、放弃feign 用 httpclient调用 。。。。
附:feign的调用栈
1、 ReflectiveFeign 被反射实例化
2、SynchronousMethodHandler.invoke
2-1、先实例化RequestTemplate 此处encode参数
2-2、executeAndDecode方法,将RequestTemplate build为request,此处会先执行拦截器
2-3、execute 执行 访问原程服务
2-4、将response decode
附上源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
// 2、SynchronousMethodHandler.invoke public Object invoke(Object[] argv) throws Throwable { // 2-1、先实例化RequestTemplate 此处encode参数 RequestTemplate template = buildTemplateFromArgs.create(argv); Retryer retryer = this .retryer.clone(); while ( true ) { try { return executeAndDecode(template); } catch (RetryableException e) { try { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); if (propagationPolicy == UNWRAP && cause != null ) { throw cause; } else { throw th; } } if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue ; } } } Object executeAndDecode(RequestTemplate template) throws Throwable { // 2-2、executeAndDecode方法,将RequestTemplate build为request Request request = targetRequest(template); if (logLevel != Logger.Level.NONE) { logger.logRequest(metadata.configKey(), logLevel, request); } Response response; long start = System.nanoTime(); try { // 2-3、execute 执行 访问原程服务 response = client.execute(request, options); } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); } throw errorExecuting(request, e); } long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); boolean shouldClose = true ; try { if (logLevel != Logger.Level.NONE) { response = logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); } if (Response. class == metadata.returnType()) { if (response.body() == null ) { return response; } if (response.body().length() == null || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) { shouldClose = false ; return response; } // Ensure the response body is disconnected byte [] bodyData = Util.toByteArray(response.body().asInputStream()); return response.toBuilder().body(bodyData).build(); } if (response.status() >= 200 && response.status() < 300 ) { if ( void . class == metadata.returnType()) { return null ; } else { Object result = decode(response); shouldClose = closeAfterDecode; return result; } } else if (decode404 && response.status() == 404 && void . class != metadata.returnType()) { Object result = decode(response); shouldClose = closeAfterDecode; return result; } else { throw errorDecoder.decode(metadata.configKey(), response); } } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); } throw errorReading(request, response, e); } finally { if (shouldClose) { ensureClosed(response.body()); } } } long elapsedTime( long start) { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); } Request targetRequest(RequestTemplate template) { // 此处会先执行拦截器 for (RequestInterceptor interceptor : requestInterceptors) { interceptor.apply(template); } return target.apply(template); } Object decode(Response response) throws Throwable { try { // 2-4、将response decode return decoder.decode(response, metadata.returnType()); } catch (FeignException e) { throw e; } catch (RuntimeException e) { throw new DecodeException(response.status(), e.getMessage(), e); } } |
feign x-www-form-urlencoded 类型请求
spring发送 content-type=application/x-www-form-urlencoded 和普通请求不太一样。
试了好多方式,最后用以下方式成功
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@FeignClient ( name = "ocr-api" , url = "${orc.idcard-url}" , fallbackFactory = OcrClientFallbackFactory. class ) public interface OcrClient { @PostMapping ( value = "/v1/demo/idcard" , headers = { "content-type=application/x-www-form-urlencoded" } ) OcrBaseResponse<IdCardResponse> getIdCarInfo( @RequestBody MultiValueMap<String, Object> request); } |
Post请求,参数使用@RequestBody 并且使用 MultiValueMap。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 测试代码 @Resource private OcrClient ocrClient; @GetMapping ( "getIdCardInfo" ) public Message getIdCardInfo() { MultiValueMap<String, Object> req = new LinkedMultiValueMap<>(); req.add( "request_id" , 12343531123L); req.add( "img_url" , "xxx.jpg" ); req.add( "source" , - 1 ); req.add( "out_business_id" , 1321434234L); OcrBaseResponse<IdCardResponse> idCarInfo = ocrClient.getIdCarInfo(req); return Message.success(idCarInfo); } |
以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。
原文链接:https://blog.csdn.net/ComeHereCH/article/details/103841567