先赞后看,养成习惯!!!❤️ ❤️ ❤️
文章码字不易,如果喜欢可以关注我哦!
如果本篇内容对你有所启发,欢迎访问我的个人博客了解更多内容:链接地址
是什么
Spring MVC是Spring框架提供的一个基于MVC(Model-View-Controller)设计模式的Web应用程序开发框架。它提供了一组组件,如控制器、模型和视图,用于快速开发Web应用程序。与其他Web框架相比,Spring MVC具有灵活的配置方式、可扩展性和可测试性等优点。
- Model,模型层,负责业务逻辑判断,数据库存取
- View,视图层,负责界面展示,向用户呈现数据的方式(html页面、图片、文本等)
- Controller,控制器,负责接收用户请求,并根据请求调用相应的模型来处理业务逻辑
处理流程
- 客户端发送请求至前端控制器DispatcherServlet;
- DispatcherServlet收到请求后,调用处理器映射器HandlerMapping;
- HandlerMapping根据请求URL找到具体的Controller;
- 通过处理器适配器HandlerAdapter找到具体执行该方法的实现类;
- Controller处理请求,并返回ModelAndView;
- DispatcherServlet通过ViewReslover(视图解析器)确定负责显示数据的具体View;
- DispatcherServlet对View进行渲染视图(即将Model填充至视图组件中),并将完整的视图响应到客户端。
SpringMVC是一种表现层框架技术,用于进行表现层功能开发
- SpringMVC是一种基于Java实现MVC模型的轻量级Web框架
- 优点
- 使用简单、开发便捷(相比于Servlet)
- 灵活性强
SpringMVC是隶属于Spring框架的一部分,主要是用来进行Web开发,是对Servlet进行了封装。
- SpringMVC是处于Web层的框架,所以其主要的作用就是用来接收前端发过来的请求和数据然后经过处理并将处理的结果响应给前端,所以如何处理请求和响应是SpringMVC中非常重要的一块内容。
- REST是一种软件架构风格,可以降低开发的复杂性,提高系统的可伸缩性,后期的应用也是非常广泛。
- SSM整合是把咱们所学习的SpringMVC+Spring+Mybatis整合在一起来完成业务开发,是对我们所学习这三个框架的一个综合应用。
- 针对web层进行了优化,采用了MVC设计模式,将其设计为controller、view和Model
入门案例总结
- 一次性工作
- 创建工程,设置服务器,加载工程。报错500注意springmvc版本
- 导入坐标
- 创建web容器启动类,加载SpringMVC配置,并设置SpringMVC请求拦截路径
- SpringMVC核心配置类(设置配置类,扫描controller包,加载Controller控制器bean)
- 多次工作
- 定义处理请求的控制器类
- 定义处理请求的控制器方法,并配置映射路径(@RequestMapping)与返回json数据(@ResponseBody)
- 为了更好的使用SpringMVC,我们将SpringMVC的使用过程总共分两个阶段来分析,分别是启动服务器初始化过程和单次请求过程
bean加载控制
- config目录存入的是配置类,写过的配置类有:
- ServletContainersInitConfig
- SpringConfig
- SpringMvcConfig
- JdbcConfig
- MybatisConfig
- controller目录存放的是SpringMVC的controller类
- service目录存放的是service接口和实现类
- dao目录存放的是dao/Mapper接口
controller、service和dao这些类都需要被容器管理成bean对象,那么到底是该让SpringMVC加载还是让Spring加载呢?
- SpringMVC加载其相关bean(表现层bean),也就是controller包下的类
- Spring控制的bean
- 业务bean(Service)
- 功能bean(DataSource,SqlSessionFactoryBean,MapperScannerConfigurer等)
分析清楚谁该管哪些bean以后,接下来要解决的问题是如何让Spring和SpringMVC分开加载各自的内容。
在SpringMVC的配置类SpringMvcConfig中使用注解@ComponentScan,我们只需要将其扫描范围设置到controller即可
在Spring的配置类SpringConfig中使用注解@ComponentScan,当时扫描的范围中其实是已经包含了controller
从包结构来看的话,Spring已经多把SpringMVC的controller类也给扫描到,所以针对这个问题该如何解决,就是咱们接下来要学习的内容。
加载Spring控制的bean的时候排除掉SpringMVC控制的bean
具体该如何排除:
- 方式一:Spring加载的bean设定扫描范围为精准范围,例如service包、dao包等
- 方式二:Spring加载的bean设定扫描范围为com.itheima,排除掉controller包中的bean
- 方式三:不区分Spring与SpringMVC的环境,加载到同一个环境中[了解即可]
SpringMVC处理请求
请求分类及处理方式
静态请求
- 定义
指请求的页面由服务器上预先准备好的静态web资源组成,如HTML、CSS、JS、IMG等,返回给客户端的信息内容是不变的。
- 处理方式
由服务器直接将请求的资源返回给客户端,服务器不处理任何逻辑,只是将预先准备好的资源返回给客户端。
动态请求
- 定义
服务器会根据用户的请求动态生成内容,将数据返回到客户端显示页面内容。
- 处理方式
由服务器从数据库中获取数据,并进行相应的逻辑处理后将处理结果返回客户端
处理静态请求
- 定义
指请求的页面由服务器上预先准备好的静态web资源组成,如HTML、CSS、JS、IMG等,返回给客户端的信息内容是不变的。
- 处理方式
由服务器直接将请求的资源返回给客户端,服务器不处理任何逻辑,只是将预先准备好的资源返回给客户端。
处理动态请求
通过在 controller 中定义对应的类及方法实现动态请求的业务逻辑处理。
注解说明
- @Controller
添加在类上;
表示该类是一个控制器,负责处理用户的请求,并将处理结果生成响应返回给客户端。
- @RequestMapping请求注解;
添加在控制器类或控制器方法上;
将HTTP请求映射到控制器中的方法,指定处理请求的路径
控制器类上:为整个控制器指定一个基础路径
控制器方法上:指定相对于基础路径的具体路径
- @ResponseBody响应注解;
添加在控制器方法上;
可以使控制器方法通过返回值的方式将响应返回给客户端
HTTP
1.1 HTTP协议
超文本传输协议(没有状态的协议,客户端服务端之前的每次对话都是一次请求响应就结束)
HTTP协议是浏览器与服务器通讯的应用层协议,规定了浏览器与服务器之间的交互规则以及交互数据的格式信息等。
- 用途 : 网页获取,数据的传输
- 特点
- 应用层协议,使用tcp进行数据传输;
- 有丰富的请求类型;
- 可以传输的数据类型众多
HTTPS协议
- 数据以密文的方式传输
- 安全,效率相较低
- 默认端口443
- https协议:SSL证书
1.2 访问网页流程
- 客户端(浏览器)通过tcp传输,发送http请求给服务端;
- 服务端接收到http请求后进行解析;
- 服务端处理请求内容,组织响应内容;
- 服务端将响应内容以http响应格式发送给浏览器;
- 浏览器接收到响应内容,解析展示.
1.3 请求和响应
1.3.1 HTTP请求 Request
浏览器给服务端发送的内容称为请求Request,一个请求包含三部分:请求行,请求头,请求体
- 请求行 : 具体的请求类别和请求内容
请求类别 GET
抽象路径 /
协议版本 HTTP/1.1
1.3.2关于请求类别
请求类别:每个请求类别表示向服务器端发请求做不同的操作
GET : 获取服务器资源
POST :新增服务器资源
PUT : 更新服务器资源
DELETE : 删除服务器资源
关于抽象路径
请求URL地址为:http://localhost:8080/
请求行为: GET / HTTP/1.1
请求URL地址为:http://localhost:8080/v1/users/login
请求行为: GET /v1/users/login HTTP/1.1
请求URL地址为:http://localhost:8080/v1/users/reg
请求行为: GET /v1/users/reg HTTP/1.1
- 请求头:对请求的进一步解释和描述
请求头是浏览器可以给服务端发送的一些附加信息,有的用来说明浏览器自身内容,有的用来告知服务端交互细节,有的告知服务端消息正文详情等。
Host: localhost:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
- 请求体: 请求参数或者提交内容
请求体通常是用户上传的信息,比如:在页面输入的注册信息,上传的附件等内容。
1.3.3响应
服务端给浏览器发送的内容称为响应Response,一个响应包含三部分:响应行,响应头,响应体。
- 响应行
HTTP/1.1 200 OK 版本信息 响应码 附加信息 响应码 : 1xx:保留 2xx:成功,表示处理成功,并正常响应 3xx:重定向,表示处理成功,但是需要浏览器进一步请求 4xx:客户端错误,表示客户端请求错误导致服务端无法处理 5xx:服务端错误,表示服务端处理请求过程出现了错误
- 响应头
响应头与请求中的消息头格式一致,表示的是服务端发送给客户端的附加信息。
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 3546 //Content-Length是用来告知浏览器响应正文的长度,单位是字节。
Content-Type 是用来告知浏览器响应正文中的内容是什么类型的数据(图片,页面等等)不同的类型对应的值是不同,浏览器接收正文前会根据上述两个响应头来得知长度和类型从而读取出来做对应的处理以渲染给用户看。
文件类型 | Content-Type对应的值 |
html | text/html |
css | text/css |
js | application/javascript |
png | image/png |
gif | image/gif |
jpg | image/jpeg |
- 响应体
响应的主体内容信息
1.4 URL
定义
URL(Uniform Resource Locator)是互联网上 统一资源定位符 的简称,用于标识和定位互联网上资源的地址。在Web浏览器中,URL是用于访问网页的地址。
- 协议(Protocol):指定访问资源的协议类型,常见的协议包括HTTP、HTTPS等。
- 主机名(Hostname):指定要访问的服务器的主机名或域名。
- 端口号(Port):指定服务器的端口号,不同的协议和应用程序使用不同的端口号。
- 路径(Path):指定要访问的资源的路径,即文件或文件夹的路径。
- 查询参数(Query Parameters):用于传递一些额外的信息给服务器,例如参数化的查询条件。
- URL地址是区分大小写的,因此在输入时要注意大小写的一致性。
- URL地址中不能包含非法字符,例如空格、特殊符号等。如果需要传递参数或附加信息,可以使用查询参数或POST请求等方式。
查询参数和路径Path之间使用 ?分隔,多个查询参数之间使用 & 分隔。
1.5 请求方法
GET请求
GET请求是HTTP协议中最常见的请求方式之一,它用于从服务器获取数据。
GET请求将查询参数附加在URL之后,通过“?”符号进行分隔。
- 查询参数
GET请求的参数通常以键值对的形式附加在URL之后。例如,以下URL中,“?name=John&age=25”是两个查询参数,分别表示名字和年龄
http://example.com/users?name=John&age=25
- 注意事项
- GET请求的参数是可见的,因此在URL中传递敏感信息时需谨慎。
- GET请求的长度有限制,因为URL的长度有限制,因此当传递大量数据时,应考虑使用POST或其他方式。
- 发送GET请求
在浏览器中输入URL地址确认即可向服务端发送GET请求。
http://localhost:8080/v1/users/login?username=xxx&password=xxx
POST请求
POST请求是另一种常见的HTTP请求方式,它用于向服务器提交数据。与GET请求不同,POST请求将数据放在请求体(Request Body)中,而不是URL中。
- 请求体
POST请求的请求体中包含要提交的数据。这些数据可以是JSON、XML或其他格式。请求体中的数据通过Content-Type头部指定。
- 请求示例
POST /users HTTP/1.1
Host: example.com
Content-Type: application/json
Request Body: {"username": "john", "password": "secret"}
-
- 使用POST请求的注意事项
- POST请求的数据是私密的,不会显示在URL中,因此更适合传递敏感信息。
- POST请求的长度没有限制,可以提交大量的数据。
- 发送POST请求
经常在 form 表单中发送。
<form action="请求地址" method="post">
<input type="text" name="username">
<input type="text" name="password">
<input type="submit" name="登录">
</form>
服务端接收参数
方式一:HttpServletRequest接收
- HttpServletRequest是Java Servlet规范中定义的一个接口,它提供了与HTTP请求相关的方法和属性。
- 在Java Web应用程序中,当客户端发送HTTP请求时,容器(例如Tomcat)会创建一个HttpServletRequest对象,该对象包含了客户端请求的所有信息,如请求的URL、请求方法、请求头、请求参数等。
- 在请求处理过程中,开发人员可以使用HttpServletRequest对象来获取客户端发送过来的请求参数。通过调用HttpServletRequest的方法,可以获取请求的参数名称、参数值以及参数的数量等信息。
/**方式1:使用HttpServletRequest接收数据*/
@RequestMapping("/v1/users/login")
@ResponseBody
public String login(HttpServletRequest request){
String username = request.getParameter("username");
String password = request.getParameter("password");
return username + ":" + password;
}
方式二:声明参数接收
可以在处理请求的方法中通过 声明参数的方式 来接收客户端传递过来的数据。
/**方式2:通过声明参数的方式接收*/
@RequestMapping("/v1/users/login")
@ResponseBody
// 好处:代码简洁,并且可以自动根据声明的类型进行转换
public String login(String username, String password){
return "username = " + username + ", password = " + password;
}
方式三:声明POJO类接收
如果客户端传递数据过多,通过 HttpServletRequest 方式接收复用性较差,通过 声明参数接收 又很繁琐;所以可以将数据封装到 POJO类 中来接收
- 第1步:controller.UserController处理登录请求
/**方式3:通过声明Pojo类接收*/
@RequestMapping("/v1/users/login")
@ResponseBody
public String login(User user){
return user.toString();
}
- 第2步:自定义pojo类,工程目录下创建entity.User
public class User {
// 客户端传递几个参数,此处就有几个属性
private String username;
private String password;
// 省略 setter() getter() 和 toString() 方法
}
- 第3步:重启工程后测试
web前端和服务器的数据交互
使用json数据格式来传输数据
web前端==>服务端 通过@RuquestBody注解完成json数据格式的转换
服务端==>web前端 通过@ResponseBody注解完成json数据格式的转换
组合注解
@RestController
等价于@Controller 加 @Response
五种类型参数传递
前面我们已经能够使用GET或POST来发送请求和数据,所携带的数据都是比较简单的数据,接下来在这个基础上,我们来研究一些比较复杂的参数传递,常见的参数种类有:
- 普通参数
- POJO类型参数
- 嵌套POJO类型参数
- 数组类型参数
- 集合类型参数
问题分析
团队多人开发,每人设置不同的请求路径,冲突问题该如何解决?
解决思路:为不同模块设置模块名作为请求路径前置
对于Book模块的save,将其访问路径设置http://localhost/book/save
对于User模块的save,将其访问路径设置http://localhost/user/save
这样在同一个模块中出现命名冲突的情况就比较少了。
JSON
JSON(JavaScript Object Notation)是一种数据交换格式,常用于不同系统间的数据传输。
在使用SpringMVC框架时,常常需要将Java对象转换成JSON格式,然后返回给前端。
JSON格式的数据通常由一对花括号 {} 括起来,在花括号中包含键值对,键值对之间使用逗号分隔。
JSON数据传输参数
SpringMVC接收JSON数据的实现步骤为:
(1)导入jackson包
(2)使用PostMan发送JSON数据
(3)开启SpringMVC注解驱动,在配置类上添加@EnableWebMvc注解
(4)Controller方法的参数前添加@RequestBody注解
Rest风格
一种资源描述方式
查看REST风格的描述,你会发现请求地址变的简单了,并且光看请求URL并不是很能猜出来该URL的具体功能
所以REST的优点有:
- 隐藏资源的访问行为,无法通过地址得知对资源是何种操作
- 书写简化
- 按照REST风格访问资源时使用==行为动作==区分对资源进行了何种操作
查询全部用户信息 GET(查询)
查询指定用户信息 GET(查询)
添加用户信息 POST(新增/保存)
修改用户信息 PUT(修改/更新)
删除用户信息 DELETE(删除)
请求的方式比较多,但是比较常用的就4种,分别是GET,POST,PUT,DELETE。
按照不同的请求方式代表不同的操作类型。
- 发送GET请求是用来做查询
- 发送POST请求是用来做新增
- 发送PUT请求是用来做修改
- 发送DELETE请求是用来做删除
- 上述行为是约定方式,约定不是规范,可以打破,所以称REST风格,而不是REST规范
- REST提供了对应的架构方式,按照这种架构设计项目可以降低开发的复杂性,提高系统的可伸缩性
- REST中规定GET/POST/PUT/DELETE针对的是查询/新增/修改/删除,但是我们如果非要用GET请求做删除,这点在程序上运行是可以实现的
- 但是如果绝大多数人都遵循这种风格,你写的代码让别人读起来就有点莫名其妙了。
- 描述模块的名称通常使用复数,也就是加s的格式描述,表示此类资源,而非单个资源,例如:users、books、accounts......
清楚了什么是REST风格后,我们后期会经常提到一个概念叫RESTful,那什么又是RESTful呢?
- 根据REST风格对资源进行访问称为==RESTful==。
什么是RESTful特征的API
- 每一个URI代表一种资源
- 客户端和服务器端之间传递着资源的某种表现
- 客户端通过HTTP的几个动作对资源进行操作,发生 状态转化
如何设计符合RESTful 特征的API
1 关于URL
- 协议 - http/https
- 域名
域名中体现出api字样,如
- 版本
- 路径
路径中避免使用动词,资源用名词表示,案例如下
https://api.example.com/v1/users https://api.example.com/v1/animals
2 HTTP动词语义
2.1 说明
- HTTP动词语义
请求动词 | 说明 |
GET(SELECT) | 从服务器取出资源(一项或多项) |
POST(CREATE) | 在服务器新建一个资源 |
PUT(UPDATE) | 在服务器更新资源 |
DELETE(DELETE) | 从服务器删除资源 |
具体案例如下:
请求动作 | 请求资源 | 说明 |
GET | /zoos | 列出所有动物园 |
POST | /zoos | 新建一个动物园 |
GET | /zoos/ID | 获取某个指定动物园的信息 |
PUT | /zoos/ID | 更新某个指定动物园的信息 |
DELETE | /zoos/ID | 删除某个动物园 |
GET | /zoos/ID/animals | 列出某个指定动物园的所有动物 |
DELETE | /zoos/ID/animals/ID | 删除某个指定动物园的指定动物 |
3 查询参数
巧用查询参数
?type_id=1:指定筛选条件 ?limit=10:指定返回记录的数量 ?offset=10:指定返回记录的开始位置。
4 HTTP状态码
用HTTP响应码表达 此次请求结果,例如
响应码 | 说明 |
200 OK - [GET] | 服务器成功返回用户请求的数据 |
404 NOT FOUND - [*] | 用户发出的请求针对的是不存在的记录 |
500 INTERNAL SERVER ERROR - [*] | 服务器发生错误 |
400 | 请求参数错误 |
405 | 请求方法错误 |
常用注解
1. @RequestMapping 注解
@RequestMapping 用于指定处理请求的 URL,它可以标注在类和方法上;
可以通过 method 参数限定处理 GET、POST、PUT、DELETE 等HTTP的请求方法,比如:
- 处理
GET 请求
@RequestMapping(value = "/v1/users", method = RequestMethod.GET)
- 处理
POST 请求
@RequestMapping(value = "/v1/users", method = RequestMethod.POST)
处理 PUT DELETE 方式的HTTP请求同理。
2. 限定请求方式的注解
2.1@GetMapping 注解
@GetMapping 只会处理 HTTP GET 请求;
是 @RequestMapping(method = RequestMethod.GET) 的缩写。
如果限定为处理GET请求,则发送其他方式请求时HTTP状态码为 405
2.2 @PostMapping 注解
@PostMapping 只会处理 HTTP POST 请求;
是 @RequestMapping(method = RequestMethod.POST) 的缩写。
如果限定为处理POST请求,则发送其他方式请求时HTTP状态码为 405
2.3 @PutMapping 注解
@PutMapping 只会处理 HTTP PUT 请求;
是 @RequestMapping(method = RequestMethod.PUT) 的缩写。
如果限定为处理PUT请求,则发送其他方式请求时HTTP状态码为 405
2.4 @DeleteMapping 注解
@DeleteMapping 只会处理 HTTP DELETE 请求;
是 @RequestMapping(method = RequestMethod.DELETE) 的缩写。
如果限定为处理DELETE请求,则发送其他方式请求时HTTP状态码为 405
3. @PathVariable 注解
@PathVariable 注解用于接受 RESTful API 中的 URL 中的变量;
通常与请求注解一起使用,可以将 URL 中的变量映射到 Controller 中的方法参数上。
3.1 用法
假如有一个 RESTful API:/v1/users/{id},
其中 {id} 是一个变量,表示用户 ID。
可以这样定义一个处理该请求的控制器方法
@GetMapping("/v1/users/{id}") public String getUserById(@PathVariable Integer id) { // 根据 ID 查询用户,并返回用户信息 }
3.2 使用示例
根据 id 查询用户,并返回该用户信息
- 地址:/v1/users/{id}
- 请求方法:GET
- 查询参数:无
- 响应类型:用户对象user
实现
- 根据
id 查询用户,并返回该用户信息(GET请求:/v1/users/{id})
@GetMapping("{id}") public User selectById(@PathVariable int id){ // 自己定义相对应的接口方法 return userMapper.selectById(id); }
文件上传
想要完成文件上传这个功能需要涉及到两个部分:
- 前端程序
- 服务端程序
我们先来看看在前端程序中要完成哪些代码:
<form action="/upload" method="post" enctype="multipart/form-data"> 姓名: <input type="text" name="username"><br> 年龄: <input type="text" name="age"><br> 头像: <input type="file" name="image"><br> <input type="submit" value="提交"> </form>
上传文件的原始form表单,要求表单必须具备以下三点(上传文件页面三要素):
- 表单必须有file域,用于选择要上传的文件
type="file" name="image"/>
- 表单提交方式必须为POST
通常上传的文件会比较大,所以需要使用 POST 提交方式
- 表单的编码类型enctype必须要设置为:multipart/form-data
普通默认的编码格式是不适合传输大型的二进制数据的,所以在文件上传时,表单的编码格式必须设置为multipart/form-data
本地存储
前面我们已分析了文件上传功能前端和后端的基础代码实现,文件上传时在服务端会产生一个临时文件,请求响应完成之后,这个临时文件被自动删除,并没有进行保存。下面呢,我们就需要完成将上传的文件保存在服务器的本地磁盘上。
代码实现:
- 在服务器本地磁盘上创建images目录,用来存储上传的文件(例:E盘创建images目录)
- 使用MultipartFile类提供的API方法,把临时文件转存到本地磁盘目录下
MultipartFile 常见方法: String getOriginalFilename(); //获取原始文件名void transferTo(File dest); //将接收的文件转存到磁盘文件中long getSize(); //获取文件的大小,单位:字节byte[] getBytes(); //获取文件内容的字节数组InputStream getInputStream(); //获取接收到的文件内容的输入流
如果直接存储在服务器的磁盘目录中,存在以下缺点:
- 不安全:磁盘如果损坏,所有的文件就会丢失
- 容量有限:如果存储大量的图片,磁盘空间有限(磁盘不可能无限制扩容)
- 无法直接访问
为了解决上述问题呢,通常有两种解决方案:
- 自己搭建存储服务器,如:fastDFS 、MinIO
- 使用现成的云服务,如:阿里云OSS,腾讯云,华为云
登录校验
- 所谓登录校验,指的是我们在服务器端接收到浏览器发送过来的请求之后,首先我们要对请求进行校验。先要校验一下用户登录了没有,如果用户已经登录了,就直接执行对应的业务操作就可以了;如果用户没有登录,此时就不允许他执行相关的业务操作,直接给前端响应一个错误的结果,最终跳转到登录页面,要求他登录成功之后,再来访问对应的数据。
首先我们在宏观上先有一个认知:
前面在讲解HTTP协议的时候,我们提到HTTP协议是无状态协议。什么又是无状态的协议?
所谓无状态,指的是每一次请求都是独立的,下一次请求并不会携带上一次请求的数据。而浏览器与服务器之间进行交互,基于HTTP协议也就意味着现在我们通过浏览器来访问了登陆这个接口,实现了登陆的操作,接下来我们在执行其他业务操作时,服务器也并不知道这个员工到底登陆了没有。因为HTTP协议是无状态的,两次请求之间是独立的,所以是无法判断这个员工到底登陆了没有。
那应该怎么来实现登录校验的操作呢?具体的实现思路可以分为两部分:
- 在员工登录成功后,需要将用户登录成功的信息存起来,记录用户已经登录成功的标记。
- 在浏览器发起请求时,需要在服务端进行统一拦截,拦截后进行登录校验。
我们要完成以上操作,会涉及到web开发中的两个技术:
- 会话技术
- 统一拦截技术
而统一拦截技术现实方案也有两种:
- Servlet规范中的Filter过滤器
- Spring提供的interceptor拦截器
会话技术
- Cookie(客户端会话跟踪技术)
- 数据存储在客户端浏览器当中
- Session(服务端会话跟踪技术)
- 数据存储在储在服务端
- JWT令牌技术
方案一 - Cookie
cookie 是客户端会话跟踪技术,它是存储在客户端浏览器的,我们使用 cookie 来跟踪会话,我们就可以在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。
比如第一次请求了登录接口,登录接口执行完成之后,我们就可以设置一个cookie,在 cookie 当中我们就可以来存储用户相关的一些数据信息。比如我可以在 cookie 当中来存储当前登录用户的用户名,用户的ID。
服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。
接下来在服务端我们就可以获取到 cookie 的值。我们可以去判断一下这个 cookie 的值是否存在,如果不存在这个cookie,就说明客户端之前是没有访问登录接口的;如果存在 cookie 的值,就说明客户端之前已经登录完成了。这样我们就可以基于 cookie 在同一次会话的不同请求之间来共享数据。
我刚才在介绍流程的时候,用了 3 个自动:
- 服务器会 自动 的将 cookie 响应给浏览器。
- 浏览器接收到响应回来的数据之后,会 自动 的将 cookie 存储在浏览器本地。
- 在后续的请求当中,浏览器会 自动 的将 cookie 携带到服务器端。
为什么这一切都是自动化进行的?
是因为 cookie 它是 HTP 协议当中所支持的技术,而各大浏览器厂商都支持了这一标准。在 HTTP 协议官方给我们提供了一个响应头和请求头:
- 响应头 Set-Cookie :设置Cookie数据的
- 请求头Cookie:携带Cookie数据的
优缺点
- 优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)
- 缺点:
- 移动端APP(Android、IOS)中无法使用Cookie
- 不安全,用户可以自己禁用Cookie
- Cookie不能跨域
- 现在的项目,大部分都是前后端分离的,前后端最终也会分开部署,前端部署在服务器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080
- 我们打开浏览器直接访问前端工程,访问url:http://192.168.150.200/login.html
- 然后在该页面发起请求到服务端,而服务端所在地址不再是localhost,而是服务器的IP地址192.168.150.100,假设访问接口地址为:http://192.168.150.100:8080/login
- 那此时就存在跨域操作了,因为我们是在 http://192.168.150.200/login.html 这个页面上访问了http://192.168.150.100:8080/login 接口
- 此时如果服务器设置了一个Cookie,这个Cookie是不能使用的,因为Cookie无法跨域
区分跨域的维度:
- 协议
- IP/协议
- 端口
只要上述的三个维度有任何一个维度不同,那就是跨域操作
举例:
http://192.168.150.200/login.html ----------> https://192.168.150.200/login [协议不同,跨域]
http://192.168.150.200/login.html ----------> http://192.168.150.100/login [IP不同,跨域]
http://192.168.150.200/login.html ----------> http://192.168.150.200:8080/login [端口不同,跨域]
http://192.168.150.200/login.html ----------> http://192.168.150.200/login [不跨域]
方案二 - Session
前面介绍的时候,我们提到Session,它是服务器端会话跟踪技术,所以它是存储在服务器端的。而 Session 的底层其实就是基于我们刚才所介绍的 Cookie 来实现的。
设置Httpsession参数
常用方法
- 设置唯一标识:
- 获取唯一标识:
- 删除唯一标识:
方案三 - JWT(JSON Web Token)令牌技术
这里我们所提到的令牌,其实它就是一个用户身份的标识,看似很高大上,很神秘,其实本质就是一个字符串
是一种用于在网络应用间传递信息的编码规范。在会话校验中,JWT可以用于进行身份验证和权限控制。
JWT校验一般包括以下步骤:
- 提供 JWT:用户在登录或进行身份验证成功后,服务器会生成一个包含用户身份信息和特定于应用程序的声明的 JWT,并将其发送给客户端。
- 客户端存储 JWT:客户端通常会将接收到的 JWT 存储在本地,例如在浏览器的本地存储(localStorage)或会话存储(sessionStorage)中。
- 发送请求:客户端在每次与服务器进行请求时,都会将 JWT 作为请求的一部分发送给服务器,通常使用 HTTP 请求的头部以 "Authorization: Bearer [JWT]" 的格式进行传递。
- 服务器校验 JWT:服务器在接收到请求后,会从请求头中提取 JWT,并进行一系列校验,包括以下几个方面:
-
- 验证 JWT 签名的有效性:服务器使用特定的密钥(或公钥)对 JWT 进行解码和验证签名,确保 JWT 没有被篡改。
- 验证 JWT 的有效期:服务器会检查 JWT 中的时间戳,确保 JWT 还在有效期内。
- 验证 JWT 的声明和权限:服务器会根据应用程序的需求,验证 JWT 中的声明和权限,确保用户有权访问请求的资源或执行特定操作。
- 返回响应:如果 JWT 校验通过,服务器会处理请求并返回相应的响应。如果 JWT 校验失败,服务器通常会返回相应的错误状态码(如 401 Unauthorized)。
JWT校验提供了一种无状态的会话验证机制,通过在客户端存储 JWT,服务器可以根据 JWT 进行身份验证和权限控制,而无需在服务器端存储会话信息,从而简化了服务器的复杂性和开销。
lombok插件
简化开发
自动创建get set toString
Lombok 支持自动生成 getter、setter、toString等方法,减少了重复性的开发工作。
日志注解 @Slf4j
日志级别:TRACE
- TRACE(追踪级别)
用于跟踪代码执行的详细信息。通常用于调试阶段,用于输出一些详细的调试信息,对性能影响较大;
- DEBUG(调试级别)
用于输出调试信息,帮助开发人员诊断问题。通常用于开发和测试阶段,例如输出方法的输入参数和返回值;
- INFO(默认的级别)
普通信息级别;用于输出程序的一般运行信息。通常用于生产环境,记录程序运行的关键信息,如系统启动、关键操作完成等;
- WARN (警告级别)
用于输出警告信息。通常用于发现一些可能的问题或不正常的情况,但不会影响程序的正常运行;
- ERROR(错误级别)
用于输出错误信息。通常用于记录程序的错误信息、异常信息,表示程序出现了严重的问题,无法正常运行
注意:在 @Slf4j 注解中,应根据不同的应用场景和需求,可以选择适当的日志级别。
在开发和测试阶段,可以将日志级别设置为DEBUG,以便获取详细的调试信息。
而在生产环境中,一般将日志级别设置为INFO或更高级别。
# 设置日志级别为WARN
logging.level.root=WARN
# 将cn.tedu包及其包中的所有类的日志级别设置为DEBUG级别
logging.level.cn.tedu=DEBUG
@Slf4j 注解优点
使用 @Slf4j 注解相比 System.out.println("xxx") 的好处
- 更加高效
使用 @Slf4j 注解输出日志,可以避免产生大量的无用日志信息,减少对内存和磁盘等资源的消耗。
而使用 System.out.println() 会产生大量冗余的输出信息,不仅对调试造成困扰,而且会对应用程序的性能产生影响。
- 日志级别更加明确
使用 @Slf4j 注解,可以根据需要输出不同级别的日志,例如,警告、错误等。通过灵活控制日志输出的级别,可以及时发现并解决问题。
而使用 System.out.println() 输出的日志级别是不可控的,并且无法选择性地过滤日志。
Knife4j
Knife4j是基于SpringBoot构建的一个文档生成工具,它可以让开发者为我们的应用生成API文档;
目的是可以更加方便的基于API文档进行测试。
生成的文档还可以导出,然后给到前端开发团队,前端开发团队可以基于API接口写具体的调用。
Knife4j的优点
- Knife4j 功能强大,易于操作。
- Knife4j 的UI界面非常美观,使用流畅。
- Knife4j 可以高度定制化,让其符合你的项目需求。
pom.xml添加依赖
在你的SpringBoot项目的pom.xml文件中,添加如下依赖:
<!--添加Knife4j依赖-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>
<dependency>
常用注解应用分析
@Api注解
添加在控制器类上的注解;
通过此注解的tags属性可以修改原本显示控制器类名称的位置的文本;
通常建议在配置的tags属性值上添加序号,例如:“01. 用户模块”、“02. 微博模块”,则框架会根据值进行排序。
- 参数说明 tags:配置模块名称
// 1. UserController
@Api(tags = "01.用户管理模块")
public class UserController {...}
@ApiOperation注解
添加在控制器类中处理请求的方法上的注解;
用于配置此方法处理的请求在API文档中显示的文本。
- 参数说明 value:配置业务名称
@ApiOperation(value = "注册功能")
@PostMapping("reg")
public int reg(@RequestBody UserRegDTO userRegDTO){...}
@ApiIgnore注解
添加在处理请求的方法的参数上;
用于表示API文档框架应该忽略此参数。
// 参数中添加@ApiIgnore注解
public int insert(@RequestBody WeiboDTO weiboDTO, @ApiIgnore HttpSession session){...}
@ApiModelProperty注解
是添加在POJO类的属性上的注解;
用于对请求参数或响应结果中的某个属性进行说明;
主要通过其value属性配置描述文本,并可通过example属性配置示例值。
- 参数说明
- value属性:配置参数名称
- required属性:配置是否必须提交此请求参数
- example属性:配置示例值
注意:如果配置了 required=true,只是一种显示效果,Knife4j框架并不具备检查功能
@Data
public class UserRegDTO {
@ApiModelProperty(value = "用户名", required = true, example = "赵丽颖")
private String username;
@ApiModelProperty(value = "密码", required = true)
private String password;
@ApiModelProperty(value = "昵称", required = true)
private String nickname;
}
@ApiImplicitParam注解
添加在控制器类中处理请求的方法上的注解;
主要用于配置非封装的参数
- 参数说明
- name:指定参数名称(参数变量名)
- value:配置参数名称
- dataType:配置数据类型
- required:配置是否必须提交此请求参数
- example:配置参数的示例值
注意:一旦使用此注解,各个参数的数据类型默认都会显示String,可以通过dataType指定数据类型
@ApiImplicitParam(name = "id", value = "微博", required=true, dataType = "int")
public WeiboDetailVO selectById(int id){...}
@ApiImplicitParams注解
添加在控制器类中处理请求的方法上的注解;
当方法有多个非封装的参数时,在方法上添加此注解,并在注解内部通过@ApiImplicitParam数组配置多个参数。
- 代码示例
此处以微博详情功能为例
/**微博详情页功能*/
@GetMapping("selectById")
@ApiOperation(value = "微博详情功能")
@ApiImplicitParams(value = {
@ApiImplicitParam(name = "id", value = "微博", required=true, dataType = "int"),
@ApiImplicitParam(name = "username", value = "用户名", required=true)
})
// 额外增加username参数,仅仅用于测试
public WeiboDetailVO selectById(int id, String username){
return weiboMapper.selectById(id);
}
统一响应结果的处理
为什么需要统一响应结果处理
在实际开发中,我们往往需要在多个控制器方法中返回相同的响应结构;
例如,统一返回接口调用成功的状态码、提示信息、以及请求结果数据。但是,如果对于每个接口都单独进行处理的话,不仅逻辑复杂,而且容易出现疏漏,进而增加前端调用的难度。
所以为了更好定义服务端返回值的格式,统一客户端对服务端响应结果的处理,我们需要将服务端返回到客户端的数据再次进行封装。
SpringMVC异常处理机制
异常分类
异常处理器我们已经能够使用了,那么在咱们的项目中该如何来处理异常呢?
因为异常的种类有很多,如果每一个异常都对应一个@ExceptionHandler,那得写多少个方法来处理各自的异常,所以我们在处理异常之前,需要对异常进行一个分类:
- 业务异常(BusinessException)
- 规范的用户行为产生的异常
- 用户在页面输入内容的时候未按照指定格式进行数据填写,如在年龄框输入的是字符串
- 不规范的用户行为操作产生的异常
- 如用户故意传递错误数据
- 规范的用户行为产生的异常
- 系统异常(SystemException)
- 项目运行过程中可预计但无法避免的异常
- 比如数据库或服务器宕机
- 项目运行过程中可预计但无法避免的异常
- 其他异常(Exception)
- 编程人员未预期到的异常,如:用到的文件不存在
将异常分类以后,针对不同类型的异常,要提供具体的解决方案
@RestControllerAdvice
名称 | @RestControllerAdvice |
类型 | ==类注解== |
位置 | Rest风格开发的控制器增强类定义上方 |
作用 | 为Rest风格开发的控制器类做增强 |
说明:此注解自带@ResponseBody注解与@Component注解,具备对应的功能
@ExceptionHandler
名称 | @ExceptionHandler |
类型 | ==方法注解== |
位置 | 专用于异常处理的控制器方法上方 |
作用 | 设置指定异常的处理方案,功能等同于控制器方法, 出现异常后终止原始控制器执行,并转入当前方法执行 |
说明:此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常
异常解决方案
- 业务异常(BusinessException)
- 发送对应消息传递给用户,提醒规范操作
- 大家常见的就是提示用户名已存在或密码格式不正确等
- 发送对应消息传递给用户,提醒规范操作
- 系统异常(SystemException)
- 发送固定消息传递给用户,安抚用户
- 系统繁忙,请稍后再试
- 系统正在维护升级,请稍后再试
- 系统出问题,请联系系统管理员等
- 发送特定消息给运维人员,提醒维护
- 可以发送短信、邮箱或者是公司内部通信软件
- 记录日志
- 发消息和记录日志对用户来说是不可见的,属于后台程序
- 发送固定消息传递给用户,安抚用户
- 其他异常(Exception)
- 发送固定消息传递给用户,安抚用户
- 发送特定消息给编程人员,提醒维护(纳入预期范围内)
- 一般是程序没有考虑全,比如未做非空校验等
- 记录日志
全局异常处理
当某个Controller方法中出现了异常,系统底层就会查找有没有定义全局异常处理对象。 这个全局异常处理对象中有没有定义对应的异常处理方法,假如有就调用此方法处理异常。假如没有,会查找对应异常的父类异常处理方法。
1.1 什么是全局异常处理器
全局异常处理器是Spring MVC框架中的一种异常处理机制,用于统一 处理由控制器抛出的异常。
全局异常处理器可以帮助我们捕获和处理控制器中的异常,并且可以根据不同的异常类型进行不同的处理操作,从而保障应用的健壮性和稳定性。
当然,Spring MVC中有内置的异常处理对象,但是呈现的结果对于用户端不友好,所以实际项目我们一般会使用全局异常处理器处理异常。
1.2 全局异常处理器的配置
Spring MVC中的全局异常处理器可以通过以下方式进行配置:
- 创建 exception.GlobalExceptionHandler类,并添加异常处理方法;使用 @ControllerAdvice 注解 或者 @RestControllerAdvice 注解标注该类;
- 在异常处理方法上添加 @ExceptionHandler 注解,用于指定控制器中需要处理的异常类型。
1.3使用流程
1)创建全局异常处理器类
工程目录下创建 exception.GlobalExceptionHandler
- @ControllerAdvice注解
定义全局异常处理器,处理Controller中抛出的异常。
- @RestControllerAdvice注解
复合注解,是@ControllerAdvice注解和@ResponseBody注解的组合;
用于捕获Controller中抛出的异常并对异常进行统一的处理,还可以对返回的数据进行处理。
2)创建异常处理方法
在异常处理方法上添加 @ExceptionHandler 注解
- @ExceptionHandler 注解
用于捕获Controller处理请求时抛出的异常,并进行统一的处理。
关于Throwable
在开发实践中,通常会添加一个处理Throwable的方法,它将可以处理所有类型的异常,则不会再出现500错误!
GlobalExceptionHandler中添加处理 Throwable 的方法
Spring Validation数据校验(目前主流)
关于Spring Validation
在实际项目我们需要对客户端传递到服务端的参数进行校验,用于判定请求参数的合法性,假如请求参数不合法则不可以再去执行后续的业务了。那如何校验呢?
第一种方式是我们在控制层方法中每次都自己进行参数有效值的判断,不合法可以抛出异常,但是工作量和代码复杂度会比较高;
第二种方式就是采用市场上主流的 Spring Validation 框架去实现校验,所以 Spring Validation 框架的主要作用是 检查参数的基本有效性。
数据校验
- 数据校验谁来做?
客户端和服务器端都要做。
- 服务端怎么校验?
- 根据对数据合法性的要求,自己手动编写数据校验的代码;
- 使用现成的验证框架 Validation
POJO类参数校验
流程
- 第1步:参数前添加@Validated 注解;
- 第2步:添加参数校验注解@NotNull ;
- 第3步: 定义对应的异常处理方法, 处理此异常 MethodArgumentNotValidException ;
- 自定义枚举状态码 VALIDATED_ERROR(3002, "参数校验失败");
- 统一响应结果的返回
@ExceptionHandler
public JsonResult doHandleXxxx(MethodArgumentNotValidException ex){
String message = ex.getFieldError().getDefaultMessage();
return new JsonResult(StatusCode.VALIDATED_ERROR, message);
}
- 第4步:重启工程,在Knife4j 中进行测试.
常用注解
- @Validated
可以添加在类上,也可以添加在参数上,参数验证注解。
- @NotNull
要求不能为null
- @NotEmpty
要求不能为空字符串,同时不能为null
- @NotBlank
要求不能为空白串,同时不能为空字符串,也不能为null
- @Size
作用于字符串类型;
限定字符串长度范围:@Size(min=x, max=x, message=x)
- @Range
作用在数值类型;
限定数值类型的范围:@Range(min=x, max=x, message=x)
- @Pattern
使用正则表达式进行验证:@Pattern(regexp='正则表达式', message='提示消息')
非POJO参数校验
在 Spring Validation 中,除了对 POJO(Plain Old Java Object)进行校验的功能外,还支持对非 POJO 进行校验,比如 String、Integer、Double 等类型的参数。
使用流程
- 在当前方法所在的类上添加 @Validated 注解
- 在参数上添加对应的检查注解
拦截器
拦截器定义
拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行, 他是是 SpringMVC 提供的一个组件,它允许我们在请求到达处理方法之前或之后,对请求拦截并进行预处理或后处理。拦截器可以帮助我们实现许多功能,如用户权限验证、记录日志、处理异常等。
特性
- 拦截:将请求拦住,执行后续操作;
- 过滤:对拦截到的请求进行统一处理;
- 放行:处理完请求后,如果满足条件则放行,让请求继续访问下一步的资源。
作用
- 在指定的方法调用前后执行预先设定的代码
- 阻止原始方法的执行
- 总结:拦截器就是用来做增强
应用场景
- 权限验证:如登录检测,进入处理器检测检测是否登录,如果没有直接返回到登录页面。
- 日志记录:记录请求信息的日志,以便进行信息监控、信息统计等。
- 性能监控:有时系统在某段时间莫名其妙负载很高,可通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。
执行顺序
(1)浏览器发送一个请求会先到Tomcat的web服务器
(2)Tomcat服务器接收到请求以后,会去判断请求的是静态资源还是动态资源
(3)如果是静态资源,会直接到Tomcat的项目部署目录下去直接访问
(4)如果是动态资源,就需要交给项目的后台代码进行处理
(5)在找到具体的方法之前,我们可以去配置过滤器(可以配置多个),按照顺序进行执行
(6)然后进入到到中央处理器(SpringMVC中的内容),SpringMVC会根据配置的规则进行拦截
(7)如果满足规则,则进行处理,找到其对应的controller类中的方法进行执行,完成后返回结果
(8)如果不满足规则,则不进行处理
(9)这个时候,如果我们需要在每个Controller方法执行的前后添加业务,具体该如何来实现?
这个就是拦截器要做的事。
拦截器和过滤器之间的区别是什么?
- 归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术
- 拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强
设置拦截器方法
@Override
//原始方法调用前执行的内容
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...");
return true;
}
@Override
//原始方法调用后执行的内容
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
}
@Override
//原始方法调用完成后执行的内容
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
拦截器中的preHandler方法,如果返回true,则代表放行,会执行原始Controller类中要请求的方法,如果返回false,则代表拦截,后面的就不会再执行了
拦截器执行的顺序是和配置顺序有关。就和前面所提到的运维人员进入机房的案例,先进后出。
- 当配置多个拦截器时,形成拦截器链
- 拦截器链的运行顺序参照拦截器添加顺序为准
- 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
- 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作
preHandle:与配置顺序相同,必定运行
postHandle:与配置顺序相反,可能不运行
afterCompletion:与配置顺序相反,可能不运行。
这个顺序不太好记,最终只需要把握住一个原则即可:==以最终的运行结果为准==
设置拦截器配置类
/**
* 注册浏览器添加规则
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
/** 拦截所有请求 */
//registry.addInterceptor(new MyIntercepter());
/** 拦截指定请求 */
registry.addInterceptor(new MyInterceptor())
.addPathPatterns("/users/login");
/** 拦截指定路径下所有请求 */
//registry.addInterceptor(new MyInterceptor())
// .addPathPatterns("/users/**");
/** 排除指定的请求 [拦截指定路径下除了指定请求之外的所有请求] */
// registry.addInterceptor(new MyInterceptor())
// .addPathPatterns("/users/**")
// .excludePathPatterns("login");
}
}