Spring 50例常见错误(十八)

文章整理来源:Spring编程常见错误50例_spring_spring编程_bean_AOP_SpringCloud_SpringWeb_测试_事务_Data-极客时间

案例44:RestTemplate 使用 MultiValueMap 提交 Body 表单

        定义一个接口,然后使用 RestTemplate 向这个接口发送请求,并在请求中定义一个 Map 做请求参数表单。

@RestController
public class HelloWorldController {
    @RequestMapping(path = "hi", method = RequestMethod.POST)
    public String hi(@RequestParam("para1") String para1, @RequestParam("para2") String para2){
        return "helloworld:" + para1 + "," + para2;
    };
}

--------------------------------------------------

RestTemplate template = new RestTemplate();
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("para1", "001");
paramMap.put("para2", "002");

String url = "http://localhost:8080/hi";
String result = template.postForObject(url, paramMap, String.class);
System.out.println(result);

        测试返回提示 400 错误,即请求中缺少 para1 表单参数

        解析:发送请求的时候,会根据当前提交的 Body 内容,遍历当前支持的所有编码解码器,找到适合的进行 Body 内容的序列化。

public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
   super.doWithRequest(httpRequest);
   Object requestBody = this.requestEntity.getBody();
   if (requestBody == null) {
       //省略其他非关键代码
   }
   else {
      Class<?> requestBodyClass = requestBody.getClass();
      Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
            ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
      HttpHeaders httpHeaders = httpRequest.getHeaders();
      HttpHeaders requestHeaders = this.requestEntity.getHeaders();
      MediaType requestContentType = requestHeaders.getContentType();
      for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
         if (messageConverter instanceof GenericHttpMessageConverter) {
            GenericHttpMessageConverter<Object> genericConverter =
                  (GenericHttpMessageConverter<Object>) messageConverter;
            if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
               if (!requestHeaders.isEmpty()) {
                  requestHeaders.forEach((key, values) -> httpHeaders.put(key, new LinkedList<>(values)));
               }
               logBody(requestBody, requestContentType, genericConverter);
               genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);
               return;
            }
         }
         else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
            if (!requestHeaders.isEmpty()) {
               requestHeaders.forEach((key, values) -> httpHeaders.put(key, new LinkedList<>(values)));
            }
            logBody(requestBody, requestContentType, messageConverter);
            ((HttpMessageConverter<Object>) messageConverter).write(
                  requestBody, requestContentType, httpRequest);
            return;
         }
      }
      String message = "No HttpMessageConverter for " + requestBodyClass.getName();
      if (requestContentType != null) {
         message += " and content type \"" + requestContentType + "\"";
      }
      throw new RestClientException(message);
   }
}

        而当前的请求是一个 HashMap ,会选用 Jackson 工具进行序列化,使用 Wireshark 抓包工具,抓取请求

        只有当 Body 使用 MultiValueMap 时候,才能使用表单提交

        解决:将 HashMap 换成 MultiValueMap 

RestTemplate template = new RestTemplate();

// 错误:
//Map<String, Object> paramMap = new HashMap<String, Object>();
//paramMap.put("para1", "001");
//paramMap.put("para2", "002");

// 修正代码:
MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<String, Object>();
paramMap.add("para1", "001");
paramMap.add("para2", "002");

String url = "http://localhost:8080/hi";
String result = template.postForObject(url, paramMap, String.class);
System.out.println(result);

The body of the entity, or request itself, can be a MultiValueMap to create a multipart request. The values in the MultiValueMap can be any Object representing the body of the part, or an HttpEntity

案例45:RestTemplate 的请求 URL 中含有特殊字符

        定义 Contoller 中的接口,使用 RestTemplate 向其发送下面的请求,请求的 URL 为 "http://localhost:8080/hi?para1=1#2"  含有特殊字符 “#” 。结果接口解析出来的 para1

@RestController
public class HelloWorldController {
    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(@RequestParam("para1") String para1){
        return "helloworld:" + para1;
    };

}
---------------------------------------------------
// 请求
String url = "http://localhost:8080/hi?para1=1#2";
HttpEntity<?> entity = new HttpEntity<>(null);

RestTemplate restTemplate = new RestTemplate();
HttpEntity<String> response = restTemplate.exchange(url, HttpMethod.GET,entity,String.class);

System.out.println(response.getBody());

        解析:para1 丢掉的 #2 实际是以 Fragment 的方式被记录下来了。这里顺便科普下什么是 Fragment(锚点 -- 用于指定网络资源中的片段 ),这得追溯到 URL 的格式定义

protocol://hostname[:port]/path/[?query]#fragment

        解析 URL 的关键点在于 UriComponentsBuilder#fromUriString 

private static final Pattern URI_PATTERN = Pattern.compile(
      "^(" + SCHEME_PATTERN + ")?" + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN +
            ")?" + ")?" + PATH_PATTERN + "(\\?" + QUERY_PATTERN + ")?" + "(#" + LAST_PATTERN + ")?");
            
public static UriComponentsBuilder fromUriString(String uri) {
   Matcher matcher = URI_PATTERN.matcher(uri);
   if (matcher.matches()) {
      UriComponentsBuilder builder = new UriComponentsBuilder();
      String scheme = matcher.group(2);
      String userInfo = matcher.group(5);
      String host = matcher.group(6);
      String port = matcher.group(8);
      String path = matcher.group(9);
      String query = matcher.group(11);
      String fragment = matcher.group(13);
      //省略非关键代码
      else {
         builder.userInfo(userInfo);
         builder.host(host);
         if (StringUtils.hasLength(port)) {
            builder.port(port);
         }
         builder.path(path);
         builder.query(query);
      }
      if (StringUtils.hasText(fragment)) {
         builder.fragment(fragment);
      }
      return builder;
   }
   else {
      throw new IllegalArgumentException("[" + uri + "] is not a valid URI");
   }
}

        解决:使用 UriComponentsBuilder#fromHttpUrl,其 Pattern 不含 “#” 字符的解析

String url = "http://localhost:8080/hi?para1=1#2";
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
URI uri = builder.build().encode().toUri();
HttpEntity<?> entity = new HttpEntity<>(null);

RestTemplate restTemplate = new RestTemplate();
HttpEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET,entity,String.class);

System.out.println(response.getBody());

        UriComponentsBuilder#fromHttpUrl 实现代码

private static final Pattern HTTP_URL_PATTERN = Pattern.compile(
      "^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" +
            PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?")
            
public static UriComponentsBuilder fromHttpUrl(String httpUrl) {
   Assert.notNull(httpUrl, "HTTP URL must not be null");
   Matcher matcher = HTTP_URL_PATTERN.matcher(httpUrl);
   if (matcher.matches()) {
      UriComponentsBuilder builder = new UriComponentsBuilder();
      String scheme = matcher.group(1);
      builder.scheme(scheme != null ? scheme.toLowerCase() : null);
      builder.userInfo(matcher.group(4));
      String host = matcher.group(5);
      if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) {
         throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
      }
      builder.host(host);
      String port = matcher.group(7);
      if (StringUtils.hasLength(port)) {
         builder.port(port);
      }
      builder.path(matcher.group(8));
      builder.query(matcher.group(10));
      return builder;
   }
   else {
      throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
   }
}

案例46:多次使用 URL Encoder

        使用上个案例的接口,同样使用 RestTemplate 发送请求,在 URL 请求中先转为 String,再发送请求,得到的结果是 :helloworld:%E5%BC%80%E5%8F%91%E6%B5%8B%E8%AF%95001

RestTemplate restTemplate = new RestTemplate();

UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/hi");
builder.queryParam("para1", "开发测试 001");
String url = builder.toUriString();

ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
System.out.println(forEntity.getBody());

        解析:执行的方式是 UriComponentsBuilder#toUriString,最终执行了 URL Encode,而在只用 restTemplate.getForEntity(url, String.class) 会再进行一次编码

        解决:

                避免多次转化

RestTemplate restTemplate = new RestTemplate();
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:8080/hi");
builder.queryParam("para1", "开发测试 001");
URI url = builder.encode().build().toUri();
ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
System.out.println(forEntity.getBody());

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值