(目录)


1. 什么是 Spring MVC?

官方描述:https://docs.spring.io/springframework/docs/current/reference/html/web.html#spring-web

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),但它通常被称为“SpringMVC”.

从上述定义我们可以得出两个关键信息:

  1. Spring MVC 是⼀个 Web 框架。(基于 Servlet 实现的)

web框架,就是基于 HTTP 协议的。 通俗来说:当用户在浏览器上输入一个 URL 地址之后,URL 地址 能够和 程序映射起来。 能够让用户的请求 被 程序接受到。 并且,经过程序的处理,能把结果返回给前端(浏览器)。 这一次基于 HTTP 的交互,就可以认为是一个 web 项目。 Spring MVC 也是一个 web 框架(spring 的 一个 web 模块)。

那么,一个 web 项目,只做三件事:

1、实现用户请求 到 程序 的 链接。(用户请求 可以 被 程序接收到,也就是上面讲的) 2、在前后端建立联系的情况下,拿到用户请求的参数。 3、拿到参数之后,进行业务处理,并将其结果返回给前端。

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

  1. Spring MVC 是基于 Servlet API 构建的

1.1 什么是 MVC?

Model View Controller(模型,视图,控制器),他是一种设计模式

这种设计模式将软件(项目)分为三部分: 模型,视图,控制器

image-20220814121214646

Model(模型) 是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。   View(视图) 是应用程序中处理数据显示的部分,通常视图是依据模型数据创建的。   Controller(控制器) 是应⽤程序中处理⽤户交互的部分。通常控制器负责从视图读取数据,控制⽤户输⼊,并向模型发送数据。


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

image-20220814121347698


1.2 MVC 和 SpringMVC 什么关系?

MVC 是一种设计思想,而 SpringMVC 是一个 具体的实现框架.

SpringMVC 是一个 基于 MVC 设计模式 和 Servlet API 实现的 Web 框架,同时 SpringMVC 又是 Spring 框架中的一个 WEB 模块,他是随着 Spring 的诞生而存在的一个框架. Spring 和 SpringMVC 诞生历史比较久远,在他们之后才有了 Spring boot.


1.3 为什么要学 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 项目。


拓展说一下:

APP 项目也可以分为两种:

  1. 基于 iOS 开发,最早使用 ObjectC,但是写法极其恶心。 所以,苹果公司,在后面又推出了 自己的语言 Swift
  2. 安卓开发,最早是基于 Java 开发,但是现在 谷歌 被 Oracle 给搞了。

所以,自己 和 idea 的公司合作,做了一门新语言 Dart。 但是!底层也还是 基于 JVM 的。

与 java 编译生成的 字节码文件,可以视为是一样的。 而且,Dart 与 Java 是非常相似的! 可以说:这门语言就是为了 堵住 Oracle 的 “嘴”。   但是呢,现在APP发展的趋势,趋近两者的混合开发

就是 苹果 和 安卓 手机,都能安装这个 APP。 关于 混合开发,已经是陈年旧事了。

最早推出的 是 Facebook 的 React Native

也是属于 门面模式的一种实现。

你可以写 React Native 自己的语法,在项目打包的时候,它有一个选择平台的功能。 根据 我们写的代码,去生成 IOS 的 代码,或者是安卓 的 代码。 不同的平台,我给你生成不同的代码,就行了。


2. Spring MVC 项目创建

我们只要基于 Spring Boot 框架添加一个 Spring Web(它使用的就是 Spring MVC)依赖,此时的项目就变成了Spring MVC项目.

image-20220814122738556

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


2.1 使用用户和程序的映射

2.1.1 方法一: 使用@RequestMapping(“/xxx”)

创一个 UserController 类,实现用户到 Spring 程序的互联互通:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@ResponseBody // 表示的是返回一个非静态页面的数据
@RequestMapping("/user") //类上面的 RequestMapping 可以省略
public class UserController {

    @RequestMapping("/sayhi")
    public String sayHi(){
        return "Hello World!";
    }
}

image-20220814123058318

@ResponseBody注解

1、放在方法上,表示该方法返回的数据,只是一个普通的数据,而非一个静态页面

2、放在上,表示该类中所有的方法返回的数据,都是一个普通的数据,而非静态页面

3、如果未加上这个注解,在我们访问方法的时候,默认就会去寻找一个名称叫做 Hello World!的页面,它发现找不到之后,就会报错。

image-20220814123604054


@RequestMapping 特征:

  1. 既能修饰类(可选)也能修饰方法
  2. 默认情况下,既支持 POST 请求方式,也支持 GET 请求方式

@RequestMapping 参数扩展(只支持某种类型的请求方式,比如 POST 类型的请求方式):

@RequestMapping(method = RequestMethod.POST,value = "/sayhi")

image-20220814123755281

只支持 GET:

@RequestMapping(method = RequestMethod.GET,value = "/sayhi")

2.1.2 方法二: 使用@PostMapping(“/xxx”)

只支持 POST 请求方式

@PostMapping("/sayhi2")

image-20220814123934861

image-20220814123943715


2.1.3 方法三: 使用 @GetMapping(“/xxx”)

只支持 GET 请求方式

@GetMapping("/sayhi3")

image-20220814124019447

image-20220814124010312

小总结:

@RequestMapping既支持 GET 也支持 POST,但他也可以设置指定 GET 或 POST @GetMapping只支持 GET 请求,PostMapping只支持 POST 请求


2.2 获取用户请求参数

2.2.1 获取单个参数

在 Spring MVC 中可以直接⽤⽅法中的参数来实现传参.

在传递单个参数的时候,最常传递的参数是 id。

image-20220814124435100

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

image-20220814124503484

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

image-20220814124537081


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

我们简单实现 回显服务。 传递什么参数,就返回什么参数。

方法还是一样的,只要 前端传递的参数名称 和 后端映射方法参数名称相同,就能够获取一个,甚至多个参数。

image-20220814124857108

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

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


2.2.3 获取对象

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

image-20220814125024298

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


2.2.4 参数重命名( @RequesParam 注解 )

某些特殊的情况下,前端传递的参数 key 和我们后端接收的 key 可以不⼀致

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

image-20220814125157890

这里有一个细节:

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

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

image-20220814125316752


使用 @RequestParam 注解注意事项:

如果在参数中添加 @RequestParam 注解,那么前端一定要传递此参数,否则就会报错,如果想要解决此问题,可以给 @RequestParam 里面添加required = false参数是否必须传递

image-20220814125456936


2.2.5 获取 JSON 格式的数据(@RequestBody)

要使用 @RequestBody 这个注解:

image-20220814125807224

通过获取普通对象的方法,来获取一个 json 格式的对象参数。显然是不行的!

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

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

image-20220814125927882


2.2.6 从 URL 地址中获取参数 (@PathVariable)

这个就厉害了,它不满足于从 请求的正文 和 URL 中的查询字符串 中 获取参数了。 而是从 URL 中获取参数

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

image-20220814130124887

image-20220814130211464


2.2.7 上传文件( @RequestPart )

现在不是之前那种简单数据了,它可能是一个数据流。 它传过来的是一个 “流”。

比如:

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

MultipartFile,就是专门用来接收文件的。

image-20220814130644674


拓展:优化存储目录

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

image-20220814130939215

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


总结:

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


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

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

image-20220814131133269

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


我们先来解决 图片名称 问题

第一种:时间戳

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

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

UUID就不会从出现 时间戳的情况。 UUID 会使用 你的网卡,随机数,等等,,,各种各样的信息来对 文件 进行命名。 在这种条件下,命名重复的概率,几乎不可能出现。


现在 存储路径的问题 搞定了,图片名称重复的问题 搞定了。 就剩下 获取 原上传图片 的 格式 了。

其实 MultipartFile 对象中,提供了 一个 APIgetOriginalFilename)。 getOriginalFilename:获取原始的文件名称

名称里面都有什么?有文件后缀啊! 想办法截取文件后缀,不就可以了嘛。

问题都有了解决方案,接下来就是实施环节。

image-20220814131405397


2.2.8 获取 Cookie

① 基于servlet 获取 Cookie 的方法
@RequestMapping("/cookie")
    public void getCookie(HttpServletRequest request) {
        // 得到全部的 Cookie
        Cookie[] cookies = request.getCookies();
        for (Cookie item : cookies) {
            log.info("Cookie Name:" + item.getName() +
                    " | Cookie Value:" + item.getValue());
        }
    }

image-20220814131620499

image-20220814131630582

这样的缺点:就是一次都把所有的 cookie 都给读出来了


② 使用 @CookieValue 注解实现 Cookie 的读取
 @RequestMapping("/cookie2")
    public String getCookie2(@CookieValue("456") String cookie) {
        return "Cookie Value:" + cookie;
    }

image-20220814131657090


2.2.9 获取 Header (请求头) 里面的信息

① Servlet 方式
 @RequestMapping("/getua")
    public String getHead(HttpServletRequest request) {
        return "header:" + request.getHeader("User-Agent");
    }

image-20220814131721005

image-20220814131732598


② 使用注解 @RequestHeader 的方式
@RequestMapping("/getua2")
    public String getHead2(@RequestHeader("User-Agent") String userAgent) {
        return "header:" + userAgent;
    }

image-20220814131751567


2.2.10 存储 Session

@RequestMapping("/setsess")
    public boolean setSession(HttpServletRequest request) {
        boolean result = false;
        // 1.得到 HttpSession
        HttpSession session = request.getSession(true); // true=如果没有会话,那么创建一个会话
        // 2.使用 setAtt 设置值
        session.setAttribute("userinfo", "张三");
        result = true;
        return result;
    }

设置(存储)Session 之前:

image-20220814131810534

存储 Session 之后:

image-20220814131911371

image-20220814131943441

保存了SessionId 就传给服务器获取


2.2.11 获取 Session

① Servlet 方式
@RequestMapping("/getsess")
    public String getSession(HttpServletRequest request) {
        String result = null;
        // 1.得到 HttpSession 对象
        HttpSession session = request.getSession(false); // false=如果有会话,使用会话,如果没有,那么不会新创建会话
        // 2.getAtt 得到 Session 信息
        if (session != null && session.getAttribute("userinfo") != null) {
            result = (String) session.getAttribute("userinfo");
        }
        return "会话: "+result;
    }

先存才取:

image-20220814132027721


② 使用注解 @SessionAttribute 的方式
@RequestMapping("/getsess2")
    public String getSession2(@SessionAttribute(value = "userinfo",
            required = false) String userinfo) {
        return "会话:" + userinfo;
    }

image-20220814132059703


请求转发或请求重定向

forward VS redirect

return 不但可以返回⼀个视图,还可以实现跳转,跳转的⽅式有两种:

1、forward请求转发; 2、redirect:是 请求重定向


forward 请求转发 实现

image-20220814132403593


当我们访问到 myForward 方法,执行到return 语句的时候。

请求转发,是 服务器端的一个行为

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

服务器端(室友)就是 一个中间商,客户端(我们)想要获取的数据(饭菜),并非来自服务器端自身。

只是说:不用我们(客户端)去打饭(获取数据),他(服务器端)会帮我们找到它,将它带回来给我们。

image-20220814132626518

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


redirect:请求重定向 实现

image-20220814132856345

通过上述操作,展示出来的效果。其实就已经体现出 转发 与 重定向的区别了。

  • 转发,它的URL 是不会改变的;
  • 重定向,URL地址变成了 访问页面的地址

这是其一。

image-20220814133053810

其二:

  • 转发,浏览器只需要发送一条请求,就能获取到网页信息。

  • 重定向,浏览器需要发送两条请求,才能获取网页信息。

image-20220814133203446


还是举那个例子:

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

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

image-20220814133317795

需要注意的是:

重定向,数据是不共享的。 就是我给你的地址,你能不能访问到数据,是不确定的。 万一,这份数据是需要权限的,但是你没有,那就访问不了了。 或者这网站,被注销了,你也是访问不了的!


区别总结

forward 和 redirect 具体区别如下:

1、 请求重定向(redirect)将请求重新定位到资源;请求转发(forward)服务器端转发。

直白来说:定义不同。 请求转发(Forward):发生在服务端程序内部,当服务器端收到一个客户端的请求之后,会先将请求,转发给目标地址,再将目标地址返回的结果转发给客户端。 重定向:甩给你目标地址,其它事情与我无关。

2、 请求重定向地址发⽣变化,请求转发地址不发⽣变化。

3、重定向浏览器会发送两条请求,转发,只有一条请求。

4、重定向,数据不共享;转发,数据共享。

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


请求转发 forward 导致的问题:

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

image-20220814133433509

我们写的这种,是相对路径。 如果访问的资源,分级(分包)存储。 此时,就需要根据当前路径做出改变。 保险的做法就是写绝对路径。 当然,相对路径写起来要简单一些。 看个人选择。   重定向,是直接给你目标访问的地址,它本身的地址并没有被改变。 而 转发,假设我们 实现请求转发的方法,是处于 一级路径,访问的源在 二级目录。 所以,它的地址发生了改变,从二级目录 变成 一级目录, 那么,一级目录中的相对资源就会丢失了。 这就是 请求转发存在的一个问题。  

这就跟借钱一样。

重定向: 张三 找李四 借钱,但李四确实没钱; 李四 告诉 张三,去王五那里看看。 张三接到了。 结果张三跑了。。。 这个时候,李四是不担责任了。 因为 钱 是张三借的。   转发: 张三 找李四 借钱,但李四确实没钱; 李四 去找王五那里借了一些钱,然后,给了张三。 结果张三跑了。。。 王五找谁还? 肯定找李四啊!因为是 李四找他借的钱。 李四肯定是脱不了关系的!!!

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