Content-Type详解

Content-Type是什么?

在HTTP协议消息头中,使用Content-Type来表示媒体类型信息。它被用来告诉服务端如何处理请求的数据,以及告诉客户端(一般是浏览器)如何解析响应的数据,比如显示图片,解析html或仅仅展示一个文本等。

Post请求的内容放置在请求体中,Content-Type定义了请求体的编码格式。数据发送出去后,还需要接收端解析才可以。接收端依靠请求头中的Content-Type字段来获知请求体的编码格式,最后再进行解析。

Content-Type的格式

Content-Type:type/subtype;parameter

type:主类型,任意的字符串,如text,如果是号代表所有;
*
subtype:子类型,任意的字符串,如html,如果是号代表所有,用“/”与主类型隔开;
*
parameter
:可选参数,如charset,boundary等。

例如:

  • Content-Type: text/html;
  • Content-Type: application/json;charset:utf-8;
  • Content-Type: application/x-www-form-urlencoded;charset:utf-8;

常见Content-Type

常见的Content-Type有数百个,下面例举了一些

  • HTML文档标记:text/html;
  • 普通ASCII文档标记:text/html;
  • JPEG图片标记:image/jpeg;
  • GIF图片标记:image/gif;
  • js文档标记:application/javascript;
  • xml文件标记:application/xml;

Post请求中常见的Content-Type类型的结构

(1) application/x-www-form-urlencoded

这是浏览器原生的form表单类型,或者说是表单默认的类型。post将请求参数以key1=value1&key2=value2这种键值对的方式进行组织,并放入到请求体中。其中中文或某些特殊字符,如”/“、”,”、“:” 等会自动进行URL转码。

(2) application/json

现在绝大部分的请求都会以json形式进行传输,post会将序列化后的json字符串直接塞进请求体中。请求体中就是Json字符串。这个使用这个类型,需要参数本身就是json格式的数据,参数会被直接放到请求实体里,不进行任何处理。服务端/客户端会按json格式解析数据(约定好的情况下)。

(3) multipart/form-data

用于在表单中上传文件

Content-Type的使用

request 的Content-Type

一般我们在开发的过程中需要注意客户端发送请求(Request)时的Content-Type设置,特别是使用ajax的时候,如果设置得不准确,很有可能导致请求失败。比如在spring中,如果接口使用了@RequestBody,spring强大的自动解析功能,会将请求实体的内容自动转换为Bean,但前提是请求的Content-Type必须设置为application/json,否正就会返回415错误。

⚠️注:415 错误是 Unsupported media type,即不支持的媒体类型。

建议:

  • 如果是一个restful接口(json格式),一般将Content-Type设置为application/json; charset=UTF-8;

  • 如果是文件上传,一般Content-Type设置为multipart/form-data

  • 如果普通表单提交,一般Content-Type设置为application/x-www-form-urlencoded

response的Content-Type

服务端响应(Response)的Content-Type最好也保持准确,虽然一般web开发中,前端解析响应的数据不会根据Content-Type,并且服务端一般能自动设置准确的Content-Type,但是如果乱设置某些情况下可能会有问题,比如导出文件,打开图片等。如果在spring项目里使用@ResponseBody,spring会将响应的Content-Type设置为application/json;charset=UTF-8;,可能会导致文件无法导出,需要注意下。

response的Content-Type设置建议:

一般情况下不需要显示设置;
如果是文件导出,Content-Type 设置为 multipart/form-data,并且添加一个Content-Disposition设置为attachment;fileName=文件.后缀。

⚠️注:Content-Disposition是Content-Type的扩展,告诉浏览器弹窗下载框,而不是直接在浏览器里展示文件。因为一般浏览器对于它能够处理的文件类型,如txt,pdf 等,它都是直接打开展示,而不是弹窗下载框。


SpringMVC获取Post请求参数的几种方式

后端使用实体类进行接收,前端传入Content-Type:application/json格式的数据

1
2
3
4
@PostMapping("/loginByUser")
public User loginByUser(@RequestBody User user) {
return user;
}

@RequestBody注解用于将请求体中的json字符串转化为java对象。

值得注意的是

  • 由于get无请求体,那么@RequestBody不能使用在get请求上。
  • @RequestBody与@RequestParam可以同时使用,@RequestBody最多只能有一个,而@RequestParam可以有多个。

后端使用实体类进行接收,前端传入Content-Type:application/x-www-form-urlencoded格式的数据

1
2
3
4
@PostMapping("/loginByUser")
public User loginByUser(User user) {
return user;
}

Content-Type:application/x-www-form-urlencoded格式的数据,数据会以key/value格式进行传输,SpringMVC会直接将请求体中的参数直接注入到对象中(注意名称匹配)。

后端使用参数进行接收,前端传入Content-Type:application/x-www-form-urlencoded格式的数据

1
2
3
4
5
@PostMapping("/loginByParam")
public User loginByParam(@RequestParam("name1") String name,
@RequestParam(value = "age", required = true, defaultValue = "20") int age) {
return new User(name, age);
}

@RequestParam可加可不加,根据属性名和参数是否匹配来判断。

后端使用Map来接收,前端传入Content-Type:application/x-www-form-urlencoded格式的数据

1
2
3
4
5
6
@PostMapping("/loginByMap")
public User loginByMap(@RequestParam Map<String, Object> map) {
String name = (String) map.get("name");
int age = Integer.parseInt((String) map.get("age"));
return new User(name, age);
}

这里类似于get请求,同样,map参数前需要加@RequestParam注解,用于将请求参数注入到map中。

值得注意的是,由于form表单形式是以key/value形式存储,都是字符串类型,因此需要将map.get(“age”)转化为String,再转化为Integer,最后再自动拆箱。

不可以将map.get(“age”)直接转化为Integer类型,因为其本质是String类型,String不能直接强转为Integer。

后端使用Map来接收,前端传入Content-Type:application/json格式的数据

1
2
3
4
5
6
@PostMapping("/loginByMap")
public User loginByMap(@RequestBody Map<String, Object> map) {
String name = (String) map.get("name");
int age = (Integer) map.get("age");
return new User(name, age);
}

同样,@RequestBody注解用于将请求体中的json字符串转化为对象属性,并注入到map中。

由于请求体中json中的age类型为number类型,因此注入到map中时,age是Integer类型,那么可以直接强转为Integer类型。

后端使用JSONObject来接收,前端传入Content-Type:application/json格式的数据

1
2
3
4
5
6
@PostMapping("/loginByJSONObject")
public User loginByJSONObject(@RequestBody JSONObject jsonObject) {
String name = jsonObject.getString("name");
int age = jsonObject.getInteger("age");
return new User(name, age);
}

@RequestBody注解用于将请求体中的json字符串转化为JSON对象。

后端使用数组来接收

1
2
3
4
@PostMapping("/array")
public Integer[] array(Integer[] a) {
return a;
}

前端传入Content-Type:application/x-www-form-urlencoded格式的数据,后端可以直接接收到。

但传入Content-Type:application/json格式的数据[1,2,3],后端则接收不到,需要加入@RequestBody注解。当然(@RequestBody List a)也是可以的。

总结:

@PathVariable、@RequestParam与@RequestBody注解三者的区别:

如果前端传入Content-Type:application/json格式的数据,直接使用@RequestBody注解将json字符串转化为对象。

如果前端传入Content-Type:application/x-www-form-urlencoded格式的数据,如果能够得出方法参数具有的属性和请求参数一样的属性时,则不需要@RequestParam注解。例如注入到Map中,则需要@RequestParam注解。

如果后端已经使用了@RequestBody注解,代表只接收application/json类型的数据,此时若再传入application/x-www-form-urlencoded类型的数据,则后台会报错。

另外,get请求的请求头没有Content-Type字段!


Git使用Merge和Rebase区别

Merge和Rebase概念概述

首先我们应该明白git rebase是用来处理git merge命令所处理的同样的问题。这两个命令都用于把一个分支的变更整合进另一个分支,只不过他们达成同样目的的方式不同。

请考虑这个场景,当你开始在一个专有的分支开发新的功能时,另一位团队成员更新了main分支的内容。这将会造成一个分叉的提交历史,对于任何一个使用Git作为代码协作工具的人来说都不会陌生。

现在假设main分支内新增的内容与你正在开发的新功能有关。为了把main分支里新增的代码应用在你的feature分支,你有两种方法:merge 和 rebase。

使用merge

最简单的方法就是把main分支合并进功能分支:

1
2
git checkout feature
git merge main

或者用下面这样的单行命令:

1
git merge feature main

这会在feature分支中创建一个合并提交,这次提交会连结两个分支的提交历史,在分支图示结构中看起来像下面这样:

合并操作很友好,因为它没有破坏性。现存的分支历史不会发生什么改变。这一特性避免了rebase操作的所有缺陷(下面会详细讨论)。

但是另一方面来说,这也意味着每当feature分支需要应用上游分支的更改时,都会在提交历史上增加一个无关的提交历史。如果main分支的更新非常活跃,这种操作也会对功能分支的提交历史产生相当程度的污染。虽然通过复杂的git log命令可以减轻这种提交历史的混乱现状,但仍然会让其他开发者对于提交历史感到费解。

使用rebase

为了替代merge操作,你也可以把feature分支的提交历史rebase到main分支的提交历史顶端:

1
2
git checkout feature
git rebase main

这些操作会把feature分支的起始历史放到main分支的最后一次提交之上,也达成了使用main分支中新代码的目的。但是,相对于merge操作中新建一个合并提交,rebase操作会通过为原始分支的每次提交创建全新的提交,从而重写原始分支的提交历史。

使用rebase操作的最大好处在于你可以让项目提交历史变得非常干净整洁。首先,它消除了git merge操作所需创建的没有必要的合并提交。其次,正如上图所示,rebase会造就一个线性的项目提交历史——也就是说你可以从feature分支的顶部开始向下查找到分支的起始点,而不会碰到任何历史分叉。这在使用git log,git bisect以及gitk等命令时更简单。

不过为了获得这种便于理解的提交历史,却需要付出两种代价:安全性和可追溯性。如果不能遵循rebase的黄金法则,重写项目提交历史会为协作工作流程带来潜在的灾难性后果。再次,rebase操作丢失了合并提交能够提供的上下文信息——所以你就无法知道功能分支是什么时候应用了上游分支的变更。

rebase操作黄金法则

一旦你明白了什么是rebase,接下来最重要的事情就是要了解什么情况下不应该使用它。关于git rebase的黄金法则就是永远不要在公共分支上使用它。

举例来说,想一想如果把main分支rebase到feature分支之上,会发生什么:

rebase命令会把main分支中的所有提交都放到feature分支的提交记录顶端。问题在于这个改变目前只出现在你的本地仓库。其他开发者仍然在原来的main分支上进行开发。由于rebase会产生全新的提交记录,所以Git会认为现在你本地的main分支与所有其他人的产生了分叉。

唯一能够同步两个不同的main分支的方式就是将其合并起来,这会产生一个冗余的合并提交,并且这次合并中的大部分提交内容都是相同的(以前的main分支和你本地的main分支中)。不用说,这下可真让人疑惑。

所以任何时候要执行git rebase命令之前,先确认“是否有其他人也正在使用此分支?”如果答案是确定的,那么你就应该停下来想想有没有其他非破坏性的操作(比如试试git revert命令)。除了这样的情况之外,重写提交历史都是安全的。

总结

如果你希望一个干净线性的提交历史,而不是含有众多合并提交相互交织的提交历史,那么应该尝试在整合分支时使用git rebase而不是git merge。

反过来说,如果你想要保存完整的提交历史,避免重写公共提交的历史,仍然可以坚持使用git merge。两者都可以,但至少你现在拥有了另一个选项,可以见机利用 git rebase的优势。