Web请求与响应(SpringBoot)
Web的工作原理可以分为以下几个步骤:
-
-
建立连接:Web浏览器与Web服务器之间建立TCP/IP连接,以便传输数据。
-
发送HTTP请求:Web浏览器向Web服务器发送HTTP请求,请求所需的Web资源。
-
接收HTTP响应:Web服务器接收HTTP请求,并通过HTTP响应将Web资源发送给Web浏览器。
-
渲染Web页面:Web浏览器接收HTTP响应,并根据所收到的数据渲染Web页面,以便呈现给用户。
-
关闭连接:Web浏览器和Web服务器之间的TCP/IP连接被关闭。
什么是 HTTP 请求?
HTTP(超文本传输协议)是一种用于 Web 服务器和客户端(浏览器)之间通信以通过 Internet 传输数据的协议。 HTTP 请求是客户端向服务器发送的请求特定资源(如网页、图像或视频)的消息。 HTTP 请求方法决定了请求的类型,例如 GET、POST、PUT、DELETE、HEAD、OPTIONS、CONNECT、TRACE 等。
HTTP请求的结构:
HTTP 请求由请求行、标头和可选的消息正文组成。
请求行包括 HTTP 方法、所请求资源的 URL(统一资源定位符)以及所使用的 HTTP 版本。
标头提供有关请求的其他信息,例如用户代理、请求的内容类型和可接受的编码格式。
消息正文是可选的,包含数据,例如表单数据或 JSON 。
HTTP请求方法:
HTTP 定义了各种请求方法,指示要对 URL 标识的资源执行的操作。最常见的 HTTP 请求方法是:
-
GET:从服务器获取资源,例如网页或图像。
-
POST:将数据提交给服务器进行处理,例如表单提交或文件上传。
-
PUT:用新数据更新服务器上的现有资源。
-
DELETE:从服务器中删除资源。
-
HEAD:检索资源的标头,不带消息正文。
-
OPTIONS:用于获取当前URL所支持的方法。若请求成功,则它会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST”。
-
CONNECT:建立到资源的网络连接,例如代理服务器。
-
TRACE:回显接收到的请求消息,用于调试目的。 HTTP 标头:
HTTP 标头用于提供有关 HTTP 请求或响应的附加信息。它们是由冒号分隔的键值对,包含在请求或响应消息中。 Header有多种类型,例如通用Header、请求Header、响应Header和实体Header。一些常见的标头包括:
-
User-Agent:向访问网站提供你所使用的浏览器类型、操作系统及版本、CPU 类型、浏览器渲染引擎、浏览器语言、浏览器插件等信息的标识
-
Accept:指定客户端接受的内容类型。
-
Content-Type:指定请求或响应消息中内容的类型。
-
Content-Length:指定消息体的长度,以字节为单位。
-
Cache-Control:指定响应的缓存指令,例如 max-age 和 must-revalidate。
-
Authorization:指定请求的身份验证凭据。 HTTP 消息体body:
HTTP 消息正文是 HTTP 请求或响应的可选部分,包含各种格式的数据,例如 HTML、JSON、XML 或二进制数据。在请求中,消息正文包含要发送到服务器的数据,例如表单数据或文件上传。在响应中,消息正文包含所请求资源的实际内容,例如 HTML 页面或图像。
总之,了解 http 请求和响应的基础知识对于 Web 开发以及 Web 服务器和客户端之间的通信至关重要。通过了解如何构建 HTTP 请求、各种请求方法、标头的使用以及可选的消息正文,开发人员可以有效地与服务器通信并创建健壮的 Web 应用程序。
什么是 HTTP 响应?
HTTP响应是由服务器向客户发送的对请求的响应。 HTTP 响应包含以下组件:
-
HTTP版本
-
状态码
-
状态消息
-
响应头
-
响应体
1、HTTP版本
响应中使用的 HTTP 版本在响应的第一行中指定。例如:
HTTP/1.1 200 OK
这指定使用 HTTP 版本 1.1 发送响应。
2、状态码
状态代码表示所请求操作的结果。 HTTP 定义了五类状态码:
-
信息提示 (100-199)
-
成功 (200-299)
-
重定向 (300-399)
-
客户端错误 (400-499)
-
服务器错误 (500-599) 每个状态码都是一个三位数字,它包含在HTTP版本之后的响应的第一行中。例如:
HTTP/1.1 200 OK
这表明操作成功,请求的资源包含在响应正文中。
3、状态信息
状态消息是对状态代码的描述。它包含在状态代码之后的响应的第一行中。例如:
HTTP/1.1 404 Not Found
这表示未找到请求的资源,状态消息提供了问题的简短描述。
4、响应头
响应标头包含有关响应的元数据。它们类似于请求标头,但提供有关响应而不是请求的信息。响应标头可以包括有关服务器、缓存策略、cookie 等的信息。
响应标头包含在第一行之后的响应中。每个标头都是一个由冒号分隔的键值对。例如:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1274
Server: Apache
在此示例中,响应包含三个标头:Content-Type、Content-Length 和 Server。
5、响应体
响应正文包含请求的资源或错误消息,如果找不到请求的资源。响应正文的格式取决于响应中包含的 Content-Type 标头。例如,如果 Content-Type 标头设置为“text/html”,则响应正文应包含 HTML 代码。
响应主体包含在响应标头之后的响应中。如果响应主体很大,它可能会被拆分成多个数据包。
HTTP 请求和响应的技术点
HTTP 请求和响应有几个开发人员需要注意的技术点。这些技术点决定了请求和响应如何在客户端和服务器之间构建和传输。
1、请求和响应标头
HTTP 请求和响应包含提供有关请求或响应的附加信息的标头。标头是在消息标头中发送的键值对,它们用于传达有关请求或响应的元数据。 Headers 有多种类型,包括通用 Headers、请求 Headers、响应 Headers 和实体 Headers。
通用标头适用于请求和响应,并提供有关整个消息的信息,例如消息格式、发送日期和时间以及消息是否可以缓存。
请求标头用于提供有关发出请求的客户端的信息,例如用户代理(用于访问服务器的软件)、接受的内容类型以及用于压缩消息正文的编码。
响应头提供了服务器对客户端请求的响应信息,如响应的内容类型、消息体的长度、响应是否可以缓存等。
实体标头用于提供有关消息正文的信息,例如内容长度和内容编码。
2、HTTP 方法
HTTP 请求使用一组方法来指定要对 URL 中标识的资源执行的所需操作。最常见的 HTTP 方法是 GET、POST、PUT、DELETE 和 HEAD。
-
GET:用于从服务器检索数据。
-
POST:用于向服务器发送数据以创建或更新资源。
-
PUT:用于更新服务器上的现有资源。
-
DELETE:用于从服务器中删除资源。
-
HEAD:用于检索资源的标头,而不是消息正文。 HTTP 状态码 HTTP 响应包括指示请求状态的状态代码。有五类状态代码,每类都有自己的一组代码:
? 1xx: 信息 - 表示服务器已收到请求并正在继续处理它。 ? 2xx: 成功——表示请求被成功接收、理解和接受。 ? 3xx: 重定向——表示客户端需要采取进一步的行动来完成请求。 ? 4xx: 客户端错误 - 表示请求包含错误的语法或无法实现。 ? 5xx: 服务器错误 - 表示服务器未能满足有效请求。 最常见的状态代码是 200 OK(请求成功)、404 Not Found(未找到请求的资源)和 500 Internal Server Error(服务器在处理请求时遇到错误)。
3、Cookies
HTTP 请求和响应还可以包括 cookie,它们是存储在客户端计算机上的小文本文件。 Cookie 用于存储有关客户端偏好或之前与服务器交互的信息。服务器可以使用 cookie 来识别客户端并提供定制的内容。
4、缓存
HTTP 请求和响应还可以被缓存,这意味着客户端或中间服务器可以存储响应的副本以备将来使用。缓存有助于减少网络流量并提高性能,但如果缓存的内容过时或陈旧,也会导致问题。
作者博客:
yhttps://blog.51cto.com/sdwml/6104070
1.基于SpringBoot请求
请求(HttpServletRequest):获取请求数据
在浏览器地址输入地址,点击回车请求服务器,这个过程就是一个请求过程。
1.1简单参数
1.1.1原始方式
在原始的Web程序当中,需要通过Servlet中提供的API:HttpServletRequest(请求对象),获取请求的相关信息。比如获取请求参数:
Tomcat接受到Http请求时:把请求的相关信息封装到HttpServletRequest对象中。
在Controller中,我们要想获取Request对象,可以直接在方法的形参中声明HttpServletRequest对象。然后就可以通过该对象来获取请求信息:
//根据指定的参数名获取请求参数的数据值
String request.getParameter("参数名")
@RestController
public class RequestController {
//原始方式
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request){
// http://localhost:8080/simpleParam?name=Tom&age=10
// 请求参数: name=Tom&age=10 (有2个请求参数)
// 第1个请求参数: name=Tom 参数名:name,参数值:Tom
// 第2个请求参数: age=10 参数名:age , 参数值:10
?
String name = request.getParameter("name");//name就是请求参数名
String ageStr = request.getParameter("age");//age就是请求参数名
?
int age = Integer.parseInt(ageStr);//需要手动进行类型转换
System.out.println(name+" : "+age);
return "OK";
}
}
1.1.2SpringBoot方式
在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=10
// 第1个请求参数: name=Tom 参数名:name,参数值:Tom
// 第2个请求参数: age=10 参数名:age , 参数值:10
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致
System.out.println(name+" : "+age);
return "OK";
}
}
结论:不论是GET请求还是POST请求,对于简单参数来讲,只要保证==请求参数名和Controller方法中的形参名保持一致==,就可以获取到请求参数中的数据值。
1.1.3参数名不一致
如果方法形参名称与请求参数名称不一致,controller方法中的形参还能接收到请求参数值吗?
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=20
// 请求参数名:name
?
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String username , Integer age ){//请求参数名和形参名不相同
System.out.println(username+" : "+age);
return "OK";
}
}
答案:运行没有报错。 controller方法中的username值为:null,age值为20
-
结论:对于简单参数来讲,请求参数名和controller方法中的形参名不一致时,无法接收到请求数据
那么如果我们开发中,遇到了这种请求参数名和controller方法中的形参名不相同,怎么办?
解决方案:可以使用Spring提供的@RequestParam注解完成映射
在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射。代码如下:
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=20
// 请求参数名:name
?
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam("name") String username , Integer age ){
System.out.println(username+" : "+age);
return "OK";
}
}
注意事项:
@RequestParam中的required属性默认为true(默认值也是true),代表该请求参数必须传递,如果不传递将报错
如果该参数是可选的,可以将required属性设置为false
@RequestMapping("/simpleParam") public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){ System.out.println(username+ ":" + age); return "OK"; }
1.2实体参数
在使用简单参数做为数据传递方式时,前端传递了多少个请求参数,后端controller方法中的形参就要书写多少个。如果请求参数比较多,通过上述的方式一个参数一个参数的接收,会比较繁琐。
此时,我们可以考虑将请求参数封装到一个实体类对象中。 要想完成数据封装,需要遵守如下规则:请求参数名与实体类的属性名相同
1.2.1简单实体类对象
定义POJO实体类:
public class User {
private String name;
private Integer age;
?
public String getName() {
return name;
}
?
public void setName(String name) {
this.name = name;
}
?
public Integer getAge() {
return age;
}
?
public void setAge(Integer age) {
this.age = age;
}
?
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
?
Controller方法
@RestController
public class RequestController {
//实体参数:简单实体对象
@RequestMapping("/simplePojo")
public String simplePojo(User user){
System.out.println(user);
return "OK";
}
}
1.2.2复杂实体类对象
上面我们讲的呢是简单的实体对象,下面我们在来学习下复杂的实体对象。
复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下:
-
User类中有一个Address类型的属性(Address是一个实体类)
复杂实体对象的封装,需要遵守如下规则:
-
请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。
定义POJO实体类:
-
Address实体类
public class Address {
private String province;
private String city;
?
public String getProvince() {
return province;
}
?
public void setProvince(String province) {
this.province = province;
}
?
public String getCity() {
return city;
}
?
public void setCity(String city) {
this.city = city;
}
?
@Override
public String toString() {
return "Address{" +
"province='" + province + '\'' +
", city='" + city + '\'' +
'}';
}
}
-
User实体类
public class User {
private String name;
private Integer age;
private Address address; //地址对象
?
public String getName() {
return name;
}
?
public void setName(String name) {
this.name = name;
}
?
public Integer getAge() {
return age;
}
?
public void setAge(Integer age) {
this.age = age;
}
?
public Address getAddress() {
return address;
}
?
public void setAddress(Address address) {
this.address = address;
}
?
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}
}
Controller方法:
@RestController
public class RequestController {
//实体参数:复杂实体对象
@RequestMapping("/complexPojo")
public String complexPojo(User user){
System.out.println(user);
return "OK";
}
}
Postman测试:
1.3数组集合参数
数组集合参数的使用场景:在HTML的表单中,有一个表单项是支持多选的(复选框),可以提交选择的多个值。
后端程序接收上述多个值的方式有两种:
-
数组
-
集合
1.3.1数组
数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数
Controller方法:
@RestController
public class RequestController {
//数组集合参数
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby){
System.out.println(Arrays.toString(hobby));
return "OK";
}
}
Postman测试:
在前端请求时,有两种传递形式:
方式一:http://localhost:8080/arrayParam?hobby=game&hobby=java
方式二:http://localhost:8080/arrayParam?hobby=game,java
1.3.2集合
集合参数:请求参数名与形参集合对象名相同且请求参数为多个,@RequestParam 绑定参数关系
默认情况下,请求中参数名相同的多个值,是封装到数组。如果要封装到集合,要使用@RequestParam绑定参数关系
Controller方法:
@RestController
public class RequestController {
//数组集合参数
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String> hobby){
System.out.println(hobby);
return "OK";
}
}
Postman测试:
方式一:http://localhost:8080/listParam?hobby=game&hobby=java
方式二:http://localhost:8080/listParam?hobby=game,java
1.4日期函数
上述演示的都是一些普通的参数,在一些特殊的需求中,可能会涉及到日期类型数据的封装。比如,如下需求:
因为日期的格式多种多样(如:2022-12-12 10:05:45 、2022/12/12 10:05:45),那么对于日期类型的参数在进行封装的时候,需要通过@DateTimeFormat注解,以及其pattern属性来设置日期的格式。
-
@DateTimeFormat注解的pattern属性中指定了哪种日期格式,前端的日期参数就必须按照指定的格式传递。
-
后端controller方法中,需要使用Date类型或LocalDateTime类型,来封装传递的参数。
Controller方法:
@RestController
public class RequestController {
//日期时间参数
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){
System.out.println(updateTime);
return "OK";
}
}
1.5JSON参数
如果是比较复杂的参数,前后端通过会使用JSON格式的数据进行传输。 (JSON是开发中最常用的前后端数据交互方式)
我们学习JSON格式参数,主要从以下两个方面着手:
-
Postman在发送请求时,如何传递json格式的请求参数
-
在服务端的controller方法中,如何接收json格式的请求参数
Postman发送JSON格式数据:
服务端Controller方法接收JSON格式数据:
-
传递json格式的参数,在Controller中会使用实体类进行封装。
-
封装规则:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。
-
@RequestBody注解:将JSON数据映射到形参的实体类对象中(JSON中的key和实体类中的属性名保持一致)
实体类:Address
public class Address {
private String province;
private String city;
//省略GET , SET 方法
}
实体类:User
public class User {
private String name;
private Integer age;
private Address address;
//省略GET , SET 方法
}
Controller方法:
@RestController
public class RequestController {
//JSON参数
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){
System.out.println(user);
return "OK";
}
}
Postman测试:
1.6路径参数
传统的开发中请求参数是放在请求体(POST请求)传递或跟在URL后面通过?key=value的形式传递(GET请求)。
在现在的开发中,经常还会直接在请求的URL中传递参数。例如:
http://localhost:8080/user/1
http://localhost:880/user/1/0
上述的这种传递请求参数的形式呢,我们称之为:路径参数。
学习路径参数呢,主要掌握在后端的controller方法中,如何接收路径参数。
路径参数:
-
前端:通过请求URL直接传递参数
-
后端:使用{…}来标识该路径参数,需要使用@PathVariable获取路径参数
Controller方法:
@RestController
public class RequestController {
//路径参数
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id){
System.out.println(id);
return "OK";
}
}
传递多个路径参数:
Controller方法:
@RestController
public class RequestController {
//路径参数
@RequestMapping("/path/{id}/{name}")
public String pathParam2(@PathVariable Integer id, @PathVariable String name){
System.out.println(id+ " : " +name);
return "OK";
}
}
2.基于SpringBoot响应
响应(HttpServletReponse):设置响应数据
服务器根据浏览器发送的请求,放回数据到浏览器在网页上进行显示的过程就叫响应。
2.1@ResponseBody
controller方法中的return的结果,怎么就可以响应给浏览器呢?
答案:使用@ResponseBody注解
@ResponseBody注解:
-
类型:方法注解、类注解
-
位置:书写在Controller方法上或类上
-
作用:将方法返回值直接响应给浏览器
-
如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器
-
但是在我们所书写的Controller中,只在类上添加了@RestController注解、方法添加了@RequestMapping注解,并没有使用@ResponseBody注解,怎么给浏览器响应呢?
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println("Hello World ~");
return "Hello World ~";
}
}
原因:在类上添加的@RestController注解,是一个组合注解。
-
@RestController = @Controller + @ResponseBody
@RestController源码:
@Target({ElementType.TYPE}) //元注解(修饰注解的注解)
@Retention(RetentionPolicy.RUNTIME) //元注解
@Documented //元注解
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(
annotation = Controller.class
)
String value() default "";
}
结论:在类上添加@RestController就相当于添加了@ResponseBody注解。
-
类上有@RestController注解或@ResponseBody注解时:表示当前类下所有的方法返回值做为响应数据
-
方法的返回值,如果是一个POJO对象或集合时,会先转换为JSON格式,在响应给浏览器
-
下面我们来测试下响应数据:
@RestController
public class ResponseController {
//响应字符串
@RequestMapping("/hello")
public String hello(){
System.out.println("Hello World ~");
return "Hello World ~";
}
//响应实体对象
@RequestMapping("/getAddr")
public Address getAddr(){
Address addr = new Address();//创建实体类对象
addr.setProvince("广东");
addr.setCity("深圳");
return addr;
}
//响应集合数据
@RequestMapping("/listAddr")
public List<Address> listAddr(){
List<Address> list = new ArrayList<>();//集合对象
Address addr = new Address();
addr.setProvince("广东");
addr.setCity("深圳");
?
Address addr2 = new Address();
addr2.setProvince("陕西");
addr2.setCity("西安");
?
list.add(addr);
list.add(addr2);
return list;
}
}
在服务端响应了一个对象或者集合,那私前端获取到的数据是什么样子的呢?我们使用postman发送请求来测试下。测试效果如下:
2.2统一响应结果
大家有没有发现一个问题,我们在前面所编写的这些Controller方法中,返回值各种各样,没有任何的规范。
如果我们开发一个大型项目,项目中controller方法将成千上万,使用上述方式将造成整个项目难以维护。那在真实的项目开发中是什么样子的呢?
在真实的项目开发中,无论是哪种方法,我们都会定义一个统一的返回结果。方案如下:
前端:只需要按照统一格式的返回结果进行解析(仅一种解析方案),就可以拿到数据。
统一的返回结果使用类来描述,在这个结果中包含:
-
响应状态码:当前请求是成功,还是失败
-
状态码信息:给页面的提示信息
-
返回的数据:给前端响应的数据(字符串、对象、集合)
定义在一个实体类Result来包含以上信息。代码如下:
public class Result {
private Integer code;//响应码,1 代表成功; 0 代表失败
private String msg; //响应码 描述字符串
private Object data; //返回的数据
public Result() { }
public Result(Integer code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
?
public Integer getCode() {
return code;
}
?
public void setCode(Integer code) {
this.code = code;
}
?
public String getMsg() {
return msg;
}
?
public void setMsg(String msg) {
this.msg = msg;
}
?
public Object getData() {
return data;
}
?
public void setData(Object data) {
this.data = data;
}
?
//增删改 成功响应(不需要给前端返回数据)
public static Result success(){
return new Result(1,"success",null);
}
//查询 成功响应(把查询结果做为返回数据响应给前端)
public static Result success(Object data){
return new Result(1,"success",data);
}
//失败响应
public static Result error(String msg){
return new Result(0,msg,null);
}
}
改造Controller:
@RestController
public class ResponseController {
//响应统一格式的结果
@RequestMapping("/hello")
public Result hello(){
System.out.println("Hello World ~");
//return new Result(1,"success","Hello World ~");
return Result.success("Hello World ~");
}
?
//响应统一格式的结果
@RequestMapping("/getAddr")
public Result getAddr(){
Address addr = new Address();
addr.setProvince("广东");
addr.setCity("深圳");
return Result.success(addr);
}
?
//响应统一格式的结果
@RequestMapping("/listAddr")
public Result listAddr(){
List<Address> list = new ArrayList<>();
?
Address addr = new Address();
addr.setProvince("广东");
addr.setCity("深圳");
?
Address addr2 = new Address();
addr2.setProvince("陕西");
addr2.setCity("西安");
?
list.add(addr);
list.add(addr2);
return Result.success(list);
}
}
使用Postman测试: