Spring RestTemplate的URLEncode分析

2,746 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

Spring的RestTemplate大家都不陌生,不熟悉的同学可参考《Springboot -- 用更优雅的方式发HTTP请求(RestTemplate详解)》了解更多RestTemplate的知识。RestTemplate使用起来非常简单方便,但要注意几个关于URLEncode的问题

一、请求的url(字符串类型)会自动URLEncode

测试代码:

RestTemplate restTemplate = new RestTemplate();
restTemplate.getForEntity("https://www.baidu.com?r=编码", String.class);

控制台日志:

image.png

源码分析:

通过以下这段代码,将url字符串进行URLEncode

URI expanded = this.getUriTemplateHandler().expand(url, uriVariables);

image.png

具体流程:

  1. 通过url生成DefaultUriBuilderFactory.DefaultUriBuilder对象
new DefaultUriBuilderFactory.DefaultUriBuilder(uriTemplate)
  1. 调用DefaultUriBuilderFactory.DefaultUriBuilder的build方法URI

    2.1 通过UriComponents的实现类HierarchicalUriComponents已分层的形式解析生成HierarchicalUriComponents对象

    this.expandInternal(new UriComponents.VarArgsTemplateVariables(uriVariableValues));
    
    protected HierarchicalUriComponents expandInternal(UriTemplateVariables uriVariables) {
        Assert.state(!this.encodeState.equals(HierarchicalUriComponents.EncodeState.FULLY_ENCODED), "URI components already encoded, and could not possibly contain '{' or '}'.");
        String schemeTo = expandUriComponent(this.getScheme(), uriVariables, this.variableEncoder);
        String userInfoTo = expandUriComponent(this.userInfo, uriVariables, this.variableEncoder);
        String hostTo = expandUriComponent(this.host, uriVariables, this.variableEncoder);
        String portTo = expandUriComponent(this.port, uriVariables, this.variableEncoder);
        HierarchicalUriComponents.PathComponent pathTo = this.path.expand(uriVariables, this.variableEncoder);
        MultiValueMap<String, String> queryParamsTo = this.expandQueryParams(uriVariables);
        String fragmentTo = expandUriComponent(this.getFragment(), uriVariables, this.variableEncoder);
        return new HierarchicalUriComponents(schemeTo, fragmentTo, userInfoTo, hostTo, portTo, pathTo, queryParamsTo, this.encodeState, this.variableEncoder);
    }
    

    2.2 对已封装好的Uri组件进行编码生成URI对象

    this.createUri(uric);
    
    private URI createUri(UriComponents uric) {
        if (DefaultUriBuilderFactory.this.encodingMode.equals(DefaultUriBuilderFactory.EncodingMode.URI_COMPONENT)) {
            uric = uric.encode();
        }
        return URI.create(uric.toString());
    }
    
    

    2.3 对Uri各部分进行编码

    public HierarchicalUriComponents encode(Charset charset) {
        if (this.encodeState.isEncoded()) {
            return this;
        } else {
            String scheme = this.getScheme();
            String fragment = this.getFragment();
            String schemeTo = scheme != null ? encodeUriComponent(scheme, charset, HierarchicalUriComponents.Type.SCHEME) : null;
            String fragmentTo = fragment != null ? encodeUriComponent(fragment, charset, HierarchicalUriComponents.Type.FRAGMENT) : null;
            String userInfoTo = this.userInfo != null ? encodeUriComponent(this.userInfo, charset, HierarchicalUriComponents.Type.USER_INFO) : null;
            String hostTo = this.host != null ? encodeUriComponent(this.host, charset, this.getHostType()) : null;
            BiFunction<String, HierarchicalUriComponents.Type, String> encoder = (s, type) -> {
                return encodeUriComponent(s, charset, type);
            };
            HierarchicalUriComponents.PathComponent pathTo = this.path.encode(encoder);
            MultiValueMap<String, String> queryParamsTo = this.encodeQueryParams(encoder);
            return new HierarchicalUriComponents(schemeTo, fragmentTo, userInfoTo, hostTo, this.port, pathTo, queryParamsTo, HierarchicalUriComponents.EncodeState.FULLY_ENCODED, (UnaryOperator)null);
        }
    }
    

    具体编码(核心逻辑:判断字符是否需要编码,如果需要就进行URLEncode):

        static String encodeUriComponent(String source, Charset charset, HierarchicalUriComponents.Type type) {
            if (!StringUtils.hasLength(source)) {
                return source;
            } else {
                Assert.notNull(charset, "Charset must not be null");
                Assert.notNull(type, "Type must not be null");
                byte[] bytes = source.getBytes(charset);
                boolean original = true;
                byte[] var5 = bytes;
                int var6 = bytes.length;
    
                int var7;
                for(var7 = 0; var7 < var6; ++var7) {
                    byte b = var5[var7];
    
                    if (!type.isAllowed(b)) {
                        original = false;
                        break;
                    }
                }
    
                if (original) {
                    return source;
                } else {
                    ByteArrayOutputStream baos = new ByteArrayOutputStream(bytes.length);
                    byte[] var13 = bytes;
                    var7 = bytes.length;
    
                    for(int var14 = 0; var14 < var7; ++var14) {
                        byte b = var13[var14];
                        if (type.isAllowed(b)) {
                            baos.write(b);
                        } else {
                            baos.write(37);
                            char hex1 = Character.toUpperCase(Character.forDigit(b >> 4 & 15, 16));
                            char hex2 = Character.toUpperCase(Character.forDigit(b & 15, 16));
                            baos.write(hex1);
                            baos.write(hex2);
                        }
                    }
    
                    return StreamUtils.copyToString(baos, charset);
                }
            }
    }
    

二、RestTemplate的Encode与java.net.URLEncode不完全一致

Spring的RestTemplate会对url进行encode,但它的encode与jdk自带的java.net.URLEncode.encode方法并不完全一致,区别在于:判断字符是否需要encode的逻辑不一样。

示例:

RestTemplate restTemplate = new RestTemplate();
restTemplate.getForEntity("https://www.baidu.com?r=编,码", String.class);
System.out.println(URLEncoder.encode("编,码", "utf-8"));

控制台日志:

16:12:09.548 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET https://www.baidu.com?r=%E7%BC%96,%E7%A0%81
16:12:09.559 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*]
16:12:09.745 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
16:12:09.746 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "text/html"
%E7%BC%96%2C%E7%A0%81

对于"编,码"字符串

Spring的encode编码结果:%E7%BC%96,%E7%A0%81

java.net.URLEncode.encode:%E7%BC%96%2C%E7%A0%81

区别:Spring不会对,encode;URLEncode会对,编码(编码结果%2C

Spring不编码的字符(对应ASCII码):

// 参考:org.springframework.web.util.HierarchicalUriComponents.Type#QUERY_PARAM
c >= 97 && c <= 122 || c >= 65 && c <= 90 || 33 == c || 36 == c || 39 == c || 40 == c || 41 == c || 42 == c || 43 == c || 44 == c || 59 == c || 58 == c || 64 == c

URLEncode不编码的字符(对应ASCII码):

// 参考:/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/src.zip!/java/net/URLEncoder.java:87
for (i = 'a'; i <= 'z'; i++) {
    dontNeedEncoding.set(i);
}
for (i = 'A'; i <= 'Z'; i++) {
    dontNeedEncoding.set(i);
}
for (i = '0'; i <= '9'; i++) {
    dontNeedEncoding.set(i);
}
dontNeedEncoding.set(' '); /* encoding a space to a + is done
                            * in the encode() method */
dontNeedEncoding.set('-');
dontNeedEncoding.set('_');
dontNeedEncoding.set('.');
dontNeedEncoding.set('*');

Spring的Encode与java.net.URLEncode不完全一致,大部分使用场景没有影响,因为即使未转码,URLDecode也可以解码。但涉及需要签名计算时,就要特别注意了!最近在项目中就踩到这样的坑:

百度开放平台,验签需要对参数进行URLEncode编码再加密生成签名。如果使用RestTemplate发起请求时,参数带有,字符时,就会验签失败。因为url上的参数URLEncode与生成签名时的URLEncode结果不一致

三、execute方法中的String urlURI url的区别

源码:

@Nullable
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor, Map<String, ?> uriVariables) throws RestClientException {
    URI expanded = this.getUriTemplateHandler().expand(url, uriVariables);
    return this.doExecute(expanded, method, requestCallback, responseExtractor);
}

@Nullable
public <T> T execute(URI url, HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
    return this.doExecute(url, method, requestCallback, responseExtractor);
}

从源码很容易得知,如果参数为String url,其为对url处理,比如URLEncode等。