问题:

在实际项目中,对传入的图片url进行下载,使用的是RestTemplate的exchange方法,具体如下:

使用以下RestTemplate的方法:

Sring url = "http://10.64.203.183:6120/pic?=d6ei2a4i9c84*33c-793=11i5m*ep5t9d5=*2pdi=*1s5i2=94b8i5d2e*14b863328-aa2e959-1b246b-43s=10d3z83";
ResponseEntity responseEntity = restTemplate
                .exchange(url, HttpMethod.GET, null, byte[].class);

用这种方式调用请求,抓拍可看出url中的特殊字符被转义:

http://10.64.203.183:6120/pic?d6ei2a4i9c84*33c-793=11i5m*ep5t9d5%3D*2pdi%3D*1s5i2%3D94b8i5d2e*14b863328-aa2e959-1b246b-43s%3D10d3z83

由于第三方的图片服务器,没有对请求的url进行解码,因此不识别转义后的url导致下载失败。

解决方法:

如果不希望被转码,则可使用
public ResponseEntity exchange(URI url, HttpMethod method, @Nullable HttpEntity requestEntity, Class responseType)
该方法,直接传入URI对象。
该对象可如下进行组装:

Sring url = "http://10.64.203.183:6120/pic?=d6ei2a4i9c84*33c-793=11i5m*ep5t9d5=*2pdi=*1s5i2=94b8i5d2e*14b863328-aa2e959-1b246b-43s=10d3z83";
URI uri = new URI(url);
restTemplate.exchange(url, HttpMethod.GET, null, byte[].class);

这样,url就不会被encode成utf-8,不会将特殊字符转义,解决了下载失败的问题。

源码分析

以下是url被转码的源码分析。

对应的包为

Sring url = "http://10.64.203.183:6120/pic?=d6ei2a4i9c84*33c-793=11i5m*ep5t9d5=*2pdi=*1s5i2=94b8i5d2e*14b863328-aa2e959-1b246b-43s=10d3z83";
ResponseEntity responseEntity = restTemplate
                .exchange(url, HttpMethod.GET, null, byte[].class);

对应源码的调用关系:

RestTemplate.class:

public  ResponseEntity exchange(String url, HttpMethod method, @Nullable HttpEntity requestEntity, Class responseType, Map uriVariables) throws RestClientException {
        RequestCallback requestCallback = this.httpEntityCallback(requestEntity, responseType);
        ResponseExtractor> responseExtractor = this.responseEntityExtractor(responseType);
        return (ResponseEntity)nonNull(this.execute(url, method, requestCallback, responseExtractor, uriVariables));
    }
RestTemplate.class:

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

从execute函数可看出,源码中会将url String对象转成URI对象。

转换的源码如下:

DefaultUriBuilderFactory.class:

public URI expand(String uriTemplate, Map uriVars) {
        return this.uriString(uriTemplate).build(uriVars);
    }

我们来看下this.uriString(uriTemplate),返回的是UriBuilder 对象,


public UriBuilder uriString(String uriTemplate) {
        return new DefaultUriBuilderFactory.DefaultUriBuilder(uriTemplate);
    }

private class DefaultUriBuilder implements UriBuilder:

public DefaultUriBuilder(String uriTemplate) {
            this.uriComponentsBuilder = this.initUriComponentsBuilder(uriTemplate);
        }

private UriComponentsBuilder initUriComponentsBuilder(String uriTemplate) {
            UriComponentsBuilder result;
            if (!StringUtils.hasLength(uriTemplate)) {
                result = DefaultUriBuilderFactory.this.baseUri != null ? DefaultUriBuilderFactory.this.baseUri.cloneBuilder() : UriComponentsBuilder.newInstance();
            } else if (DefaultUriBuilderFactory.this.baseUri != null) {
                UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate);
                UriComponents uri = builder.build();
                result = uri.getHost() == null ? DefaultUriBuilderFactory.this.baseUri.cloneBuilder().uriComponents(uri) : builder;
            } else {
                result = UriComponentsBuilder.fromUriString(uriTemplate);
            }

            if (DefaultUriBuilderFactory.this.encodingMode.equals(DefaultUriBuilderFactory.EncodingMode.TEMPLATE_AND_VALUES)) {
                result.encode();
            }

            this.parsePathIfNecessary(result);
            return result;
        }

result.encode();该语句设置了该对象编码格式:

UriComponentsBuilder.class
public final UriComponentsBuilder encode() {
        return this.encode(StandardCharsets.UTF_8);
    }

public UriComponentsBuilder encode(Charset charset) {
        this.encodeTemplate = true;
        this.charset = charset;
        return this;
    }

至此,获取到一个UriBuilder对象,且该对象中的charset设置成utf-8。
以上代码,我们解释到了this.uriString(uriTemplate),返回的是UriBuilder 对象,接下来我们看this.uriString(uriTemplate).build(uriVars)中build(uriVars)。

UriComponentsBuilder.class

public URI build(Map uriVariables) {

        return this.buildInternal(UriComponentsBuilder.EncodingHint.ENCODE_TEMPLATE).expand(uriVariables).toUri();
    }

/**
被上以函数调用,传入的hint是UriComponentsBuilder.EncodingHint.ENCODE_TEMPLATE
*/
private UriComponents buildInternal(UriComponentsBuilder.EncodingHint hint) {
        Object result;
        if (this.ssp != null) {
            result = new OpaqueUriComponents(this.scheme, this.ssp, this.fragment);
        } else {
            //走到该逻辑
            HierarchicalUriComponents uric = new HierarchicalUriComponents(this.scheme, this.fragment, this.userInfo, this.host, this.port, this.pathBuilder.build(), this.queryParams, hint == UriComponentsBuilder.EncodingHint.FULLY_ENCODED);
            //下面的判断为真,因此result赋值为uric.encodeTemplate(this.charset),其中this.charset在上面已被设置成utf-8
            result = hint == UriComponentsBuilder.EncodingHint.ENCODE_TEMPLATE ? uric.encodeTemplate(this.charset) : uric;
        }

        if (!this.uriVariables.isEmpty()) {
            result = ((UriComponents)result).expand((name) -> {
                return this.uriVariables.getOrDefault(name, UriTemplateVariables.SKIP_VALUE);
            });
        }

        return (UriComponents)result;
    }

HierarchicalUriComponents.class

/**
该对象将对象中的内容均进行utf-8编码
*/
HierarchicalUriComponents encodeTemplate(Charset charset) {
        if (this.encodeState.isEncoded()) {
            return this;
        } else {
            this.variableEncoder = (value) -> {
                return encodeUriComponent(value, charset, HierarchicalUriComponents.Type.URI);
            };
            HierarchicalUriComponents.UriTemplateEncoder encoder = new HierarchicalUriComponents.UriTemplateEncoder(charset);
            String schemeTo = this.getScheme() != null ? encoder.apply(this.getScheme(), HierarchicalUriComponents.Type.SCHEME) : null;
            String fragmentTo = this.getFragment() != null ? encoder.apply(this.getFragment(), HierarchicalUriComponents.Type.FRAGMENT) : null;
            String userInfoTo = this.getUserInfo() != null ? encoder.apply(this.getUserInfo(), HierarchicalUriComponents.Type.USER_INFO) : null;
            String hostTo = this.getHost() != null ? encoder.apply(this.getHost(), this.getHostType()) : null;
            HierarchicalUriComponents.PathComponent pathTo = this.path.encode(encoder);
            MultiValueMap queryParamsTo = this.encodeQueryParams(encoder);
            return new HierarchicalUriComponents(schemeTo, fragmentTo, userInfoTo, hostTo, this.port, pathTo, queryParamsTo, HierarchicalUriComponents.EncodeState.TEMPLATE_ENCODED, this.variableEncoder);
        }
    }

如上,因此调用RestTemplate类的public ResponseEntity exchange(String url, HttpMethod method, @Nullable HttpEntity requestEntity, Class responseType)函数,传入的url会被转码成utf-8。

这样,传输过程中特殊字符就不会被转义