content-type 遇到的问题详解

10 篇文章 0 订阅

https://www.cnblogs.com/kaiblog/p/7565231.html

 

我们经常需要在HttpResponse中设置一些headers,我们使用Spring MVC框架的时候我们如何给Response设置Header呢?

Sooooooooooooo easy, 看下面的代码:

1

2

3

4

5

6

7

@RequestMapping(value = "/rulelist", method = RequestMethod.GET)

@ResponseBody

public String getRuleList(HttpServletRequest request,

        HttpServletResponse response) {

    response.addHeader("test""test");

    return service.getRuleList();

}

通过验证,我们可以看到test项已经被成功添加到response的头部信息

1

2

3

4

Content-Length: 2 kilobytes

Content-Type:   text/plain;charset=ISO-8859-1

Server: Apache-Coyote/1.1

test: test

接下来,我们希望修改Content-Type,从而统一服务器端和客户端的内容编码。我们继续修改代码,

1

2

3

4

5

6

7

@RequestMapping(value = "/rulelist", method = RequestMethod.GET)

@ResponseBody

public String getRuleList(HttpServletRequest request,

        HttpServletResponse response) {

    response.addHeader("Content-Type""application/json;charset=UTF-8");

    return service.getRuleList();

}

接下来,我们验证一下结果:

1

2

3

Content-Length: 2 kilobytes

Content-Type:   text/plain;charset=ISO-8859-1

Server: Apache-Coyote/1.1

和我们预想的并一样,response的content-type header没有被设置成"application/json;charset=UTF-8",很令人困惑。

那么,接下来让我们来探索下Spring MVC内部是如何处理这一过程的。首先我们先要对Spring MVC框架处理Http请求的流程有一个整体的了解。

下图清晰地向大家展示了Spring MVC处理HTTP请求的流程,(图片来自网络)

具体流程如下:

1. DispatcherServlet接收到Request请求

2. HandlerMapping选择一个合适的Handler处理Request请求

3-4. 选择合适的HandlerAdapter,调用用户编写的Controller处理业务逻辑。(HandlerAdapter主要是帮助Spring MVC支持多种类型的Controller)

5. Controller将返回结果放置到Model中并且返回view名称给Handler Adapter

6. DispatcherServlet选择合适的ViewResolver来生成View对象

7-8. View对象利用Model中的数据进行渲染并返回数据

相信大家对于上面的处理流程并不陌生,上面的流程图向我们展示了SpringMVC生成ModelAndView并返回response的大体流程。

下面我们来看看我们上面代码片段的处理流程是如何进行的?

从上面的流程图我们可以看到,content-type header是单独被处理的,具体过程可以参考下面的源码(AbstractMessageConverterMethodProcessor):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType,

        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)

        throws IOException, HttpMediaTypeNotAcceptableException {

 

    Class<?> returnValueClass = getReturnValueType(returnValue, returnType);

    HttpServletRequest servletRequest = inputMessage.getServletRequest();

    List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest); //适合的兼容media types类型实际上,我们可以使用produces = {}来指定我们需要的mediatype

    List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass);

 

    Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();

    for (MediaType requestedType : requestedMediaTypes) {

        for (MediaType producibleType : producibleMediaTypes) {

            if (requestedType.isCompatibleWith(producibleType)) {

                compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));

            }

        }

    }

    if (compatibleMediaTypes.isEmpty()) {

        if (returnValue != null) {

            throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);

        }

        return;

    }

 

    List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);

    MediaType.sortBySpecificityAndQuality(mediaTypes);

 

    MediaType selectedMediaType = null;   //选择最匹配的mediaType

    for (MediaType mediaType : mediaTypes) {

        if (mediaType.isConcrete()) {

            selectedMediaType = mediaType;

            break;

        }

        else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {

            selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;

            break;

        }

    }

 

    if (selectedMediaType != null) {

        selectedMediaType = selectedMediaType.removeQualityValue();

        for (HttpMessageConverter<?> messageConverter : this.messageConverters) {         //遍历messageConvertors, 寻找可以处理相应返回类型和mediatype的HttpMessageConvertor

            if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {

                returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType,

                        (Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage);

                if (returnValue != null) {         //这里将会填充mediatype到header,并将httpmessage发送给请求者

                    ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);

                    if (logger.isDebugEnabled()) {

                        logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +

                                messageConverter + "]");

                    }

                }

                return;

            }

        }

    }

 

    if (returnValue != null) {

        throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);

    }

}

接下来,将选择好的mediatype写入到HttpOutputMessage中

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)

        throws IOException, HttpMessageNotWritableException {

 

    final HttpHeaders headers = outputMessage.getHeaders();     //设置contenttype到HttpOutputMessage

    if (headers.getContentType() == null) {

        MediaType contentTypeToUse = contentType;

        if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {

            contentTypeToUse = getDefaultContentType(t);

        }

        if (contentTypeToUse != null) {

            headers.setContentType(contentTypeToUse);

        }

    }

    if (headers.getContentLength() == -1) {

        Long contentLength = getContentLength(t, headers.getContentType());

        if (contentLength != null) {

            headers.setContentLength(contentLength);

        }

    }

      /* 省略了不相干代码 */

}

最终的Headers设置在ServletServerHttpResponse类中完成,

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

private void writeHeaders() {

    if (!this.headersWritten) {

        for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {

            String headerName = entry.getKey();

            for (String headerValue : entry.getValue()) {         //将复合类中之前设置的header(content-type)内容补充到servletResponse

                this.servletResponse.addHeader(headerName, headerValue);

            }

        }

        // HttpServletResponse exposes some headers as properties: we should include those if not already present

        if (this.servletResponse.getContentType() == null && this.headers.getContentType() != null) {

            this.servletResponse.setContentType(this.headers.getContentType().toString());

        }

        if (this.servletResponse.getCharacterEncoding() == null && this.headers.getContentType() != null &&

                this.headers.getContentType().getCharSet() != null) {

            this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharSet().name());

        }

        this.headersWritten = true;

    }

}

从上述的代码中,我们可以看到在RequestResponseBodyMethodProcessor这个ReturnValueHandler中,media-type被单独的逻辑进行处理,因此直接在ServletResponse中设置content-type header并不能正常生效。

需要在@RequestMapping中添加produces = {} 进行设置才可以。

 

 

https://blog.csdn.net/u014209205/article/details/81147783

 

介绍
        http协议是建立在tcp/ip协议之上的应用层协议,主要包括三个部分,状态行,头部信息,消息主体。对应一个http请求就是:请求行,请求头,请求体。

        协议规定post提交的数据,必须包含在消息主体中entity-body中,但是协议并没有规定数据使用什么编码方式。开发者可以自己决定消息主体的格式。

        数据发送出去后,需要接收的服务端解析成功,一般服务端会根据content-type字段来获取参数是怎么编码的,然后对应去解码。

        在最早的http post请求中,只支持application/x-www-form-urlencoded,参数都是通过浏览器的url传递。其实是不支持文件上传的,这样有很多不便。在1995年的时候,出台了rfc1867,也就是《RFC 1867 From-based file upload in HTML》,用以支持文件上传。所以content-type扩充了multipart/form-data用以支持向服务器发送二进制数据。后来随着web应用的增多,增加了诸如application/json的类型。

application/x-www-form-urlencoded
        在最开始的请求方式中,请求参数都是放在url中,表单提交的时候,都是以key=&value=的方式写在url后面。这也是浏览器表单提交的默认方式。

        此时可以直接调用request.getInputStream或request.getReader获取到请求内容,再解析出具体的参数。后者只是对前者的一个封装,可以让调用者更方便字符内容的处理。可以看到:

    @Override
    public BufferedReader getReader() throws IOException {
        if (this.reader == null) {
            this.reader = new BufferedReader(new InputStreamReader(getInputStream(), 
getCharacterEncoding()));
        }
        return this.reader;
    }
    public InputStreamReader(InputStream in, String charsetName)
        throws UnsupportedEncodingException
    {
        super(in);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
    }
        也可以通过request.getParameter获取到参数。

        但是需要注意,getInputStream、getReader、getParameter在一定的场景是互斥的。

multipart/form-data
        此种方式多用于文件上传,表单数据都保存在http的正文部分,各个表单项之间用boundary分开。

        一次完整的抓包如下:

POST /ecard/uploadFaceImage?timestamp=1531906535406 HTTP/1.0
Host: www.example.com
X-Real-IP: 183.156.142.242
X-Forwarded-For: 183.156.142.242
Connection: close
Content-Length: 230101
sign: 9a7d3b4978979ef65a12e34ae1cf7b2d
accept: */*
user-agent: Mozilla/5.0 (Linux; U; Android 6.0.1; zh-CN; OPPO R9s Build/MMB29M) 
AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 
UCBrowser/11.8.8.968 UWS/2.13.1.42 Mobile Safari/537.36 UCBS/2.13.1.42_180629181124 
ChannelId(1) NebulaSDK/1.8.100112 Nebula AlipayDefined(nt:WIFI,ws:360|0|3.0) 
AliApp(AP/10.1.28.560) AlipayClient/10.1.28.560 Language/zh-Hans useStatusBar/true 
isConcaveScreen/false
Cookie: ssl_upgrade=0; spanner=6tlJA6NZwnkqTDN+BMhdT7lbzLPsFJUeXt2T4qEYgj0=
Accept-Encoding: gzip
Content-Type: multipart/form-data; boundary=pgRq9HriiaBmfSo5rfyEJPtcumxb4fd6o15f_3G
 
--pgRq9HriiaBmfSo5rfyEJPtcumxb4fd6o15f_3G
Content-Disposition: form-data; name="personCode"
Content-Type: text/plain; charset=US-ASCII
Content-Transfer-Encoding: 8bit
 
DM1203
--pgRq9HriiaBmfSo5rfyEJPtcumxb4fd6o15f_3G
Content-Disposition: form-data; name="DM1203"; filename="123524587.jpg"
Content-Type: 
Content-Transfer-Encoding: binary
  图片二进制数据(特别长)
--pgRq9HriiaBmfSo5rfyEJPtcumxb4fd6o15f_3G--
HTTP/1.1 200 
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: Origin, No-Cache, X-Requested-With, 
If-Modified-Since, Pragma, Cache-Control, Expires, Content-Type
Access-Control-Allow-Credentials: true
XDomainRequestAllowed: 1
Content-Type: application/json;charset=UTF-8
Date: Wed, 18 Jul 2018 09:35:36 GMT
Connection: close
 
{"retCode":1,"msg":"success","data":null}
        可以看到里面有一个boundary分界,值为:pgRq9HriiaBmfSo5rfyEJPtcumxb4fd6o15f_3G,请求时,会放在Content-Type后面传到服务器,服务器根据这个边界解析数据,划分段,每一段都是一项数据。每一项中的name属性就是唯一的id

        此时用request.getParameter是取不到数据的,这个时候需要通过request.getInputStream来获取数据。这时取到的是一个InputStream,无法直接取到指定的表单项。但是有很多开源的组件可以直接利用,比如apache的fileupload组件。通过这些开源的upload组件,提供的api,就可以直接从request中取得指定的表单项。

ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> list = upload.parseRequest(request);
        上面的代码中,接下来就可通过遍历list获取参数了。

application/json
        现在越来越多的应用使用application/json,用来告诉服务端消息主体是序列化的json字符串。由于json规范的流行,各大浏览器都开始原生支持JSON.stringfy。

        而且spring对这个content-Type上传的数据有很好的支持,可以直接通过@RequestBody进行接收。也是当前完美适配当前流行的RestApi。

参考

Multipart/form-data POST文件上传详解

MVC实现,使用form-data传递数据
--------------------- 
作者:石硕页 
来源:CSDN 
原文:https://blog.csdn.net/u014209205/article/details/81147783 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值