文章整理来源:Spring编程常见错误50例_spring_spring编程_bean_AOP_SpringCloud_SpringWeb_测试_事务_Data-极客时间
URL 的长度有限,所能携带的信息也因此受到了制约。如果想提供更多的信息,Header 往往是不二之举。Header 是介于 URL 和 Body 之外的第二大重要组成,它提供了更多的信息以及围绕这些信息的相关能力,例如 Content-Type 指定了我们的请求或者响应的内容类型,便于去做解码。虽然 Spring 对于 Header 的解析,大体流程和 URL 相同,但是 Header 本身具有自己的特点。
案例22:接受 Header 使用错 Map 类型
定义请求,接受 Header
@RequestMapping(path = "/hi1", method = RequestMethod.GET)
public String hi1(@RequestHeader() Map map){
return map.toString();
};
发送请求如下,在 Head 中为 myheader 定义两值,但却只解析出了一个
GET http://localhost:8080/hi1
myheader: h1
myheader: h2
--------------------------------------------------------------------
{myheader=h1, host=localhost:8080, connection=Keep-Alive, user-agent=Apache-HttpClient/4.5.12 (Java/11.0.6), accept-encoding=gzip,deflate}
解析:对于一个 Header 的解析,主要有两种方式,分别实现在 RequestHeaderMethodArgumentResolver 和 RequestHeaderMapMethodArgumentResolver 中,它们都继承于 AbstractNamedValueMethodArgumentResolver,但是应用的场景不同。
对于一个标记了 @RequestHeader 的参数,如果它的类型是 Map,则使用 RequestHeaderMapMethodArgumentResolver,否则一般使用的是 RequestHeaderMethodArgumentResolver。
因为案例中的参数类型定义为 Map,所以使用的自然是 RequestHeaderMapMethodArgumentResolver。而且解析方法 resolveArgument() 代码如下
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Class<?> paramType = parameter.getParameterType();
if (MultiValueMap.class.isAssignableFrom(paramType)) {
MultiValueMap<String, String> result;
if (HttpHeaders.class.isAssignableFrom(paramType)) {
result = new HttpHeaders();
}
else {
result = new LinkedMultiValueMap<>();
}
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
String[] headerValues = webRequest.getHeaderValues(headerName);
if (headerValues != null) {
for (String headerValue : headerValues) {
result.add(headerName, headerValue);
}
}
}
return result;
}
else {
Map<String, String> result = new LinkedHashMap<>();
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) {
String headerName = iterator.next();
//只取了一个“值”
String headerValue = webRequest.getHeader(headerName);
if (headerValue != null) {
result.put(headerName, headerValue);
}
}
return result;
}
}
这里并不是 MultiValueMap,所以我们会走入 else 分支。这个分支首先会定义一个 LinkedHashMap,然后将请求一一放置进去,并返回
解决:要完整接收到所有的 Header,不能直接使用 Map 而应该使用 MultiValueMap 或者 HttpHeaders
//方式 1
@RequestHeader() MultiValueMap map
//方式 2
@RequestHeader() HttpHeaders map
案例 23:错认为 Header 名称首字母可以一直忽略大小写
定义如下请求
@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestHeader("MyHeader") String myHeader, @RequestHeader MultiValueMap map){
return myHeader + " compare with : " + map.get("MyHeader");
};
发送请求
GET http://localhost:8080/hi2
myheader: myheadervalue
---------------------------------------------
得到结果:
myheadervalue compare with : null
直接获取 Header 是可以忽略大小写的,但是如果从接收过来的 Map 中获取 Header 是不能忽略大小写的
解析:1. 对于"@RequestHeader("MyHeader") String myHeader"的定义,Spring 使用的是 RequestHeaderMethodArgumentResolver 来做解析,此方法会忽略大小写
2. 存取 Map 的 Header 是没有忽略大小写的
解决:1. 注意大小写
2. 使用 HttpHeaders 是忽略大小写的,故推荐此方法
@RequestMapping(path = "/hi2", method = RequestMethod.GET)
public String hi2(@RequestHeader("MyHeader") String myHeader, @RequestHeader HttpHeaders map){
return myHeader + " compare with : " + map.get("MyHeader");
};
-----------------------------------------------
public HttpHeaders() {
this(CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)));
}
案例24:试图在 Controller 中自定义 CONTENT_TYPE 等
在 请求的处理中尝试去定义 Http 返回的接受类型
@RequestMapping(path = "/hi3", method = RequestMethod.GET)
public String hi3(HttpServletResponse httpServletResponse){
httpServletResponse.addHeader("myheader", "myheadervalue");
httpServletResponse.addHeader(HttpHeaders.CONTENT_TYPE, "application/json");
return "ok";
};
结果 Content-Type 并没有设置成我们想要的"application/json",而是"text/plain;charset=UTF-8"
解析:1. Response#addHeader 方法会检查 name 是不是 “Content-Type”,如果是则会作为 coyoteResponse 成员的值了 ,而没有添加到 Header 中去
private void addHeader(String name, String value, Charset charset) {
//省略其他非关键代码
char cc=name.charAt(0);
if (cc=='C' || cc=='c') {
//判断是不是 Content-Type,如果是不要把这个 Header 作为 header 添加到 org.apache.coyote.Response
if (checkSpecialHeader(name, value))
return;
}
getCoyoteResponse().addHeader(name, value, charset);
}
--------------------------------------------------------------------
private boolean checkSpecialHeader(String name, String value) {
if (name.equalsIgnoreCase("Content-Type")) {
setContentType(value);
return true;
}
return false;
}
2. 在请求返回后,会根据返回的类型挑选合适 MediaType 进行处理返回,步骤为 a1. 决定用哪一种 MediaType 返回 ; a2.决定完 MediaType 信息后,即可去选择转化器并执行转化 ;
上述例子中由于 Content-Type ="application/json" 并没有添加到 Header 中,则会根据 Accept 定义的类型决定,且 TEXT_PLAIN 默认优先级较高,故用 TEXT_PLAIN
//决策返回值是何种 MediaType
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
//如果 header 中有 contentType,则用其作为选择的 selectedMediaType。
if (isContentTypePreset) {
selectedMediaType = contentType;
}
//没有,则根据“Accept”头、返回值等核算用哪一种
else {
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
//省略其他非关键代码
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
//省略其他关键代码
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
//省略其他关键代码
}
解决:1. 修改请求中的 Accept 头,约束返回类型
GET http://localhost:8080/hi3
Accept:application/json
2. 标记返回类型
@RequestMapping(path = "/hi3", method = RequestMethod.GET, produces = {"application/json"})