SpringMVC实战与源码分析

SpringMVC的基本概念

MVC模型

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,是一种用于设计创建 Web 应用程序表现层的模式。
enter description here

SpringMVC介绍

SpringMVC是一种基于Java的实现MVC设计模型的请求驱动类型的轻量级Web框架,属于Spring FrameWork的后续产品,已经融合在SpringWeb Flow里面。Spring框架提供了构建 Web 应用程序的全功能 MVC 模块。使用Spring可插入的MVC架构,从而在使用Spring进行WEB开发时,可以选择使用Spring的Spring MVC框架或集成其他MVC开发框架,如Struts1(现在一般不用),Struts2等。 SpringMVC已经成为目前最主流的 MVC 框架之一,并且随着Spring3.0的发布,全面超越 Struts2,成为最优秀的 MVC 框架。它通过一套注解,让一个简单的Java类成为处理请求的控制器,而无须实现任何接口。同时它还支持RESTful编程风格的请求。

  • 前端控制器(DispatcherServlet)
  • 请求到处理器映射(HandlerMapping)
  • 处理器适配器(HandlerAdapter)
  • 视图解析器(ViewResolver)
  • 处理器或页面控制器(Controller)
  • 验证器( Validator)
  • 命令对象(Command 请求参数绑定到的对象就叫命令对象)
  • 表单对象(Form Object 提供给表单展示和提交到的对象就叫表单对象)

SpringMVC与Struts2比较

共同点:

  • 它们都是表现层框架,都是基于MVC模型编写的;
  • 它们的底层都离不开原始ServletAPI;
  • 它们处理请求的机制都是一个核心控制器;

区别:

  • Spring MVC的入口是Servlet,而Struts2是Filter
  • Spring MVC是基于方法设计的(单例),而Struts2是基于类,Struts2每次执行都会创建一个动作类(多例)。所以Spring MVC会稍微比Struts2快些。
  • Spring MVC使用更加简洁,同时还支持 JSR303,处理ajax的请求更方便 (JSR303 是一套JavaBean参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。)
  • Struts2的OGNL表达式使页面的开发效率相比Spring MVC更高些,但执行效率并没有比JSTL提升,尤其是struts2的表单标签,远没有html执行效率高。

入门Demo

enter description here

准备

利用Maven搭建一个webapp项目并将目录补全,并在pom.xml中引入坐标:

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <spring.version>5.0.2.RELEASE</spring.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.0</version>
      <scope>provided</scope>
    </dependency>

  </dependencies>

配置

配置核心控制器,在web.xml中:

  <servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

在resources目录下创建SpringMVC的配置文件springmvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       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
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启注解扫描-->
    <context:component-scan base-package="top.tjtulong"/>

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

    <!-- 开启SpringMVC框架注解的支持 -->
    <mvc:annotation-driven/>

</beans>

Controller

创建处理请求的类Controller,用注解自动注入:

// 控制器类
@Controller
public class HelloController {

    @RequestMapping(path = "/hello")
    public String sayHello() {
        System.out.println("Hello SpringMVC");
        return "success";
    }
}

主页面index.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Demo</title>
</head>
<body>
<h3>入门程序</h3>
<a href="hello">入门程序</a>
</body>
</html>

成功页面WEB-INF/pages/success.jsp

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

于是在主页面点击超链接后自动跳转到success.jsp页面

总结

enter description here
基于组件方式:
enter description here

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

使用<mvc:annotation-driven>自动加载RequestMappingHandlerMapping(处理映射器)和RequestMappingHandlerAdapter(处理适配器),可用在SpringMVC.xml配置文件中使用<mvc:annotation-driven>替代注解处理器和适配器的配置。

RequestMapping注解

@RequestMapping注解用于建立请求URL和处理请求方法之间的对应关系。

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

出现位置

  1. 类上: 请求URL的第一级访问目录,此处不写的话,就相当于应用的根目录,写的话需要以/开头。 它出现的目的是为了使我们的URL可以按照模块化管理:
    例如: 账户模块:
  • /account/add
  • /account/update
  • /account/delete …
    account的RequsetMapping写在类上,使URL更加精细。
  1. 方法上: 请求URL的第二级访问目录。

属性

  • value:用于指定请求的URL,它和path属性的作用是一样的;
  • method:用于指定请求的方式;
  • params:用于指定限制请求参数的条件,它支持简单的表达式。要求请求参数的key和value必须和配置的一模一样。 例如:
    params = {"accountName"},表示请求参数必须有accountName
    params = {"moeny!100"},表示请求参数中money不能是100。
  • headers:用于指定限制请求消息头的条件。

注意: 以上四个属性只要出现2个或以上时,他们的关系是与的关系。

// 控制器类
@Controller
@RequestMapping(path = "/user")
public class HelloController {
    /**
     * RequestMapping注解
     * @return
     */
    @RequestMapping(path = "/testRequestMapping", method = {RequestMethod.GET},
            params = {"username=xiao"}, headers = {"Accept"})
    public String testRequestMapping() {
        System.out.println("test RequestMapping...");
        return "success";
    }
}

请求参数绑定

表单中请求参数都是基于key=value的。 SpringMVC绑定请求参数的过程是通过把表单提交请求参数,作为控制器中方法参数进行绑定的。

<a href="param/testParam?username=hehe&password=123">请求参数绑定</a>

请求参数为username,可以直接在Controller中的方法上绑定:

@Controller
@RequestMapping("/param")
public class ParamController {

    @RequestMapping("testParam")
    public String teatParam(String username) {
        System.out.println("执行了..." + username);
        return "success";
    }
}

绑定实体类型

创建一个pojo类型和pojo中的pojo:

public class Account implements Serializable {
    private String username;
    private String password;
    private Double money;

    private User user;

    public Double getMoney() {
        return money;
    }

    public void setMoney(Double money) {
        this.money = money;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    @Override
    public String toString() {
        return "Account{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", money=" + money +
                ", user=" + user +
                '}';
    }
}

内部pojo:User

public class User implements Serializable {
    private String uname;
    private Integer age;
}

在前端传入参数:

把数据封装Account类中
<form action="param/saveAccount" method="post">
    姓名:<input type="text" name="username" /><br/>
    密码:<input type="text" name="password" /><br/>
    金额:<input type="text" name="money" /><br/>
    用户姓名:<input type="text" name="user.uname" /><br/>
    用户年龄:<input type="text" name="user.age" /><br/>
    <input type="submit" value="提交" />
</form>

控制器代码:

    /**
     * 请求参数绑定到JavaBean的类中
     * @param account
     * @return
     */
    @RequestMapping("/saveAccount")
    public String saveAccount(Account account) {
        System.out.println(account);
        return "success";
    }

可以在控制台看到参数被绑定:

Account{username='xiao', password='111', money=234.0, user=User{uname='xiaoge', age=22}}

解决中文乱码

<!--配置解决中文乱码的过滤器-->
  <filter>
    <filter-name>characterEncodingFilter</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>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

绑定集合类型

public class Account implements Serializable{

    private String username;
    private String password;
    private Double money;

    private List<User> list;
    private Map<String,User> map;
}

把数据封装到Account,类中存在list和map集合:

    <form action="param/saveAccount" method="post">
        姓名:<input type="text" name="username" /><br/>
        密码:<input type="text" name="password" /><br/>
        金额:<input type="text" name="money" /><br/>

        用户姓名:<input type="text" name="list[0].uname" /><br/>
        用户年龄:<input type="text" name="list[0].age" /><br/>

        用户姓名:<input type="text" name="map['one'].uname" /><br/>
        用户年龄:<input type="text" name="map['one'].age" /><br/>
        <input type="submit" value="提交" />
    </form>

自定义类型转换器

SpringMVC可以自动将表单提交的字符串类型转换成各种类型。

但一些情况如果格式不正确会出现400错误

<a href="account/deleteAccount?date=2018-01-01">根据日期删除账户</a>

日期格式不正确,应该为2018/01/01才能自动类型转换为date。

配置一个自己的类型转换器StringToDateConverter:

/**
 * 把字符串转换日期
 */
public class StringToDateConverter implements Converter<String, Date> {

    /**
     * String source    传入进来字符串
     * @param source
     * @return
     */
    public Date convert(String source) {
        // 判断
        if (source == null) {
            throw new RuntimeException("请您传入数据");
        }
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd");

        try {
            // 把字符串转换日期
            return df.parse(source);
        } catch (Exception e) {
            throw new RuntimeException("数据类型转换出现错误");
        }
    }
}

在springmvc.xml中配置该转换器:

    <!-- 开启SpringMVC框架注解的支持 -->
    <mvc:annotation-driven conversion-service="conversionService"/>

    <!--配置自定义类型转换器-->
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="top.tjtulong.utils.StringToDateConverter"></bean>
            </set>
        </property>
    </bean>

获取Servlet原生API

把原生API直接写在控制的方法参数中使用即可。

     /**
     * 原生的API
     * @return
     */
    @RequestMapping("/testServlet")
    public String testServlet(HttpServletRequest request, HttpServletResponse response){
        System.out.println("执行了...");
        System.out.println(request);

        HttpSession session = request.getSession();
        System.out.println(session);

        ServletContext servletContext = session.getServletContext();
        System.out.println(servletContext);

        System.out.println(response);
        return "success";
    }

常用注解

RequestParam

当传入的参数名称与控制器方法不同时,必须使用@RequestParam注解

<a href="anno/testRequestParam?name=潇哥">RequestParam</a>

控制器:

/**
 * 常用注解
 */
@Controller
@RequestMapping("/anno")
public class AnnoController {

    @RequestMapping("/testRequestParam")
    public String testRequestParam(@RequestParam(name="name") String username){
        System.out.println("执行了...");
        return "success";
    }
}

RequestBody

作用: 用于获取请求体内容。直接使用得到是key=value&key=value…结构的数据。 get请求方式不适用。

属性: required:是否必须有请求体,默认值是:true。当取值为true时,get请求方式会报错。如果取值为false,get请求得到是null。

    /**
     * 获取到请求体的内容
     * @return
     */
    @RequestMapping("/testRequestBody")
    public String testRequestBody(@RequestBody String body){
        System.out.println("执行了...");
        System.out.println(body);
        return "success";
    }

PathVariable

restful风格

Restful(表述性状态转移)风格的URL:
请求路径一样,可以根据不同的请求方式去执行后台的不同方法。简单的说就是:URL定位资源,用HTTP动词(GET,POST,DELETE,DETC)描述操作。

核心思想就是每个资源都有它的状态,不同状态下,可对它进行的操作不一样。

restful风格的URL优点

  1. 结构清晰
  2. 符合标准
  3. 易于理解
  4. 扩展方便

PathVariable注解

作用:拥有绑定url中的占位符的。例如:url中有/delete/{id},{id}就是占位符。

<a href="user/hello/1">入门案例</a>

控制器:

    /**
     * PathVariable注解
     * @return
     */
    @RequestMapping(value="/testPathVariable/{sid}")
    public String testPathVariable(@PathVariable(name="sid") String id){
        System.out.println("执行了...");
        System.out.println(id);
        return "success";
    }

RequestHeader

作用:获取指定请求头的值
属性:value:请求头的名称

    /**
     * 获取请求头的值
     * @param header
     * @return
     */
    @RequestMapping(value = "/testRequestHeader")
    public String testRequestHeader(@RequestHeader(value = "Accept") String header, HttpServletRequest request, HttpServletResponse response) throws IOException {
        System.out.println("执行了...");
        System.out.println(header);
        return "success";
    }

CookieValue

作用:用于获取指定cookie的名称的值
属性:value:cookie的名称

    /**
     * 获取Cookie的值
     * @return
     */
    @RequestMapping(value="/testCookieValue")
    public String testCookieValue(@CookieValue(value="JSESSIONID") String cookieValue){
        System.out.println("执行了...");
        System.out.println(cookieValue);
        return "success";
    }

ModelAttribute

出现在方法上:表示当前方法会在控制器方法执行前线执行。
出现在参数上:获取指定的数据给参数赋值。

应用场景:当提交表单数据不是完整的实体数据时,保证没有提交的字段使用数据库原来的数据。

     /**
     * ModelAttribute注解
     * @return
     */
    @RequestMapping(value="/testModelAttribute")
    public String testModelAttribute(@ModelAttribute("abc") User user){
        System.out.println("testModelAttribute执行了...");
        System.out.println(user);
        return "success";
    }

    @ModelAttribute
    public void showUser(String uname, Map<String,User> map){
        System.out.println("showUser执行了...");
        // 通过用户查询数据库(模拟)
        User user = new User();
        user.setUname(uname);
        user.setAge(20);
        user.setDate(new Date());
        map.put("abc",user);
    }

showUser会先于testModelAttribute执行。

SeesionAttributes

用于多次执行控制器方法间的参数共享。

@Controller
@RequestMapping("/anno")
@SessionAttributes(value={"msg"})//把msg=xxx存到session域
public class AnnoController {
    /**
     * SessionAttributes的注解
     * @return
     */
    @RequestMapping(value="/testSessionAttributes")
    public String testSessionAttributes(Model model){
        System.out.println("testSessionAttributes...");
        // 底层会存储到request域对象中
        model.addAttribute("msg","美美");
        return "success";
    }
}

响应数据和结果视图

返回值

字符串

controller方法返回字符串可以指定逻辑视图名,通过视图解析器解析为物理视图地址

@Controller
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/testString")
    public String testString(Model model){
        System.out.println("testString方法执行了...");

        User user = new User();
        user.setUsername("xiao");
        user.setPassword("916521");
        user.setAge(22);

        model.addAttribute("user",user);

        return "success";
    }
}

可以通过EL表达式在jsp中输出Model中的pojo:

${user.username}
${user.password}
${user.age}

void

  1. 转发
@RequestMapping("/testVoid")
    public void testString(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("testVoid方法执行了...");

        //请求转发
        request.getRequestDispatcher("/WEB-INF/pages/success.jsp").forward(request, response);
        return;
    }
  1. 重定向:
response.sendRedirect("testRetrunString")
  1. 通过response 指定响应结果,例如响应json 数据:
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("json 串");

ModelAndView

ModelAndView是SpringMVC为我们提供的一个对象,该对象也可以用作控制器方法的返回值。

    @RequestMapping("/testModelAndView")
    public ModelAndView testModelAndView() {
        // 创建ModelAndView对象
        ModelAndView mv = new ModelAndView();
        System.out.println("testModelAndView方法执行了...");
        // 模拟从数据库中查询出User对象
        User user = new User();
        user.setUsername("小凤");
        user.setPassword("456");
        user.setAge(30);

        // 把user对象存储到mv对象中,底层也会把user对象存入到request对象
        mv.addObject("user", user);

        // 跳转到哪个页面
        mv.setViewName("success");

        return mv;
    }

返回ModelAndView类型时,浏览器跳转只能是请求转发

转发和重定向

     /**
     * 使用关键字的方式进行转发或者重定向
     * @return
     */
    @RequestMapping("/testForwardOrRedirect")
    public String testForwardOrRedirect(){
        System.out.println("testForwardOrRedirect方法执行了...");

        // 请求的转发
        // return "forward:/WEB-INF/pages/success.jsp";

        // 重定向
        return "redirect:/index.jsp";
    }

ResponseBody响应json数据

用jQuery模拟ajax请求,首先需要放行jQuery的静态资源:

<!--前端控制器,哪些静态资源不拦截-->
    <mvc:resources mapping="/js/**" location="/js/"/>

发送Ajax请求:

    <script src="js/jquery.min.js"></script>
    <script>
        //页面加载,绑定单击事件
        $(function () {
            $("#btn").click(function () {
                //alert("hello btn");
                // 发送ajax请求
                $.ajax({
                    // 编写json格式,设置属性和值
                    url:"user/testAjax",
                    contentType:"application/json;charset=UTF-8",
                    data:'{"username":"hehe","password":"123","age":30}',
                    dataType:"json",
                    type:"post",
                    success:function(data){
                        // data服务器端响应的json的数据,进行解析
                        alert(data);
                        alert(data.username);
                        alert(data.password);
                        alert(data.age);
                    }
                });

            });
        });
    </script>

控制器收到json并自动解析成实体类,再将返回对象转换为json格式:

    /**
     * 模拟异步请求响应
     */
    @RequestMapping("/testAjax")
    public @ResponseBody User testAjax(@RequestBody User user){
        System.out.println("testAjax方法执行了...");
        // 客户端发送ajax的请求,传的是json字符串,后端把json字符串封装到user对象中
        System.out.println(user);
        // 做响应,模拟查询数据库
        user.setUsername("haha");
        user.setAge(40);
        // 做响应
        return user;
    }

文件上传

文件上传的前提

  • form表单的enctype取值必须是:multipart/form-data (默认值是:application/x-www-form-urlencoded) enctype:是表单请求正文的类型
  • method属性取值必须是Post
  • 提供一个文件选择域<input type=”file” />

引入依赖:

    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.3.1</version>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.4</version>
    </dependency>

原理

enter description here

控制器代码

前端:

<form action="/file/fileUpLoad" method="post" enctype="multipart/form-data">
    选择文件:<input type="file" name="upload"/><br/>
    <input type="submit" value="上传"/>
</form>

控制器:

    /**
     * SpringMVC文件上传
     * @return
     */
    @RequestMapping("/fileupload2")
    public String fileuoload2(HttpServletRequest request, MultipartFile upload) throws Exception {
        System.out.println("springmvc文件上传...");

        // 使用fileupload组件完成文件上传
        // 上传的位置
        String path = request.getSession().getServletContext().getRealPath("/uploads/");
        // 判断,该路径是否存在
        File file = new File(path);
        if(!file.exists()){
            // 创建该文件夹
            file.mkdirs();
        }

        // 说明上传文件项
        // 获取上传文件的名称
        String filename = upload.getOriginalFilename();
        // 把文件的名称设置唯一值,uuid
        String uuid = UUID.randomUUID().toString().replace("-", "");
        filename = uuid+"_"+filename;
        // 完成文件上传
        upload.transferTo(new File(path,filename));

        return "success";
    }

跨服务器上传文件

应用服务器-------->图片服务器

    /**
     * 跨服务器文件上传
     * @return
     */
    @RequestMapping("/fileupload3")
    public String fileuoload3(MultipartFile upload) throws Exception {
        System.out.println("跨服务器文件上传...");

        // 定义上传文件服务器路径
        String path = "http://localhost:9090/uploads/";

        // 说明上传文件项
        // 获取上传文件的名称
        String filename = upload.getOriginalFilename();
        // 把文件的名称设置唯一值,uuid
        String uuid = UUID.randomUUID().toString().replace("-", "");
        filename = uuid+"_"+filename;

        // 创建客户端的对象
        Client client = Client.create();

        // 和图片服务器进行连接
        WebResource webResource = client.resource(path + filename);

        // 上传文件
        webResource.put(upload.getBytes());

        return "success";
    }

SpringMVC的异常处理

异常处理思路:Controller调用service,service调用dao,异常都是向上抛出的,最终由DispatcherServlet找异常处理器进行异常的处理。

enter description here

首先自定义异常类:

/**
 * 自定义异常类
 */
public class SysException extends Exception{
    //存储提示信息
    private String message;

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public SysException(String message) {
        this.message = message;
    }
}

自定义异常处理器:

/**
 * 异常处理器
 */
public class SysExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
        //获取到异常对象
        SysException exception = null;
        if (e instanceof SysException) {
            exception = (SysException) e;
        } else {
            e = new SysException("系统正在维护");
        }

        ModelAndView mv = new ModelAndView();
        mv.addObject("errorMsg",e.getMessage());
        mv.setViewName("error");
        return mv;
    }
}

对异常处理器组件进行注册:

<!--配置异常处理器-->
    <bean id="sysExceptionResolver" class="top.tjtulong.exception.SysExceptionResolver"></bean>

这样配置后,当服务器再次出现异常时,可直接显示异常信息:
${errorMsg}

拦截器

SpringMVC框架中的拦截器用于对处理器进行预处理和后处理的技术。

可以定义拦截器链,连接器链就是将拦截器按着一定的顺序结成一条链,在访问被拦截的方法时,拦截器链中的拦截器会按着定义的顺序执行。

拦截器和过滤器的功能比较类似,也有区别

  1. 过滤器是Servlet规范的一部分,任何框架都可以使用过滤器技术。
  2. 拦截器是SpringMVC框架独有的。
  3. 过滤器配置了/* ,可以拦截任何资源。
  4. 拦截器只会对控制器中的方法进行拦截。

创建类,实现HandlerInterceptor接口,重写需要的方法:

/**
* 自定义拦截器1
* @author rt
*/
public class MyInterceptor1 implements HandlerInterceptor{
    /**
    * controller方法执行前,进行拦截的方法
    * return true放行
    * return false拦截
    * 可以使用转发或者重定向直接跳转到指定的页面。
    */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {
        System.out.println("拦截器执行了...");
        return true;
    }
}

配置拦截器:

<!-- 配置拦截器 -->
<mvc:interceptors>
    <mvc:interceptor>
        <!-- 哪些方法进行拦截 -->
        <mvc:mapping path="/user/*"/>
        <!-- 哪些方法不进行拦截-->
        <mvc:exclude-mapping path=""/>
        <!-- 注册拦截器对象 -->
        <bean class="top.tjtulong.demo1.MyInterceptor1"/>
    </mvc:interceptor>
</mvc:interceptors>

三个拦截器方法

  1. preHandle方法是controller方法执行前拦截的方法
  • 可以使用request或者response跳转到指定的页面
  • return true放行,执行下一个拦截器,如果没有拦截器,执行controller中的方法。
  • return false不放行,不会执行controller中的方法。
  1. postHandle是controller方法执行后执行的方法,在JSP视图执行前。
  • 可以使用request或者response跳转到指定的页面
  • 如果指定了跳转的页面,那么controller方法跳转的页面将不会显示。
  1. postHandle方法是在JSP执行后执行
  • request或者response不能再跳转页面了

拦截器链

    <!--配置拦截器-->
    <mvc:interceptors>
        <!--配置拦截器-->
        <mvc:interceptor>
            <!--要拦截的具体的方法-->
            <mvc:mapping path="/user/*"/>
            <!--不要拦截的方法
            <mvc:exclude-mapping path=""/>
            -->
            <!--配置拦截器对象-->
            <bean class="top.tjtulong.controller.cn.itcast.interceptor.MyInterceptor1" />
        </mvc:interceptor>

        <!--配置第二个拦截器-->
        <mvc:interceptor>
            <!--要拦截的具体的方法-->
            <mvc:mapping path="/**"/>
            <!--不要拦截的方法
            <mvc:exclude-mapping path=""/>
            -->
            <!--配置拦截器对象-->
            <bean class="top.tjtulong.controller.cn.itcast.interceptor.MyInterceptor2" />
        </mvc:interceptor>
    </mvc:interceptors>

补充:一个web应用部署在Tomcat下的目录结构

webapps
  |- hello
    |-index.html
    |-WEB-INF
      |-web.xml
      |-classes
      |-lib
  • hello:应用目录,hello就是应用的名称(在IDEA中创建项目时是web目录,执行时自动改名为项目名);
  • index.html:应用资源。应用下可以有多个资源,例如css、js、html、jsp等,也可以把资源放到文件夹中,例如:hello\html\index.html,这时访问URL为:http://localhost:8080/hello/html/index.html;
  • WEB-INF:这个目录名称必须是大写,这个目录下的东西是无法通过浏览器直接访问的,也就是说放到这里的东西是安全的;
  • web.xml:应用程序的部署描述符文件,可以在该文件中对应用进行配置;
  • classes:存放class文件的目录;
  • lib:存放jar包的目录。

SpringMVC源码分析

参考:田小波的博客

一个请求的流程

enter description here

首先,用户的浏览器发出了一个请求,这个请求经过互联网到达了我们的服务器。Servlet容器首先接待了这个请求,并将该请求委托给前端控制器DispatcherServlet进行处理。接着DispatcherServlet将该请求传给了处理器映射组件HandlerMapping,并获取到适合该请求的拦截器和处理器。在获取到处理器后,DispatcherServlet还不能直接调用处理器的逻辑,需要进行对处理器进行适配。处理器适配成功后,DispatcherServlet通过处理器适配器HandlerAdapter调用处理器的逻辑,并获取返回值ModelAndView。之后,DispatcherServlet需要根据ModelAndView解析视图。解析视图的工作由ViewResolver完成,若能解析成功,ViewResolver会返回相应的视图对象View。在获取到具体的View对象后,最后一步要做的事情就是由View渲染视图,并将渲染结果返回给用户。

在请求过程中要经过许多组件:
enter description here
从上面的流程中可以看出,Spring MVC对各个组件的职责划分的比较清晰。DispatcherServlet负责协调,其他组件则各自做分内之事,互不干扰。经过这样的职责划分,代码会便于维护。

DispatcherServlet

HttpServlet

HttpServlet从名字上就可看出,这个类是和HTTP协议相关。该类的关注点在于怎么处理HTTP请求,比如其定义了doGet方法处理GET类型的请求,定义了doPost方法处理POST类型的请求等。我们若需要基于Servlet写Web应用,应继承该类,并覆盖指定的方法。doGet和doPost等方法并不是处理的入口方法,所以这些方法需要由service方法调用才行,这个方法的实现如下:

@Override
public void service(ServletRequest req, ServletResponse res)
    throws ServletException, IOException {
    HttpServletRequest  request;
    HttpServletResponse response;
    
    if (!(req instanceof HttpServletRequest &&
            res instanceof HttpServletResponse)) {
        throw new ServletException("non-HTTP request or response");
    }

    request = (HttpServletRequest) req;
    response = (HttpServletResponse) res;

    // 调用重载方法,该重载方法接受 HttpServletRequest 和 HttpServletResponse 类型的参数
    service(request, response);
}

protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
    String method = req.getMethod();

    // 处理 GET 请求
    if (method.equals(METHOD_GET)) {
        long lastModified = getLastModified(req);
        if (lastModified == -1) {
            // 调用 doGet 方法
            doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            if (ifModifiedSince < lastModified) {
                maybeSetLastModified(resp, lastModified);
                doGet(req, resp);
            } else {
                resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            }
        }

    // 处理 HEAD 请求
    } else if (method.equals(METHOD_HEAD)) {
        long lastModified = getLastModified(req);
        maybeSetLastModified(resp, lastModified);
        doHead(req, resp);

    // 处理 POST 请求
    } else if (method.equals(METHOD_POST)) {
        // 调用 doPost 方法
        doPost(req, resp);
    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);
    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);
    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req,resp);
    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req,resp);
    } else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[1];
        errArgs[0] = method;
        errMsg = MessageFormat.format(errMsg, errArgs);
        
        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

第二个 service 方法中,该方法根据请求类型分发请求。我们可以根据需要覆盖指定的处理方法。

DispatcherServlet族谱

继承关系图:
enter description here

  • Aware
    在Spring中,Aware类型的接口用于向Spring“索要”一些框架中的信息。比如当某个bean实现了ApplicationContextAware接口时,Spring在运行时会将当前的ApplicationContext实例通过接口方法setApplicationContext传给该bean。
  • EnvironmentCapable
    EnvironmentCapable仅包含一个方法定义getEnvironment,通过该方法可以获取到环境变量对象。
  • HttpServletBean
    HttpServletBean 是 HttpServlet 抽象类的简单拓展。HttpServletBean 覆写了父类中的无参 init 方法,并在该方法中将 ServletConfig 里的配置信息设置到子类对象中,比如 DispatcherServlet。
  • FrameworkServlet
    FrameworkServlet是Spring Web框架中的一个基础类,该类会在初始化时创建一个容器。同时该类覆写了doGet、doPost等方法,并将所有类型的请求委托给doService方法去处理。doService是一个抽象方法,需要子类实现。
  • DispatcherServlet
    DispatcherServlet协调各个组件工作。除此之外,DispatcherServlet还有一个重要的事情要做,即初始化各种组件,比如HandlerMapping、HandlerAdapter等

DispatcherServlet源码简析

enter description here

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // 获取可处理当前请求的处理器 Handler,对应流程图中的步骤②
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null || mappedHandler.getHandler() == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // 获取可执行处理器逻辑的适配器 HandlerAdapter,对应步骤③
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // 处理 last-modified 消息头
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (logger.isDebugEnabled()) {
                    logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                }
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }

            // 执行拦截器 preHandle 方法
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // 调用处理器逻辑,对应步骤④
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            // 如果 controller 未返回 view 名称,这里生成默认的 view 名称
            applyDefaultViewName(processedRequest, mv);

            // 执行拦截器 postHandle 方法
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        
        // 解析并渲染视图
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
        HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {

    boolean errorView = false;

    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }

    if (mv != null && !mv.wasCleared()) {
        // 渲染视图
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        if (logger.isDebugEnabled()) {...
    }

    if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        return;
    }

    if (mappedHandler != null) {
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    Locale locale = this.localeResolver.resolveLocale(request);
    response.setLocale(locale);

    View view;
    /*
     * 若 mv 中的 view 是 String 类型,即处理器返回的是模板名称,
     * 这里将其解析为具体的 View 对象
     */ 
    if (mv.isReference()) {
        // 解析视图,对应步骤⑤
        view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
        if (view == null) {
            throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                    "' in servlet with name '" + getServletName() + "'");
        }
    }
    else {
        view = mv.getView();
        if (view == null) {
            throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
                    "View object in servlet with name '" + getServletName() + "'");
        }
    }

    if (logger.isDebugEnabled()) {...}
    try {
        if (mv.getStatus() != null) {
            response.setStatus(mv.getStatus().value());
        }
        // 渲染视图,并将结果返回给用户。对应步骤⑥和⑦
        view.render(mv.getModelInternal(), request, response);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {...}
        throw ex;
    }
}

handlerAdapter

handlerAdapter对应的是Controller,有三种最常用的HandlerAdapter,分别是:

  1. RequestMappingHandlerAdapter
  2. HttpRequestHandlerAdapter
  3. SimpleControllerHandlerAdapter

RequestMappingHandlerAdapter主要是适配注解类处理器,注解类处理器就是我们经常使用的@Controller的这类处理器。

HttpRequestHandlerAdapter主要是适配静态资源处理器,静态资源处理器就是实现了HttpRequestHandler接口的处理器,这类处理器的作用是处理通过SpringMVC来访问的静态资源的请求。

SimpleControllerHandlerAdapter是Controller处理适配器,适配实现了Controller接口或Controller接口子类的处理器。

对于实现Controller接口的控制类,执行接口方法即可,对于注解方法的处理器,需要通过反射得到参数名,再映射参数,再执行(invoke)方法

猜你喜欢

转载自blog.csdn.net/TJtulong/article/details/106237854