整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

SpringMVC概述和基础知识详解

SpringMVC概述和基础知识详解

. SpringMVC概述及原理

1. SpringMVC是什么

Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面。Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块。使用 Spring 可插入的 MVC 架构,从而在使用Spring进行WEB开发时,可以选择使用Spring的Spring MVC框架或集成其他MVC开发框架,如Struts1(现在一般不用),[Struts 2](https://baike.baidu.com/item/Struts 2/2187934)(一般老项目使用)等。

SpringMVC 已经成为目前最主流的 MVC 框架之一, 从 Spring3.0 的发布, 就已全面超越 Struts2,成为最优秀的 MVC 框架。它通过一套注解,让一个简单的 Java 类成为处理请求的控制器,而无须实现任何接口。同时它还支持RESTful 编程风格的请求。

2. MVC和三层架构

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


控制器(Controller):Servlet,控制器主要处理用户的请求

视图(View):HTML, JSP, 前端框架

模型(Model):逻辑业务程序(后台的功能程序), Service, Dao, JavaBean

JavaWEB发展史

Model1

所有的业务逻辑交给jsp单独处理完成,一个web项目只存在DB层和JSP层,所有的东西都耦合在一起,对后期的维护和扩展极为不利。

Model2 第一代

JSP Model2有所改进,把业务逻辑的内容放到了JavaBean中,而JSP页面负责显示以及请求调度的工作。虽然第二代比第一代好了些,但JSP还是把view和controller的业务耦合在一起。依然很不理想。

Model2 第二代(三层架构)

Model2第二代就是现在大力推广的和使用的mvc,将一个项目划分为三个模块,各司其事互不干扰,既解决了jsp所形成的耦合性,又增加了逻辑性、业务性以及复用性和维护性

表示层(web层):包含JSP,Servlet等web相关的内容

业务逻辑层(Service):处理业务,不允许出现servlet中的request、response。

数据层(Data Access Object):也叫持久层,封装了对数据库的访问细节。

其中 web层相当于mvc中的view+controller,Service层和dao层相当于mvc中的model。

3. SpringMVC 在三层架构的位置

MVC模式:(Model-View-Controller):为了解决页面代码和后台代码的分离.


二. 入门示例

1. 配置流程-基于XML的配置

1.1.搭建普通Maven项目



使用插件将项目转换为web项目

转换成功:

查看是否生成webapp目录和maven项目打包方式是否变为war

添加SpringMVC依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.0.6.RELEASE</version>
</dependency>

查看关系依赖图

1.2.在web.xml配置核心控制器

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	version="2.5">

	<!--spring核心(前端控制器)-->
	<servlet>
		<servlet-name>dispatcher</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	</servlet>
	<servlet-mapping>		<!--只有*.form后缀的请求才会进入springmvc-->
		<servlet-name>dispatcher</servlet-name>
		<url-pattern>*.form</url-pattern>
	</servlet-mapping>
</web-app>

1.3.创建一个Spring的配置文件

<?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
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="com.dfbz"/>

    <!--开启springmvc注解支持-->
    <mvc:annotation-driven/>
</beans>

mvc:annotation-driven 说明

在 SpringMVC 的各个组件中,处理器映射器、处理器适配器、视图解析器称为 SpringMVC 的三大组件。

在springmvc早期版本中需要我们自己加载springmvc的三大组件(现在我们使用的版本5.0.6会自动加载这三大组件)

**处理器映射器:RequestMappingHandlerMapping **

处理器适配器:RequestMappingHandlerAdapter

处理器解析器:ExceptionHandlerExceptionResolver

在早期的版本中使用 <mvc:annotation-driven> 自动加载这三大组件,但是高版本的不需要<mvc:annotation-driven>来加载

同时它还提供了:数据绑定支持,@NumberFormatannotation支持,@DateTimeFormat支持,@Valid支持,读写XML的支持(JAXB,读写JSON的支持(Jackson)。我们处理响应ajax请求时,就使用到了对json的支持(配置之后,在加入了jackson的core和mapper包之后,不写配置文件也能自动转换成json)

springmvc配置文件说明

注意:默认的Spring配置文件放在WEB-INF下,名为{servlet-name}-servlet.xml

{servlet-name}指的是,核心控制器配置的名字

如:dispatcherServlet-servlet.xml

当请求被springmvc处理时,springmvc会去默认路径下加载xxxx-servlet.xml核心配置文件

但是我们在开发中一般都是把配置文件写在classes下的,我们可以在web.xml中设置springmvc配置文件的路径

<!--spring核心(前端控制器)-->
<servlet>
	<servlet-name>dispatcher</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<!--配置springmvc配置文件的位置-->
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:dispatcher-servlet.xml</param-value>
	</init-param>
</servlet>
<servlet-mapping>		<!--只有*.form后缀的请求才会进入springmvc-->
	<servlet-name>dispatcher</servlet-name>
	<url-pattern>*.form</url-pattern>
</servlet-mapping>

1.4.创建一个业务控制器

package com.dfbz.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class HelloController{

    //代表此方法的访问路径为/hello.form
    @RequestMapping("/hello.form")
    public ModelAndView hello(){
        
        ModelAndView mav=new ModelAndView();
        mav.addObject("msg","小标");
        mav.setViewName("/hello.jsp");

        return mav;
    }

}

1.5.创建一个返回的视图页面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
   <title>Title</title>
</head>
<body>
   ${msg },欢迎您!
</body>
</html>

1.7. SpringMVC的工作流程

1、客户端发送请求给前端控制器(DispatcherServlet)

2、dispatcherServlet接收到请求调用HandlerMapping处理器映射器

3、处理器映射器根据请求的url找对应的处理器,生成处理器对象(handler)返回

4、dispatchServlet将handler传入处理器适配器执行

5、处理器适配器执行handler

6、执行完成最终封装一个ModelAndView

7、将ModelAndView返回给前端控制器

8、前端控制器将请求的路径交给视图解析器进行解析

9、最终封装一个View对象给dispatcherServlet,此View对象封装了响应参数

10、JSP页面渲染数据

11、响应客户端

1.8 SpringMVC源码分析

我们知道SpringMVC实质上是对servlet进行封装,让我们的开发更加简便

1. 准备工作

我们知道springmvc在工作开始之前会加载默认的处理器映射器、处理器适配器、处理器解析器等

可以在spring-webmvc-5.0.6.RELEASE.jar源码包下查看DispatcherServlet.properties文件看有哪些处理器是springmvc默认加载的

2. 查看DispatcherServlet的继承体系:

我们发现DispatcherServlet最终还是继承与HttpServlet,那么我们就直接找service方法吧!

经打断点发现,最终会走向DispacherServlet的doDispacher方法!

此时请求进入DispatcherServlet,按照我们画图分析的结果应该是把请求交给处理器映射器HandlerMapping最终返回一个Handler

3. 查看HandlerMapping接口:

4. 寻找HandlerMapping实现类:


接下来进入处理器适配器HandlerAdapter执行handler最终返回一个ModelAndView

5. 查看HandlerAdapter接口:

6. 查看HandlerAdapter实现类:


然后请求交给视图解析器进行解析最终返回一个View对象

7. 查看View接口:

8. 查看View实现类:


9. 查看View信息:

1.9.核心控制器

SpringMVC自带了拦截器请求的核心控制器.所以就可以在请求过来的时候,直接启动Spring框架

默认情况下,Spring容器是在核心控制器DispatcherServlet获得请求后才启动的.

能不能网站启动的时候,Spring容器就立刻启动.

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup><!-- 启动服务器的时候,Servlet就创建实例 --> 
</servlet>

三. Controller方法返回值

返回值


返回String

可以返回视图字符串,解析器会自动解析

@RequestMapping("/demo3_1")
public String demo3_1(){

    return "/success.jsp";           //直接返回字符串
}

返回ModelAndView

ModelAndView是SpringMVC帮我们提供的,即"模型和视图"

@RequestMapping("/demo3_2")
public ModelAndView demo3_2(){          //返回ModelAndView
    ModelAndView mav=new ModelAndView();
    mav.setViewName("success.jsp");
    mav.addObject("username","东方标准");
    return mav;
}

返回void

一般用于使用原生的Servlet对象或者ajax请求

@RequestMapping("/demo3_3")		//返回void(一般用于ajax)
public void demo3_2(HttpServletResponse response) throws IOException {          

    response.setContentType("text/html;charset=utf8");
    response.getWriter().write("东方标准");

}

转发和重定向


SpringMVC提供了一个 String 类型返回值之后会自动解析forward、redirect等特殊字符串

视图解析器配置的前缀和后缀解析不支持forward、redirect

:forward:代表转发request.getRequestDispatcher(url).forward(request,response)

:redirect:代表重定向response.sendRedirect(url)

@RequestMapping("/demo3_4")
public String demo3_4(Model model)  {          //转发

    System.out.println("执行啦!");
    model.addAttribute("username","东方标准");
    return "forward:/success.jsp";
}
@RequestMapping("/demo3_5")
public String demo3_5(HttpSession session)  {          //重定向
    session.setAttribute("password","admin");
    System.out.println("执行啦!");

    return "redirect:/success.jsp";
}

四. 映射路径-@RequestMapping

1. 探究RequestMapping

注解式处理器映射器,对类中标记了@ResquestMapping的方法进行映射。根据@ResquestMapping定义的url匹配@ResquestMapping标记的方法,匹配成功返回HandlerMethod对象给前端控制器。HandlerMethod对象中封装url对应的方法Method。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
}

命名空间

按照我们传统的url写法不能很好的规定请求路径,即不能按照业务来区分请求

例如现在user需要定义一个findById,goods也需要定义一个findById,此时就会有冲突,针对这种现象我们可以在类上定义一个命名空间,即在类上使用@RequestMapping注解,类似于一级目录,以后访问此类下的任意资源都需要加上此目录

类上

请求 URL 的第一级访问目录。此处不写的话,就相当于应用的根目录。 写的话需要以/开头。它出现的目的是为了使我们的 URL 可以按照模块化管理:

例如:

user模块:

/user/register

/user/update

/user/findById

goods模块:

/goods/add

/goods/update

/goods/findById

映射路径的有三种:标准的映射路径,带通配符的映射路径,带路径变量的映射路径

方法上

请求 URL 的第二级访问目录。

属性:

  • value:用于指定请求的 URL。 它和 path 属性的作用是一样的。
  • method:用于指定请求的方式。
  • params:用于指定限制请求参数的条件。 它支持简单的表达式。 要求请求参数的 key 和 value 必须和配置的一模一样。

params={“userId”},表示请求参数必须有 userId,区分大小写

params={“userId!=20”},表示请求参数中 id不能是 20。可以不携带userId参数,区分大小写

headers:用于指定限制请求消息头的条件。

package com.dfbz.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class HelloController{

    public HelloController(){
        System.out.println("Hello创建了");
    }
    /*
    	代表此方法的访问路径为/hello.form,如果携带userId参数那么参数必须不能等于1
    	并且提交方式一定要为POST
    */
    @RequestMapping(value="/hello.form",params="userId!=1",method=RequestMethod.POST)
    public ModelAndView hello(){

        ModelAndView mav=new ModelAndView();
        mav.addObject("msg","小标");
        mav.setViewName("/hello.jsp");

        return mav;
    }

}

小结:

1.参数必须包括:params={“username”,“password”}

2.参数不能包括:params={"!userid"}

3参数值必须是指定的值:params={“username=zhangsan”})

4.参数值必须不是指定的值:params={“userid!=123”})

2.3. RESTFUL

所谓的路径变量,就是将参数放在路径里面,而不是放在?的后面

如:原get请求方法 /login.mvc?username=’zhangsan’&pwd=’123456’

路径变量写法:

/zhangsan/123456/login.form

2.3.1 什么是RESTFUL

REST(英文:Representational State Transfer,简称REST)RESTful是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。

2.3.2 RESTFUL示例:


请求方式有几种?7种

jsp、html,只支持get、post。

基于restful风格的url:

添加

http://localhost:8080/SpringMVC_01/user

提交方式: post

修改

http://localhost:8080/SpringMVC_01/user

提交方式:put

删除

http://localhost:8080/SpringMVC_01/user/101

提交方式:delete

查询

http://localhost:8080/SpringMVC_01/user/102

提交方式:get

五. 数据绑定

1. 数据绑定是什么

SpringMVC里面,所谓的数据绑定就是将请求带过来的表单数据绑定到执行方法的参数变量中,或将服务器数据绑定到内置对象,传递到页面

2. 自动绑定的数据类型

2.1 自动绑定数据类型

  • 基本数据类型:基本数据类型+String+包装类
  • 包装数据类型(POJO):包装实体类
  • 数组和集合类型:List、Map、Set、数组等数据类型

2.2 内置绑定数据自动绑定:

ServletAPI:

HttpServletRequest

HttpServletResponse

HttpSession

SpringMVC内置对象

Model

ModelMap

ModelAndView

Model和ModelMap默认都是存储了Request请求作用域的数据的对象

这个两个对象的作用是一样.就将数据返回到页面.

测试页面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>测试ServletAPI绑定</h3>
<p><a href="/demo1.form"></a></p>
<h3>测试参数类型绑定</h3>
<p><a href="/demo2.form?username=zhangsan">属性参数绑定</a></p>

<h3>测试对象参数绑定</h3>
<form action="/demo3.form" method="post">
    <input type="text" name="username">
    <input type="text" name="password">
    <input type="text" name="address">

    <input type="submit" value="对象参数绑定">

</form>

<h3>测试数组类型绑定</h3>
<form action="/demo4.form" method="post">
    <input type="checkbox" value="1" name="ids">
    <input type="checkbox" value="2" name="ids">
    <input type="checkbox" value="3" name="ids">

    <input type="submit" value="数组类型绑定">
</form>

<h3>测试Pojo对象数据绑定</h3>
<hr>
<form action="/demo6.form" method="post">

    <h3>Pojo封装数组</h3>
    <input type="checkbox" value="1" name="ids">
    <input type="checkbox" value="2" name="ids">
    <input type="checkbox" value="3" name="ids">

    <br>

    <h3>Pojo封装user</h3>
    <input type="text" name="user.username">
    <input type="text" name="user.password">
    <input type="text" name="user.address">
    <br>

    <h3>Pojo封装list</h3>
    <input type="text" name="userList[1].name">
    <input type="text" name="userList[1].password">
    <input type="text" name="userList[1].address">
    <input type="text" name="userList[0].name">
    <input type="text" name="userList[0].password">
    <input type="text" name="userList[0].address">

    <br>

    <h3>Pojo封装map</h3>
    <input type="text" name="map['username']"><br>
    <input type="text" name="map['password']"><br>
    <input type="text" name="map['age']"><br>
    <input type="text" name="map['address']"><br>
    <input type="submit" value="Pojo对象参数绑定"><br>

</form>
</body>
</html>

测试Controller:

package com.dfbz.controller;

import com.dfbz.entity.Pojo;
import com.dfbz.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Arrays;
import java.util.List;

@Controller
public class DemoController {

    @RequestMapping("/demo1")            //自定义属性绑定
    public String demo1(HttpServletRequest request, HttpServletResponse response, HttpSession session){

        request.setAttribute("username","zhangsan");

        return "/success.jsp";
    }

    @RequestMapping("/demo2")            //属性参数绑定
    public String demo2(String username){

        System.out.println(username);

        return "/success.jsp";
    }

    @RequestMapping("/demo3")            //对象参数绑定
    public String demo3(User user){

        System.out.println(user);

        return "/success.jsp";
    }


    @RequestMapping("/demo4")            //数组参数绑定
    public String demo4(String[] ids){

        System.out.println(Arrays.toString(ids));
        return "/success.jsp";
    }

    @RequestMapping("/demo5")            //Pojo对象参数绑定
    public String demo5(Pojo pojo){

        System.out.println(pojo);

        return "/success.jsp";
    }
}

3. Post提交方式乱码解决

测试GET/POST提交中文数据

页面:

<h3>测试Post乱码</h3>
<form action="/demo" method="post">
    <input type="text" name="username">
    <input type="text" name="password">
    <input type="text" name="address">

    <input type="submit">
</form>

controller:

@RequestMapping("/demo7.form")            //Pojo对象参数绑定
public String demo7(User user){

    System.out.println(user);

    return "/success.jsp";
}

发现POST乱码

解决post提交乱码我们可以配置spring提供的过滤器

<!--解决Post提交中文乱码-->
<filter>
    <filter-name>characterEncoding</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <!--要使用的字符集,一般我们使用UTF-8(保险起见UTF-8最好)-->
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>characterEncoding</filter-name>
    <!--代表所有请求都经过编码处理-->
    <url-pattern>/*</url-pattern>
</filter-mapping>

4. SpringMVC常用注解

SpringMVC有一些数据是不能自动绑定,需要我们使用它提供的注解强制绑定.

遇到需要强制绑定的几种情况

a. 默认参数绑定的是表单数据,如果数据不是来自表单(如restful),那么必须需要强制绑定

b. 数据是来自表单的,但是参数名不匹配,那么也需要强制绑定

c. 数据是来自表单的,但是需要将数据绑定在Map对象里面,需要强制绑定

4.1. @PathVariable:绑定路径参数

这个注解是绑定路径参数的.

/**
 * http://localhost:8080/20/zhangsan/demo9.form
 * @param id
 * @param username
 * @return
 */
@RequestMapping("/{id}/{username}/demo9")            //@PathVariable绑定restful请求
public String demo9(
    @PathVariable("id") Integer id,@PathVariable("username") String username
    ){

    System.out.println(id);
    System.out.println(username);

    return "/success.jsp";
}
<a href="/20/zhangsan/demo9.form">测试restful风个绑定</a>

4.2. 通过@RequestParam绑定表单数据

接收的参数的变量名与表单的name属性不一样

/**
 * http://localhost:8080/demo8.form?name=zhangsan
 * @param username
 * @return
 */
@RequestMapping("/demo8")            //@RequestParam强制数据类型绑定
public String demo8(@RequestParam("name") String username) {

    System.out.println(username);

    return "/success.jsp";
}
<a href="/demo8.form?name=zhangsan">测试强制类型绑定</a>

4.3. @CookieValue获得Cookie值的注解

/**
* 获得JSP 页面,JSESSIOINID这个Cookie值
* @param cookevalue
*/
@RequestMapping("/demo10")
public void getcookie(@CookieValue(value="JSESSIONID") String cookevalue){
    //输出Cookie
    System.out.println(cookevalue);
}
<a href="/demo10.form">测试@CookieValue注解</a>

4.4. @RequestHeader获得指定请求头的值

@RequestMapping("/demo11")
//获取请求头host的值封装到value中
public void demo10(@RequestHeader("host")String value){

    System.out.println(value);
}
<a href="/demo11.form">测试@RequestHeader注解</a>

4.5 @SessionAttributes 注解

把Model和ModelMap中的指定的key或者指定的属性的值也存入一份进session域

package com.dfbz.controller;

import com.dfbz.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;

import javax.servlet.http.HttpServletRequest;

@Controller
/**
 * names: 代表此类中model/modelMap的username属性将会添加到一份进入session
 * types: 此类中指定的类型会添加一份到session中
 */
@SessionAttributes(names={"username"},types={User.class,String.class,Integer.class} )
public class Demo2Controller {

    @RequestMapping("/demo12.form")
    public String demo12(Model modelMap) {
        modelMap.addAttribute("username","zhangsan");
        modelMap.addAttribute("password","admin");
        modelMap.addAttribute("age",20);
        User user=new User();
        user.setUsername("xiaodong");
        modelMap.addAttribute("user",user);
        return "/success.jsp";
    }
}
<a href="/demo12.form">测试@SessionAttribute注解</a>

@SessionAttribute注解:

从session中获取一个值封装到参数中

/**
 * @SessionAttribute:从session中获取一个值
 * @param username
 * @param user
 * @return
 */
@RequestMapping(value="/demo6")
public String demo5(@SessionAttribute("password") String username,@SessionAttribute("user") User user){
    System.out.println(username);

    return "/success.jsp";
}

5. 格式化参数类型

SpringMVC之所以能够帮我们实现自动数据类型转换是因为SpringMVC提供了非常多的转换器(Converter)

例如:


发现他们都实现Converter接口,如果我们需要之定义转换器必须实现Converter接口

案例:

实现日期的转换

实体类:

public class User {
    private Integer id;
    private String username;
    private String password;
    private String address;
    private Date birthday;
}

表单:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

<form action="/testConverter.form">
    <input type="text" name="username">
    <input type="text" name="password">
    <input type="text" name="address">
    <input type="text" name="birthday">

    <input type="submit">
</form>

</body>
</html>

controller:

@RequestMapping("/testConverter.form")
public String testConverter(User user) {
    System.out.println(user);
    return "/success.jsp";
}

转换器类:

package com.dfbz.converter;

import org.springframework.core.convert.converter.Converter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Converter<传入进来的类型,转换之后的类型>
 */
public class MyConverter implements Converter<String,Date> {
    @Override
    public Date convert(String str) {

        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");
        try {
            Date date=sdf.parse(str);
            return date;

        } catch (ParseException e) {
            e.printStackTrace();
        }

        return null;
    }
}

dispatcher-servlet.xml配置

<!--在SpringMVC配置的转换器中添加我们自定义的转换器-->
<bean id="myConverters" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <!-- 配置多个转换器 -->
    <property name="converters">
        <set>
            <!-- 配置转换器(自定义转化器类) -->
            <bean class="com.dfbz.converter.MyConverter"></bean>
        </set>
    </property>
</bean>

<!--开启springmvc注解支持并重新指定转换器-->
<mvc:annotation-driven conversion-service="myConverters"/>

六. Controller的生命周期

Spring框架默认创建的对象是单例.所以业务控制器是一个单例对象.

SpringMVC提供了,request,session ,globalsession三个生命周期

  • request:每次新的请求,创建一个新的实例.
  • session:每次会话创建一个新的实例. 就是同一个浏览器,就使用同一个实例
  • globalsession:基于集群的session

每个Session创建一个实例

迎关注头条号:java小马哥

周一至周日早九点半!下午三点半!精品技术文章准时送上!!!

精品学习资料获取通道,参见文末

1、回顾servlet 与jsp 执行过程

2、Spring MVC请求处理流程

3,mvc 体系结构详解

URL映射

表单参数映射

调用目标Control

数据模型映射

视图解析

异常处理

DispatcherServlet

DispatcherServlet它的作用有点儿类似于网关,DispatcherServlet负责将请求转发到不同的Controller上去。

配置DispatcherServlet

/ 后面不能写成/*否则容易出问题。

编写Controller,继承controller类。实现handlerRequest接口。指定ModelAndView。这是一种方式。

第二种方式 继承 HttpRequestHandler类。那么DispatcherServlet是如何去匹配不同的controller的呢?这就需要了解SpringMVC的体系结构。整个体系结构都搞懂才行。下面就从一个全局的视角去看Spring MVC的体系结构。

SpringMVC的体系结构图

DispatcherServlet是通过HandlerMapping的一组映射关系去找到我们的目标Controller

通过上面我们知道Controller存在的形式是多种多样的。可以通过Controller接口的形式,也可以通过@Controller注解的形式存在。等等等。。。有这么多不同的Controller形式DispatcherServlet是如何去执行的呢?这里面就涉及到一个HandlerAdapter控制器适配器。找到数据模型的一个映射。然后试图解析是通过ViewResolver这个东东。他支持各种各样的试图解析。view用于具体的试图解析。HandlerExceptionResolver异常拦截解析器。

下面就围绕这张图去逐一进行源码分析。

一,HandlerMapping源码分析

HandlerMapping是一个接口,他有两个实现类,MatchableHandlerMapping和AbstractHandlerMapping。HandlerMapping在getHandler里面并没有返回handler(我们的目标执行器,也就是我们的controller)而是返回一个执行链条。HandlerExecutionChain。

根据下图可知HandlerExecutionChain里面有一个getHandler方法,这里面返回了Handler。但是具体的是哪个一个Handler是不确定的,因为他是Object类型的。所以通过动态代理的方式是不能实现的。只能通过这个执行链条去返回目标Handler。后续的文章我们会对执行链条做深入的讲解。

然后看上面的类继承关系图。左面继承了AbstractUrlHandlerMapping其中AbstractDetectingUrlHandlerMapping起到自动发现的作用,他是根据Bean的名称,而且是必须以/开头。比如这样

<bean name="/hello.do" class="com.tuling.control.SimpleControl"/>

然后SimpleUrlHandlerMapping。如果不需要自动发现功能,则使用这个。SimpleUrlHandlerMapping是自己配置URL和controller之间的一个关系。所以他们之间的区别就是一个自动发现,一个手动配置。AbstractHandlerMethodMapping是对方法进行映射RequestMappingInfoHandlerMapping这个大家比较熟悉了,就是通过@RequestMapping进行映射的。如果配置了SimpleUrlHandlerMapping或者BeanNameUrlHandlerMapping那么默认的就会失效。SimpleUrlHandlerMapping和BeanNameUrlHandlerMapping可以同时配置,都可以映射到我们的Controller控制器。

SimpleUrlHandlerMapping的初始化执行流程

对initApplicationContext()方法进行调用。其中super.initApplicationContext();这里是初始化一些拦截器。比如自动发现拦截器啊等等等。this.registerHandlers(this.urlMap);开始进行Url的匹配点进去看一下可知如果urlMap不为空才进行匹配。第一步是!url.startsWith("/")如果不是/开头的。就会把你的URL加上/然后获取Handler,就是我们的控制器。然后判断handler是不是String类型的,。如果是进行去除空格的处理。然后执行this.registerHandler(url, handler);点进去发现,将handler转化为Object类型。然后判断是否是懒加载并且是String类型。然后将Handler转化成String的字符串。然后获取一个ApplicationContext对象。判断handler是都是单例。如果是通过ApplicationContext.getBean(handlerName)获取到一个Object对象。然后获取handlerMap,判断是否是空,如果不为空说明你配置了两个name属性此时会抛出异常。接着判断你的url是否等于/如果是设置一个根目录的Handler通过localhost:8080/即可访问我们的handler。然后判断是否等于/*如果是就设置一个默认的和Handler。也就是说你永远都不会担心找不到这个url的异常。

最后如果以上情况都不是,则通过this.handlerMap.put(urlPath, resolvedHandler);将url和handler绑定在一起。如果说handler是懒加载那么此时map中存储的value就是beanName。这就是SimpleUrlHandlerMapping的初始化流程。

SimpleUrlHandlerMapping的访问执行流程

我们知道最终执行调用的一定是DispatcherServlet。所以我们从这里开始入手。找到getHandler

首先通过Iterator var2=this.handlerMappings.iterator();或取到所有的Mapping然后遍历。遍历一次取出一个handlerMapping然后调用handlerMapping。点进去看看。第一行getHandlerInternal这个方法很重要。点进去看看,在这个方法里第一行通过getUrlPathHelper().getLookupPathForRequest(request);去解析我们地址栏上的URL。比如或取到的是/hello.do。第二行通过Object handler=lookupHandler(lookupPath, request);去查找对应的handler。点进去发现就是从上面put进去的handlerMap找到对应的handler。在最后return一个buildPathExposingHandler。点进去发现HandlerExecutionChain这里面配置了一个拦截器链,那么返回的并不是我们最终的handler。

那么getHandlerInternal方法的第二行也执行完了。最终拿到得是一个拦截器链,如果说拦截器链为空则判断第一行的url解析器拿到得/hello.do是否等于/如果是Object rawHandler=getRootHandler()设置为根节点的handler,然后判断rawHandler是否等于空,如果是则调用默认的handler。如果不等于空呢,则直接通过beanName从Spring容器找到Handler。 再调用buildPathExposingHandler。还是获取一个拦截器链。最终,将拦截器链返回。getHandlerInternal方法执行结束。返回到getHandler方法里。那么如果说,没有获取到拦截器链,则获取默认的handler。如果不为空,则直接通过beanName从容器中获取到handler。然后,去创建一个拦截器链条,又加入了一个拦截器。最后进一步获取handler。然后返回HandlerExecutionChain。最后通过HandlerAdapter ha=this.getHandlerAdapter(mappedHandler.getHandler());获取到的才是我们的handler。SimpleUrlHandlerMapping执行流程到此介绍完毕,说实话SpringMVC的代码写的跟IOC\aop差着一大块儿的水平感觉。

Controller 接口:

HttpRequestHandler 接口:

HttpServlet 接口:

@RequestMapping方法注解

可以看出 Handler 没有统一的接口,当dispatchServlet获取当对应的Handler之后如何调用呢?调用其哪个方法?这里有两种解决办法,一是用instanceof 判断Handler 类型然后调用相关方法 。二是通过引入适配器实现,每个适配器实现对指定Handler的调用。spring 采用后者。

HandlerAdapter详解

HandlerAdapter中有三个接口

1supports接口,主要的作用是传入一个handler看看是否可以被执行。如果可以返回true。

2handle处理接口,处理完返回一个ModelAndView。

3getLastModified,用作缓存处理,获取最后一次修改时间。

我们知道HandlerAdapter对应适配了4种handler关系图如下。

举个例子,如果说我们写了一个Servlet,然后继承HttpServlet此时如果不配置SimpleServletHandlerAdapter适配器,那么就会报错,报500的异常。但是如果配置了这个适配器,那么SpringMVC给我们配置的默认适配器SimpleControllerHandlerAdapter就会失效。所以需要手动的配置一下。下面看一下源码即可验证这个说法。上面讲到返回一个拦截器链条。然后下面的代码就是获取适配器如下图。

点进getHandlerAdapter方法。这个方法里做了一个do-while循环。通过support方法判断是否是可用的适配器。

在下图的方法里判断是否是我们配置的适配器,如果是则返回true。 拿到适配器以后就开始了真正的调用。

此时适配器找到了以后就开始真正的调用handler也就是我们servlet或者是controller。然后通过一个叫ViewResolver的接口类去解析。其中有一个接口叫resolveViewName。他返回一个View。然后View里面有一个render接口。通过里面的model进行处理,封装HTML。通过response进行写入。所以它是先有ViewResolver才有的View。

然后看看View有哪些实现类如下图。

第一个AbstractTemplateViewResolver里面有以下两种视图支持,比如我们常用的FreeMarkerViewReslover。

第二个BeanNameViewResolver可以通过这种方式实现自定的视图解析器,生明一个自定义View类。然后实现View接口。在render里面去做返回。然后controller里面通过ModelAndView视图解析器的构造器去加载我们的自定义View类型。

第三个就是InternalResourceViewResolver。这个就是我们最常用的,通过配置前缀,后缀等信息去实现视图解析。

下图是通过BeanName的形式做的视图解析

最后附上一张整体的流程图

封面图源网络,侵权删除)

私信头条号,发送:“资料”,获取更多“秘制” 精品学习资料

如有收获,请帮忙转发,您的鼓励是作者最大的动力,谢谢!

一大波微服务、分布式、高并发、高可用的原创系列文章正在路上,

欢迎关注头条号:java小马哥

周一至周日早九点半!下午三点半!精品技术文章准时送上!!!

十余年BAT架构经验倾囊相授


对于 Web 应用程序而言,我们从浏览器发起一个请求,请求经过一系列的分发和处理,最终会进入到我们指定的方法之中,这一系列的的具体流程到底是怎么样的呢?

Spring MVC 请求流程

记得在初入职场的时候,面试前经常会背一背 Spring MVC 流程,印象最深的就是一个请求最先会经过 DispatcherServlet 进行分发处理,DispatcherServlet 就是我们 Spring MVC 的入口类,下面就是一个请求的大致流转流程(图片参考自 Spring In Action):

  1. 一个请求过来之后会到达 DispatcherServlet,但是 DispatcherServlet 也并不知道这个请求要去哪里。
  2. DispatcherServlet 收到请求之后会去查询处理器映射(HandlerMapping),从而根据浏览器发送过来的 URL 解析出请求最终应该调用哪个控制器。
  3. 到达对应控制器(Controller)之后,会完成一些逻辑处理,而且在处理完成之后会生成一些返回信息,也就是 Model,然后还需要选择对应的视图名。
  4. 将模型(Model)和视图(View)传递给对应的视图解析器(View Resolver),视图解析器会将模型和视图进行结合。
  5. 模型和视图结合之后就会得到一个完整的视图,最终将视图返回前端。

上面就是一个传统的完整的 Spring MVC 流程,为什么要说这是传统的流程呢?因为这个流程是用于前后端没有分离的时候,后台直接返回页面给浏览器进行渲染,而现在大部分应用都是前后端分离,后台直接生成一个 Json 字符串就直接返回前端,不需要经过视图解析器进行处理,也就是说前后端分离之后,流程就简化成了 1-2-3-4-7(其中第四步返回的一般是 Json 格式数据)。

Spring MVC 两大阶段

Spring MVC主要可以分为两大过程,一是初始化,二就是处理请求。初始化的过程主要就是将我们定义好的 RequestMapping 映射路径和 Controller 中的方法进行一一映射存储,这样当收到请求之后就可以处理请求调用对应的方法,从而响应请求。

初始化

初始化过程的入口方法是 DispatchServlet 的 init() 方法,而实际上 DispatchServlet 中并没有这个方法,所以我们就继续寻找父类,会发现 init 方法在其父类(FrameworkServlet)的父类 HttpServletBean 中。

HttpServletBean#init()

在这个方法中,首先会去家在一些 Servlet 相关配置(web.xml),然后会调用 initServletBean() 方法,这个方法是一个空的模板方法,业务逻辑由子类 FrameworkServlet 来实现。

FrameworkServlet#initServletBean

这个方法本身没有什么业务逻辑,主要是初始化 WebApplicationContext 对象,WebApplicationContext 继承自 ApplicationContext,主要是用来处理 web 应用的上下文。

FrameworkServlet#initWebApplicationContext

initWebApplicationContext() 方法主要就是为了找到一个上下文,找不到就会创建一个上下文,创建之后,最终会调用方法 configureAndRefreshWebApplicationContext(cwac) 方法,而这个方法最终在设置一些基本容器标识信息之后会去调用 refresh()方法,也就是初始化 ioc 容器。

当调用 refresh() 方法初始化 ioc 容器之后,最终会调用方法 onRefresh(),这个方法也是一个模板钩子方法,由子类实现,也就是回到了我们 Spring MVC 的入口类 DispatcherServlet。

DispatchServlet#onRefresh

onRefresh() 方法就是 Spring MVC 初始化的最后一个步骤,在这个步骤当中会初始化 Spring MVC 流程中可能需要使用到的九大组件。

Spring MVC 九大组件

MultipartResolver

这个组件比较熟悉,主要就是用来处理文件上传请求,通过将普通的 Request 对象包装成 MultipartHttpServletRequest 对象来进行处理。

LocaleResolver

LocaleResolver 用于初始化本地语言环境,其从 Request 对象中解析出当前所处的语言环境,如中国大陆则会解析出 zh-CN 等等,模板解析以及国际化的时候都会用到本地语言环境。

ThemeResolver

这个主要是用户主题解析,在 Spring MVC 中,一套主题对应一个 .properties 文件,可以存放和当前主题相关的所有资源,如图片,css样式等。

HandlerMapping

用于查找处理器(Handler),比如我们 Controller 中的方法,这个其实最主要就是用来存储 url 和 调用方法的映射关系,存储好映射关系之后,后续有请求进来,就可以知道调用哪个 Controller 中的哪个方法,以及方法的参数是哪些。

HandlerAdapter

这是一个适配器,因为 Spring MVC 中支持很多种 Handler,但是最终将请求交给 Servlet 时,只能是 doService(req,resp) 形式,所以 HandlerAdapter 就是用来适配转换格式的。

HandlerExceptionResolver

这个组件主要是用来处理异常,不过看名字也很明显,这个只会对处理 Handler 时产生的异常进行处理,然后会根据异常设置对应的 ModelAndView,然后交给 Render 渲染成页面。

RequestToViewNameTranslator

这个主键主要是从 Request 中获取到视图名称。

ViewResolver

这个组件会依赖于 RequestToViewNameTranslator 组件获取到的视图名称,因为视图名称是字符串格式,所以这里会将字符串格式的视图名称转换成为 View 类型视图,最终经过一系列解析和变量替换等操作返回一个页面到前端。

FlashMapManager

这个主键主要是用来管理 FlashMap,那么 FlashMap 又有什么用呢?要明白这个那就不得不提到重定向了,有时候我们提交一个请求的时候会需要重定向,那么假如参数过多或者说我们不想把参数拼接到 url 上(比如敏感数据之类的),这时候怎么办呢?因为参数不拼接在 url 上重定向是无法携带参数的。

FlashMap 就是为了解决这个问题,我们可以在请求发生重定向之前,将参数写入 request 的属性 OUTPUT_FLASH_MAP_ATTRIBUTE 中,这样在重定向之后的 handler 中,Spring 会自动将其设置到 Model 中,这样就可以从 Model 中取到我们传递的参数了。

处理请求

在九大组件初始化完成之后,Spring MVC 的初始化就完成了,接下来就是接收并处理请求了,那么处理请求的入口在哪里呢?处理请求的入口方法就是 DispatcherServlet 中的 doService 方法,而 doService 方法又会调用 doDispatch 方法。

DispatcherServlet#doDispatch

这个方法最关键的就是调用了 getHandler 方法,这个方法就是会获取到前面九大组件中的 HandlerMapping,然后进行反射调用对应的方法完成请求,完成请求之后后续还会经过视图转换之类的一些操作,最终返回 ModelAndView,不过现在都是前后端分离,基本也不需要用到视图模型,在这里我们就不分析后续过程,主要就是分析 HandlerMapping 的初始化和查询过程。

DispatcherServlet#getHandler

这个方法里面会遍历 handllerMappings,这个 handllerMappings 是一个 List 集合,因为 HandlerMapping 有多重实现,也就是 HandlerMapping 不止一个实现,其最常用的两个实现为 RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping。

AbstractHandlerMapping#getHandler

AbstractHandlerMapping 是一个抽象类,其 getHandlerInternal 这个方法也是一个模板方法:

getHandlerInternal 方法最终其会调用子类实现,而这里的子类实现会有多个,其中最主要的就是 AbstractHandlerMethodMapping 和 AbstractUrlHandlerMapping 两个抽象类,那么最终到底会调用哪个实现类呢?

这时候如果拿捏不准我们就可以看一下类图,上面我们提到,HandlerMapper 有两个非常主要的实现类:RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping。那么我们就分别来看一下这两个类的类图关系:

可以看到,这两个实现类的抽象父类正好对应了 AbstractHandlerMapping 的两个子类,所以这时候具体看哪个方法,那就看我们想看哪种类型了。

  • RequestMappingHandlerMapping:主要用来存储 RequestMapping 注解相关的控制器和 url 的映射关系。
  • BeanNameUrlHandlerMapping:主要用来处理 Bean name 直接以 / 开头的控制器和 url 的映射关系。

其实除了这两种 HandlerMapping 之外,Spring 中还有其他一些 HandllerMapping,如 SimpleUrlHandlerMapping 等。

提到的这几种 HandlerMapping,对我们来说最常用,最熟悉的那肯定就是 RequestMappingHandlerMapping ,在这里我们就以这个为例来进行分析,所以我们应该

AbstractHandlerMethodMapping#getHandlerInternal

这个方法本身也没有什么逻辑,其主要的核心查找 Handler 逻辑在 lookupHandlerMethod 方法中,这个方法主要是为了获取一个 HandlerMethod 对象,前面的方法都是 Object,而到这里变成了 HandlerMethod 类型,这是因为 Handler 有各种类型,目前我们已经基本跟到了具体类型之下,所以类型就变成了具体类型,而如果我们看的的另一条分支线,那么返回的就会是其他对象,正是因为支持多种不同类型的 HandlerMapping 对象,所以最终为了统一执行,才会需要在获得 Hanlder 之后,DispatcherServlet 中会再次通过调用 getHandlerAdapter 方法来进一步封装成 HandlerAdapter 对象,才能进行方法的调用

AbstractHandlerMethodMapping#lookupHandlerMethod

这个方法主要会从 mappingRegistry 中获取命中的方法,获取之后还会经过一系列的判断比较判断比较,因为有些 url 会对应多个方法,而方法的请求类型不同,比如一个 GET 方法,一个 POST 方法,或者其他一些属性不相同等等,都会导致最终命中到不同的方法,这些逻辑主要都是在 addMatchingMappings 方法去进一步实现,并最终将命中的结果加入到 matches 集合内。

在这个方法中,有一个对象非常关键,那就是 mappingRegistry,因为最终我们根据 url 到这里获取到对应的 HandlerMtthod,所以这个对象很关键:

看这个对象其实很明显可以看出来,这个对象其实只是维护了一些 Map 对象,所以我们可以很容易猜测到,一定在某一个地方,将 url 和 HandlerMapping 或者 HandlerMethod 的映射关系存进来了,这时候其实我们可以根据 getMappingsByUrl 方法来进行反推,看看 urlLookup 这个 Map 是什么时候被存入的,结合上面的类图关系,一路反推,很容易就可以找到这个 Map 中的映射关系是 AbstractHandlerMethodMapping 对象的 afterPropertiesSet 方法实现的(AbstractHandlerMethodMapping 实现了 InitializingBean 接口),也就是当这个对象初始化完成之后,我们的 url 和 Handler 映射关系已经存入了 MappingRegistry 对象中的集合 Map 中。

AbstractHandlerMethodMapping 的初始化

afterPropertiesSet 方法中并没有任何逻辑,而是直接调用了 initHandlerMethods。

AbstractHandlerMethodMapping#initHandlerMethods

initHandlerMethods 方法中,首先还是会从 Spring 的上下文中获取所有的 Bean,然后会进一步从带有 RequestMapping 注解和 Controller 注解中的 Bean 去解析并获得 HandlerMethod。

AbstractHandlerMethodMapping#detectHandlerMethods

这个方法中,其实就是通过反射获取到 Controller 中的所有方法,然后调用 registerHandlerMethod 方法将相关信息注册到 MappingRegistry 对象中的各种 Map 集合之内:

AbstractHandlerMethodMapping#register

registerHandlerMethod 方法中会直接调用 AbstractHandlerMethodMapping 对象持有的 mappingRegistry 对象中的 regidter方法,这里会对 Controller 中方法上的一些元信息进行各种解析,比如参数,路径,请求方式等等,然后会将各种信息注册到对应的 Map 集合中,最终完成了整个初始化。

总结

本文重点以 RequestMappingHandlerMapping 为例子分析了在 Spring 当中如何初始化 HandlerMethod,并最终在调用的时候又是如何根据 url 获取到对应的方法并进行执行最终完成整个流程。