🔥 本文由 程序喵正在路上 原创,CSDN首发!
💖 系列专栏:JavaWeb开发
🌠 首发时间:2024年2月6日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
概述
请求响应:
- 请求(HttpServletRequest):获取请求数据
- 响应(HttpServletResponse):设置响应数据
- BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端
- CS架构:Client/Server,客户端/服务器架构模式
请求
Postman
前后端分离开发
前面,我们介绍了最为主流的开发模式——前后端分离。在这种开发模式中,前端程序和后端程序是分开开发。前面我们测试后端程序的时候,是通过浏览器地址栏输入地址进行测试,但是这种发出的都是 GET 请求,无法测试 POST 请求。
postman
- postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件
- 作用:常用于进行接口测试
下载安装postman
-
点击连接 https://pan.baidu.com/s/1R7aK65AOaehS1OtafsVrHA?pwd=y0wc 进入下载
-
双击 .exe 文件进行安装即可,不用其他操作
-
安装完成
-
注册登录
如何创建工作空间
-
点击 Workspaces 新建 workspace:
-
设置一下信息:
-
创建完自动进入工作空间:
如何添加请求
-
点击这个加号即可添加请求
-
添加请求,点击 Send 发起请求:
-
添加请求界面说明:
-
保存请求
-
给请求命名和创建一个文件夹来存放请求
简单参数
原始方式
- 在原始的web程序中,获取请求参数,需要通过HttpServletRequest 对象手动获取
步骤
-
创建一个新的 springboot 工程
-
创建类,写入原始方式获取参数的代码
import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class RequestController { //原始方式 @RequestMapping("/simpleParam") public String simpleParam(HttpServletRequest request) { String name = request.getParameter("name"); String ageStr = request.getParameter("age"); int age = Integer.parseInt(ageStr); System.out.println(name + ": " + age); return "Ok"; } }
-
启动 springboot 工程,打开 postman,新建文件夹,新建请求进行测试
原始方式有点繁琐,并且需要我们手动进行类型转换
SpringBoot方式
- 简单参数:参数名与形参变量名相同,定义形参即可接收参数
SpringBoot 方式的代码简洁了很多:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RequestController {
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String name, Integer age) {
System.out.println(name + ": " + age);
return "Ok";
}
}
再进行测试:
POST 请求
如果是 POST 请求,那么我们就要到 Body 里面去设置参数:
@RequestParam注解
- 简单参数:如果方法形参名称与请求参数名称不匹配,可以使用
@RequestParam
完成映射 - 注意:
@RequestParam
中有一个required
属性,其默认值为true
,代表该请求参数必须传递,如果不传递可能会报错。 如果该参数是可选的,可以将required
属性设置为false
比如,我们将形参 name 改为 username(修改了代码后记得重启工程),会发现获取不到参数:
我们加上 @RequestParam
注解,默认该参数必须传递:
实体参数
前面我们只传递了两个参数,如果现在有 20 个参数,我们一个个写就会很繁琐,这种情况,我们可以定义一个实体对象来存储所有的参数。
简单实体对象
- 简单实体对象:请求参数名与形参对象属性名相同,定义POJO接收即可
这里我们准备了一个测试案例,要求在代码中用实体对象来接收参数:
步骤
-
创建包 pojo,在 pojo 下创建实体类 User
快捷键 Alt + Insert 快速生成 Get、Set 和 toString 方法:
-
在
RequestController.java
中添加 simplePojo 方法 -
启动工程,到 postman 中点击测试
如果我们将请求中的 name 改一下,就获取不到参数了:
如果现在 User 类增加了一个属性 Address,并且这个属性也是一个类,那么实体对象应该怎么写呢?
复杂实体对象
- 复杂实体对象:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套POJO属性参数
这里,我们准备了一个比较复杂的测试案例:
步骤:
-
添加 Address 类
-
修改一下 User 类,别忘了重新生成 toString() 方法
package com.xixi.pojo; 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 + '}'; } }
-
在
RequestController.java
中添加 complexPojo 方法//复杂实体参数 @RequestMapping("/complexPojo") public String complexPojo(User user) { System.out.println(user); return "Ok"; }
-
重启工程,进行测试
数组集合参数
前面我们学习 Form 表单的时候,用户填的信息有时候是多选的,每个选项在提交的时候都要提交给服务端,那么服务端应该如何接收呢?
答案是用数组或者集合。
数组参数
- 数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数
这里,我们准备了一个测试案例:
步骤:
-
在
RequestController.java
中添加 arrayParam 方法//数组参数 @RequestMapping("/arrayParam") public String arrayParam(String[] hobby) { System.out.println(Arrays.toString(hobby)); return "Ok"; }
-
启动服务,测试
集合参数
- 集合参数:请求参数名与形参集合名称相同且请求参数为多个,@RequestParam 绑定参数关系
准备一个测试案例,将数组参数的案例改一下路径即可:
步骤:
-
在
RequestController.java
中添加 listParam 方法//集合参数 @RequestMapping("/listParam") public String listParam(@RequestParam List<String> hobby) { System.out.println(hobby); return "Ok"; }
-
重启服务,进行测试
日期参数
日期参数:使用 @DateTimeFormat
注解完成日期参数格式转换
首先,我们准备一个测试案例:
步骤:
-
在
RequestController.java
中添加 dateParam 方法//日期参数 @RequestMapping("/dateParam") public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime) { System.out.println(updateTime); return "Ok"; }
-
重启服务,进行测试
Json参数
JSON参数:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数,需要使用 @RequestBody
标识
首先,我们准备一个测试案例:
步骤:
-
创建实体类 User 和 Address,用来接收 Body 中的内容,前面我们已经创建了,所以这一步可以省略
-
在
RequestController.java
中添加 jsonParam 方法//json参数 @RequestMapping("/jsonParam") public String jsonParam(@RequestBody User user) { System.out.println(user); return "OK"; }
-
重启服务,进行测试
路径参数
路径参数:通过请求URL直接传递参数,使用 {…}
来标识该路径参数,需要使用 @PathVariable
获取路径参数
首先,我们准备一个测试案例:
步骤:
-
在
RequestController.java
中添加 pathParam 方法//路径参数 @RequestMapping("/path/{id}") public String pathParam(@PathVariable Integer id) { System.out.println(id); return "Ok"; }
-
重启服务,进行测试
如果有多个路径参数,应该怎么写呢?
比如,现在我们的请求是这样的:
在这种情况下,我们的 pathParam 方法就要写成下面这样:
//多个路径参数
@RequestMapping("/path/{id}/{name}")
public String pathParam2(@PathVariable Integer id, @PathVariable String name) {
System.out.println(id + " : " + name);
return "Ok";
}
测试:
响应
前面,我们在方法中直接返回一个字符串,就能响应客户端,这都是注解 @ResponseBody
的功劳
@ResponseBody
- 类型:方法注解、类注解
- 位置:Controller方法上/类上
- 作用:将方法返回值直接响应,如果返回值类型是 实体对象/集合 ,将会转换为JSON格式响应
- 说明:
@RestController = @Controller + @ResponseBody
,所以我们添加了@RestController
注解,就不用添加@ResponseBody
了
接下来,我们就来演示一下 @ResponseBody
这个注解的作用。
首先,我们准备一个名为 ResponseController.java
的程序,将其放在 Controller 包下:
package com.xixi.controller;
import com.xixi.pojo.Address;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
* 测试响应数据
*/
@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 来进行测试。
返回值类型是实体对象或者集合,将会转换为JSON格式响应
ResponseController.java
中每个方法都称为一个功能接口:
统一的响应结果
在前面的测试中,三个功能接口响应的形式都不一样。在大型项目中,我们会有成千上万个功能接口。如果没有一套统一的开发规范,前端人员发来请求后,会收到各式各样的响应,那么前端人员就需要对每个响应进行单独的解析,因为它们的形式都不相同,这个工程量是非常大的,成本也会直线上升,而且不便于管理和难以维护。
所以我们需要一个统一的响应结果,这个响应结果要具备通用性,需要满足所有的业务场景。这个统一的响应结果我们可以考虑使用一个实体对象 Result 来接收。
有了 Result 类后,我们功能接口的返回结果都要封装为一个 Result 类返回。
下面,我会给出 Result 类的代码,请将其添加到 pojo 目录下
package com.xixi.pojo;
/**
* 统一响应结果封装类
*/
public class Result {
private Integer code;//1 成功 , 0 失败
private String msg; //提示信息
private Object data; //数据 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(Object data) {
return new Result(1, "success", data);
}
public static Result success() {
return new Result(1, "success", null);
}
public static Result error(String msg) {
return new Result(0, msg, null);
}
@Override
public String toString() {
return "Result{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
接下来,我们要对 ResponseController.java
中的方法进行改造,将它们的返回结果用 Result 类封装起来:
再测试一下:
案例
需求:获取员工数据,返回统一响应结果,在页面渲染展示
加载并解析 emp.xml 文件中的数据,完成数据处理,并在页面展示。
具体步骤:
- 在 pom.xml 文件中引入 dom4j 的依赖,用于解析 XML 文件
- 引入资料中的解析 XML 的工具类 XMLParserUtils、对应的实体类 Emp、XML文件 emp.xml
- 引入资料中的静态页面文件,放在 resources 下的 static 目录下
- 编写 Controller 程序,处理请求,响应数据
-
在 pom.xml 文件中引入 dom4j 的依赖,用于解析 XML 文件
<!-- 解析XML --> <dependency> <groupId>org.dom4j</groupId> <artifactId>dom4j</artifactId> <version>2.1.3</version> </dependency>
-
引入解析 XML 的工具类 XMLParserUtils、对应的实体类 Emp、XML文件 emp.xml
在 com.xixi 下创建一个包 utils 专门用来放工具类:
将
XMLParserUtils.java
工具类粘贴到 utils 包下;将员工类 Emp.java 粘贴到 pojo 下;
将员工信息资源 emp.xml 粘贴到 resources 下;
-
引入准备好的静态页面文件,放在 resources 下的 static 目录下
-
编写 Controller 程序,处理请求,响应数据
在 Controller 目录下创建 EmpController.java:
import com.xixi.pojo.Emp; import com.xixi.pojo.Result; import com.xixi.utils.XmlParserUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class EmpController { @RequestMapping("/listEmp") public Result list() { //1. 加载并解析emp.xml String file = this.getClass().getClassLoader().getResource("emp.xml").getFile(); System.out.println(file); List<Emp> empList = XmlParserUtils.parse(file, Emp.class); //2. 对数据进行转换处理 empList.forEach(emp -> { //处理gender String gender = emp.getGender(); if ("1".equals(gender)) { emp.setGender("男"); } else if ("2".equals(gender)) { emp.setGender("女"); } //处理job String job = emp.getJob(); if ("1".equals(job)) { emp.setJob("讲师"); } else if ("2".equals(job)) { emp.setJob("班主任"); } else if ("3".equals(job)) { emp.setJob("就业指导"); } }); //3. 响应数据 return Result.success(empList); } }
需要说明一下,
list()
方法响应的请求路径需要与前端页面中 axios 异步请求的路径一致,同时因为 axios 异步请求没有添加参数,所以list()
方法也不用添加参数 -
启动服务,进行测试
来到浏览器,输入 localhost:8080/emp.html 访问我们的页面:
分层解耦
在前面写的 EmpController 中,我们将所有代码都写在一个方法中,包括数据访问、逻辑处理和接受请求、响应数据。但是在大型项目中,我们要求每个功能接口只拥有单一的功能,也就是每个接口只做一件事,这才符合单一职责原则。
三层架构
- controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据
- service:业务逻辑层,处理具体的业务逻辑
- dao:数据访问层(Data Access Object)(持久层),负责数据访问操作,包括数据的增、删、改、查
接下来,我们利用三层架构来对 EmpController 的代码进行优化。
步骤:
-
在 com.xixi 包下创建包 dao 和包 service,用来存放数据访问和逻辑处理的代码
-
先写 dao 部分的代码,为了让代码更具备灵活性,我们写一个接口 EmpDao,然后在 dao 目录下创建一个包 impl 来存放接口的实现类,在 impl 下再添加一个实现类 EmpDaoA
EmpDao.java
import com.xixi.pojo.Emp; import java.util.List; public interface EmpDao { //获取员工列表数据 public List<Emp> listEmp(); }
EmpDaoA.java
import com.xixi.dao.EmpDao; import com.xixi.pojo.Emp; import com.xixi.utils.XmlParserUtils; import java.util.List; public class EmpDaoA implements EmpDao { @Override public List<Emp> listEmp() { //1. 加载并解析emp.xml String file = this.getClass().getClassLoader().getResource("emp.xml").getFile(); System.out.println(file); List<Emp> empList = XmlParserUtils.parse(file, Emp.class); return empList; } }
-
接下来写 service 部分的代码,和 dao 操作差不多,也是要创建接口,然后再写实现类
EmpService.java
import com.xixi.pojo.Emp; import java.util.List; public interface EmpService { //获取员工列表 public List<Emp> listEmp(); }
EmpServiceA.java
import com.xixi.dao.EmpDao; import com.xixi.dao.impl.EmpDaoA; import com.xixi.pojo.Emp; import com.xixi.service.EmpService; import java.util.List; public class EmpServiceA implements EmpService { private EmpDao empDao = new EmpDaoA(); @Override public List<Emp> listEmp() { //1. 调用dao, 获取数据 List<Emp> empList = empDao.listEmp(); //2. 对数据进行转换处理 - gender, job empList.forEach(emp -> { //处理 gender 1: 男, 2: 女 String gender = emp.getGender(); if ("1".equals(gender)) { emp.setGender("男"); } else if ("2".equals(gender)) { emp.setGender("女"); } //处理job - 1: 讲师, 2: 班主任 , 3: 就业指导 String job = emp.getJob(); if ("1".equals(job)) { emp.setJob("讲师"); } else if ("2".equals(job)) { emp.setJob("班主任"); } else if ("3".equals(job)) { emp.setJob("就业指导"); } }); return empList; } }
-
最后改造 Controller 层
EmpController.java
import com.xixi.pojo.Emp; import com.xixi.pojo.Result; import com.xixi.service.EmpService; import com.xixi.service.impl.EmpServiceA; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class EmpController { private EmpService empService = new EmpServiceA(); @RequestMapping("/listEmp") public Result list() { //1. 调用service, 获取数据 List<Emp> empList = empService.listEmp(); //2. 响应数据 return Result.success(empList); } }
-
重启服务,进行测试
还是可以成功获取到页面,说明优化成功
拆分后的调用顺序:
拆分前后对比:
分层解耦
- 内聚:软件中各个功能模块内部的功能联系
- 耦合:衡量软件中各个层/模块之间的依赖、关联的程度,最好的状态是解耦合,也就是各个层/模块之间没有依赖关系
- 软件设计原则:高内聚低耦合
在前面我们利用三层架构改造的代码中,Controller 层中需要用到 EmpServiceA 的对象,当我们想将 EmpServiceA 换为 EmpServiceB 时,Controller 层的代码也要改动。这说明 Controller 层和 Service 层是耦合的。
那我们该如何解除它们之间的耦合呢?
我们想出了一种办法,就是增加一个容器,用来存放各种类的对象,Controller 层里面只定义对象而不实现对象,需要对象就到容器里找,而 Service 层的对象就可以存放在容器中。这样,当我们想把 EmpServiceA 换为 EmpServiceB 时,只需要将 EmpServiceB 的对象放到容器中,然后 Controller 层再去容器里找即可。
这两个过程分别称为控制反转和依赖注入
- 控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
- 依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
- Bean对象:IOC容器中创建、管理的对象,称之为bean。
IOC & DI入门
①. Service 层及 Dao 层的实现类,交给 IOC 容器管理
在类前面加个 @Component
注解即可
②. 为 Controller 及 Service 注入运行时,依赖的对象
将 new 的部分删掉,在定义的对象前面加上 @Autowired
注解即可
③. 运行测试
界面加载成功,没问题
这样,当我们需要用哪个类的对象时,就在哪个类前面加上 @Component
注解即可,很方便,其他类也不用改动
IOC详解
Bean的声明
要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
后面 3 个注解分别对应 Controller 层、Service 层和 Dao 层,在 web 开发中,推荐使用这 3 个注解。当遇到除 Controller 层、Service 层和 Dao 层外的类时,才使用 @Component
注解
注意事项
- 声明 bean 的时候,可以通过 value 属性指定 bean 的名字,如果没有指定,默认为首字母小写的类名
- 使用以上四个注解都可以声明 bean,但是在 springboot 集成 web 开发中,声明控制器 bean 只能用
@Controller
Bean组件扫描
-
前面声明bean的四大注解,要想生效,还需要被组件扫描注解
@ComponentScan
扫描 -
@ComponentScan
注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication
中,默认扫描的范围是启动类所在包及其子包 -
所以,我们尽量将所有代码都写在启动类所在包及其子包,这样我们就不用再去显式配置
@ComponentScan
注解
DI详解
Bean注入
-
@Autowired
注解,默认是按照类型进行,如果存在多个相同类型的 bean,将会报出如下错误: -
通过以下几种方案来解决:
-
@Primary
在要使用类前面再加上
@Primary
注解
-
@Qualifier
-
@Resource
改用
@Resource
-
@Resource
与 @Autowired
区别
@Autowired
是 spring 框架提供的注解,而@Resource
是 JDK 提供的注解@Autowired
默认是按照类型注入,而@Resource
默认是按照名称注入