好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

关于feign对x-www-form-urlencode类型的encode和decode问题

对x-www-form-url encode 类型的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

查看更多关于关于feign对x-www-form-urlencode类型的encode和decode问题的详细内容...

  阅读:23次