SpringMVC

什么是SpringMVC

什么是MVC

  • MVC是模型(Model)、视图(View)、控制器(Controller)的简写,是一种软件设计规范。
  • 是将业务逻辑、数据、显示分离的方法来组织代码。
  • MVC主要作用是降低了视图与业务逻辑间的双向偶合
  • MVC不是一种设计模式,MVC是一种架构模式。当然不同的MVC存在差异。

Model(模型):数据模型,提供要展示的数据,因此包含数据和行为,可以认为是领域模型或JavaBean组件(包含数据和行为),不过现在一般都分离开来:Value Object(数据Dao) 和 服务层(行为Service)。也就是模型提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。

View(视图):负责进行模型的展示,一般就是我们见到的用户界面,客户想看到的东西。

Controller(控制器):接收用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。也就是说控制器做了个调度员的工作。

最典型的MVC就是JSP + servlet + javabean的模式。

为什么SpringMVC

如果使用Servlet开发过JavaWeb项目,就能明白这种开发方式的功能局限性与低复用性,也就是说每添加一个功能就需要新增一个Servlet,同时还要配备好相应的环境。为了降低代码与环境的耦合度,SpringMVC应运而生。

当然,连接前后端脱离不开Servlet。SpringMVC本质也是围绕DispatcherServlet设计的。而DispatcherServlet负责将前端请求分发给对应控制器(Controller类,业务逻辑处理则是类中的各个方法)。相比于原先前端请求寻找Servlet,并在Servlet的doPost()完成业务逻辑处理,解耦程度已经有了大幅提升。

SpringMVC初略流程

SpringMVC的作用原理可以和Servlet的工作原理比较,可以发现以下不同:

  • Servlet是通过url可以直接找到处理该请求的控制器,通过注释@WebServlet或者映射<servlet-mapping>配置路径。而SpringMVC是将所有请求统一定位到DispatcherServlet,再由DispatcherServlet负责找到控制器。

  • Servlet返回前端页面有四种响应方法,其中主要是重定向或跳转,为前端页面传值通过转发的request.setAttribute(),再由前端jsp文件的request.getAttribute()获取值。而SpringMVC把需要传的值和响应结果全部装在ModelAndView中,再由视图解析器处理并最后返回给用户。

SpringMVC实践

源码下载:SpringMVC-Hello.rar 视频教学:第一个SpingMVC 配套学习笔记:狂神说SpringMVC02:第一个MVC程序

新建maven父项目SpringMVC,在pom.xml中配置好以下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.5</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
</dependencies>

又新建maven子项目SpringMVC-Hello,添加web框架支持生成Web目录。在IDEA中配置好Tomcat服务器并注意添加好Artifacts。因为这个Web项目是由Maven管理的,所以一定要手动在File 》 Project Structure中为本项目手动添加lib目录,并将jar包全部导入该目录下(点击+导入),否则会导致404问题

  • 配置web.xml,注册DispatcherServlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--1.注册DispatcherServlet-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<!--/ 匹配所有的请求;(不包括.jsp)-->
<!--/* 匹配所有的请求;(包括.jsp)-->
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

这里仅是注册了DispatcherServlet,内部逻辑通过<param-value>classpath:springmvc-servlet.xml</param-value>定义。

  • 初始化DispatcherServlet

main.resource.springmvc-servlet.xml中配置以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--添加处理映射器和处理器适配器-->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>
<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!--配置请求与处理器的对应关系-->
<bean id="/hello" class="com.theo.Controller.HelloController"/>
</beans>

简单来说:HandlerMapping会将URL对应到id="/hello"HandlerAdapter会适配对应的处理器class="com.theo.Controller.HelloController"。总的来说,这两个的作用就是找到处理请求的处理器。

  • 编写控制器

控制器继承Controller接口,需要返回ModelAndView,这里我们先不调用Moudle层。创建com.theo.Controller.HelloController

1
2
3
4
5
6
7
8
9
10
11
public class HelloController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
ModelAndView modelAndView = new ModelAndView();
//业务代码
modelAndView.addObject("message", "HelloSpringMVC");
//视图跳转
modelAndView.setViewName("Welcome");
return modelAndView;
}
}

这个控制器需要交付给SpringIoC容器的,以后每写一个控制器就要注意配置一个Bean。就像每一个Servlet都要配置路径一样。

ModelAndView两个功能,装入业务处理结果和指示跳转页面。modelAndView.addObject("message", "HelloSpringMVC")不用多说,到时可以在jsp中通过${message}直接获取。值得注意的是视图跳转并没有用页面的绝对路径,因为生成绝对路径这件事在视图解析器做了。

回到springmvc-servlet.xml配置的视图解析器,配置的两个属性分别是拼接前缀和拼接后缀,这是用来将Welcome拼接成/WEB-INF/jsp/Welcome.jsp,这样就能精确返回页面给用户了。

  • 编写返回页面

创建返回页面:web/WEB-INF/jsp/Welcome.jsp

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
${message}
</body>
</html>
  • 测试

启动Tomcat,访问http://localhost:8080/SpringMVC_Hello_war_exploded/hello即可获得结果`HelloSpringMVC`

SpringMVC执行原理详解

理清楚SpringMVC的执行流程后,我们再细作剖析SpringMVC的执行原理:

  1. DispatcherServlet表示前置控制器,是整个SpringMVC的控制中心。用户发出请求,DispatcherServlet接收请求并拦截请求。

    我们假设请求的url为 : http://localhost:8080/SpringMVC/hello,如上url拆分成三部分:

    http://localhost:8080服务器域名;SpringMVC部署在服务器上的web站点;hello表示控制器

  2. HandlerMapping为处理器映射。DispatcherServlet调用HandlerMapping,HandlerMapping根据请求url查找Handler。

  3. HandlerExecution表示具体的Handler,其主要作用是根据url查找控制器,例子中解析出需要找到hello对应的控制器。

  4. HandlerExecution将解析后的信息传递给DispatcherServle等。

  5. HandlerAdapter按照hello找到对应的控制器。

  6. Handler让具体的Controller执行。

  7. Controller将具体的执行信息返回给HandlerAdapter,如ModelAndView。

  8. HandlerAdapter将视图逻辑名或模型(ModelAndView)传递给DispatcherServlet。

  9. DispatcherServlet调用视图解析器(ViewResolver)来解析HandlerAdapter传递的逻辑视图名。

  10. 视图解析器将解析后的逻辑视图名传给DispatcherServlet。

  11. DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图。

  12. 最终视图呈现给用户。

注解开发SpringMVC

在编写控制器部分我们提到了:每写一个控制器就要注意配置一个Bean。就像每一个Servlet都要配置路径一样。

使用注解将不再需要亲自到配置文件中装配Bean了,类似于Servlet中用到的@WebServlet

新建Moudel:SpringMVC-annotation,添加web支持以及手动导入lib的jar包!!!

由于Maven可能存在资源过滤的问题,我们将该Moudel的pom.xml配置完善

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
  • 配置web.xml,注册DispatcherServlet

和配置版一样没有区别。这里解释下*/ 和 /\ 的区别**

< url-pattern > / </ url-pattern > 不会匹配到.jsp, 只针对我们编写的请求;即:.jsp 不会进入spring的 DispatcherServlet类 。< url-pattern > /* </ url-pattern > 会匹配 *.jsp,会出现返回的jsp视图再次进入spring的DispatcherServlet 类,导致找不到对应的controller所以报404错。

  • 初始化DispatcherServlet

main.resources.sprinmvc-servlet下额外添加context和mvc的约束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">

<!-- 自动扫描包,让指定包下的注解生效,由IOC容器统一管理 -->
<context:component-scan base-package="com.theo.Controller"/>
<!-- 让SpringMVC不处理静态资源如:.js .css .mp3 .mp4 .html-->
<mvc:default-servlet-handler />
<!-- 自动配置好HandlerMapping和HandlerAdapter-->
<mvc:annotation-driven />

<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
id="internalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>

</beans>

可以发现这里Controller没有装配到SpringIoC中。

注意,之后的**控制器用了@RequestMapping及其衍生注解后,将不再需要配置<mvc:default-servlet-handler /><mvc:annotation-driven />**。因为这两个的工作就是根据请求找到处理器,而@RequestMapping就能直接在处理器上标注好了需要处理的请求。

  • 编写Controller

编写一个Java控制类:com.theo.Controller.HelloController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller	//这是Spring中@Component的衍生注解!!!
public class Hellocontroller {
@RequestMapping("/hello")
public String hello(Model model) {
model.addAttribute("message", "HelloSpringMVC");
return "Welcome";
}

@RequestMapping("/nihao")
public String nihao(Model model) {
model.addAttribute("message", "你好SpringMVC");
return "Welcome";
}
}
  1. @Controller是为了让Spring IOC容器初始化时自动扫描到,当然前提是配置好了自动扫描包的路径。

  2. @RequestMapping是为了映射请求路径,这里因为只有方法上都有映射所以访问时应该是/hello;

  3. 方法中声明Model类型的参数是为了把Action中的数据带到视图中;

  4. 方法返回的结果是视图的名称hello,加上配置文件中的前后缀变成WEB-INF/jsp/hello.jsp。

很明显地注意到,注解下的Controller并没有显式地返回ModelAndView,而是不同方法返回了返回页面的字符串。其实暗地下@Controller帮我们创建了ModelAndView并打包好了,底层的逻辑还是一样的。

还有一点。通过注解我们将原本的一个类就是一个控制器完全简化成了一个方法就是一个控制器。这里就很简便的创造出了两个控制器,分别对应处理/hello和/nihao两个请求

还有一点很重要!!方法的参数设置不单单只有Model,设置其他基本类型参数也能接受,而且是直接从网页请求中获取!!

1
2
3
4
5
@RequestMapping("/sum")
public String sum(int a,int b,Model model) {
model.addAttribute("message", "获取结果为a+b="+(a+b));
return "Welcome";
}

访问页面http://localhost:8080/SpirngMVC_annotation_war_exploded/sum?a=1&b=2,在请求中传递参数a=1,b=2。返回页面会出现结果a+b=3。

相比于Servlet,这将省去int a = request.getParameter("a")的步骤!!!处理表单提交的请求时非常管用!!!

  • 编写返回页面

和配置版一样,简单通过${message}获取信息就可。用户输入/hello和/nihao能获得不同字符串。

RestFul风格

像前面的URLhttp://localhost:8080/SpirngMVC_annotation_war_exploded/sum?a=1&b=2,这种GET请求方式会很不安全,而且传参如果比较多,整个URL就会很冗长。使用RestFul风格后这条URL就可以改为http://localhost:8080/SpirngMVC_annotation_war_exploded/sum/1/2

1
2
3
4
5
@RequestMapping("/sum/{a}/{b}")
public String sum(@PathVariable int a,@PathVariable int b, Model model) {
model.addAttribute("message", "a+b="+(a+b));
return "Welcome";
}

这样就算应用了RestFul风格,之前的GET请求方式将无法找到这个控制器。

RestFul风格还有别的玩法,通过改变@ResquestMapping实现只接受GET/POST/DELETE/PUT/PATCH请求。

@GetMapping() @PostMapping() @DeleteMapping() @PutMapping()

1
2
3
4
@GetMapping("/sum/{a}/{b}")
public String sum_get(@PathVariable int a,@PathVariable int b, Model model) {...}
@PostMapping("/sum/{a}/{b}")
public String sum_post(@PathVariable int a,@PathVariable int b, Model model) {...}

这样即使是访问同一地址,使用不同的请求方式也会调用不同的控制器,从而实现同一地址的复用。

Controller

转发与重定向

在控制器中,我们直接return "Welcome",视图解析器就会帮我们拼接成目标页面,这相当于一种转发,因为你可以发现地址栏是不变的
但是如果不打算转发到一个jsp页面,就不能通过视图解析器。

return “forward:/sum/1/1”:以这种形式就能转发到另一个控制器,而不会被视图解析器拼接了。

reuturn "redirect:/Welcome.jsp":这种形式就是重定向到一个页面,也不会通过视图解析器的拼接。

接收请求参数

@RequestParam(请求参数)

控制器方法可以设置参数,然后前端在请求中设置参数值,后端就能获取参数了。但是如果前端请求的属性名与控制器方法的属性名不一致,就会报500错误。这样的话,前后端光是在参数名这一方面就需要严格把控,前后端很难分离。

我们可以在后端制定标准,严格限制前端请求的参数名必须一致,因为后端总与数据库连接,以数据库为准的参数名可以大量减少数据库方面的调试。

1
2
3
4
5
@GetMapping("/UserName")
public String username(@RequestParam("username") String name,Model model) {
model.addAttribute("message", name);
return "Welcome";
}

这样前端请求必须通过.../UserName?username=TZQ来传递参数,否则报400错误。

如果我们的项目连接了数据库,有一个实体类User:

1
2
3
4
5
public class User {
int id;
String username;
String password;
}

前端通过表单提交的方式发送请求,我们当然可以在方法中设置三个接收参数并一一配置@RequestParam。但我们也可以只设置一个接收参数User user

1
2
3
4
5
@GetMapping("/User")
public String user(User user,Model model) {
model.addAttribute("message", user);
return "Welcome";
}

这种方式要特别注意表单中设置的属性名也要和实体类的属性名相对应,否则无法自动装配成User对象。

@PathVariable(路径变量)

这种注解常搭配RestFul风格使用

@RequestHeader(请求头)

打开F12可以发现除了设置的参数,默认添加的请求头也可以获取。请求头也是一个个键值对形成的集合,我们想获取User-Agent或是Content-type就可以这么写:

1
2
3
4
@RequestMapping("/request")
public void request(@RequestHeader("User-Agent") String agent) {
System.out.println(agent);
}

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 Edg/90.0.818.51

@CookieValue(Cookie值)

如果浏览器有我们预先给的Cookie,因为每次请求都会带上该Cookie,我们就可以接收该Cookie值

1
2
3
4
@RequestMapping("/cookie")
public void cookie(@CookieValue("Idea-5e3b9fc0") String cookie) {
System.out.println(cookie); //IDEA默认会设置Cookie
}

4ca7a654-6a60-4884-a1eb-7004e64dc8be

@RequestBody(请求体)

因为只有POST方式才有请求体,我们通过表单提交POST请求。

1
2
3
4
@PostMapping(value = "/user_post")
public void user_post(@RequestBody String body) {
System.out.println(body);
}

username=tzq&password=123456789

@RequestAttribute(请求域)

这种多用于跳转页面后获取请求域带来的值,比如@RequestMapping(“/before”)的控制器要转发到@RequestMapping(“/after”)的控制器,before控制器先设置HttpServletRequest request并放入键值对,随后通过return ”forword: /after“,after控制器便可以用@RequestAttribute(“key”) String key获取键值对。

@MatrixVariable(矩阵变量)

你可能会见到这样的请求:localhost:8080/user;age=20;name=tzq。分号隔离的就是矩阵变量。

矩阵变量很少用,建议看视频。

数据回显三种方式

ModelAndView

使用配置版SpringMVC写控制器时需要返回ModelAndView,可以返回去回顾下。

Model

使用注解版SpringMVC写控制器是用Model来装返回结果的,这种方式比较常用。

ModelMap

继承了LinkMap。没屁用,有时间我再回来补充。

return

在注解版SpringMVC中,可以通过@ResponseBody指示控制器返回的字符串不通过视图解析器,这样也就没必要添加参数Model了:

1
2
3
4
5
6
@ResponseBody
@RequestMapping("/user")
public String user(){
User user = new User(123,"谭志强");
return user.toString();
}

这种回显方式因为是直接将字符串放回到页面上,不经过视图解析器的转发,所以不会走web.xml配置的过滤器。这样就仍然会出现中文乱码的问题。

解决乱码

Servlet解决乱码是会设置一层过滤器专门处理请求和响应的中文乱码问题,在Spring中可以在web.xml配置文件设置这种过滤器

1
2
3
4
5
6
7
8
9
10
11
12
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

一定要注意<url-pattern>/*</url-pattern>,如果单写/是不能过滤所有页面的!!!!

为什么有些资源不通过过滤器?

  • //*有什么区别?为什么乱码过滤器设置/*而注册DispatcherServlet时设置/?个人理解如下:

如果我们访问网页http://localhost:8080/SpirngMVC_annotation_war_exploded/user,这时DispatcherServlet无论配置`/`还是`/*`,这个请求都会进入DispatcherServlet。之后这个请求进入控制器后,由视图解析器转发到另一页面http://localhost:8080/SpirngMVC_annotation_war_exploded/WEB-INF/jsp/Welcome.jsp,这个请求还会尝试走一遍DispathcerServlet过滤器,如果这时配置的是`/*`,那么这个页面就还会进入DispatcherServlet,而不会返回给前端了。

同样的,对于乱码过滤器,他是要求所有请求都不产生乱码问题,因而这里必须配置/*!!!!!

总结来说,如果请求是由前端通过地址栏发过来的,他将通过所有过滤器,如果控制器做出了转发或重定向的处理,新请求会通过/*的过滤器。而return返回就类似于PrintWriter(),既不是转发也不是重定向,这样就不会走任何过滤器(包括乱码过滤器)。所以return返回就会产生乱码

return返回的乱码解决方法

  • 对单处理器解决乱码
1
2
3
4
5
6
@ResponseBody
@RequestMapping(value="/user",produces="application/json;charset=utf-8")
public String user(){
User user = new User(123,"谭志强");
return user.toString();
}
  • 匹配解决乱码

springmvc-servlet.xml中添加配置,这能解决所有处理器的return返回的乱码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8"/>
</bean>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
<property name="failOnEmptyBeans" value="false"/>
</bean>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!