JavaEE进阶 - Spring MVC 程序开发 - 细节狂魔

x33g5p2x  于2022-08-17 转载在 Java  
字(18.8k)|赞(0)|评价(0)|浏览(344)

什么是 Spring MVC?

官⽅对于 Spring MVC 的描述是这样的:
Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, “Spring Web MVC,” comes from the name of its source module (spring-webmvc), but it is more commonly known as “Spring MVC”.

翻译为中⽂:

Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从⼀开始就包含在 Spring 框架中。它的正式名称“Spring Web MVC”来⾃其源模块的名称(Spring-webmvc),但它通常被称为“Spring MVC”。

题外话:
1、因为 Spring Web MVC 是基于 Servlet API,所以 Servlet 是 Spring MVC 的 “父亲”。
因此,Servlet 那一套编程方法,在 Spring MVC 中,也是可以使用的!!!
但是,一般不推荐使用 servlet 的编程方式。
因为,Spring MVC 更简单!

2、Spring Web MVC,从⼀开始就包含在 Spring 框架中。
即:Spring 是一个很大体系(框架),Spring MVC 只是属于 Spring 体系中的一个 Web 模块。
这也是为什么在学习 Spring 的时候,我们都都是通过 main 方法去访问bean方法的原因。因为我们没有引入 web 模块,因此想要通过 浏览器输入 URL 来访问 方法,是不行的。

从上述定义我们可以得出两个关键信息:
1、 Spring MVC 是⼀个 Web 框架。(基于 Servlet 实现的)

web框架,就是基于 HTTP 协议的。
通俗来说:当用户在浏览器上输入一个 URL 地址之后,URL 地址 能够和 程序映射起来。
能够让用户的请求 被 程序接受到。
并且,经过程序的处理,能把结果返回给前端(浏览器)。
这一次基于 HTTP 的交互,就可以认为是一个 web 项目。
Spring MVC 也是一个 web 框架(spring 的 一个 web 模块)。
那么,一个 web 项目,只做三件事:
1、实现用户请求 到 程序 的 链接。(用户请求 可以 被 程序接收到,也就是上面讲的)
2、在前后端建立联系的情况下,拿到用户请求的参数。
3、拿到参数之后,进行业务处理,并将其结果返回给前端。

这也是 本文讲解 Spring MVC 的重点。

如果有看过我前面博客的读者,回想一下:我在将 spring 的时候,并没有涉及 前端。
只有在讲解 Spring Boot 的时候,涉及到 web内容。
我们不是引入了一个 spring web 嘛,那个就是 前端框架。
有了这个 框架的支持,那么,项目就是一个 Spring MVC 的项目。
也就是说:其实前面创建的 Spring Boot 项目 是 一个 Spring MVC 项目。

2、 Spring MVC 是基于 Servlet API 构建的。
然⽽要真正的理解什么是 Spring MVC?我们⾸先要搞清楚什么是 MVC?

MVC 定义

MVC 是 Model View Controller (模型视图控制器)的缩写,它是软件⼯程中的⼀种软件架构模式,它把软件系统分为模型、视图和控制器三个基本部分。

Model(模型) 是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。

View(视图) 是应用程序中处理数据显示的部分,通常视图是依据模型数据创建的。

Controller(控制器) 是应⽤程序中处理⽤户交互的部分。通常控制器负责从视图读取数据,控制⽤户输⼊,并向模型发送数据。

下面我们来进一步分析 四者之间的关系。

通过这个 MVC 模型,就可以让 用户 与 程序 之间,是可以进行完美的交互的 。
间接来说:(忽略细节)
前端发送的请求数据,会先给 controller。
controller 验证完数据之后,就会将其给 Model。
Model 在和 数据库交互之后,将其得到的结果返回给 controller。
此时 controller 收到的数据,还不能直接返回给前端。
controller 需要将数据 交给 服务器的视图(View),进行处理和渲染。
最终,将渲染得到的结果,返回给前端。
此时,用户就看到的源码 就是 html标签 的内容。(浏览器的开发者工具可以查看)
看到的页面,就是浏览器对 HTML标签内容的解析。

此时,我们基于 MVC 这三个部分,就可以实现一个 web 项目了。

web 项目:用户通过浏览器输入一个 URL,发送一个 HTTP请求,能够与 服务器 进行一次交互 和 响应。

MVC 和 Spring MVC 的关系

两者之间的关系,就像是 IOC(控制反转) 和 DI(依赖注入)之间的关系。
IOC 是思想,DI 是具体实现。
MVC 和 Spring MVC 的关系,也是如此。
MVC 是一种 设计思想(策略)。
Spring MVC 是 具体实现的框架。

总结

Spring MVC 是⼀个实现了 MVC 模式,并继承了 Servlet API 的 Web 框架。既然是 Web 框架,那么当⽤户在浏览器中输⼊了 url 之后,我们的 Spring MVC 项 ⽬就可以感知到⽤户的请求。

同时 Spring MVC 又是 Spring 框架中的 一个 WEB 模块,它是随着 Spring 的 诞生 而 存在的一个框架。
Spring 和 Spring MVC 诞生的历史是比较久远的,在它们之后才有了 Spring Boot。
Spring 大概是在 2002 年 上市的,而 Spring Boot,大概比 Spring 晚 十年 诞生的。
Spring Boot 是在 2.0 版本的时候,占据了中国市场。

为什么要学 Spring MVC?

现在绝大部分的 Java 项目都是基于 Spring(或 Spring Boot)的,而 Spring 的核心就是 Spring MVC。

也就是说 Spring MVC 是 Spring 框架的核心模块,而 Spring Boot 是 Spring 的“脚手架”,因此我们可以推断出,现在市面上绝大部分的 Java 项目 都是 Spring MVC 项目,这是我们要学 Spring MVC 的原因。
现在的项目,有两种:1、PC,电脑上运行的项目,2、移动端,手机上的运行的项目。

PC 项目,又可以分为2种:1、网页版项目,2、客户端的项目
但是不管你 PC 项目 属于哪一种,只要你需要 数据持久化,就是用到 Spring MVC。
而那种小工具(对本地文件进行查找和管理的工具),是用不到 数据库的,它肯定是不会使用到 Java 程序。这种小项目(个人项目 / 小工具),我们大概率是不从事这方面的开发。
因为 能要你的公司,它肯定是能赚到钱的。
赚不到钱,人家招你干嘛?赔钱嘛。。。
工作之后,不管是 做 哪一种 PC 开发,都是要连接数据库的。
只要是用到了 数据库的,那一定是前后端分离的。
所以说:我们提供的就是 后端程序。
那后端程序是基于什么协议呢?》》》 HTTP协议
所以说,HTTP协议 需要使用什么框架?
一定 web(Spring MVC) 框架嘛!

手机项目也是一样,拿着手机去刷抖音,看头条。。。
这些内容都是来自于数据库的。
数据库,这个东西实在我们手机上存的吗?很显然,不现实!
数据库,这东西一定是存储在服务器端的。
而服务器端是水提供的?
是 手机开发工程师,他自己做后端开发吗?
肯定不做啊!手机开发工程师 做的是 PC时代的前端(做网页的)。
很早以前,PC 的前端是做网页的。
现在,PC 的前端是 做 APP的,就是说现在的网页,已经不是一个简简单单 HTML 页面,而是可以视为是一个 APP,具有很多复杂的功能。
但是,后端一定是调用 服务接口,而 服务接口 大部分调用额是 HTTP协议。HTTP 协议 就需要用到 Spring MVC 框架,因为它底层就是针对 Servlet 封装的。
因此 它 一定是一个 web 项目。

由此可知,我们到公司之后,大部分项目,大约 99% 都是 Spring MVC 项目。
那么,你觉得 Spring MVC 重不重要呢? 非常重要!!!!

这里简洁的说一下:APP 项目也可以分为两种。
1、基于 iOS 开发,最早使用 ObjectC,但是写法极其恶心。
所以,苹果公司,在后面又推出了 自己的语言 Swift。

2、安卓开发,最早是基于 Java 开发,但是现在 谷歌 被 Oracle 给搞了。
所以,自己 和 idea 的公司合作,做了一门新语言 Dart。
但是!底层也还是 基于 JVM 的。
与 java 编译生成的 字节码文件,可以视为是一样的。
而且,Dart 与 Java 是非常相似的!
可以说:这门语言就是为了 堵住 Oracle 的 “嘴”。

但是呢,现在APP发展的趋势,趋近两者的混合开发。
就是 苹果 和 安卓 手机,都能安装这个 APP。
关于 混合开发,已经是陈年旧事了。
最早推出的 是 Facebook 的 React Native。
也是属于 门面模式的一种实现。
你可以写 React Native 自己的语法,在项目打包的时候,它有一个选择平台的功能。
根据 我们写的代码,去生成 IOS 的 代码,或者是安卓 的 代码。
不同的平台,我给你生成不同的代码,就行了。

因此,移动端开发组 是很头疼的,做一个 APP,需要分2组(IOS,安卓)。
那么问题来了:一个组里是有一个 leader 的。
那么,对于 这个 leader 来说:他需要对这两种开发方式 精通!!
这对leader的压力,就非常大了!!!!

回过头,我们在创建 Spring Boot 项⽬时,我们勾选的 Spring Web 框架其实就是 Spring MVC 框架,如下图所示:

从宏观来说:
即使 Spring Boot 添加 Spring Web 框架,也可以说它是一个 Spring 项目,或者是 Spring Web 和 Spring MVC 项目,都可以!不算错!
从微观来说:
Spring MVC 项目,是基于 Spring Boot 框架 所实现的。

简单来说,咱们之所以要学习 Spring MVC 是因为它是⼀切项⽬的基础,我们以后创建的所有 Spring、Spring Boot 项⽬基本都是基于 Spring MVC 的。

Spring MVC 项目的创建

Spring MVC 项⽬创建和 Spring Boot 创建项⽬相同(Spring MVC 使⽤ Spring Boot 的⽅式创建),在创建的时候选择 Spring Web 就相当于创建了 Spring MVC 的项⽬

删除几个不常用文件。(可不删,根据实际情况决定)

学习 Spring MVC 的 三个目标

学习 Spring MVC 我们只需要掌握以下 3 个功能:
1、 连接的功能:将⽤户(浏览器)和 Java 程序连接起来,也就是访问⼀个地址能够调⽤到我们的Spring 程序。
2、 获取参数的功能:⽤户访问的时候会带⼀些参数,在程序中要想办法获取到参数。
3、输出数据的功能:执⾏了业务逻辑之后,要把程序执⾏的结果返回给⽤户。
对于 Spring MVC 来说,掌握了以上 3 个功能就相当于掌握了 Spring MVC

Spring MVC项目的连接(用户 和 程序 的 映射)

方法1:@RequestMapping(“/xx”)

在 Spring MVC 中使⽤ @RequestMapping 来实现 URL 路由映射.
这个我在讲 Spring Boot 知识的时候,已经用过了。
这里,我们再来稳固一下。

这里徐亚提醒的是: 一级路径,通常是为了辅助 二级路径。
因为一个项目,需要访问的东西有很多。
不可能全写在一个类里面,而且类也很多
为了避免混淆,所以通过这样的方式,来锁定要访问的类中方法。

在这里,我再 给你们加强一下记忆。

@ResponseBody注解
1、放在方法上,表示该方法返回的数据,只是一个普通的数据,而非一个静态页面。
2、放在类上,表示该类中所有的方法返回的数据,都是一个普通的数据,而非静态页面。
3、如果未加上这个注解,在我们访问方法的时候,默认就会去寻找一个名称叫做 Hello World!的页面,它发现找不到之后,就会报错。

问题来了:为什么默认情况下,返回的是一个静态页面呢?

请思考一个问题:
Spring MVC 是随着 Spring 诞生,而创建的。
Spring 是 2002 年 诞生的,也就是说 2003 年就有了 Spring MVC 了。
20年前,我们才刚出生不久,或者没出生。
20年前,前后端分离吗?
没有!!!
09 ~ 10 年的时候,才是前后端分离,逐渐普及的时间。
也就是在那个时间,有些公司使用了 前后端分离,有些公司没有 前后端分离。
直到 14 年 的时候,形式就一边倒了,全部支持前后端分离了。

这就原因所在,Spring MVC 在诞生之初,还没有前后端分离的概念。
作为一个 Java 程序员,既要写 后端代码,还要去写前端的页面。
那个时候,模板用的非常多!!
那时候是没有前后端工程师的,只要有一个 Java 工程师,就够了。
最多,在配置一个美工(设计)就OK了。
所以,那个时候,不用多想,我肯定是返回的一个页面。
为什么默认返回一个静态页面,就是一个历史遗留问题。
到今天,格局全然不同!
前后端已经分离,再去返回一个静态页面,肯定是不行的,
但是!又不能舍弃 这个设置。
因为,前面开发的项目,你不能说 不要 就 不要啊!!
那都是钱砸出来的。
因此,保留这个机制,是为了以前的代码还可以用。
而 添加 @ResponseBody,即实现了前后端分离。又能保存以前返回页面的机制。
这样就实现 项目 与 时代的兼容!

虽然不用返回宇哥界面了,但还是要写两个注解,稍微有点麻烦。

我们还要更简单的写法、是一个组合注解。
现在暂时放放,后面再讲。

先继续深入了解 @RequestMapping。

@RequestMapping 是 post 还是 get 请求?

@RequestMapping 默认是 get ⽅式的请求。
这一点其实在 浏览器通过 URL 访问方法的时候,就已经证明了这一点。
通过 直接向浏览器的地址栏输入URL,默认生成 get 请求。
不相信,可以使用postman 来验证。

那么,@RequestMapping 支持 POST 方法吗?

答案:@RequestMapping,不仅支持 GET 方法,还支持 POST 方法的!

总结 && 拓展

@RequestMapping 特征:
1、既能修饰类(可选),也能修饰方法。
2、默认情况下,@RequestMapping 既支持POST 方法,也支持 GET 请求方法。
但是问题来了:如果公司制定只能使用 POST 方法,不能使用 GET 方法。
这种公司是存在的,目的就是为了方便管理 。

像 @RequestMapping 这种 两种都支持,就还好。
但是,有些接口只支持 一种 方法 的请求,可能是 POST,也可能是 GET,这都说不准。
主要看实际情况。
但是,这就会造成一个项目中,既有支持 get,又有支持post 的方法。
这就会导致一个问题:
每个人对代码的理解是不一样的。这就会增加程序员(前后端都包括)的负担。
因为 在看每个接口在进行调用的时候,他要去看 说明文档,然后,才能确定你当前这个接口是支持 GET,还是POST的 。
一些公司指定使用 一种 方法,就是为了避免这种情况。
程序不用去思考使用那种方法来写代码,直接按照公司的要求来。

那么,我的问题就是:能不能让 @RequestMapping 只支持一种方法。

答案是可以的!
通过设置属性值,就可以做到!!!
这个属性叫做 method

需要注意的是:@RequestMapping 直接给路由参数,默认就是给 value 属性赋值,前面只是我偷油了。。

还有一个细节:
类上的 RequestMapping 的路由参数,可以省略 / 的。
(虽然可以不加,但是还是建议加,2个字规范。)

方法2 和 方法 3:@GetMapping 和 PostMapping

你想的没有错,就是@RequestMapping 分解出来的 两个注解。
意思很直白,你们应该是能懂的。
@GetMapping :只支持 Get 方法

@PostMapping:只支持 Post 方法。

获取用户请求参数

传递单个参数

在 Spring MVC 中可以直接⽤⽅法中的参数来实现传参.
在传递单个参数的时候,最常传递的参数是 id。
因为 id 通常是通过 primary key’(主键)来设置的,具有唯一性。
有助于我们获取到唯一的数据。
这里我们不去访问数据,直接通过访问方法来演示一下就行,
主要是了解用法

上述这种方式,只要 获取的参数名称,与前端传输的参数名称一致,就能获取到对应的参数值。

如果 前端参数名称 和 映射方法的参数名称,不一样。
不但获取不到,还会报错!

图中说到 映射方法的参数类型,不能使用基本数据类型。
虽然正常情况下,代码运行没问题,和 使用 包装类 的效果一样。
但是像遇到上述这种特殊问题(前端传输参数名称 和 映射方法名称 不相符),代码就会报错。

获取多个参数 / 表单参数传递(非对象)

我们简单实现 回显服务。
传递什么参数,就返回什么参数。
方法还是一样的,只要 前端传递的参数名称 和 后端映射方法参数名称相同,就能够获取一个,甚至多个参数。

如果你看这个 URL 有点懵,建议你还是先去看看 我的 HTTP协议的文章,再来看这个。

但是目前这个代码存在很大的问题,就是参数一旦增加。
我们就得手动去添加,还可能漏掉某个参数,
而且,维护的时候,也很难看清楚代码,参数太多了!维护性极低。

那么,我们该怎么去处理呢?
接着往下看!

获取多个参数 / 获取对象

获取对象,那就很简单了!!!
映射方法 只需要 一个 对象参数 就行了。

获取对象参数的重点,还是在 前端传参的时候,参数名称 要与 对象中的属性名称相对应,Spring MVC 就会自动 对 对象的属性 进行赋值。
如果名称不匹配,那么,这个参数传输的毫无意义。

拓展

后端参数重命名(后端参数映射:@RequestParam)

跟给 Bean 对象重命名 是一样的意思。
就是给 参数 改个名字。
某些特殊的情况下,前端传递的参数 key 和我们后端接收的 key 可以不⼀致!

⽐如前端传递了⼀个time 给后端,⽽后端⼜是由 createtime 字段来接收的,这样就会出现参数接收不到的情况.
如果出现这种情况,我们就可以使⽤ @RequestParam 来重命名前后端的参数值。

特殊情况,比如:
在将来的某一天,公司晚上突然加班,需要紧急添加一个需求。
更重要的是:产品明天就要上线了。
紧急加班嘛,代码就写的有点“粗”。
前后端,并没沟通好。
假设,做的一个登录功能的扩展
然后,产品是凌晨 2点上线的。
代码也就没有经过严格的测试。
因为 测试可能都下班,都是研发人员 去硬怼的。

第二天用的时候,运维发现了问题,通知 开发 / 测试 人员,某某功能有问题。
比如:无论用户怎么输入,一直提示填写用户名。

最终,排查出的问题:
前端在传输参数的时候,参数名字 叫做 name,而后端参数是叫做 username。
这样就会导致一直拿不到 用户名的值。
这个时候,我们有两种解决方案。
1、让后端的代码适应前端的代码,修改映射方法的参数名称(username >> name)。
但是一个问题也随之而来。
就是啊,这个 username 已经在映射方法中,使用了很多次了。
这一改,方法中的代码全都得动!
而且存在漏改的风险!

2、让前端的代码,适应后端的代码。
让前端传回的参数 name >> username
这也会存在一个问题,像这种做完一个大项目的时候。
前端的人都休假,回到家直接关手机睡觉了。
但是这个bug 又很紧急!
总结:前端失联,后端不想改。

这个时候,就可以使用 后端参数重命名了。

后端参数重命名,就是把前端传过来的参数名称,进行转移赋值。

就是说:前端参数名称不变,还是name;后端接收到之后,将其参数名称的数值,赋值到 后端的 username 参数上。
此时,对于 后端来说,前端的 name,就是 username 了。
因为两者的value 值,都是一样的。

具体的实现方法,还是通过注解的方式来解决。
使用 @RequestParam 注解,就可以搞定了。

这里有一个细节:
如果这个重命名操作的属性,是对象里面的,就不能这么去用!!!

上面的那个 username ,并不是 UserInfo 中的属性,只是一个普通的 字符串变量名.
我都没将 UserInfo 类注入到Spring 中,更别提获取到类中的属性。

虽然也有方法可以处理这种情况,但不是这个注解,是另一个。
(后面在 讲 MyBatis 的时候,会说这个)

但如果映射方法的参数是一个对象,那就没办法了。
这个你就只能和前端交流了,让他配合你的操作。
将 前端参数名称 修改成 和 后端对象中的属性名称 一样。
或者你配合他的代码。。。。
将类中的属性名修改成 和 前端参数一样的名称。

总之,看谁能说服谁。。。总会有一方妥协的。

口才,对于开发人员,也是非常重要的。
开发人员,不仅要和 产品经理 据理力争,还要和前端讲道理,讲配合。
测试,也同样需要沟通。
有时候他认为这是一个bug,然而并不是,或者说不重要。
因为压根就不会出现这种情况,没人会这样去使用软件。
因此这个bug等同于虚设。

@RequestParam 的进一步理解: required属性

表单参数传递(对象)

表单传递对象参数,还是一样的。
接受对象参数时,前端参数该怎么传,就这么传。

上面是一个 get 方法的请求。

下面我们来通过 postman 构造一个 post 方法的 表单请求

总的来说,没有难点。

@RequestBody 接收JSON对象 - 特殊

有的人看到这个,感觉so easy!
既然 JSON 也是一个对象,这跟上面没有区别嘛。
还是一样的,前端直接传输一个 JSON 对象,映射方法这边还是使用一个对象去接收。
下面,我就来验证一下,程序能不能接收一个 JSON 对象的数据?

通过获取普通对象的方法,来获取一个 json 格式的对象参数。显然是不行的!
由此,不难得出结论:
只要不是 json 格式的数据,无论是 GET 还是 POST 方法的请求,都没有问题!
这一点在前面的例子中,就已经充分体现出来了。

也就是说:想要通过接收 json 对象,需要通过其它的方法来实现。
当然还是注解,这个注解就是标题的 @ RequestBody 注解

PS:注意!不要迷糊了!!!!
我们使用的 @RequestBody,不是 @ResponseBody。。。
@ResponseBody: 表示返回的数据是一个非静态的页面。

而 @RequestBody,是告诉 映射方法接下来接收到的数据是一个JSON 格式的数据。
方便对 JSON 数据,进行解析。

结论:
服务器端想要实现 json 数据的接收,就需要使用 @RequestBody 注解。

获取URL中参数@PathVariable - 特殊

这个就厉害了,它不满足于从 请求的正文 和 URL 中的查询字符串 中 获取参数了。
而是从 URL 中获取参数。
注意!我的描述:
@PathVariable注解,不满足从 请求的正文 和 URL 中的查询字符串(参数部分),获取 参数。
而是直接从 URL地址 中 获取参数。

不得不说:这种sao操作,作者也是第一次听闻。

举个例子:DOTA 都知道吧?一个推塔游戏。

比如,
我们现在想要获取到一个 英雄的信息。

一般情况下,我们获取英雄信息的方式,都是同 URL 的 查询字符串部分来获取的。
如:http://data2.uuu9.com?heroname=影魔

而通过 URL地址的方式来获取英雄信息:http://data2.uuu9.com / 66 / 影魔 /
假设 66 是它的 ID。
那么,通过以往的方式去获取这个英雄的信息,可以吗?
很明显,是不可以!!!!
我们前面所学的方法,就没有一个针对 URL地址部分的!

那么问题来了,为什么有正常的方式不用,用这种非主流的方式来获取参数呢?

这个就涉及到 SU(搜索引擎) 的优化。
什么是 SU 的优化呢?

需要注意的是:
使用 第二种形式的URL,也是存在 " 缺点 " 的!

不过 Spring MVC 又提供了一个新的注解 @PathVariable。

其实也很好理解:path variable == 路径变量。

随之而来的是:路径肯定是存在两种形式的。
1、参数
2、路径
那我们该如何区分 那个是参数,那个是路径呢?
路径的写法不变,还是 “/xxx”
而参数的,就是在 / 后面,加上 大括号,将其后面的内容括起来: “/{xxx}”

需要注意的一点:

上传文件 - @RequestPart

现在不是之前那种简单数据了,它可能是一个数据流。
它传过来的是一个 “流”。
比如:
我们在进行用户注册的时候,要上传一个头像(图片)。
也就是说:上传的数据,就是这个图片的字节流。
类似于这种上传文件的形式,我应该怎么去获取呢?
通过 @RequestPart 注解,并将 文件名称 作为 参数放入其中,同时还需要借助 Spring 提供 MultipartFile对象。
MultipartFile,就是专门用来接收文件的。

还没完,其实上述操作,还存在问题。

问题就出在 图片 和 存储路径,都被写死了。

我们再来想象一个场景。

所以,不能这么去写!!!

拓展:不同平台运行的配置文件设置 - 优化存储目录

首先,我们先来补充 关于日志文件的知识补充。

这样做的好处:
哪怕配置文件再多,我们也就只需要修改 主配置文件中的一个参数,就可以调用对应的配置文件了 。

总结:
1、针对各个平台,创建专属的配置文件
2、配置文件的命名规则:application + 分隔符“ - ” + 平台名称(可简写) + .格式【必须这么去写 】
3、在主配置文件中设置 运行的配置文件。【spring.profiles.active】

图片名称不能重复 && 获取原图格式 问题

先把 主配置文件中 的 参数 改成 develop (开发环境)

要不然识别不了,毕竟我们现在是在本地操作。

下面,我们先来解决 图片名称 问题。
第一种:时间戳
通过 时间戳 来命名图片,是存在问题的。
其实 “巧合”,在互联网中是很常见的。
因此,很有可能就有那么几个人,同时,上传图片。
还是会造成 图片名称 的重复,导致 原先的图片被覆盖。
只是发生的概率要小一些。
因此,不可取的。
因为 它不适合用于 并发执行的情况。

第二种:UUID(全局唯一标识符)

UUID就不会从出现 时间戳的情况。
UUID 会使用 你的网卡,随机数,等等,,,各种各样的信息来对 文件 进行命名。
在这种条件下,命名重复的概率,几乎不可能出现。
现在 存储路径的问题 搞定了,图片名称重复的问题 搞定了。
就剩下 获取 原上传图片 的 格式 了。
其实 MultipartFile 对象中,提供了 一个 API(getOriginalFilename)。
getOriginalFilename:获取原始的文件名称
名称里面都有什么?有文件后缀啊!
想办法截取文件后缀,不就可以了嘛。
问题都有了解决方案,接下来就是实施环节。

注意!我们通过取巧的方式,来做的。
其实 uid 来也可以进行操作。
但是 局限性很强!
使用 uid 来 命名 图片,只能使用一次。【唯一性】
但是!可能还有其它图片,都是与用户相关的。
当然,你可以通过添加前缀的方式来实现。

总的来说:还是更推荐使用 UUID 的方式,因为它更通用性更强。

获取Cookie/Session/header

这三个是比较特殊,在有些场合,才会用到,
其中 获取 Session 是比较常见的。
这三项,通常都不是(使用的一方)用户 自动去上传的。
都是由 前端工程师,或者是由系统给我们上传的。

比如说:
Cookie,是浏览器自身去实现的。
它会在每一次请求中,都会把域名底下所有的 cookie 都带上。
这都属于 浏览器自身的行为。
当然,如果前端工程师愿意的话,他也可以 发送 cookie 给后端。

Session,也是一样。
也是前端的浏览器自动发送给后端的。
但是后端是需要获取到 Session 的。
毕竟,它用的比较多。
因为,用户在登录的时候,是需要通过 session 来验证用户信息的。

header,有些场景下,前端还会在 header 里面,设置自定义的header。
然后,传递到后端。
系统的header信息,有些时候,也是需要获取的。
至于为什么要获取 header,因为 header 里面有一个 userAge。
userAge 里面就记录了当前访问用户的操作系统信息,以及他所使用的浏览器信息。
那我就可以每一次请求一个接口的时候,统计用户 的 浏览器 和 操作系统 的信息。
在拿到 用户 浏览器 和 操作系统的信息之后,方便对其进行分析。
根据 分析的结果,做一个排列,看看哪种浏览器的永辉最多,那个操作系统的用户最多。
来进行针对性处理和优化。
就是说:产品能够更好的兼容这些比较热门的操作系统 和 浏览器。

总之,一句话:我可以不用,但是不能不会!
至少我是知道有这么几个东西的。

知识铺垫:获取 Request 和 Response 对象

因为 Spring MVC 就是 基于 Servlet API 来实现的。
也就是说:Servlet 那一套 是 完全适用于 Spring MVC 的。
在 Spring MVC 里面,每一个方法都有2个 “隐藏参数”、
【HttpServletRequest 和 HttpServletResponse】

下面我们就来验证一下功能。

根据结果,更加证明了 Spring MVC 是 支持 Servlet API 的。

获取 Cookie 的方式有两种:
1、基于 Servlet 提供 的 API 来获取 Cookie

虽然,方法是可以的.。
但是这种读取的方式,其实是有点“繁琐的”!
因为需要先获取 请求对象,通过对象提供的方法,来获取 所有的cookie。
而 繁琐,就体现在这一步!
通常情况,我们获取 cookie 数据的时候,不会全拿,只拿取其中的一个。

由此,Spring MVC 提供一个新的注解,来往下看。

2、简洁的获取 Cookie - @CookieValue

这就很方便了!
想要那个 cookie 值,将其 cookieName 作为 @CookieValue 注解 的 参数,就能获取到对应的value值。
当然,你还需要准备一个 “参数” 来接收。

获取 header(获取请求头中的信息)

获取 header 的 方法,也有两种:
1、 基于 Servlet API

2、简洁获取 Header—@RequestHeader

相比于 Servlet API,注解的方式,还是更简单一点的.

存储 和 获取 Session

为什么 Session 多出一个 存储 的 操作呢?
原因很简单!
就跟获取 cookie 的 时候一样!
你都没有 session,我还获取干什么??
所以,才会多出这一步。

更特别的是:关于 存储 操作,只能通过 servlet API。
因为存储操作,是没有办法用参数来表示的。

读取操作,是有两种方法的。
1、servlet API

2、更简洁的获取 session - 使用 注解 :@SessionAttribute

至于为什么需要加上一个 required 属性,并置为 false!
这因为 在使用 注解 获取 session 对象的时候,没有判断句!
即:如果访问 getSession2 方法的时候,没有设置 session 会话,并且内置属性。
此时,它就直接报错!因为它找不到对应 key 值!
这是因为 @SessionAttribute 注解中,有required的属性。
这个前面在讲 @RequestParam 的时候,就讲了。
不会像 servlet 那样,返回一个 空值(页面什么都不显示)。
这个值,必须得有!!!

解决的方法,就是将 required 属性,置为false。
表示这个 key 值,并不是非要找到!
而是找不到就算了!

返回数据给前端

通过上⾯的学习我们知道,默认请求下⽆论是 Spring MVC 或者是 Spring Boot 返回的是视图(xxx.html),⽽现在都是前后端分离的,后端只需要返给给前端数据即可,这个时候我们就需要使⽤@ResponseBody 注解了。
这我们已经在前面演示了。
这里再稳固一下。

当我们加上 @ResponseBody 注解的时候,它认为我们返回的是一个非静态页面数据。
它就会直接显示。

既可以修饰类,也可以修饰方法。
修饰类,表示 类中所有的方法的返回值,都是一个非静态页面的数据。
修饰方法,表示 该方法的返回值,是一个非静态页面的数据。

但是,还是有点麻烦。。
毕竟,还是需要写两个注解。

于是,Spring MVC 又提供了一个新的注解。
结合了 @Controller 和 @ResponseBody,两者之间的功能。
这个新的组合注解叫做 @RestController

简单来说:可以将 @RestController 理解为是 HTTP 的 controller 类注解。

这是目前主流的使用注解。

那么,什么@RestController 能够代替两个注解的工作呢?
这四因为 @RestController 里面实现了这两个 注解。
【按住 Ctrl,左键点击 RestController,就可以进入源码。】

练习

实现计算器功能 - form 表单

可使⽤ postman 传递参数,或使⽤ form 表单的⽅式提交参数。
前端⻚⾯代码:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initialscale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>计算器示例</title>
</head>
<body>
<form action="/calc">
    <h1>计算器</h1>
    数字1:<input name="num1" type="text"><br>
    数字2:<input name="num2" type="text"><br>
    <input type="submit" value=" 点击相加 ">
</form>
</body>
</html>

代码,我已经给你了,下面来跟着我的脚步来看具体的实现步骤。

是不是非常简单?!
我们不用再去 写一些获取参数的操作了,直接当成一个普通方法来处理.
需要强调的是:参数名称 要与前端传输的参数名称 一致,
不一样也行,使用 @RequestParam 来进行重命名操作,记得将 required 属性置为 false。

另外,方法的参数,需要使用包装类。
如果使用 基础数据类型来接收前端参数,如果前端返回一个 null,就会 数值 与 类型不匹配。
还有,因为使用 包装类,所以允许前端返回一个 null 值。
因此,我们需要做一步 校验(验证 数据是否非空),避免造成空指针异常。
所以,我们再稍微修改一下代码。

此时,计算器的功能,才算真正完成了!

这里,我们还可做一个业务优化。
在显示结果的页面,设置一个返回键。

拓展: Spring MVC 的 热部署 设置

在经过上述的学习,我们不难发现:每次修改代码之后,都需要重启 项目。
这就麻烦!这里我们就可以通过设置热部署来解法双手。
在这里需要明白一件事:
为什么需要重启项目,我们的效果才是根据最新的代码来呈现的。

因此,作为一个“合格”的程序员,怎么可以让自己那么累呢!
当然是 配置热部署。让它 “实时更新” target 目录中的内容。

“实时更新”:修改完代码之后,大概在 3 ~ 4 s 之后,才会热部署完成。
即:自动更新了 target 中的内容(实际上,自动生成了一个新的target。将原来的覆盖,所以才会那么耗时!)

热部署的运行原理:
它会检测 当前源代码中,有没有发生改动?
如果改动了,热部署 就会拿到一个 改动事件。
在拿到源代码改动事件之后,它会自动帮我们重启项目。

虽然,还是通过重启项目的方式,来让改动的代码生效。
但是!我们不需要再去手动点击重启了!

自动重启项目消耗的时间,前面也说了。
大概 3~4 s 的样子,热部署就完成了。
此时,你再去访问,看到的效果,就是最新的了。

下面,我们就来配置热部署。

1.添加热部署框架⽀持

在 pom.xml 中添加如下框架引⽤:
PS: 一般我们在创建 Spring Boot(Spring MVC) 项目的时候,就已经添加了这个框架。

如果你忘记加了,或者是一个老项目,也不要紧。
前面在讲 日志文件 添加 lombok 依赖的时候,给你们介绍了一款插件 Edit Starters

可以快捷插入所需的框架支持。
这个我就不多讲,如果你没装,你就先去看这个部分。
看完,你就知道怎么操作了。

2、Settings 开启项⽬⾃动编译

3.开启运⾏中热部署

刚才开启的是 idea 的热部署。
和 程序运行热部署设置没关系。
设置情况分为两种。

1、低版本 Idea 设置(IntelliJ IDEA 2021.2 之前的版本)

2、⾼版本 Idea 设置(IntelliJ IDEA 2021.2 之后版本)

经过上述操作,我们的热部署就完成了。

4.使⽤ Debug 启动项目(⾮Run)

这是因为有些人的idea,即使前面三步都完成了,热部署仍然无法生效。
使用 debug 运行项目,就不会存在这个问题。
当然 你 使用 run ,热部署也能生效。
那就随便了。

效果展示

看到这个效果,说明热部署已经设置成功了,并且已经运行了。
以后,就不用我们去手动重启项目了。

模拟登录功能,前端使⽤ ajax,后端返回 json 给前端。

前端代码

知识铺垫:前端三剑客之 JavaScript
,前端三剑客之 CSS,前端三剑客之 HTML

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initialscale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="js/jquery-1.9.1.min.js"></script>
    <title>Document</title>
    <script>
        // ajax 提交
        function mysub(){
            //1、判空
            // 输入框中的值
            let username =  jQuery('#username');
            let password = jQuery('#password');
            // jQuery.trim 去空格
            if(jQuery.trim(username.val()) == ''){
                alert("请输入用户名!");
                return;
            }
            if(jQuery.trim(password.val()) == ''){
                alert("请输入密码!");
                return;
            }
            jQuery.ajax({
                // 请求使用方法
                type: "POST",
                //  映射方法路径
                url: '/user/login2',
                // 请求正文
                data: {"username": username.val(),"password": password.val()},
                // 除了 服务器范湖的结果
                success: function(result){
                    alert(result);
                }
            });
        }
    </script>
</head>
<body>
<div style="text-align: center;">
    <h1>登录</h1>
    ⽤户:<input id="username">
    <br>
    密码:<input id="password" type="password">
    <br>
    <input type="button" value=" 提交 " onclick="mysub()"
           style="margin-top: 20px;margin-left: 50px;">
</div>
</body>
</html>

将上述的代码,放入一个 HTML 文件中。

需要注意的是 ajax 是需要依赖于 jQuery 的。
所以我创建一个 js 包,用来存储 jQuery。

后端代码

效果图

小拓展:前端返回一个json数据,后端可以接收。

前面,我们是直接接收 参数的。

前面也讲过,要想接收一个 JSON 数据,是需要借助 @RequestBody 注解的。
问题,就在于,前端如何构造一个 JSON 数据。(了解)

后端代码支持获取 JSON 格式的数据,前端整体不改,值修改访问路径。

得出结论:
前后端的数据格式要匹配。
不然会报错!
回到最初的问题:
前端如果构造一个Json格式的数据。

得出结论:
前端在构造 json 格式的数据的时候,不经要将返回的数据设置成 json格式的数据,并且返回值的类型也需要设置成 JSON 格式。

而后端在接收 json 数据的时候,需要使用 @RequestBody 注解

请求转发或请求重定向 - 重点

forward VS redirect

return 不但可以返回⼀个视图,还可以实现跳转,跳转的⽅式有两种:
1、forward 是请求转发;
2、redirect:请求重定向。

forward 请求转发 实现

当我们访问到 myForward 方法,执行到return 语句的时候。
请求转发,是 服务器端的一个行为。
服务器端会访问这个页面,并且将页面的数据 返回给 客户端。

这个就好比,我们在大学时期,我们想吃饭的时候,叫室友爸爸带饭一样。
此时,我们就相当于是客户端,室友爸爸就是服务器。
食堂的饭菜,就是想要的网页数据。
当我们向室友爸爸提出带饭请求,室友爸爸接收到请求之后,他就会去访问食堂大妈。
获取 大妈手中的饭菜,将其带回给我们。

简单来说:服务器端(室友)就是 一个中间商,客户端(我们)想要获取的数据(饭菜),并非来自服务器端自身。
只是说:不用我们(客户端)去打饭(获取数据),他(服务器端)会帮我们找到它,将它带回来给我们。

由张图我们可以看出,对于 目标服务的数据,是共享的。
无论是谁访问,都可以访问该页面的数据。

我们再来看看fiddler抓包的结果

除了使用注解的方式来实现 请求转发,我们还可以基于 Servlet API 来实现请求转发。

相比与注解,servlet 就比较麻烦了!
servlet,先是要获取请求调度器,来获取转发资源,最后,通过 forward方法,将 请求获取的资源,写入 响应中。

而 Spring MVC 直接一个键值对,就完事了。

其实最简单直接的方法,就是 通过 return 直接返回 页面。

至于效果,我就不展示了。
前面已经玩过了。

redirect:请求重定向 实现

请求重定向,也有两种实现方式。
1、 注解方式

通过上述操作,展示出来的效果。其实就已经体现出 转发 与 重定向的区别了。
转发,它的URL 是不会改变的;
重定向,URL地址变成了 访问页面的地址
这是其一。

其二:
转发,浏览器只需要发送一条请求,就能获取到网页信息。
重定向,浏览器需要发送两条请求,才能获取网页信息。


还是举那个例子:
我们代表客户端,室友弟弟代表服务器端,饭菜就是网页数据。
此时,我们像往常一样,向室友爸爸发送一个 带饭请求。
但是!他这个弟弟居然居然拒绝了,而且说是要去约会!!!
告诉我们,只能自己去食堂买饭了。(这就是第一个请求,返回的302响应)
那我们还能怎么办,当然弟弟的幸福重要。。。
于是我们只能自己去买饭了。(这就对应着第二个请求)

简单来说:
重定向,是前端的一种行为。
直白来说:前端 向 后端索要数据,后端只告诉了它去哪里拿,前端需要自己去获取。

这就好比 问路,别人只告诉了我们怎么走,并不会领着我们去往目的地。
【PS:别人怕你套路他,你也怕别人套路你】
需要注意的是:重定向,数据是不共享的。
就是我给你的地址,你能不能访问到数据,是不确定的。
万一,这份数据是需要权限的,但是你没有,那就访问不了了。
或者这网站,被注销了,你也是访问不了的!

2、 Servlet API

区别总结

forward 和 redirect 具体区别如下:
1、 请求重定向(redirect)将请求重新定位到资源;请求转发(forward)服务器端转发。
直白来说:定义不同。
请求转发(Forward):发生在服务端程序内部,当服务器端收到一个客户端的请求之后,会先将请求,转发给目标地址,再将目标地址返回的结果转发给客户端。
重定向:甩给你目标地址,其它事情与我无关。

2、 请求重定向地址发⽣变化,请求转发地址不发⽣变化。
3、重定向浏览器会发送两条请求,转发,只有一条请求。
4、重定向,数据不共享;转发,数据共享。

5、 请求重定向与直接访问新地址效果一致,不存在原来的外部资源不能访问;请求转发服务器端转发有可能造成原外部资源不能访问。

请求转发 forward 导致的问题:
请求转发如果资源和转发的⻚⾯不在⼀个⽬录下,会导致外部资源不可访问。

我们写的这种,是相对路径。
如果访问的资源,分级(分包)存储。
此时,就需要根据当前路径做出改变。
保险的做法就是写绝对路径。
当然,相对路径写起来要简单一些。
看个人选择。

重定向,是直接给你目标访问的地址,它本身的地址并没有被改变。
而 转发,假设我们 实现请求转发的方法,是处于 一级路径,访问的源在 二级目录。
所以,它的地址发生了改变,从二级目录 变成 一级目录,
那么,一级目录中的相对资源就会丢失了。
这就是 请求转发存在的一个问题。

这就跟借钱一样。
重定向:
张三 找李四 借钱,但李四确实没钱;
李四 告诉 张三,去王五那里看看。
张三接到了。
结果张三跑了。。。
这个时候,李四是不担责任了。
因为 钱 是张三借的。

转发:
张三 找李四 借钱,但李四确实没钱;
李四 去找王五那里借了一些钱,然后,给了张三。
结果张三跑了。。。
王五找谁还?
肯定找李四啊!因为是 李四找他借的钱。
李四肯定是脱不了关系的!!!

请求转换,就存在这样的问题。
如果目录的层级是不一样的,那么它的相对地址也是不一样的。
此时,使用请求转发,就有可能出问题。

6、代码实现不同。

这个很明显!
关键字都不一样,好吧!!!!
一个 forward(转发),一个 redirect(重定向)

相关文章

微信公众号

最新文章

更多

目录