发现WebclientURLEncoder的表现非常不稳定,所以找了一下正确的使用方式,首先是一个issue TestRestTemplate does the url encoding twice if I pass the URI as a string · Issue #8888 · spring-projects/spring-boot · GitHub ,一个老哥发现当exchange 里传入 URI 类的时候,exchange不会有任何encode行为,但是在传入一个字符串的时候会被encode。

下面有bclozel的回答👇:

1
2
3
4
Hi  [@georgmittendorfer](https://github.com/georgmittendorfer) ,
I think this is the expected behavior in Spring Framework (see [SPR-16202](https://jira.spring.io/browse/SPR-16202) and [SPR-14828](https://jira.spring.io/browse/SPR-14828) for some background on this).
As a general rule, providing a URI String to RestTemplate means that this String should be expanded (using optional method parameters) and then encoded. If you want to take full control over the encoding, you should then use the method variant taking a URI as a parameter.
The Spring Framework team recently added [some more documentation on the subject of URI encoding](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#web-uri-encoding) . Does that help?

看来Spring认为这是一个正常的行为,即传入字符串的时候我就要对你进行encode,你传URI对象的时候我不管。继续点进去看看官方文档 Web on Servlet Stack
1.5.3 中被补上了详细的使用细节,首先它将URI的组成分为两个部分,一个叫做URI template,一个叫做URI variables。然后提供了4种encode的模式:

  • TEMPLATE_AND_VALUES:会先对template进行encode,然后在扩展的时候再对variables进行encode。
  • VALUES_ONLY:不会对template进行encode,在扩展之前对variables进行encode。
  • URI_COMPONENTS:和第二种类似,不过是在扩展之后对values进行encode。
  • NONE:不做任何的encode。

再来看看我们遇到的问题,在我们的一个库中,用错误的姿势使用的URI

1
2
3
4
return webClient
.get()
.uri("/doItemHighCommissionPromotionLinkByAll?" + Utils.pojo2UrlQuery(request))
.exchange();

直接传入了一个字符串,会发现调用的方法是:

1
2
3
4
5
@Override
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
return uri(uriBuilderFactory.expand(uriTemplate, uriVariables));
}

原来我们传入的是一个没有任何占位符的uriTemplate,而现在依赖的WebClient版本中默认的encode模式是EncodingMode.URI_COMPONENTS,也就是根本不会管template部分。所以我们发现为什么传入一个字符串的时候没有被自动encode。

那么是不是把encode模式改为EncodingMode. TEMPLATE_AND_VALUES,让它会encode template就没问题了呢?也不是,比如http://api.vephp.com/hcapi?detail=1&vekey=V00003484Y95498091&para=https://uland.taobao.com/coupon/edetail?activityId=8932eb9980234090851d448195fe363c&itemId=578614572836
这个url,para传入的又是一个url,跟了一下解析代码,会发现template在解析的时候会把query map解成:

1
2
3
4
5
6
{
"detail": 1,
"vekey": "V00003484Y95498091",
"para": "https://uland.taobao.com/coupon/edetail?activityId=8932eb9980234090851d448195fe363c",
"itemId": "578614572836"
}

因为它解析query params的正则长这样:

1
"([^&=]+)(=?)([^&]+)?"

所以后果是para会被encode(而且还是错误的encode,这是一个针对template的encode,不是正紧的urlEncode),但是itemId部分不会。所以最终也只是得到了一个错误的encode结果。

综上所述有三种使用方式:

  • 传入的还是一个字符串,不过在构造的时候自己去做urlEncode,然后在WebClient的设置里将encode模式设为后三种。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    return WebClient
    .builder()
    .exchangeStrategies(strategies)
    .baseUrl(endpoint)
    .uriBuilderFactory(providesUriBuilderFactory(endpoint));

    private static DefaultUriBuilderFactory providesUriBuilderFactory(String endpoint) {
    DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(endpoint);
    factory.setEncodingMode(EncodingMode.NONE);
    return factory;
    }
  • 传入一个URI对象,构造的时候自己去做urlEncode,不用关心WebClient里的设置。

  • 正确的使用url templateurl variables进行构造,那就不用自己去做urlEncode。

    1
    2
    3
    URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
    .queryParam("q", "{q}")
    .build("New York", "foo+bar")

最后你可能会发现自己在进行urlEncode的时候,还是会有问题。可以阅读下 Java URL encoding: URLEncoder vs. URI - Stack OverflowJava equivalent to JavaScript’s encodeURIComponent that produces identical output? - Stack Overflow
简单点说就是 URLEncoder.encode() 方法不是你真正想用到的方法,你可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static String encodeURIComponent(String s) {
String result;

try {
result = URLEncoder.encode(s, "UTF-8")
.replaceAll("\\+", "%20")
.replaceAll("\\%21", "!")
.replaceAll("\\%27", "'")
.replaceAll("\\%28", "(")
.replaceAll("\\%29", ")")
.replaceAll("\\%7E", "~");
} catch (UnsupportedEncodingException e) {
result = s;
}

return result;
}

(好傻啊

Comments

2020-01-20
Contents

⬆︎TOP