SpringMVC

目录

1、Spring MVC的介绍与执行流程

1.1 MVC模式介绍

MVC模式的全名是Model-View-Controller,即模型(Model )-视图(View )-控制器(Controller)的缩写。首先要明确的一点是:MVC模式它不是类,也不是什么框架,它只是一种开发的设计思想。将业务逻辑、数据处理、界面显示分别抽取出来统一放到一个地方,从而使同一个程序可以使用不同的表现形式。

所以MVC的的程序分为三个核心的模块,这三个模块的详细介绍如下:

  • 模型(Model):负责封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。模型层有对数据直接访问的权力,例如对数据库的访问。它不关心它会如何被视图层显示或被控制器调用,它只接受数据并处理,然后返回一个结果。
  • 视图(View):负责应用程序对用户的显示,它从用户那里获取输入数据并通过控制层传给业务层处理,然后再通过控制层获取业务层返回的结果并显示给用户。
  • 控制器(Controller):负责控制应用程序的流程,它接收从视图层传过来的数据,然后选择Model层中的某个业务来处理,接收Model层返回的结果并选择视图层中的某个视图来显示结果。

MVC模式在实际编程中的设计步骤:

模块 设计层面 可选框架
模型(Model) service+dao+entity jdbc,hibernate,mybatis
视图(View) webapp页面 jsp,thymeleaf,vue
控制器(Controller) controller servlet,structs2,springmvc

1.2 什么是Spring MVC

Spring MVC属于SpringFrameWork的后续产品,已经融合在Spring Web Flow里面。Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块。使用 Spring 可插入的 MVC 架构,从而在使用Spring进行WEB开发时,可以选择使用Spring的Spring MVC框架或集成其他MVC开发框架。我们知道框架的目的就是帮助我们简化开发,而Spring MVC也是要简化我们日常Web开发的。SpringMVC提供了总开关DispatcherServlet;请求处理器映射器(Handler Mapping)和处理适配器(Handler Adapter),视图解析器(View Resolver)进行视图管理;动作处理器Controller接口(包含ModelAndView,以及处理请求响应对象请求和响应),配置灵活,支持文件上传,数据简单转化等强大功能。

SpringMVC现在已经成为目前最主流的MVC框架之一,并随着Spring的一直优化更新,已经将Struts2框架淘汰了,继而成为最优秀的MVC框架。SpringMVC通过一套注解,让一个简单的Java类成为处理请求的控制器,而无需实现任何接口。同时它还支持RESTful编程风格的请求。

1.3 Spring MVC的特点

  1. 轻量级,简单易学,简洁灵活
  2. 高效,基于请求响应的MVC框架,底层封装了Servlet
  3. 进行更简洁的 Web 层的开发
  4. 能简单的进行 Web 层的单元测试
  5. 与Spring兼容性好,天生与 Spring 框架集成(如 IoC 容器、AOP 等)
  6. 提供强大的约定大于配置的契约式编程支持
  7. 功能强大:支持RESTful风格、灵活的数据验证、灵活的本地化、格式化和数据绑定机制等
  8. 能使用任何对象进行数据绑定,不必实现特定框架的 API
  9. 对静态资源的支持
  10. 更加简单的异常处理
  11. 支持灵活的URL到页面控制器的映射
  12. 非常容易与其他视图技术集成,如 Thymeleaf、FreeMarker 等等,因为模型数据不放在特定的 API 里,而是放在一个 Model 里(Map 数据结构实现,因此很容易被其他框架使用)

1.4 Spring MVC的执行流程(非常重要)

下图为一个较完整的SpringMVC工作执行流程图。Spring MVC的执行流程非常的重要,因为不出意外的话面试官考察SpringMVC的知识点99%都会问这个,所以我们必须对SpringMVC的执行流程非常熟悉。

17

补充在SpringMVC中的逻辑视图与物理视图:

  • 逻辑视图:可以是ModelAndView的setViewName("success")中的success,也可以是return "success",这时设置跳转的success页面都是逻辑视图。
  • 物理视图:它是实际的页面路径了,如:/WEB-INF/pages/success.jsp

具体的执行流程介绍:

  1. 用户在浏览器发送请求到中央调度器DispatcherServlet进行处理
  2. 中央调度器DispatcherServlet收到请求后,将请求转发给处理器映射器HandlerMapping
  3. 处理器映射器HandlerMapping根据request请求的URL等信息找到能够进行处理的Handler、以及相关的拦截器interceptor,并构造HandlerExecutionChain执行链,然后将构造好的执行链对象返回给中央调度器DispatcherServlet,执行链包含一个处理器对象和一个或多个拦截器。
  4. 中央调度器DispatcherServlet根据处理器执行链的处理器,能够找到对应的处理器适配器HandlerAdapter
  5. 处理器适配器HandlerAdapter调用相应的处理器Handler,即我们写的具体的Controller方法执行
  6. Controller处理完后返回ModelAndView给HandlerAdapter(ModelAndView表示SpringMVC的封装对象,将model和view封装在一起)。
  7. 处理器HandlerAdapter将Controller执行结果ModelAndView返回给中央调度器DispatcherServlet
  8. 中央调度器DispatcherServlet调用试图解析器ViewReslover处理ModelAndView
  9. 视图解析器ViewReslover解析后根据逻辑视图名解析成物理视图名即具体的页面地址,生成并返回具体对象View给中央调度器DispatcherServlet(springmvc封装对象,是一个接口)。
  10. 中央调度器DispatcherServlet调用视图对象,让其自己进行渲染,即进行数据填充,形成响应对象(即将模型数据model填充至视图中)
  11. 最后中央调度器DispatcherServlet向用户返回响应

如果上面这个图还有点不懂的话,可以参考下面这个时序图。

image

1.5 Spring MVC的核心组件说明

  1. DispatcherServlet:中央调度器,也称前端控制器,在MVC架构中相当于C,即控制层,它是整个流程的控制中心,由它调用其它组件处理用户的请求,中央调度器的存在降低了组件之间的耦合性,系统扩展性提高,由框架实现。
  2. HandlerMapping:处理器映射器,负责根据用户请求url找到要执行的Handler,springmvc提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。由框架实现。
  3. Handler(需要程序员开发具体的Controller):处理器,Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。由于Handler涉及到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发Handler。
  4. HandlerAdapter:处理器适配器,通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。由框架实现。
  5. ViewResolver:视图解析器,View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。由框架实现。
  6. View(需要程序员开发jsp等页面):视图,springmvc框架提供了很多的View视图类型的支持,包括:jsp、jstlView、freemarkerView、pdfView等。一般需要我们根据业务需求开发具体的页面。

1.6 Spring MVC详细流程图

综上所述,总结下SpringMVC的详细流程图(图片来自网络,侵删):

image

参考资料:

2、SpringMVC的第一个案例

2.1 创建一个maven工程

首先创建一个webapp模块的Maven项目,如下:

image

创建好之后导入如下Maven依赖:

<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.2.7.RELEASE</spring.version>
</properties>
<dependencies>
    <!--Spring相关依赖-->
    <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>
    <!--Servlet相关依赖-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>
    <!--jsp相关依赖-->
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.0</version>
        <scope>provided</scope>
    </dependency>
    <!--单元测试的依赖-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
</dependencies>

创建后项目的整体目录如下:

image

2.2 在web.xml中配置DispatcherServlet

<?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">

    <!-- 配置编码过滤器,防止乱码 -->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <!--支持异步处理-->
        <async-supported>true</async-supported>
        <!-- 配置encoding,告诉指定的编码格式,这里设置为UTF-8 -->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <!-- 解决请求乱码 -->
        <init-param>
            <param-name>forceRequestEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
        <!-- 解决响应乱码 -->
        <init-param>
            <param-name>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 配置SpringMVC核心控制器 -->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

        <!-- 配置DispatcherServlet的初始化参数:读取springmvc.xml -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <!-- servlet启动时加载 -->
        <load-on-startup>1</load-on-startup>
        <!--支持异步处理-->
        <async-supported>true</async-supported>
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <!--
        /* 匹配所有资源(永远不要这样写)
        /  匹配所有请求(推荐) 除了jsp
        *.扩展名  匹配所有请求(推荐),例如*.do
        -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

注意:我们也可以不通过使用contextConfigLocation配置项来指定SpringMVC的配置文件,而使用默认的配置文件为: /WEB-INF/-servlet.xml,那么SpringMVC会自动去WEB-INF下面找一个叫 [servlet名字]-servlet.xml的文件,不推荐使用这种方式。

  • DispatcherServlet:它是前端核心控制器,主要负责请求的分派。前端的请求由这里分派给指定的处理器。
  • load-on-startup:当值为0或者大于0时,表示容器在应用启动时就加载这个servlet;当是一个负数时或者没有指定时,则指示容器在该servlet被调用时才加载。正数的值越小,启动该servlet的优先级越高。
  • url-pattern:表示拦截哪些请求, “/” 表示拦截所有请求。也可以如”.扩展名”表示拦截所有以扩展名结尾的请求。例如”.do”

2.3 创建spring-mvc.xml

包扫描,注册视图解析器

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

    <!-- 扫描指定包下的注解 -->
    <context:component-scan base-package="com.thr.controller"/>

    <!-- 配置注解驱动,它的主要的作用是:注册映射器HandlerMapping和适配器HandlerAdapter 两个类型的Bean -->
    <!--HandlerMapping的实现为实现类RequestMappingHandlerMapping,它会处理 @RequestMapping 注解,并将其注册到请求映射表中-->
    <!--HandlerAdapter的实现为实现类RequestMappingHandlerAdapter,它是处理请求的适配器,确定调用哪个类的哪个方法,并且构造方法参数,返回值 -->
    <!--在使用SpringMVC是一般都会加上该配置 -->
    <mvc:annotation-driven/>

    <!-- 注册视图解析器 -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- 配置前缀 -->
        <property name="prefix" value="/WEB-INF/pages/"/>
        <!-- 配置后缀 -->
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>
  • InternalResourceViewResolver解析器可以解析该资源。其主要职责是:根据处理器的返回值找到资源页面,然后装配页面数据,最后返回给前台。
  • prefix和suffix属性可以指定资源页面的前缀和后缀,可以直接把资源位置定位到项目的/WEB-INF下面。

2.4 创建Controller类

  • @Controller:将该Bean注册到Spring的容器中
  • @RequestMapping:该注解可以用在类和方法上,它是用来映射请求的URL。
    • 用在类上: 表示初步的请求映射信息,所有响应请求都是以该地址作为父路径。
    • 用在方法上: 表示在父路径的基础上进一步细分映射信息。
    • 类定义处未标注@RequestMapping:则映射的请求URL从方法处开始。
package com.thr.controller;

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

@Controller //声明Bean对象,为一个控制器组件
public class HelloController {
    /**
     * 1. @RequestMapping 注解是用来映射请求的 URL
     * 2. 返回值会通过视图解析器解析为实际的物理视图, 对应 InternalResourceViewResolver 视图解析器,它会做如下的解析:
     *      -通过 prefix + returnVal + suffix 这样的方式得到实际的物理视图, 然后做转发操作.
     *      -这里得到的物理视图为:/WEB-INF/pages/success.jsp
     */
    @RequestMapping("/hello")
    public String sayHello(){
        System.out.println("Hello SpringMVC");
        //访问成功后跳转到success页面
        return "success";
    }
}

2.5 编写视图文件

在webapp下编写index.jsp文件,项目启动后会默认访问该页面。

注意:在第一行中需要加入这个参数isELIgnored="false",否则到时候可能访问不到页面。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Insert title here</title>
    </head>
    <body>
        <a href="${pageContext.request.contextPath}/hello">第一个Spring MVC程序</a>
    </body>
</html>

创建目标页面(success.jsp),在WEB-INF/pages/下创建success.jsp,注:WEB-INF下的文件是不能直接通过浏览器访问的,只能通过请求进行访问。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Hello</title>
    </head>
    <body>
        <h1>成功访问!!!</h1>
    </body>
</html>

2.6 启动Tomcat服务器测试

启动Tomcat服务器,打开主页访问:

image

点击超链接,成功跳转到相应页面,说明我们的第一个SpringMVC程序创建成功了。

image

补充:新版IDEA(我的是2020.3)注意这里:

image

3、SpringMVC绑定参数

3.1 普通类型和POJO类型

3.1.1 参数绑定综述

我们知道,SpringMVC是用来处理前端的一些请求,当用户在页面触发某种请求时,一般会将一些参数(key/value)带到后台,然后将数据再通过视图返回给用户。在Spring MVC中可以通过参数绑定,将客户端请求的key/value数据绑定到Controller处理器方法的形参上,显然,这是很关键的一个问题。

当用户发送一个请求时,根据Spring MVC的请求处理流程,前端控制器会请求处理器映射器HandlerMapping返回一个处理器(或处理器链),然后请求处理器适配器HandlerAdapter执行相应的Handler处理器。此时,处理器适配器HandlerAdapter会调用Spring MVC提供的参数绑定组件将请求的key/value数据绑定到Controller处理器方法对应的形参上。

Spring MVC中有一些默认支持的请求类型,这些类型可以直接在Controller类的方法中定义,在参数绑定的过程中遇到该种类型就直接进行绑定。其默认支持的类型有以下几种:HttpServletRequest、HttpServletResponse、HttpSession及Model/ModelMap。

  1. HttpServletRequest可以通过request对象获取请求信息。
  2. HttpServletResponse可以通过response对象处理响应信息。
  3. HttpSession可以通过session对象得到session中存放的对象。
  4. Model是一个接口,ModelMap是一个接口实现,它的作用就是将Model数据填充到request域,跟ModelAndView类似(ModelAndView表示封装了Model和View的对象,推荐使用它)。

在参数绑定过程中,如果遇到上面类型就直接进行绑定。也就是说,我们可以在Controller的方法的形参中直接定义上面这些类型的参数,然后SpringMVC会自动绑定

注意:从前端发送过来的请求参数都是String类型的,所以SpringMVC还提供了一些默认的格式转换器,这些格式转换器会自动根据数据类型进行类型转换。比如age会自动由字符串转换成了int类型。

3.1.2 简单类型参数绑定

对于简单类型的参数,这些类型是直接在Controller类的方法中定义,当用户发送请求后,Spring MVC在处理key/value信息时,就会以key名寻找Controller类的方法中具有相同名称的形参并进行绑定,例如下面的例子,从前台页面提交图书的信息,然后在后台打印传来的数据。具体Controller代码如下:

注意:Controller中的形参需要与前台提交的name属性一样才能完成绑定,如果请求中不包含其中的某个形参,此时是不会报错的,默认使用该参数时要进行空校验。

[1]、创建用于处理前台请求的Controller处理器:

//@Controller:表示一个Controller实例,该实例由Spring容器管理
@Controller
public class BookController {
    //配置请求的地址
    @RequestMapping(value = "book", method = RequestMethod.POST)
    public String getInfo(Integer id, String name, String publisher) {
        System.out.println("获取到的数据为:ID--" + id + ",名称--" + name + ",出版社--" + publisher);
        //成功后跳转的页面
        return "success";
    }
}

[2]、编写用于提交图书信息的页面(index.jsp):

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>首页</title>
    </head>
    <body>
        <form action="${pageContext.request.contextPath}/book" method="post">
            <table>
                <tr>
                    <td>图书ID:</td>
                    <td><input type="text" name="id"></td>
                </tr>
                <tr>
                    <td>图书名称:</td>
                    <td><input type="text" name="name"></td>
                </tr>
                <tr>
                    <td>出版社:</td>
                    <td><input type="text" name="publisher"></td>
                </tr>
                <tr>
                    <td colspan="2">
                        <input type="submit" value="提交">
                    </td>
                </tr>
            </table>
        </form>
    </body>
</html>

[3]、然后启动Tomcat进行测试:

image

image

3.1.3 单一POJO类型绑定

上面简单参数类型绑定都是需要在Controller中编写一个个的形参才能完成绑定,但是我们在实际开发中一般不这么干,因为有可能前台提交的数据项很多而且复杂,如果这样写就要在Controller中编写大量形参来接收,极大的降低了开发效率,平时一般都会使用POJO类型来绑定数据。

[1]、创建一个Book实体类:

public class Book {
    //书的ID
    private Integer id;
    //书名
    private String name;
    //出版社
    private String publisher;

    //getter,setter和toString省略
}

[2]、将上面Controller代码中接收的参数修改为一个pojo类型,如下所示:

//@Controller:表示一个Controller实例,该实例由Spring容器管理
@Controller
public class BookController {
    //配置请求的地址
    @RequestMapping(value = "book", method = RequestMethod.POST)
    public String getInfo(Book book) {
        System.out.println("获取到的数据为:ID--" + book.getId()
                           + ",名称--" + book.getName()
                           + ",出版社--" + book.getPublisher());
        //成功后跳转的页面
        return "success";
    }
}

实现的效果和上面简单参数类型一模一样,但是一定要注意:前端提交的name属性必须与POJO里面的属性一致,这样SpringMVC才会自动帮我们绑定数据,否则会绑定失败!!!

3.1.4 嵌套POJO类型绑定

嵌套POJO类型就是在一个Java实体类中包含了其它的实体类,此时Spring MVC依然可以解析并成功绑定该类型的包装类。

[1]、首先创建一个用于嵌套的Author类,表示图书的作者信息,新建的Author类如下:

/**
 * Author实体
 */
public class Author {
    private String name;//作者名称
    private String age;//作者年龄

    //getter,setter和toString省略
}

[2]、创建Book类,里面添加了一个嵌套的pojo类型属性author:

public class Book {
    //书的ID
    private Integer id;
    //书名
    private String name;
    //出版社
    private String publisher;
    //图书作者
    private Author author;

    //getter,setter和toString省略
}

[3]、Controller类:当前端页面发出请求后,处理器适配器会解析这种格式的name,将该参数当做POJO类的成员参数绑定起来,作为Controller方法的形参。这样在Controller方法中就可以通过POJO类获取其POJO类的其他类的对象。

//@Controller:表示一个Controller实例,该实例由Spring容器管理
@Controller
public class BookController {
    //配置请求的地址
    @RequestMapping(value = "book", method = RequestMethod.POST)
    public String getInfo(Book book) {
        System.out.println("获取到的数据为:ID--" + book.getId()
                + ",名称--" + book.getName()
                + ",出版社--" + book.getPublisher()
                + ",作者信息--" + book.getAuthor());
        //成功后跳转的页面
        return "success";
    }
}

[4]、提交图书信息的页面(index.jsp)。注意:由于Book类包含了Author类,将其作为其属性,那么在进行提交的时候,就必须在input的name属性指定为嵌套对象.属性的形式。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>首页</title>
    </head>
    <body>
        <form action="${pageContext.request.contextPath}/book" method="post">
            <table>
                <tr>
                    <td>图书ID:</td>
                    <td><input type="text" name="id"></td>
                </tr>
                <tr>
                    <td>图书名称:</td>
                    <td><input type="text" name="name"></td>
                </tr>
                <tr>
                    <td>出版社:</td>
                    <td><input type="text" name="publisher"></td>
                </tr>
                <tr>
                    <td>作者名称:</td>
                    <td><input type="text" name="author.name"></td>
                </tr>
                <tr>
                    <td>作者年龄:</td>
                    <td><input type="text" name="author.age"></td>
                </tr>
                <tr>
                    <td colspan="2">
                        <input type="submit" value="提交">
                    </td>
                </tr>
            </table>
        </form>
    </body>
</html>

[5]、再次将项目部署到Tomcat上后,测试的结果如下所示:

image

image

3.1.5 补充:@RequestMapping

@RequestMapping注解是用来映射请求,也就是通过它来指定控制器可以处理哪些URL请求。这个注解的使用非常简单,可以在类和方法上使用。

  • 用于方法上,表示映射当前Handler中的方法。
  • 用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。

3.1.5.1 只用在方法上

@Controller
public class HelloController {

    //当请求地址为 /show 的时候,这个方法会被执行
    @RequestMapping(value = "/show")
    public String show1(){
        return "success";
    }

    //请求为 /show1 或者 /show2 都可以访问到该方法
    @RequestMapping(value = {"/show1","/show2"})
    public String show2(){
        return "success";
    }
}

3.1.5.2 用在类上

在我们实际的项目中,可能有很多的接口,例如用户、订单、商品等等,为了更好的区分它们,一般用户的请求都是/user/xxx格式的,订单相关的请求都是 /order/xxx 格式的,所以为了方便处理,这里的前缀(就是 /order、/user)可以统一在 Controller 上面处理。

@Controller
@RequestMapping(value = "/user")
public class HelloController {
    
    @RequestMapping(value = "/show")
    public String show1(){
        return "success";
    }
}

当类上加了 @RequestMapping 注解之后,此时,要想访问到 /show,地址就应该用 /user/show 来访问了。

3.1.5.3 请求方法限定

@RequestMapping 中的 method 属性主要用来定义接收浏览器发来的何种请求。在Spring中,使用枚举类RequestMethod来定义浏览器请求的方式。

@Controller
@RequestMapping(value = "/user")
public class HelloController {

    //表示该方法只能被 GET 请求访问
    @RequestMapping(value = "/show1",method = RequestMethod.GET)
    public String show1(){
        return "success";
    }
    //表示该方法只能被 POST 请求访问
    @RequestMapping(value = "/show2",method = RequestMethod.POST)
    public String show2(){
        return "success";
    }
    //表示该方法将同时接收通过 GET 和 POST 方式发来的请求
    @RequestMapping(value = "/show3",method = {RequestMethod.GET,RequestMethod.POST})
    public String show3(){
        return "success";
    }
}

注:如果你强行用POST请求来访问GET请求的话,或报405的错误!

3.1.5.4 带占位符的URL

这个功能是Spring 3.0 新增的功能,占位符使用{}括起来,可以通过 @PathVariable 将 URL 中的占位符绑定到控制器的处理方法的参数中。带占位符的URL示例:

@Controller
@RequestMapping(path = "/user")
public class UserController {
        
	@RequestMapping(value="/{id}", method=RequestMethod.GET)
	public String show(@PathVariable("id") Integer id) {
		return "success";
	}
}

在这个控制器中 show() 方法将可以接收 user/1、user/2、user/3等等的路径请求,请求的方法必须为GET,使用 @PathVariable 为应用实现 REST 规范提供了具大的便利条件。

@PathVariable

节选自: @RequestParam和@PathVariable的用法与区别

Web应用中的URL通常不是一成不变的,例如微博两个不同用户的个人主页对应两个不同的URL:http://weibo.com/user1和http://weibo.com/user2。我们不能对于每一个用户都编写一个被@RequestMapping注解的方法来处理其请求,也就是说,对于相同模式的URL(例如不同用户的主页,他们仅仅是URL中的某一部分不同,为他们各自的用户名,我们说他们具有相同的模式)。

可以在@RequestMapping注解中用{ }来表明它的变量部分,例如:

@RequestMapping(value="/user/{username}")

这里的{username}就是我们定义的变量规则,username是变量的名字,那么这个URL路由可以匹配下列任意URL并进行处理:

  • /user/Tom
  • /user/Jerry
  • /user/Jack2

在路由中定义变量规则后,通常我们需要在处理方法(也就是@RequestMapping注解的方法)中获取这个URL的具体值,并根据这个值(例如用户名)做相应的操作,SpringMVC提供的@PathVariable可以帮助我们:

@RequestMapping(value="/user/{username}")
public String userProfile(@PathVariable(value="username") String username) {
    return "user"+username;
}

在上面的例子中,当@Controller处理HTTP请求时,userProfile的参数username会自动设置为URL中对应变量username(同名赋值)的值

可以定义URL路由,其中包含多个URL变量:

@RequestMapping(value = "/user/{username}/blog/{blogId}")
public String getUserBlog(@PathVariable String username, @PathVariable int blogId) {
    return "user:" + username + "blog->" + blogId;
}

这种情况下,Spring能够根据名字自动赋值对应的函数参数值,当然也可以在@PathVariable中显示地表明具体的URL变量值。

在默认情况下,@PathVariable注解的参数可以是一些基本的简单类型:int,long,Date,String等,Spring能够根据URL变量的具体值及函数参数类型来进行转换。例如/user/fpc/blog/1,会将fpc的值赋给username,而1赋给int变量blogId。

运行结果:

img

匹配正则表达式

很多时候,需要对URL变量进行更加精确的定义。例如,用户名只可能包含大小写字母,数字,下划线,我们希望:

  • /user/fpc是一个合法的URL
  • /user/#$$$则是一个不合法的URL

除了简单地定义{username}变量,还可以定义正则表达式进行更精确地控制,定义语法是{变量名: 正则表达式}。[a-zA-Z0-9_]+是一个正则表达式,表示只能包含小写字母,大写字母,数字,下划线。如此设置URL变量规则后,不合法的URL则不会被处理,直接由SpringMVC框架返回404NotFound。

@RequestMapping(value = "/user/{username: [a-zA-Z0-9]+}/blog/{blogId}")

3.1.6 补充:@RequestParam

@RequestParam用于将请求参数区域的数据映射到Handler方法的参数上。使用场景:例如在上面在绑定数据时,绑定的规则是:表单中name属性的值要和Handler方法中形参的值相对应,这样才能将数据绑定成功,否则服务端接收不到前端传来的数据。有时有一些特殊情况,前端的name属性值与后端Handler方法中的形参不一致,这个时候就可以通过 @RequestParam 注解来解决。

语法:@RequestParam(value=”参数名”,required=”true|false”,defaultValue=””)

  • value:请求中传入参数的名称。
  • required:该参数是否为必传项,默认为true,表示该请求路径中必须包含该参数,如果不包含就报错。
  • defaultValue:默认参数值,如果设置了该值,required=true将失效,自动为false,如果没有传该参数,就使用默认值。

@RequestParam注解的简单举例:

<!-- 表单数据,这里的name属性值为name。 -->
<input type="text" name="name">
/**
 * -@RequestParam的使用
 */
@Controller
@RequestMapping("hello")
public class HelloController {
    //表示将请求路径中参数名称为name映射为userName,相当于重命名一样
    @RequestMapping("one")
    public String show1(@RequestParam("name") String userName) {
        System.out.println(userName);
        return "success";
    }
    //表示请求路径中不一定要包含名称为name的参数
    @RequestMapping("two")
    public String show2(@RequestParam(value = "name", required = false) String userName) {
        System.out.println(userName);
        return "success";
    }
    //表示请求路径中必须包含名称为name的参数,如果name参数的值为空,则使用默认的值"hello"
    @RequestMapping("three")
    public String show3(@RequestParam(value = "name" ,required=true, defaultValue = "hello") String userName) {
        System.out.println(userName);
        return "success";
    }
}

注:@RequestMapping("hello")中的url请求地址中,不加/是不规范的,但/无论加不加都能正常访问

原因:spring会判断路径的最前面是否有斜线(/),如果没有,会拼一个斜线(/)

image-20220825232252709

3.2 集合类型绑定

3.2.1 List集合绑定

SpringMVC中,如果前端需要传递批量数据时,就可以使用List来接收,此时的 List 集合本身需要放在一个封装对象中,也就是作为一个嵌套的对象类型。List 中可以是基本数据类型,也可以是对象。例如一本书有时不止是一个作者,也可以有多个,所以我们可以在Book类中添加一个Author的集合类,代码如下所示:

[1]、编写实体类Author:

public class Author {
    private String name;
    private String age;
    //getter,setter和toString省略
}

[2]、编写实体类Book:

public class Book {
    //书的ID
    private Integer id;
    //书名
    private String name;
    //出版社
    private String publisher;
    //图书作者(可以有多位)
    private List<Author> author;

    //getter,setter和toString省略
}

[3]、编写前端提交图书页面index.jsp。注意:绑定List集合时,前端页面中每一组数据的input控件的name属性的格式为:集合名[下标].属性,当请求传递到后端时,处理器适配器会根据name的格式将请求参数解析为相应的List集合。前台的JSP代码如下所示:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>首页</title>
    </head>
    <body>
        <form action="${pageContext.request.contextPath}/book" method="post">
            图书 ID:<input type="text" name="id"/><br/>
            图书名称:<input type="text" name="name"/><br/>
            出版社:<input type="text" name="publisher"/><br/>
            List类型-作者1名称:<input type="text" name="author[0].name"/><br/>
            List类型-作者1年龄:<input type="text" name="author[0].age"/><br/>
            List类型-作者2名称:<input type="text" name="author[1].name"/><br/>
            List类型-作者2年龄:<input type="text" name="author[1].age"/><br/>
            <input type="submit" value="提交">
        </form>
    </body>
</html>

[4]、编写Controller类中的具体代码如下:

//@Controller:表示一个Controller实例,该实例由Spring容器管理
@Controller
public class BookController {
    //配置请求的地址
    @RequestMapping(value = "book", method = RequestMethod.POST)
    public String getInfo(Book book) {
        System.out.println("获取到的数据为:ID--" + book.getId()
                           + ",名称--" + book.getName()
                           + ",出版社--" + book.getPublisher()
                           + "\n作者信息--" + book.getAuthor());
        //成功后跳转的页面
        return "success";
    }
}

[5]、启动Tomcat测试代码,运行后的结果如下所示:

image

image

3.2.2 数组绑定

如果前台传输了多个相同name的数据,比如多选框,SpringMVC可以在处理器方法中使用数组参数接收这些数据。

[1]、实体类Book:

public class Book {
    //书的ID
    private Integer id;
    //书名
    private String name;
    //出版社
    private String publisher;
    //图书类型
    private String[] bookType;
    
    //getter,setter和toString省略
}

[2]、提交数据页面:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>首页</title>
    </head>
    <body>
        <form action="${pageContext.request.contextPath}/book" method="post">
            图书 ID:<input type="text" name="id"/><br/>
            图书名称:<input type="text" name="name"/><br/>
            出版社:<input type="text" name="publisher"/><br/>
            图书类型 :<input type="checkbox" name="bookType" value="Java">Java
            <input type="checkbox" name="bookType" value="C++">C++
            <input type="checkbox" name="bookType" value="PHP">PHP
            <input type="checkbox" name="bookType" value="Python">Python <br/>
            <input type="submit" value="提交">
        </form>
    </body>
</html>

[3]、Controller控制器中的代码:

//@Controller:表示一个Controller实例,该实例由Spring容器管理
@Controller
public class BookController {
    //配置请求的地址
    @RequestMapping(value = "book", method = RequestMethod.POST)
    public String getInfo(Book book) {
        System.out.println("图书ID:"+book.getId());
        System.out.println("图书名称:"+book.getName());
        System.out.println("出版社:"+book.getPublisher());
        System.out.println("图书类型:");
        for (String bookType : book.getBookType()) {
            System.out.println(bookType);
        }
        //成功后跳转的页面
        return "success";
    }
}

[4]、运行结果如下:

image

image

3.2.3 Map集合绑定

SpringMVC也可以绑定Map集合类型,Map的绑定其实和List的绑定是差不多的。虽然Map的使用比较灵活,但是对于这里绑定数据而言就显得乏力了,可维护性比较差,所以因此一般不推荐使用。

[1]、给上面的Book类添加一个Map属性:

public class Book {
    //书的ID
    private Integer id;
    //书名
    private String name;
    //出版社
    private String publisher;
    //图书作者(可以有多位)
    private List<Author> author;
    //添加Map集合
    private Map<String, Author> authorMap;

    //getter,setter和toString省略
}

[2]、Controller类中的具体代码如下:

//@Controller:表示一个Controller实例,该实例由Spring容器管理
@Controller
public class BookController {
    //配置请求的地址
    @RequestMapping(value = "book", method = RequestMethod.POST)
    public String getInfo(Book book) {
        System.out.println("获取到的数据为:ID--" + book.getId()
                           + ",名称--" + book.getName()
                           + ",出版社--" + book.getPublisher());

        //遍历Map集合
        Map<String, Author> authorMap = book.getAuthorMap();
        Set<Map.Entry<String, Author> entries = authorMap.entrySet();
        for (Map.Entry<String, Author> entry : entries) {
            System.out.println("键key:"+entry.getKey()+"--作者名称:"
                               +entry.getValue().getName()+"--作者年龄:"+entry.getValue().getAge());
        }

        //成功后跳转的页面
        return "success";
    }
}

[3]、在前端index.jsp页面额外添加如下JSP代码。当想把页面上的批量数据通过Spring MVC转换为Web端的Map类型的对象时,每一组数据的input控件的name属性使用Map名['key值']的形式。当请求传递到Web端时,处理器适配器会根据name的格式将请求参数解析为相应的Map集合。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>首页</title>
    </head>
    <body>
        <form action="${pageContext.request.contextPath}/book" method="post">
            图书 ID:<input type="text" name="id"/><br/>
            图书名称:<input type="text" name="name"/><br/>
            出版社:<input type="text" name="publisher"/><br/>
            Map类型-作者1名称:<input type="text" name="authorMap['a1'].name"/><br/>
            Map类型-作者1年龄:<input type="text" name="authorMap['a1'].age"/><br/>
            Map类型-作者2名称:<input type="text" name="authorMap['a2'].name"/><br/>
            Map类型-作者2年龄:<input type="text" name="authorMap['a2'].age"/><br/>
            <input type="submit" value="提交">
        </form>
    </body>
</html>

[4]、将项目部署到Tomcat然后启动测试,效果如下所示:

image

image

4、SpringMVC自定义类型转换器

4.1 转换器的概述

实际上在SpringMVC框架中,框架本身就内置了很多类型转换器,这些默认的类型转换器,可以将String类型的数据,自动转换为相应类型的数据。比如在前面的各种演示案例中,表单提交的无论是int还是double类型的请求参数,通过默认转换器均可直接接收到相应类型的相应数据,而非接收到String再手动转换,都是会自动帮你转换。

注意:从前端发送过来的请求参数都是String类型的,所以SpringMVC还提供了一些默认的格式转换器,这些格式转换器会自动根据数据类型进行类型转换。比如age会自动由字符串转换成了int类型。

这些默认的转换器就在包 org.springframework.core.convert.converter 下,如图所示:

image

但是有的时候内置的类型转换器不足够满足业务需求,因为默认转换器并不是可以将用户提交的String转换为所有用户需要的类型。此时,就需要自定义类型转换器了。例如,在Spring MVC的默认类型转换器中,没有日期类型的转换器,因为日期的格式太多。若要使表单中提交的日期字符串,被处理器方法形参直接接收为日期类型,则需要自定义类型转换器了,否则肯定会报错。下面就来学习一下怎么操作的。

4.2 实现Converter接口

详细可参考:SpringMVC对日期类型的转换 - ngulc - 博客园 (cnblogs.com)

依旧使用前面的提交Book举例,首先创建一个实现Converter接口的DateConverter实现类,代码如下:

package com.thr.convert;
 
import org.springframework.core.convert.converter.Converter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
 
/**
 * 自定义转换器
 */
//@Component
public class DateConverter implements Converter<String, LocalDate> {
    //Java8 的日期格式化器
    DateTimeFormatter formatter=DateTimeFormatter.ofPattern("yyyy-MM-dd");
    public LocalDate convert(String source) {
        // 根据格式化器将String类型解析为LocalDate类型
        return LocalDate.parse(source, formatter);
    }
}

上面是在自定义的参数类型转换器中,将一个前台传来的 String 转为 LocalDate 对象。

然后我们在Book类添加一个日期类型的属性,用来表示图书的出版日期,Book实体类代码如下:

public class Book {
    //书的ID
    private Integer id;
    //书名
    private String name;
    //出版社
    private String publisher;
    //图书出版日期
    private LocalDate publishDate;
 
    //getter,setter和toString省略
}

接下来,在 SpringMVC 的配置文件(spring-mvc.xml)中配置相关的 Bean使之生效。

<!-- 注册自定义转换器 -->
<bean id="dateConverter" class="com.thr.convert.DateConverter"/>
<!-- 注册类型转换服务bean -->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <ref bean="dateConverter"/>
        </set>
    </property>
</bean>
<!-- 配置MVC注解驱动。-->
<mvc:annotation-driven conversion-service="conversionService"/>

创建用于提交图书信息的index.jsp页面,具体代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>首页</title>
    </head>
    <body>
        <form action="${pageContext.request.contextPath}/book" method="post">
            <table>
                <tr>
                    <td>图书ID:</td>
                    <td><input type="text" name="id"></td>
                </tr>
                <tr>
                    <td>图书名称:</td>
                    <td><input type="text" name="name"></td>
                </tr>
                <tr>
                    <td>出版社:</td>
                    <td><input type="text" name="publishingHouse"></td>
                </tr>
                <tr>
                    <td>图书出版日期:</td>
                    <td><input type="date" name="publishDate"></td>
                </tr>
                <tr>
                    <td colspan="2">
                        <input type="submit" value="提交">
                    </td>
                </tr>
            </table>
        </form>
    </body>
</html>

image

image

3、使用@InitBinder注解(推荐)

当然也可以不在XML中配置类型转换服务,而是使用@InitBinder注解来实现,推荐这种方式。在需要日期转换的 Controller 中使用 SpringMVC 的注解 @InitBinder 和Spring 自带的 WebDateBinder 类来操作,WebDataBinder 是用来绑定请求参数到指定的属性编辑器。由于前端传到 Controller 里的值是 String 类型的,当往 Model 里 Set这个值的时候,如果 set 的这个属性是个对象,Spring 就会去找到对应的 editor 进行转换,然后再 SET 进去。注意:@InitBinder用于在@Controller中标注的方法,表示为当前控制器注册一个属性编辑器,只对当前的Controller有效。这个方法会在控制器中其他方法之前调用,这样就可以预先处理数据。

//@Controller:表示一个Controller实例,该实例由Spring容器管理
@Controller
public class BookController {

    //添加initBinder注解
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        //注册原类型和目标类型的转换格式
        binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                System.out.println("-----进入了initBinder()----");
                setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd")));
            }
        });
    }
    //配置请求的地址
    @RequestMapping(value = "book", method = RequestMethod.POST)
    public String getInfo(Book book) {
        System.out.println(book.getId());
        System.out.println(book.getName());
        System.out.println(book.getPublisher());
        System.out.println(book.getPublishDate());
        //成功后跳转的页面
        return "success";
    }
}

5、SpringMVC中的转发与重定向

5.1 转发与重定向

在SpringMVC中,如果当处理器对请求处理完毕后,在不是返回JSON数据的情况下,一般都会跳转到其它的页面,此时有两种跳转方式:请求转发与重定向。在SpringMVC中分别对应forward和redirect这两个关键字。

关键字 描述 SpringMVC实现 原生servlet实现
forward 表示转发 forward:"目标页面"(例如forward:"/WEB_INF/success.jsp" request.getRequestDispatcher("xx.jsp").forward()
redirect 表示重定向 redirect:"目标页面" | redirect:"目标请求" response.sendRedirect("xxx.jsp")

5.2 SpringMVC转发指令

@RequestMapping("/forward")
public String forward() {
    return "forward:/target.jsp";
}

表示方法执行完成后转发到/target.jsp

5.3 SpringMVC重定向指令

  • 表示方法执行完成后重定向到/target.jsp
@RequestMapping("/redirect")
public String redirect() {
    return "redirect:/target.jsp";
}

👏注:这里重定向中使用的路径和我们以前的写法不同,没有以Web应用名称开头,这是因为SpringMVC会替我们加上。

  • 表示方法执行完成后重定向到/find请求
@RequestMapping("/redirect")
public String redirect() {
    return "redirect:/find";
}

5.4 使用原生对象完成转发

@RequestMapping("/forward")
public void forward2(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    request.getRequestDispatcher("/target.jsp").forward(request, response);
}

注意:使用原生request对象执行转发后,handler方法的返回值就必须是void,意思是我们自己指定了响应方式,不需要SpringMVC再进行处理了。一个请求只能有一个响应,不能在handler方法里面给一个,然后SpringMVC框架再给一个。

5.5 使用原生对象完成重定向

@RequestMapping("/redirect")
public void redirect2(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.sendRedirect(request.getContextPath()+"/target.jsp");
}

使用原生response对象执行重定向后,handler方法的返回值同样需要设置为void,原因同上。

6、将数据放入域对象中

三大域:

  1. request
  2. session
  3. application

6.1 Request域

在SpringMVC中,一般在进行页面跳转时,都会带上该处理器处理完成的数据。我们都知道,SpringMVC的底层是封装了Servlet的代码,所以在SpringMVC中可以使用Servlet中的域对象,当然SpringMVC也额外给我们封装一些其它的域对象供我们使用,如Model、ModelMap。

注:Model、ModelMap和Map的底层其实都是封装了Request请求。

6.1.1 使用Model对象

在方法中将Model以形参的形式设置,使用addAttribute设置要传输的数据(注意是addAttribute属性,不是setAttribute)。返回值就是页面名称,这种方式比较常用。

@RequestMapping("/requestAttrModel")
public String requestAttrModel(Model model) {
    model.addAttribute("username", "张三");
    model.addAttribute("password", "123456");
    return "success";
}

6.1.2 使用ModelMap

ModelMap 是个Map集合可以使用Map的基本功能,ModelMap 也定义了addAttribute()方法

@RequestMapping("/requestAttrModelMap")
public String requestAttrModelMap(ModelMap modelMap) {
    modelMap.addAttribute("username", "张三");
    modelMap.put("password","123456");
    return "success";
}

6.1.3 使用Map

@RequestMapping("/requestAttrMap")
public String requestAttrMap(Map<String, Object> map) {
    map.put("mapName", "mapValue");
    return "success";
}

6.1.4 使用HttpServletRequest

这是Servlet中原生的Request域对象。

@RequestMapping("/requestAttrRequest")
public String requestAttrRequest(HttpServletRequest request) {
    request.setAttribute("grapeAttrName", "grapeAttrName");
    return "success";
}

6.2 Session域

向session域存入数据,真正有效的办法只有一个,直接使用Servlet原生的Session域对象

@RequestMapping("/sessionAttr")
public String sessionAttr(HttpSession session) {
    session.setAttribute("sessionAttrName", "sessionAttrValue");
    return "success";
}

6.3 Application域

向application域存入数据需要先拿到ServletContext对象。前面介绍过获取ServletContext对象的两种方法。拿到ServletContext对象后调用setAttribute()方法,即可,用的比较少,可以用来加载网页中不经常改变的数据。

@Autowired
private ServletContext servletContext;

@RequestMapping("/application/scope")
public String applicationScope() {
    servletContext.setAttribute("appName", "appValue");
    return "success";
}

6.4 ModelAndView(推荐)

ModelAndView我们平时中使用的最多的,它表示封装了ModelMap请求域和视图的对象(即数据模型+视图)。使用步骤如下所示:

  • 首先new一个ModelAndView实例
  • 使用addObject()设置需要像页面传输的数据
  • 使用setViewName()设置需要跳转的视图页面
  • 最后将ModelAndView对象返回
  • 这样前台就可以通过EL表达式${name}获取数据了
@RequestMapping("/modelAndView")
public ModelAndView testModelAndView() {
    
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject("mavAttrName1", "mavAttrValue1");
    modelAndView.addObject("mavAttrName2", "mavAttrValue2");
    modelAndView.setViewName("success");
    return modelAndView;
}

如果要在ModelAndView中使用转发和重定向也可以在setViewName()中设置。

//转发
modelAndView.setViewName("forward:/WEB-INF/pages/show.jsp");

//重定向,注:重定向不能访问/WEB-INF资源,因为是两次请求
modelAndView.setViewName("redirect:/hello.jsp");

7、SpringMVC访问静态资源

7.1 静态资源访问问题

我们平时在开发Web项目时,不可避免的需要使用到静态资源,例如要用到项目中的图片文件、HTML文件、CSS文件、JavaScript文件等等凡是浏览器直接可以使用且不需要Tomcat解析的资源都是静态资源。但是我们在配置SpringMVC核心控制器dispatcherServlet时,配置的<url-pattern>/</url-pattern>表示拦截除jsp以外的所有请求,对于前台页面来说想要使用静态资源比如图片、引入js、css等,就必须向服务器发送请求才能获取,而此时我们是将dispatcherServlet设置为拦截了除jsp以外所有请求,所以对于这些静态资源就会获取不到,导致页面出现404错误!

下面就是对静态资源访问的一些解决方案。

补充:<url-pattern>/</url-pattern>表示拦截除jsp以外的所有请求。JSP为什么不拦截呢?因为tomcat容器定义了一个名称为Jsp的Servlet,在这里并没有重写

<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
</servlet-mapping>

7.2 解决方法

7.2.1 解决方法一

方法一:使用<mvc:default-servlet-handler/>

只需在springmvc的xml配置文件中配置这个标签即可:

<!-- 开启默认的servlet  -->
<mvc:default-servlet-handler/>
<!-- 配置MVC注解驱动 -->
<mvc:annotation-driven/>

<mvc:default-servlet-handler /> 表示开启默认的servlet,在 WEB 容器启动的时候会在上下文中定义一个 DefaultServletHttpRequestHandler,它暂时会取代DispatcherServlet处理请求的功能,所以这个时候会出现一个新的问题,就是后台Handler方法中映射的请求会失效,所以还必须配合<mvc:annotation-driven />使用,设置<mvc:annotation-driven />是将默认servlet无法处理的请求交给dispatcherServlet处理

这样配合使用之后,当浏览器输入请求时,如果该请求已经作了映射,那么会接着交给后台对应的处理程序,如果没有作映射,就交给 WEB 应用服务器默认的 Servlet 处理,从而找到对应的静态资源,只有再找不到资源时才会报错。

一般Web应用服务器默认的Servlet名称是”default”,因此DefaultServletHttpRequestHandler可以找到它。如果你所有的Web应用服务器的默认Servlet名称不是”default” 或者在缺省 Servlet 名称未知的情况下使用了不同的 Servlet 容器,则需要通过default-servlet-name属性显示指定:

<mvc:default-servlet-handler default-servlet-name="xxx" />

7.2.2 解决方法二

方法二:使用<mvc:resources />

<!-- 配置静态资源的路径和映射 -->
<mvc:resources mapping="/static/**" location="/static/"/>
<!-- 配置MVC注解驱动 -->
<mvc:annotation-driven/>

location:表示静态资源的位置,就是指定不要拦截的目录,这里指在根目录static文件下的所有文件。注意:这些文件必须是在webapp根目录下的路径

mapping:表示静态资源的映射请求路径,/**表示匹配任意深度的路径。上面表示以resources开头的请求路径,例如:/resources/a,/resources/a/b

在映射路径的定义中,最后是两个 *,这是一种 Ant 风格的路径匹配符号,一共有三个通配符:

通配符 含义
** 匹配多层路径
* 匹配一层路径
? 匹配任意单个字符

注意:本地资源路径,必须是webapp根目录下的路径

7.2.3 解决方法三

方法三:在web.xml中配置defaultServlet的mapping

配置要写在DispatcherServlet的前面, 让defaultServlet先拦截,这个就不会进入Spring了

<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.js</url-pattern>
    <url-pattern>*.css</url-pattern>
    <url-pattern>*.html</url-pattern>
    <url-pattern>*.htm</url-pattern>
    <url-pattern>*.png</url-pattern>
    <url-pattern>*.jpg</url-pattern>
    <url-pattern>*.gif</url-pattern>
    <url-pattern>*.ico</url-pattern>
</servlet-mapping>

8、<mvc:view-controller/>标签的使用

<mvc:view-controller/>标签的作用可以让我们在Controller类中少写一点代码,我们只需在XML中配置一下请求即可即可,其实我感觉没什么卵用,还不如直接在Controller中写呢!😂😂

那么它是怎么来使用的呢?假设有下面这样一个handler方法:

@RequestMapping("/hello")
public String hello() {
    return "success";
}

这个方法内部没有做任何处理,仅仅是把一个URL地址"/hello"映射到视图"success"。此时我们就可以使用<mvc:view-controller/>标签来简化一下。

<mvc:view-controller path="/hello" view-name="success"/>
<mvc:annotation-driven/>

标签内部的两个属性介绍如下:

  • path=”/hello” 就是你访问的路径(相当于RequestMapping(“/hello”))
  • view-name=”success”就是你要跳转的视图页面(如success.jsp,相当于return “success”) 配置了这个后对于/success请求就会直接交给dispatcherServlet处理,然后使用ViewResolver进行解析。

上面配置的是请求转发,还可以配置重定向请求:

@RequestMapping("/index")
public String index(){
    return "redirect:success.jsp";//注意这里访问不是WEB-INF下面的JSP
}

用mvc:view-controller来代替:

<mvc:view-controller path="/hello" view-name="redirect:success.jsp"/>
<mvc:annotation-driven/>

最后注意事项:

  • 使用了这个标签后必须配置 <mvc:annotation-driven />,否则会造成所有的@Controller注解无法解析,导致404错误。
  • 如果请求存在处理器,则这个标签对应的请求处理将不起作用。因为请求是先去找处理器处理,如果找不到才会去找这个标签配置。

9、<mvc:annotation-driven/>标签介绍

9.1 关于<mvc:annotation-driven/>作用

  1. <mvc:annotation-driven /> 会自动向容器中注册如下组件,并且会代替之前默认的组件:

    • HandlerMapping :请求映射,负责根据用户请求url找到要执行的Handler

      • RequestMappingHandlerMapping支持@RequestMapping注解

      • BeanNameUrlHandlerMapping:将controller类的名字映射为请求url

      • HandlerAdapter : 处理器适配器,用于请求处理,通过HandlerAdapter对处理器进行执行

        • RequestMappingHandlerAdapter:处理@Controller和@RequestMapping注解的处理器
        • HttpRequestHandlerAdapter:处理继承了HttpRequestHandler创建的处理器
        • SimpleControllerHandlerAdapter:处理继承自Controller接口的处理器
    • ExceptionResolver:处理异常的解析器

      • ExceptionHandlerExceptionResolver
        • ResponseStatusExceptionResolver
        • DefaultHandlerExceptionResolver

    <mvc:annotation-driven /> 最主要的作用是配置两个最常用的组件RequestMappingHandlerMappingRequestMappingHandlerAdapter

    • RequestMappingHandlerMapping是HandlerMapping的实现类,它会在容器启动的时候,扫描容器内的bean,解析带有@RequestMapping注解的方法,并将其解析为url和handlerMethod键值对方式注册到请求映射表中。
    • RequestMappingHandlerAdapter是HandlerAdapter的实现类,它是处理请求的适配器,说白了,就是确定调用哪个类的哪个方法,并且构造方法参数,返回值。

    <mvc:annotation-driven/>是告知Spring容器,我们启用注解驱动,支持@RequestMapping注解,这样我们就可以使用@RequestMapping来配置处理器。

    <context:component-scan/>标签是告诉Spring容器来扫描指定包下的类,并注册被@Component,@Controller,@Service,@Repository等注解标记的组件相似。

  2. 还将提供以下支持:

    • 支持使用 ConversionService 实例对表单参数进行类型转换

    • 支持使用 @NumberFormat、@DateTimeFormat 注解完成数据类型的格式化

    • 支持使用 @Valid 注解对 JavaBean 实例进行 JSR 303 验证

    • 支持使用 @RequestBody 和 @ResponseBody 注解

9.2 <mvc:annotation-driven/>在什么时候必须配置

  1. 直接配置响应的页面:无需经过控制器来执行结果 ;但会导致其他请求路径失效,需要配置<mvc:annotation-driven/>标签
    • <mvc:view-controller path="/success" view-name="success"/>
  2. RESTful-CRUD操作,删除时,通过jQuery执行delete请求时,找不到静态资源,需要配置<mvc:annotation-driven/>标签
    • <mvc:default-servlet-handler /> 将在 SpringMVC 上下文中定义一个 DefaultServletHttpRequestHandler,它会对进入 DispatcherServlet 的请求进行筛查,如果发现是没有经过映射的请求,就将该请求交由 WEB 应用服务器默认的 Servlet 处理,如果不是静态资源的请求,才由 DispatcherServlet 继续处理。
  3. 配置类型转换器服务时,需要指定转换器服务引用
    • <mvc:annotation-driven conversion-service=“conversionService”/> 会将自定义的 ConversionService 注册到 Spring MVC 的上下文中
  4. 后面完成JSR 303数据验证,也需要配置

9.3 关于<mvc:annotation-driven/>配合使用的几种情况

[1]、既没有配置 <mvc:default-servlet-handler /> 也没有配置 <mvc:annotation-driven />

结果:动态资源像@RequestMapping映射的资源能访问,静态资源(.html,.js,.img)不能访问

这里用到的是默认的注解请求映射——DefaultAnnotationHandlerMapping,它里面有一个handlerMap里面包含了映射,所以动态的能访问。静态不能访问,就是因为里面没有保存静态资源映射。

image

[2]、配置了 <mvc:default-servlet-handler /> 但没有配置 <mvc:annotation-driven />

结果:可以加载静态资源,动态资源不行。

可以发现DefaultAnnotationHandlerMapping没有了,所以不能存储那些请求信息了。

image

[3]、既配置了<mvc:default-servlet-handler /> 又配置 <mvc:annotation-driven />

结果:动态资源和静态资源都能访问

参考链接:

10、文件的上传

10.1 单个文件上传

SpringMVC对文件的上传做了很好的封装,所以使用 SpringMVC 可以非常方便的实现文件上传。从 Spring3.1 开始,对于文件上传,提供了两个处理器:

  • CommonsMultipartResolver:兼容性较好,可以兼容 Servlet3.0 之前的版本,但是它依赖了 commons-fileupload 这个第三方工具,所以如果使用这个,一定要添加 commons-fileupload 依赖。
  • StandardServletMultipartResolver:兼容性较差,它适用于 Servlet3.0 之后的版本,它不依赖第三方工具,使用它,可以直接做文件上传。

本文使用 CommonsMultipartResolver 解析器来举例,并且以图片的上传的案例来介绍,下面是相关的具体操作:

[1]、导入文件上传相关的依赖包。

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>

[2]、在 SpringMVC 的配置文件中配置 CommonsMultipartResolver 解析器:

注意:CommonsMultipartResolver 这个 Bean 一定要有 id,并且 id 必须是 multipartResolver

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

    <!-- 开启包扫描 -->
    <context:component-scan base-package="com.thr"/>

    <!-- 配置访问静态资源 -->
    <mvc:resources mapping="/**" location="/"/>
    <!-- 配置MVC注解驱动 -->
    <mvc:annotation-driven/>

    <!-- 注册视图解析器 -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- 配置前缀 -->
        <property name="prefix" value="/WEB-INF/pages/"/>
        <!-- 配置后缀 -->
        <property name="suffix" value=".jsp"/>
    </bean>

    <!-- 配置文件上传数据专用的解析器 -->
    <!-- 这个bean的id必须是multipartResolver -->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 设置文件编码格式 -->
        <property name="defaultEncoding" value="utf-8"/>
        <!-- 设置最大上传大小 -->
        <property name="maxUploadSize" value="#{1024*1024}"/>
    </bean>
</beans>

[3]、编写文件上传的页面

注意:上传文件的表单中有三点需要注意:

  • 要点1:请求方式必须是POST
  • 要点2:enctype属性必须是multipart/form-data , 以二进制的方式传输数据
  • 要点3:文件上传框使用input标签,type属性设置为file
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>文件上传</title>
</head>
<body>
<h1>单文件上传演示案例</h1>
<form action="${pageContext.request.contextPath}/upload" method="post" enctype="multipart/form-data">
    请选择上传的图片:<input type="file" name="file"/><br/>
    <input type="submit" value="上传单个文件"/>
</form>
</body>
</html>

[4]、编写文件上传的Controller。此时会用到MultipartFile这个接口,MultipartFile 封装了请求数据中的文件,此时这个文件存储在内存中或临时的磁盘文件中,需要将其转存到一个合适的位置,因为请求结束后临时存储将被清空。在 MultipartFile 接口中有如下方法:

方法名 返回值 描述
getContentType() String 获取文件内容的类型
getOriginalFilename() String 获取文件的原名称
getName() String 获取form表单中文件组件的名字
getInputStream() InputStream 将文件内容以输入流的形式返回
getSize() long 获取文件的大小,单位为byte
isEmpty() boolean 文件是否为空
transferTo(File dest) void 将数据保存到一个目标文件中

具体的Controller代码如下所示:

/**
 * 单个文件的上传
 */
@Controller
public class UploadController {

    @Autowired
    private ServletContext servletContext;

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public String uploadFile(MultipartFile file, Model model, HttpServletRequest request) throws IOException {

        // 获取图片文件名
        String originalFilename = file.getOriginalFilename();
        // 使用UUID给图片重命名,并且去掉UUID的四个"-"
        String fileName = UUID.randomUUID().toString().replaceAll("-", "");
        //获取图片的后缀
        String extName = originalFilename.substring(originalFilename.lastIndexOf("."));
        // 拼接新的图片名称
        String newFileName = fileName + extName;

        // 声明转存文件时,目标目录的虚拟路径(也就是浏览器访问服务器时,能够访问到这个目录的路径)
        String virtualPath = "/images";

        // 准备转存文件时,目标目录的路径。File对象要求这个路径是一个完整的物理路径。
        // 而这个物理路径又会因为在服务器上部署运行时所在的系统环境不同而不同,所以不能写死。
        //String getRealPath(String path) :返回资源文件在服务器文件系统上的真实路径(文件的绝对路径)。参数 path 代表资源文件的虚拟路径,它应该以正斜线(/)开始,/ 表示当前 Web 应用的根目录,如果 Servlet 容器不能将虚拟路径转换为文件系统的真实路径,则返回 null
        String targetDirPath = servletContext.getRealPath(virtualPath);
        System.out.println("targetDirPath = " + targetDirPath);

        // 创建File类型的对象用于文件转存
        File filePath = new File(targetDirPath, newFileName);

        if (!filePath.getParentFile().exists()) {
            //如果文件夹不存在就新建一个
            filePath.getParentFile().mkdirs();
        }

        // 调用transferTo()方法实现文件转存
        file.transferTo(filePath);

        // 为了让下一个页面能够将图片展示出来,将图片访问路径存入模型,把访问图片的路径准备好
        String picPath = servletContext.getContextPath() + "/images/" + newFileName;
        System.out.println("picPath = " + picPath);

        model.addAttribute("picPath", picPath);

        return "target";
    }
}
关于getRealPath(),首先就要谈谈它到底是用来干什么的?那么我们就应该先了解一个网站关于虚拟路径的概念。
在一个网站中,假设我们访问`http://localhost:8080/myWeb/test.jsp`,那么从这个URL地址中我们看到整个的项目名叫myWeb,也知道他的虚拟路径在tomcat服务器的webapp根路径下。但是实际路径到底是不是在tomcat的webapp路径下呢?
答案:可以是也可以不是。
我们默认可以把项目加载到tomcat下的webapp下,但是也可以通过tomcat的server.xml去将一个实际的物理路径映射到tomcat的webapp下。在server.xml中配置如下就可以了:`<Context path="/myWeb" docBase="E:/aaa" debug="0" reloadable="true"/>`
那么谈了这么多getRealPath()是干什么的呢?
答案显而易见了,他就是用来获取网站的实际物理路径的。
通常我们可以通过`request.getSession().getServletContext().getRealPath()`来获取网站的物理路径。例如myWeb项目的物理路径被配置在E:/aaa下,那么我们使用getRealPath()得到的就是“E:/aaa”。
getRealPath("/upload")也可以有参数,作用就是获取在项目根路径下的子文件夹的物理路径。即E:/aaa/upload。

[5]、编写上传成功的目标页面,用于显示图片。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>目标页面</title>
</head>
<body>
    <h1>上传成功</h1>
    <img src="${requestScope.picPath}" />
</body>
</html>

[6]、运行结果如下图所示:

image

2、多个文件上传

多个文件的上传就是在单个上传文件的基础上增加了一个循环的步骤,比较的简单,具体操作如下所示。

[1]、编写多个文件上传的页面

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>文件上传</title>
</head>
<body>
<h1>多文件上传演示案例</h1>
<form action="${pageContext.request.contextPath}/uploadMore" method="post" enctype="multipart/form-data">
    请选择上传的图片:<input type="file" name="files"/><br/>
    请选择上传的图片:<input type="file" name="files"/><br/>
    请选择上传的图片:<input type="file" name="files"/><br/>
    <input type="submit" value="上传单个文件"/>
</form>
</body>
</html>

[2]、编写多个文件上传的Controller

/**
 * 多个文件的上传
 */
@Controller
public class UploadController {

    @Autowired
    private ServletContext servletContext;

    @RequestMapping(value = "/uploadMore", method = RequestMethod.POST)
    public String uploadFileMore(@RequestParam("files") List<MultipartFile> fileList, Model model, HttpServletRequest request) throws IOException {
        //用于存储发送至前台页面展示的路径
        List<String> filePathNames = new ArrayList<>();

        // 定义存储目标文件的目录
        String targetDirPath = servletContext.getRealPath("/images");
        System.out.println("targetDirPath = " + targetDirPath);

        //循环遍历请求中的文件
        for (MultipartFile file : fileList) {
            // 获取图片文件名
            String originalFilename = file.getOriginalFilename();
            // 使用UUID给图片重命名,并且去掉UUID的四个"-"
            String fileName = UUID.randomUUID().toString().replaceAll("-", "");
            //获取图片的后缀
            String extName = originalFilename.substring(originalFilename.lastIndexOf("."));
            // 拼接新的图片名称
            String newFileName = fileName + extName;
            // 创建File类型的对象用于文件转存
            File filePath = new File(targetDirPath, newFileName);
            if (!filePath.getParentFile().exists()) {
                //如果文件夹不存在就新建一个
                filePath.getParentFile().mkdirs();
            }
            // 调用transferTo()方法实现文件转存
            file.transferTo(filePath);
            // 为了让下一个页面能够将图片展示出来,将图片访问路径存入模型,把访问图片的路径准备好
            String picPath = servletContext.getContextPath() + "/images/" + newFileName;
            System.out.println("picPath = " + picPath);
            filePathNames.add(picPath);
        }
        if (filePathNames.size() > 0) {
            model.addAttribute("filePathNames", filePathNames);
        }
        return "targetmore";
    }
}

[3]、上传成功后展示图片的页面

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>目标页面</title>
</head>
<body>
    <h1>上传成功</h1>
    <c:forEach items="${filePathNames}" var="fileName">
        <img src="${fileName}" />
    </c:forEach>
</body>
</html>

[4]、运行结果如下图所示:

image

3、关于已上传文件的保存问题

[1]、保存到Tomcat服务器上的缺陷

  • 当用户上传文件非常多的时候,拖慢Tomcat运行的速度
  • 当Web应用重新部署时,会导致用户以前上传的文件丢失
  • 当Tomcat服务器以集群的形式运行时,文件不会自动同步

[2]、建议的解决方案

  • 自己搭建文件服务器,例如:FastDFS等
  • 第三方文件服务
    • 阿里云提供的OSS对象存储服务
    • 七牛云
    • ……

11、SpringMVC的拦截器

11.1 拦截器介绍

SpringMVC中的拦截器类似于 Servlet 开发中的过滤器Filter ,只不过拦截器的功能更为强大。SpringMVC 中的拦截器是非常重要和相当有用的,它的主要作用是拦截指定的用户请求,并进行相应的预处理后处理其拦截的时间点在“处理器映射器根据用户提交的请求映射出了所要执行的处理器类,并且也找到了要执行该处理器类的处理器适配器,在处理器适配器执行处理器之前。当然,在处理器映射器映射出所要执行的处理器类时,已经将拦截器与处理器组合为了一个处理器执行链,并返回给了中央调度器。

拦截器与过滤器的区别:

具体参考:过滤器 和 拦截器的 6个区别,别再傻傻分不清了_程序员小富的博客-CSDN博客_过滤器和拦截器

  1. 拦截器是基于java的反射机制的,而过滤器是基于函数的回调。
  2. 拦截器不依赖于servlet容器,而过滤器依赖于servlet容器。
  3. 拦截器只对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
  4. 拦截器可以访问action上下文、值、栈里面的对象,而过滤器不可以。
  5. 在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
  6. 拦截器可以获取IOC容器中的各个bean,而过滤器不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。

拦截器,本质类似于AOP,它主要的应用场景

  • 日志记录:记录请求信息的日志,以便进行信息监控、信息统计等。
  • 权限检查:如登录检测,进入处理器检测是否登录,没有登录返回登录页面。
  • 性能监控:记录拦截器进入处理器和离开处理器的时间。
  • 通用行为:读取cookie中的用户信息放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器的需要都可以使用拦截器实现。

拦截器的三要素:

  1. 拦截
  2. 过滤
  3. 放行

11.2 拦截器的两种实现方式

  • 实现拦截器处理器接口(推荐):org.springframework.web.servlet.HandlerInterceptor
    • preHandle(HttpServletRequest request,HttpServletResponse response, Object handler)预处理回调方法在Controller前执行返回true继续执行下一个流程(interceptor或handler)。返回false中断执行,不会再调用拦截器或处理器。可以用于身份认证、身份授权。比如如果认证没有通过表示用户没有登陆,需要此方法拦截不再往下执行(return false),否则就放行(return true)。
    • postHandle(HttpServletRequest request,HttpServletResponse response, Object handler,ModelAndView modelAndView)后处理回调方法在进入Controller后,返回ModelAndView之前执行可以通过对ModeAndView进行处理或对视图进行处理,ModeAndView可能为null。应用场景:从modelAndView出发:将公用的模型数据(比如菜单导航之类的)在这里传到视图,也可以在这里统一指定视图。
    • afterCompletion(HttpServletRequest request,HttpServletResponse response, Object handler, Exception ex)整个请求完毕的回调方法在视图渲染完毕时回调。应用场景:统一异常处理,统一日志处理等。
  • 继承拦截器适配器类:org.springframework.web.servlet.handler.HandlerInterceptorAdapter
    • 实现拦截器需要重写三个接口,拦截器适配器为这三个方法做了空实现,可以继承这个类,根据需要重写拦截器的1~3个方法。

11.3 拦截器的定义与配置

在 SpringMVC 中,定义拦截器推荐使用实现HandlerInterceptor接口的方式。也就是自定义拦截器必须实现HandlerInterceptor接口,并实现该接口中提供的三个方法,代码如下:

/**
 * 定义一个拦截器
 */
public class HandlerInterceptor1 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("拦截器1的preHandle方法执行了...");
        //返回值true表示放行,false表示不放行
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("拦截器1的postHandle方法执 行了");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("拦截器1的afterCompletion方法执行了");
    }
}

在spring-mvc.xml配置文件中配置拦截器:

  • <mvc:interceptors>:使用 <mvc:interceptor> 标签对拦截器进行作用范围的设置。
  • 使用<mvc:mapping path="" />:设置处理的请求,可以使用通配符,可以配置多个。
  • 使用<mvc:exclude-mapping path="" />:设置不需要拦截的请求,可以使用通配符,可以配置多个。但是使用的前提是需要先配置需要处理的请求范围,即需要先配置了<mvc:mapping path="" />才行,否则会有错误。
<!-- 配置拦截器,拦截器可以有0或多个 -->
<mvc:interceptors>
    <!-- 配置一个拦截器 -->
    <mvc:interceptor>
        <!-- mvc:mapping用于指定当前所注册的拦截器可以拦截的请求路径,path路径/**表示拦截所有请求,两个*表示匹配多级目录URL地址,例如/aaa/bbb/ccc -->
        <mvc:mapping path="/**"/>
        <bean id="handlerInterceptor1" class="com.thr.interceptor.HandlerInterceptor1"/>
    </mvc:interceptor>
</mvc:interceptors>

启动Tomcat测试,随便访问一个请求,查看控制台的打印结果:

image

11.4 多个拦截器的执行顺序

在 SpringMVC 中是可以配置多个拦截器,它们按照定义的先后顺序执行,但是拦截器中的方法执行顺序却是不一样的,下面来验证一下。分别创建三个拦截器,它们的配置如下:

<!-- 配置拦截器,拦截器可以有0或多个 -->
<mvc:interceptors>
    <!-- 配置一个拦截器 -->
    <mvc:interceptor>
        <!-- mvc:mapping/用于指定当前所注册的拦截器可以拦截的请求路径,url路径/**表示拦截所有请求 -->
        <mvc:mapping path="/**"/>
        <bean id="handlerInterceptor1" class="com.thr.interceptor.HandlerInterceptor1"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean id="handlerInterceptor2" class="com.thr.interceptor.HandlerInterceptor2"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean id="handlerInterceptor3" class="com.thr.interceptor.HandlerInterceptor3"/>
    </mvc:interceptor>
</mvc:interceptors>

启动Tomcat测试,随便访问一个请求,查看控制台的打印结果:

image

可以发现,先是执行了拦截器1、2、3 的preHandle()方法,然后再逆序执行了3个拦截器的postHandle()方法,最后逆序执行拦截器的afterCoompletion()方法。

通过观察分析总结多个拦截器的执行流程如下所示:

image

在SpringMVC中,拦截器是一个链式的,只有前面的拦截器放行时后面的拦截器才能够执行,例如把第一个拦截器的predHandle()方法设置为false。

image

再次访问请求时,由于第一个拦截器不放行,导致后面的都执行不了,所以拦截器何时放行时非常重要的!!!

image

11.5 拦截器登录实例

[1]、创建实现登陆的Controller方法

/**
 * 登录Controller
 */
@Controller
public class LoginController {
 
    @RequestMapping(value = "login")
    public String login(String username, String password, HttpServletRequest request){
        //获取session对象
        HttpSession session = request.getSession();
        //模拟登录,实际从数据库获取
        if ("admin".equals(username)&&"123456".equals(password)){
            session.setAttribute("username",username);
            session.setAttribute("password",password);
            return "success";
        }else {
            //登录失败,返回登录界面,重新登录
            session.setAttribute("errorMsg","账号或密码错误!");
            return "redirect:/login.jsp";
        }
    }
}

[2]、登陆验证拦截器的实现

  • 如果用户 session 不存在则跳转到登陆页面
  • 如果用户 session 存在放行,则放行继续操作。
/**
 * 拦截器登录案例
 */
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(request.getContextPath());
        ////获取请求的url
        String uri = request.getRequestURI();
        //判断当前请求地址是否是登录地址
        if (uri.indexOf("login")>0){
            return true;
        }
        //获取session对象
        HttpSession session = request.getSession();
        //判断session中是否有用户身份信息,如果不为空,说明用户已经登录过,放行
        String username = (String) session.getAttribute("username");
        if(username != null) {
            return true;
        }
        //执行到这里表示用户身份需要验证,说明用户之前没有登录过,跳转到登录页面
        //转发
        //request.getRequestDispatcher("login.jsp").forward(request, response);
        //重定向
        response.sendRedirect("login.jsp");
        //默认拦截
        return false;
    }
 
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

[3]、配置该拦截器:

<!--  配置登录拦截器  -->
<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <bean id="loginInterceptor" class="com.thr.interceptor.LoginInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

[4]、登录页面以及回显的页面

  • 登录页面:login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>登录</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/login" method="post">
    <table style="width: 300px;height: 100px;">
        <tr>
            <td style="text-align: center">用户名:</td>
            <td><input type="text" name="username" id="username"/></td>
        </tr>
        <tr>
            <td style="text-align: center">密  码:</td>
            <td><input type="password" name="password" id="password"/></td>
        </tr>
        <tr>
            <td></td>
            <td><span style="color: red">${errorMsg}</span></td>
        </tr>
        <tr>
            <td></td>
            <td><input type="submit" id="login_button" value="用户登录"/></td>
        </tr>
    </table>
</form>
</body>
</html>
  • 登录成功页面:success.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isELIgnored="false" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>成功</title>
</head>
<body>
    登录用户:${username}<br>
    登录密码:${password}
</body>
</html>

12、处理JSON数据

12.1 处理JSON相关的注解

  • @ResponseBody:将Controller中方法的返回值转为JSON对象响应给客户端,可以作用在方法上或者方法的返回值上。
  • @RequestBody:将Http请求中JSON对象的数据转为对应的Java对象,用在方法的形参上。
  • @RestController:它是@Controller+@ResponseBody的组合,作用在类上,表示将该类的Bean由IOC容器管理,并且该类中的所有方法均以JSON格式返回。

准备工作:

由于SpringMVC处理JSON数据跟Jackson有关,所以需要导入Jackson的依赖,以免发生异常:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.2</version>
</dependency>

Jackson根据它的默认方式序列化和反序列化Java对象,我们可以灵活的使用 Jackson 的注解。Jackson中常用的注解及用法如下。

注解 用法
@JsonProperty 用于属性,把属性的名称序列化时转换为另外一个名称。示例:@JsonProperty("birth_ Date")
@JsonFormat 用于属性或者方法,把日期格式转指定的格式。示例:@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss:SSS", timezone = "GMT+8")
@JsonIgnore 用于属性或者方法上,用来完全忽略被标注的字段和方法对应的属性,即便这个还有其它注解
@JsonIgnoreProperties 和@JsonIgnore 的作用相同,不同之处是 @JsonIgnoreProperties 是作用在类上的,示例:@JsonIngoreProperties(value={"userName","userPwd"})
@JsonPropertyOrder 用于类,指定属性在序列化时JSON中的顺序 ,示例:@JsonPropertyOrder({ "userBirth", "userName" })
@JsonInclude 属性值为null的不参与序列化。例子:@JsonInclude(JsonInclude.Include.NON_NULL)

创建一个实体类用于测试数据:

public class User {
    private Integer id;
    private String userName;
    private String userPwd;
    // Jackson中的日期格式化
    @JsonFormat(pattern = "yyyy-MM-dd") 
    private Date userBirth;
    // setter getter 无参 全参构造器 toString方法省略
}

12.2 使用@ResponseBody返回JSON数据

当我们使用了@ResponseBody注解来修饰方法或者类,则方法的返回值就不要再设置为页面的跳转了,而是要写成需要返回的数据,SpringMVC会自动将其转为JSON格式的对象返回给客户端。后台Controller代码:

@Controller
public class AjaxController {

    @RequestMapping("/findAll")
    @ResponseBody
    public List<User> findAll(){
        List<User> list = new ArrayList<>();
        list.add(new User(1,"张三","123456",new Date()));
        list.add(new User(2,"李四","abcd1236",new Date()));
        list.add(new User(3,"王五","a4f5ffe",new Date()));
        return list;
    }
}

PostMan测试的结果如下:

image

12.3 使用@RequestBody接收前台JSON数据

由于Jackson的存在,前台的字段会自动与实体中的字段进行匹配。后台Controller代码如下:

@Controller
public class AjaxController {

    @RequestMapping("/addUser")
    @ResponseBody
    public User addUser(@RequestBody User user){
        System.out.println(user.toString());
        return user;
    }
}

PostMan测试的结果如下所示:

image

前面都是使用PostMan进行的测试。下面是前台页面通过Ajax发送请求的案例,参数为JSON对象,前台页面如下所示:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>ajax发送json</title>
    <script src="https://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
    <script type="text/javascript">
        $(function () {
            // 定义JSON字符串
            let user_jsonStr = {"id": "3", "userName": "李四", "userPwd": "123456", "userBirth": new Date()};
            // 转为JSON对象
            let jsonObj = JSON.stringify(user_jsonStr);

            $("#btn").click(function () {
                $.ajax({
                    "url": "${pageContext.request.contextPath}/addUser",
                    "type": "post",      // 请求的方式
                    "data": jsonObj,     // 发送的数据,上面已经定义
                    "dataType": "json",  // 响应回来的数据格式
                    "contentType": "application/json;charset=UTF-8", // 表示发送给服务器的数据类型,这里设置为json格式
                    "success": function (resp) {
                        console.log(resp);
                    }
                });
            });
        });
    </script>
</head>
<body>
    <button id="btn">发送JSON数据</button>
</body>
</html>

测试的结果为:

image

image

关于@RequestBody的详细使用可以参考:https://blog.csdn.net/justry_deng/article/details/80972817/

13、SpringMVC实现RESTful风格

13.1 REST的概念

REST为“Representational State Transfer”的缩写,中文释义为“表现层状态转换”,REST不是一种标准,而是一种设计风格。是目前最流行的一种互联网软件架构风格。它倡导结构清晰、符合标准、易于理解、扩展方便的Web架构体系,主张严格按照HTTP协议中定义的规范设计结构严谨的Web应用架构体系。由于REST所倡导的理念让Web应用更易于开发和维护,更加优雅简洁,所以正得到越来越多网站的采用。

  • 资源(Resources):网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的存在。可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的 URI 。要获取这个资源,访问它的URI就可以,因此 URI 即为每一个资源的独一无二的识别符。

  • 表现层(Representation):把资源具体呈现出来的形式,叫做它的表现层(Representation)。比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式。

  • 状态转化

    (State Transfer):每发出一个请求,就代表了客户端和服务器的一次交互过程HTTP协议,是一个无状态协议,即所有的状态都保存在服务器端因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是 “表现层状态转化”。

    具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:

    • GET 用来获取资源
    • POST 用来新建资源
    • PUT 用来更新资源
    • DELETE 用来删除资源
HTTP方法名 使用场景 资源操作 是否幂等 是否安全
GET 从服务器取出资源(一项或多项) SELECT
POST 在服务器新建一个资源 INSERT
PUT 在服务器更新资源(客户端提供完整资源数据) UPDATE
DELETE 从服务器删除资源 DELETE
  • 幂等性:对同一REST接口的多次访问,得到的资源状态是相同的。
  • 安全性:对该REST接口访问,不会使服务器端资源的状态发生改变。

RESTful:就是符合REST原则的架构方式即可称为RESTful。

13.2 REST 风格的 URL 请求

一般在非RESTful风格设计的应用中基本上只使用POST、GET类型的HTTP动作方法,而在RESTful风格设计的应用中充分利用了HTTP协议的另外动作方法,统一了数据操作的接口,使得URL请求变得简洁化、透明化。

image

13.3 REST风格URL的好处

  1. 含蓄,安全:使用问号键值对的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏。使用REST风格携带数据不再需要明显的暴露数据的名称。
  2. 风格统一:URL地址整体格式统一,从前到后始终都使用斜杠划分各个内容部分,用简单一致的格式表达语义。
  3. 无状态:在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了系统设计的复杂度。
  4. 严谨,规范:严格按照HTTP1.1协议中定义的请求方式本身的语义进行操作。
  5. 简洁,优雅:过去做增删改查操作需要设计4个不同的URL,现在一个就够了。
  6. 丰富的语义:通过URL地址就可以知道资源之间的关系,如下所示:

http://localhost:8080/shop
http://localhost:8080/shop/product
http://localhost:8080/shop/product/cellPhone
http://localhost:8080/shop/product/cellPhone/iPhone

13.4 SpringMVC对四种请求方式的支持

在@RequestMapping注解中,我们可以使用method熟悉来设置对四种请求的支持:

  • @RequestMapping(value = "/get",method = RequestMethod.GET)
  • @RequestMapping(value = "/post",method = RequestMethod.POST)
  • @RequestMapping(value = "/put",method = RequestMethod.PUT)
  • @RequestMapping(value = "/delete",method = RequestMethod.DELETE)

但是可以发现上面的注解中大体都是相似了,所以SpringMVC给我们提供了简化的版本:

  • @GetMapping(value = "/get"):对应GET请求
  • @PostMapping(value = "/post"):对应POST请求
  • @PutMapping(value = "/put"):对应PUT请求
  • @DeleteMapping(value = "/delete"):对应DELETE请求

注:在SpringMVC中对RESTful支持,主要通过注解来实现,所以下面再介绍三个相关注解:

  • @ResponseBody:响应内容转换为JSON格式
  • @RequestBody:请求内容转换为JSON格式
  • @RestContrller:等同@Controller+@ResponsrBody

13.5 浏览器对REST的支持

由于浏览器表单只支持 GET 和 POST 请求,所以为了让浏览器实现 DELETE 和 PUT 请求,Spring 为我们提供了一个过滤器:org.springframework.web.filter.HiddenHttpMethodFilter,可以为我们将 GET 和 POST 请求通过过滤器转化成 PUT 或 DELETE 等其他形式。

下面是HiddenHttpMethodFilter的使用方法:

①在web.xml中进行配置,拦截所有资源,注意:它必须作用于解决乱码过滤器CharacterEncodingFilter的后面,否则处理乱码的过滤器就会失效。

<!-- 配置 org.springframework.web.filter.HiddenHttpMethodFilter 过滤器 -->
<filter>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>    
<filter-mapping>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <!-- 拦截所有请求 -->
    <url-pattern>/*</url-pattern>
</filter-mapping>

[1]、转PUT请求:

POST请求转为PUT请求非常简单,只需在表单隐藏域中通过_method请求参数附带请求方式名称即可。

<!-- 将POST请求转为PUT请求 -->
<!-- 表单需要按照HiddenHttpMethodFilter的要求来写 -->
<!-- 要求1:请求本身是必须是POST -->
<!-- 要求2:指定新请求方式的请求参数名称必须是_method -->
<form action="${pageContext.request.contextPath}/update/emp" method="post">
    <input type="hidden" name="_method" value="PUT" />
    .......
</form>

[2]、转DELETE请求:

通过点击超链接执行删除操作。这是一个难点,超链接中没有表单隐藏域,所以需要将超链接转换为表单进行提交,这就需要借助于JavaScript。

①、在页面上创建一个action属性为空的form表单

<!-- 将超链接的GET请求转换为DELETE请求 -->
<!-- 1、提供一个通用的表单,用来将GET请求先转换为POST,然后再发送_method请求参数 -->
<!-- action属性不能写死,将来点击哪一个超链接,目标地址就和那个超链接一致 -->
<form id="commonForm" action="" method="post">
<input type="hidden" name="_method" value="delete" />
</form>

②、给所有超链接绑定单击响应函数

<script type="text/javascript" src="${pageContext.request.contextPath}/script/jquery-1.7.2.js"></script>
<script type="text/javascript">
	$(function () {
		$(".removeEmp").click(function () {

			// 在单击响应函数中获取当前点击的超链接的URL地址
			var targetUrl = this.href;
			console.log(targetUrl);

			// 通过id获取到表单的jQuery对象,然后设置action属性值,再提交表单
			$("#commonForm").attr("action", targetUrl).submit();

			// 取消控件的默认行为
			return false;
		});
	});
</script>

③、超链接

<a class="removeEmp" href="${pageContext.request.contextPath}/remove/emp/1">模拟删除</a><br/>
<a class="removeEmp" href="${pageContext.request.contextPath}/remove/emp/2">模拟删除</a><br/>
<a class="removeEmp" href="${pageContext.request.contextPath}/remove/emp/3">模拟删除</a><br/>

13.6 @PathVariable注解

@PathVariable作用:通过URL地址携带的数据需要通过@PathVariable注解来获取。它的用法如下:

<a href="${pageContext.request.contextPath}/emp/2">一个参数情况</a><br/>
//请求路径为:/emp/2
@RequestMapping("/emp/{empId}")
public String testPathVariable(@PathVariable("empId") Integer empId) {
    System.out.println("empId="+empId);
    return "result";
}

对于请求路径中有多个数据,@PathVariable注解也是支持的。

<a href="${pageContext.request.contextPath}/send/message/tom/tell/jerry">多个参数情况</a><br/>
//请求路径为:/send/message/tom/tell/jerry
@RequestMapping("/send/message/{foo}/tell/{bar}")
public String sendMessage(@PathVariable("foo") String foo,
                          @PathVariable("bar") String bar) {
    System.out.println("foo = " + foo);
    System.out.println("bar = " + bar);
    return "target";
}

14、数据校验

在Web应用MVC三层架构体系中,表述层负责接收浏览器提交的数据,业务逻辑层负责数据的处理。为了能够让业务逻辑层基于正确的数据进行处理,我们需要在表述层对数据进行检查,将错误的数据隔绝在业务逻辑层之外。在实际的项目中,一般会有两种校验数据的方式:客户端校验和服务端校验

  • 客户端校验:这种校验一般是在前端页面使用JS代码进行校验,主要是验证输入数据的合法性,不合法的数据则没有必要再发送至服务端了,前端校验可以有效的提高用户体验,但是无法确保数据完整性,因为前端用户可以方便的拿到请求地址,然后直接发送请求,传递非法参数。
  • 服务端校验:可以有效的保证数据安全与完整性,但是用户体验要差一点,所以客户端校验和服务端校验通常两者结合使用。

本文是服务端的校验。Spring MVC提供了多种校验机制,其中有Bean ValidationSpring Validator接口校验。在Spring 4.0之后,支持Bean Validation 1.0(JRS-303)Bean Validation 1.1(JRS-349)校验,可以单独集成Hibernate的validation校验框架,用于服务端的数据校验。

14.1 校验概述

JSR 303是Java为Bean数据合法性校验提供的标准框架,它已经包含在JavaEE 6.0标准中。JSR 303通过在Bean 属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。

注解 作用
@Null 标注的属性必须为null
@NotNull 标注的属性必须不为null
@AssertTrue 标注的属性必须为true
@AssertFalse 标注的属性必须为false
@Min(value) 标注的属性必须是一个数字,并且其值必须大于或等于value
@Max(value) 标注的属性必须是一个数字,并且其值必须小于或等于value
@DecimalMin(value) 必须大于或等于value
@DecimalMax(value) 必须小于或等于value
@Size(max,min) 大小必须在max和min限定的范围内
@Digits(integer,fratction) 值必须是一个数字,且必须在可接受的范围内
@Past 只能用于日期型,且必须是过去的日期
@Future 只能用于日期型,且必须是将来的日期
@Pattern(value) 必须符合指定的正则表达式

JSR 303只是一套标准,需要提供其实现才可以使用。而Hibernate Validator是JSR 303的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解:

注解 作用
@Email 必须是格式正确的Email地址
@Length 被注释的字符串大小必须在指定的范围内
@NotEmpty 被注释的字符串不能是空字符串
@Range 被注释的元素必须在指定的范围内

特别注意:@NotEmpty、@NotNull和@NotBlank三种的区别:

  • @NotNull:一般用在基本数据类型上(包括包装类),对象不能为null,但可以为empty,即为空集(size = 0)。
  • @NotEmpty:可以作用在String、List、Map和Array等,对象不能为null,而且长度必须大于0 (size > 0)
  • @NotBlank:只能作用在String上,不能为null,而且调用trim()后,长度必须大于0 ,即:必须有实际字符

14.2 普通校验

[1]、导入校验相关的依赖

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>

[2]、在springmvc.xml配置文件中配置校验器

<!-- 配置校验器 -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <!-- 校验器-->
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
    <!-- 指定校验使用的资源文件,如果不指定则默认使用classpath下的ValidationMessages.properties -->
    <property name="validationMessageSource" ref="messageSource"/>
</bean>
<!-- 校验错误信息配置文件 -->
<bean id="messageSource"
      class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <!-- 资源文件名-->
    <property name="basenames">
        <list>
            <value>classpath:CustomValidationMessages</value>
        </list>
    </property>
    <!-- 资源文件编码格式 -->
    <property name="defaultEncoding" value="utf-8"/>
    <!-- 对资源文件内容缓存时间,单位秒 -->
    <property name="cacheSeconds" value="120"/>
</bean>

[3]、将配置的校验器注入到处理器适配器中

<!-- 配置MVC注解驱动,配置注入校验器 -->
<mvc:annotation-driven validator="validator"/>

[4]、创建校验错误的信息,在项目中创建一个名称为CustomValidationMessages.properties 的文件(因为上面的配置文件中叫这个名字):

#添加校验错误提示信息
user.id.isEmpty="用户的ID不能为空!"
user.userName.isEmpty="用户名不能为空!"
user.userName.length="用户名为1~6个字符!"
user.userPwd.isEmpty="密码不能为空!"
user.userPwd.length="密码的长度为5~15个字符!"
user.userEmail.isEmpty="邮箱不能为空!"
user.userEmail.format="输入的邮箱格式不正确!"

[5]、在pojo实体类中添加校验规则

public class User {

    @NotNull(message = "{user.id.isEmpty}")
    private Integer id;

    @NotEmpty(message = "{user.userName.isEmpty}")
    @Length(min = 1, max = 6, message = "{user.userName.length}")
    private String userName;

    @NotEmpty(message = "{user.userPwd.isEmpty}")
    @Length(min = 5, max = 15, message = "{user.userPwd.length}")
    private String userPwd;

    @NotEmpty(message = "{user.userEmail.isEmpty}")
    @Email(message = "{user.userEmail.format}")
    private String userEmail;

    // getter setter 构造器 toString省略...
}

[6]、捕获校验错误信息Controller代码

注意:@Validated注解和BindingResult是配对出现的,中间不能穿插其它的形参,否则会报400错误,你要进入其它形参可以在它两的后面写。

/**
 * 数据校验controller
 */
@Controller
public class ValidateController {

    @ResponseBody
    @RequestMapping("/validate")
    // 形参前面加上@Validated注解表示这个实体类需要进行数据校验
    // BindingResult 封装数据绑定的结果
    public void validate(@Validated User user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            //校验未通过,获取所有的异常信息并展示出来
            List<ObjectError> allErrors = bindingResult.getAllErrors();
            for (ObjectError allError : allErrors) {
                System.out.println(allError.getObjectName() + ":" + allError.getDefaultMessage());
            }
        }
    }
}

[7]、测试结果如下所示:

①、页面上什么也不输入

image image

②、输入部分错误格式的内容(密码长度小于5,邮箱格式错误!)

image image

14.3 分组校验

在进行校验的时候,校验的规则一般都是写在实体类上面的,而有时一个实体类会被多处使用,例如不同的Controller中需要使用同一个实体,当不同的Controller方法对同一个实体对象进行校验时,每个Controller方法需要不同的校验,所以对于这种情况,就需要使用分组校验。

[1]、首先定义校验组,所谓的校验组,它其实就是空接口:

注意:分组接口中不需要编写任何的方法定义,该接口仅仅作为分组校验的一个标识接口。

// 分组检验接口1
public interface ValidationGroup1 {
}

// 分组检验接口2
public interface ValidationGroup2 {
}

[2]、在实体类中为每一个校验的规则设置所属组:

public class User {

    // groups属性表示校验属于哪个组,可以定义多个
    @NotNull(message = "{user.id.isEmpty}", groups = {ValidationGroup2.class})
    private Integer id;

    @NotEmpty(message = "{user.userName.isEmpty}", groups = {ValidationGroup1.class, ValidationGroup2.class})
    @Length(min = 1, max = 6, message = "{user.userName.length}", groups = {ValidationGroup1.class, ValidationGroup2.class})
    private String userName;

    @NotEmpty(message = "{user.userPwd.isEmpty}", groups = {ValidationGroup1.class})
    @Length(min = 5, max = 15, message = "{user.userPwd.length}", groups = {ValidationGroup1.class})
    private String userPwd;

    @NotEmpty(message = "{user.userEmail.isEmpty}", groups = {ValidationGroup2.class})
    @Email(message = "{user.userEmail.format}", groups = {ValidationGroup2.class})
    private String userEmail;

    // getter setter 构造器 toString省略...
}

[3]、然后在接收参数的地方,指定校验组(配置完成后,属于 ValidationGroup2 这个组的校验规则,才会生效。):

/**
 * 数据校验controller
 */
@Controller
public class ValidateController {

    @ResponseBody
    @RequestMapping("/validate")
    // @Validated注解value参数为:ValidationGroup2.class,表示只有这个组内的校验规则会生效,其它都不会生效
    public void validate(@Validated(value = {ValidationGroup2.class}) User user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            //校验未通过,获取所有的异常信息并展示出来
            List<ObjectError> allErrors = bindingResult.getAllErrors();
            for (ObjectError allError : allErrors) {
                System.out.println(allError.getObjectName() + ":" + allError.getDefaultMessage());
            }
        }
    }
}

[4]、最后测试一下效果有没有:

前面指定的校验组为ValidationGroup2组,而实体中只有userPwd属性只属于ValidationGroup1组,所以可以看到控制台打印的信息中,关于密码的校验信息是没有打印的,说明是校验的ValidationGroup2组。

image-20220825153254017

15、全局异常处理

15.1 SpringMVC全局异常处理的四种方式

在项目上线之后,往往会出现一些不可预料的异常信息,对于逻辑性或设计性问题,开发人员或者维护人员需要通过日志,查看异常信息并排除异常;而对于用户,则需要为其呈现出其可以理解的异常提示页面,让用户有一个良好的使用体验。所以异常的处理对于一个Web项目来说是非常重要的。Spring MVC提供了强大的异常处理机制。

SpringMVC提供的异常处理主要有以下四种方式:

  • 使用 Spring MVC 提供的简单异常处理器 SimpleMappingExceptionResolver
  • 实现异常处理接口 HandlerExceptionResolver
  • 使用 @ExceptionHandler 注解实现异常处理
  • 使用 @ControllerAdvice + @ExceptionHandler注解

注意:如果XML中也配置了相同的映射关系,那么SpringMVC会优先采纳基于注解的映射

在SpringMVC中处理异常的推荐方式:使用 @ControllerAdvice + @ExceptionHandler 注解实现全局异常处理。

15.2 通过SimpleMappingExceptionResolver实现

SimpleMappingExceptionResolver异常处理器是SpringMVC定义好的异常处理器。使用 SimpleMappingExceptionResolver 进行异常处理的优缺点:

  • 优点:集成简单、有良好的扩展性、对已有代码没有入侵性等
  • 缺点:该方法仅能获取到异常信息,若在出现异常时,对需要获取除异常以外的数据的情况不适用。

下面在SpringMVC的XML文件中配置 SimpleMappingExceptionResolver 对象,配置如下。

<!-- 配置异常映射 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver" id="exceptionResolver">
    <!-- 指定默认的异常响应页面。若发生的异常不是exceptionMappings中指定的异常,则使用默认异常响应页面。 -->
    <property name="defaultErrorView" value="error"></property>
    <!-- exceptionAttribute属性:设置将异常对象存入请求域时使用的属性名 -->
    <!-- 如果没有配置这个属性,那么默认使用"exception"作为属性名。源码中文档说明如下:
           * Set the name of the model attribute as which the exception should be exposed. Default is "exception". -->
    <property name="exceptionAttribute" value="exception"></property>
    <!-- 用于指定具体的不同类型的异常所对应的异常响应页面。 -->
    <property name="exceptionMappings">
        <props>
            <!-- key属性:指定异常类型 -->
            <!-- 文本标签体:指定和异常对应的逻辑视图名称 -->
            <prop key="java.lang.ArithmeticException">show-message</prop>
            <prop key="java.lang.RuntimeException">show-runtime-message</prop>
            <prop key="java.lang.Exception">show-exception-message</prop>
        </props>
    </property>
</bean>

上面配置了三个异常类型,那么它的匹配规则是什么呢?是从最大的异常开始,还是精确匹配呢?

  • 匹配规则1:如果异常对象能够在映射关系中找到精确匹配的规则,那么就执行这个精确匹配的规则
  • 匹配规则2:如果异常对象能够在映射关系中找到多个匹配的规则,优先采纳精确匹配的规则
  • 匹配规则3:如果异常对象能够在映射关系中找到多个匹配的规则,且没有精确匹配的,那么会采纳范围更接近的那个
  • 匹配规则4:如果注解中也配置了相同的映射关系,那么SpringMVC会优先采纳基于注解的映射

结论:在整个SpringMVC全局异常处理中,当异常发生时,会优先采取精确匹配的规则,没有的话会采纳范围更接近的那个,其它的同理。

下面创建模拟出现异常的Controller方法:

@RequestMapping(value = "/exception")
public String exceptionHandler(){
    // 模拟出现异常
    System.out.println(10 / 0);
    return "success";
}

用于展示异常信息的页面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>异常信息页面</title>
</head>
<body>
    <h1>系统信息</h1>
    异常对象:${requestScope.exception}<br/>
    异常消息:${requestScope.exception.message}<br/>
</body>
</html>

测试的结果如下图所示:

image

15.3 通过实现HandlerExceptionResolver接口

上面使用的是Spring MVC定义好的SimpleMappingExceptionResolver异常处理器,可以实现发生指定异常后跳转到指定的页面。但若要实现在捕获到指定异常时,执行一些额外操作它是完成不了的。此时,就需要自定义异常处理器,需要使用到HandlerExceptionResolver接口。

首先新建一个自定义异常类 CustomException:

/**
 * 自定义异常
 */
public class CustomException extends Exception{

    private String message;
    
    public CustomException(String message) {
        super(message);
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

然后创建一个实现 HandlerExceptionResolver 接口的实现类,并且实现其唯一的方法resolveException(),这种方式可以进行全局的异常处理。

/**
 * 异常处理,通过实现HandlerExceptionResolver接口来实现
 */
@Component
public class GlobalException implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest httpServletRequest,
                                         HttpServletResponse httpServletResponse,
                                         Object o, 
                                         Exception exception) {
        System.out.println(exception.getMessage());
        CustomException customException = null;

        //解析出异常类型,如果该异常类型是自定义的异常,则直接取出异常信息,否则用自定义异常输出一下
        if (exception instanceof CustomException){
            customException = (CustomException) exception;
        }else {
            customException = new CustomException("出现了未知的错误!!!");
        }
        // 获取错误信息
        String message = customException.getMessage();
        System.out.println(message);
        System.out.println("---------");
        // 将错误信息带到页面输出
        ModelAndView mv = new ModelAndView();
        mv.addObject("exception",customException);
        mv.setViewName("show-exception-message");
        return mv;
    }
}

resolveException方法的参数“Exception e”即为Controller或其下层抛出的异常。参数“Object o”就是处理器适配器要执行的Handler对象。resolveException方法的返回值类型是ModelAndView,也就是说,可以通过这个返回值类设置发出异常时显示的页面。

15.4 使用 @ExceptionHandler注解

@ExceptionHandler注解用来将一个方法标注为异常处理方法。该注解中只有一个可选的属性value,是一个Class<?>数组,用于指定该注解的方法所要处理的异常类,即所要匹配的异常。被该注解修饰的方法的返回值为异常处理后的跳转页面,其返回值可以是ModelAndView、String,或void;方法名随意,方法的参数可以是 Exception 及其子类对象、Model、HttpServletRequest、HttpServletResponse 等。系统会自动为这些方法参数赋值。

@ExceptionHandler注解处理异常的作用域:单个类,只针对当前Controller。

/**
 * 基于注解的异常处理
 */
@Controller
public class ExceptionController {
    @RequestMapping(value = "/exception1")
    public String exception1() {
        // 模拟出现异常
        System.out.println(10 / 0);
        return "success";
    }

    @RequestMapping(value = "/exception2")
    public void exception2() throws CustomException {
        // 模拟出现异常
        throw new CustomException("我抛出了一个异常!!!");
    }

    //处理自定义异常
    @ExceptionHandler({CustomException.class, ArithmeticException.class})
    public String exceptionHandler1(Exception e, Model model) {
        // 打印错误信息
        System.out.println(e.getMessage());
        e.printStackTrace();
        // 将错误数据存入请求域
        model.addAttribute("exception", e);
        return "show-annotation-message";
    }
}

上面的代码运行结果:

image

注意:如果在Controller中单独使用这个注解是有缺陷的,就是不能够全局处理异常,因为进行异常处理的方法必须与出错的方法在同一个Controller里面,也就是说每个Controller类中都要写一遍,所以实用性不高。

解决方案:可以将处理异常的信息抽取出来放在一个BaseController,然后对需要处理异常的Controller继承该类即可。

public class BaseController {
    //处理自定义异常
    @ExceptionHandler({CustomException.class, ArithmeticException.class})
    public String exceptionHandler1(Exception e, Model model) {
        // 打印错误信息
        System.out.println(e.getMessage());
        e.printStackTrace();
        // 将错误数据存入请求域
        model.addAttribute("exception", e);
        return "show-annotation-message";
    }
}

但是还是存在同样的问题,每个类都得继承它,可见这种方式同样不可取,所以一般使用下面这种方式:@ControllerAdvice和@ ExceptionHandle 注解配合使用。

15.5 用 @ControllerAdvice+@ ExceptionHandler注解(推荐)

上面说到 @ExceptionHandler注解标注的异常处理方法必须与出错的方法在同一个Controller里面,所以这种方式是只对应单个Controller类。那么此时有一种更好的解决方案:可以使用@ControllerAdvice+@ExceptionHandler注解来解决,这个是 Spring 3.2 带来的新特性。

两者一起使用的作用域:全局异常处理,针对全部Controller中的指定异常类

@ControllerAdvice和@ ExceptionHandler 这两个注解配合使用的代码如下:

/**
 * 基于注解的异常处理  @ControllerAdvice+@ExceptionHandler
 */
// 这个注解表示当前类是一个异常映射类
@ControllerAdvice
public class MyException {

    // 在@ExceptionHandler注解中指定异常类型
    @ExceptionHandler(value = {CustomException.class, ArithmeticException.class})
    public ModelAndView exceptionMapping(Exception exception) {// 方法形参位置接收SpringMVC捕获到的异常对象

        // 可以将异常对象存入模型;将展示异常信息的视图设置为逻辑视图名称
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("exception", exception);
        modelAndView.setViewName("show-annotation-message");
        // 打印一下信息
        System.out.println(exception.getMessage());
        return modelAndView;
    }
}

注:@ControllerAdvice 注解的内部是使用@Component 注解修饰的,可以点进源码查看运行:

image

@Component,@Service,@Controller,@Repository注解修饰的类,就是把这个类的对象交由Spring IOC容器来管理,相当于配置文件中的 <bean id="" class=""/>

16、SSM的简易整合,基于RESTful风格

16.1 整合前言

本文是一个简单的SSM整合案例,基于RESTful风格,主要是简单记录一下学习笔记。

开发的相关环境:

  • JDK:1.8
  • Spring:5.2.7.RELEASE
  • Mybatis:3.5.3
  • MySQL数据库
  • Druid连接池

数据库源文件:

-- ----------------------------
-- Table structure for t_emp
-- ----------------------------
DROP TABLE IF EXISTS `t_emp`;
CREATE TABLE `t_emp`  (
    `emp_id` int(11) NOT NULL AUTO_INCREMENT,
    `emp_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    `emp_salary` double(10, 2) NULL DEFAULT NULL,
    `emp_address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    PRIMARY KEY (`emp_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_emp
-- ----------------------------
INSERT INTO `t_emp` VALUES (1, '张三', 13000.00, '广东深圳南山区');
INSERT INTO `t_emp` VALUES (2, '李四', 6000.00, '山东菏泽曹县');
INSERT INTO `t_emp` VALUES (3, '赵六', 10000.00, '上海浦东区');

导入相关的依赖:

<!--定义版本信息-->
<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>
    <junit.version>4.12</junit.version>
    <spring.version>5.2.7.RELEASE</spring.version>
    <mybatis.version>3.5.3</mybatis.version>
    <mybatis.spring.version>2.0.6</mybatis.spring.version>
    <mybatis.generator.version>1.4.0</mybatis.generator.version>
    <log4j.version>1.2.17</log4j.version>
    <mysql.version>8.0.23</mysql.version>
    <druid.version>1.2.4</druid.version>
</properties>

<dependencies>
    <!--Spring相关依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <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>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.6</version>
    </dependency>
    <!--Mybatis相关依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>${mybatis.version}</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>${mybatis.spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.generator</groupId>
        <artifactId>mybatis-generator-core</artifactId>
        <version>${mybatis.generator.version}</version>
    </dependency>
    <!--数据库相关依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.version}</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid.version}</version>
    </dependency>
    <!--日志相关依赖-->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>${log4j.version}</version>
    </dependency>
    <!--单元测试依赖-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
    </dependency>
    <!--处理json数据的依赖-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.5</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>2.9.5</version>
    </dependency>
</dependencies>

16.2 相关的配置文件

注:配置文件都放在maven项目的resources目录下。

连接数据库的文件(db.properties):

datasource.driver-class-name=com.mysql.cj.jdbc.Driver
datasource.url=jdbc:mysql://localhost:3306/ssm?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&allowMultiQueries=true
datasource.username=root
datasource.password=root

Spring的核心xml配置文件(applicationContext.xml):

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

    <!-- ================一、连接数据库================ -->
    <!--1、引入外部资源文件-->
    <context:property-placeholder location="classpath:db.properties"/>

    <!--2、配置数据源-->
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
        <property name="driverClassName" value="${datasource.driver-class-name}"/>
        <property name="url" value="${datasource.url}"/>
        <property name="username" value="${datasource.username}"/>
        <property name="password" value="${datasource.password}"/>
    </bean>

    <!-- ===================二、整合Mybatis==================== -->
    <!-- 1.配置SqlSessionFactory -->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactory">

        <!-- 装配数据源 -->
        <property name="dataSource" ref="dataSource"/>

        <!-- 配置Mybatis全局配置的两个方案只能二选一 -->
        <!-- (1)、配置Mybatis全局配置方案一:还是在Mybatis自己的配置文件中配,在Spring中指定配置文件位置 -->
        <!-- Mybatis全局配置文件的位置使用configLocation指定 <property name="configLocation" value=""/> -->
        <!-- (2)、配置Mybatis全局配置方案二(推荐):在Spring配置文件配置 -->
        <!-- 在Spring中执行Mybatis全局配置 -->
        <property name="configuration">
            <bean class="org.apache.ibatis.session.Configuration">
                <property name="mapUnderscoreToCamelCase" value="true"/><!--驼峰原则-->
                <property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"/><!--打印SQL-->
            </bean>
        </property>
        <!-- 配置别名,使用包扫描-->
        <property name="typeAliasesPackage" value="com.thr.entity"/>
        <!-- 指定Mapper配置文件的位置XxxMapper.xml -->
        <property name="mapperLocations" value="classpath:com/thr/mapper/*Mapper.xml"/>
    </bean>

    <!-- 2.对Mapper接口所在的包进行扫描 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" id="mapperScannerConfigurer">
        <!-- 指定Mybatis中存放Mapper接口的包 -->
        <property name="basePackage" value="com.thr.mapper"/>
        <!-- 可选,如果不写,Spring启动时候。容器中自动会按照类型去把SqlSessionFactory对象注入进来 -->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

    <!-- ===================三、配置声明式事务==================== -->
    <!-- 1.配置事务管理器的bean -->
    <bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 2.开启基于注解的声明式事务 -->
    <tx:annotation-driven/>
    <!-- 3.配置自动扫描的包 -->
    <context:component-scan base-package="com.thr.service"/>
</beans>

SpringMVC相关的xml配置文件(spring-mvc.xml):

<?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">

    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.thr.controller"/>

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

    <!-- 配置default-servlet-handler将没有映射的请求放行,静态资源(图片、js、css等)交回给servlet容器(Tomcat)处理 -->
    <mvc:default-servlet-handler/>
    <!-- 开启SpringMVC的注解驱动功能(标配) -->
    <mvc:annotation-driven/>
</beans>

日志的配置(log4j.properties):

# 全局日志配置 INFO DEBUG ERROR
log4j.rootLogger=INFO, stdout
# MyBatis 日志配置
log4j.logger.cn.kgc.kb09=TRACE
# 控制台输出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%ns

16.3 Mybatis逆向工程生成代码

逆向工程的配置文件(generatorConfig.xml):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

    <!--targetRuntime="MyBatis3Simple"表示生成简易版本,这里创建原始版本,参数为MyBatis3-->
    <context id="DB2Tables" targetRuntime="MyBatis3">
        <commentGenerator>
            <!-- 是否去除自动生成的注释,true:是;false:否 -->
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>
        <!--数据库连接的信息:驱动类、连接地址、用户名、密码 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/ssm?useUnicode=true&amp;characterEncoding=utf8&amp;
                                       useSSL=false&amp;serverTimezone=GMT%2B8"
                        userId="root"
                        password="root">
        </jdbcConnection>
        <!-- 默认false,把JDBC DECIMAL和NUMERIC类型解析为Integer,为true时把JDBC DECIMAL 和
            NUMERIC 类型解析为java.math.BigDecimal -->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>

        <!-- javaBean的生成策略,targetProject:POJO类生成的位置,注意要加上项目名称,在这里搞了好久 -->
        <javaModelGenerator targetPackage="com.thr.entity" targetProject="./ssm/src/main/java">
            <!-- enableSubPackages:是否让schema作为包的后缀 -->
            <property name="enableSubPackages" value="true"/>
            <!-- 从数据库返回的值被清理前后的空格 -->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>
        <!-- SQL映射文件的生成策略,targetProject:mapper映射文件生成的位置 -->
        <sqlMapGenerator targetPackage="com.thr.mapper" targetProject="./ssm/src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>
        <!-- Mapper接口的生成策略,targetPackage:mapper接口生成的位置 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.thr.mapper" targetProject="./ssm/src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!-- 配置表信息 -->
        <!-- schema即为数据库名 tableName为对应的数据库表 domainObjectName是要生成的实体类 enableByExample是否生成example类   -->
        <table schema="ssm" tableName="t_emp" domainObjectName="Employee"/>
    </context>
</generatorConfiguration>

逆向工程生成相关代码的启动类:

/**
 * 逆向工程核心生成代码
 */
public class GeneratorSql {
    public static void main(String[] args) throws Exception {
        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        // 指定逆向工程配置文件
        String file = GeneratorSql.class.getResource("/generatorConfig.xml").getFile();
        File configFile = new File(file);
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        myBatisGenerator.generate(null);
    }
}

执行后会生成如下相关的代码。

image

16.4 业务层(Service)和控制层(Controller)

编写Service接口(EmployeeService):

public interface EmployeeService {

    List<Employee> findAll();

    Employee findById(Integer id);

    void add(Employee employee);

    void update(Employee employee);

    void delete(Integer id);
}

编写Service接口的实现类(EmployeeServiceImpl):

@Service
@Transactional
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Override
    public List<Employee> findAll() {
        return employeeMapper.selectByExample(new EmployeeExample());
    }

    @Override
    public Employee findById(Integer id) {
        return employeeMapper.selectByPrimaryKey(id);
    }

    @Override
    public void add(Employee employee) {
        employeeMapper.insert(employee);
    }

    @Override
    public void update(Employee employee) {
        employeeMapper.updateByPrimaryKey(employee);
    }

    @Override
    public void delete(Integer id) {
        employeeMapper.deleteByPrimaryKey(id);
    }
}

编写控制层Controller代码(EmployeeController):

@RestController
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    /**
     * 查询所有员工信息
     * @return
     */
    @GetMapping("/emp")
    public List<Employee> findAll(){
        List<Employee> list = employeeService.findAll();
        System.out.println(list);
        return list;
    }

    /**
     * 根据ID查询
     * @param id
     * @return
     */
    @GetMapping("/emp/{id}")
    public Employee findById(@PathVariable("id") Integer id){
        return employeeService.findById(id);
    }

    /**
     * 添加
     * @param employee
     */
    @PostMapping("/emp")
    public void add(Employee employee){
        employeeService.add(employee);
    }

    /**
     * 修改,这里接收前端发来的JSON格式的数据
     * @param employee
     */
    @PutMapping("/emp")
    public void update(@RequestBody Employee employee){
        employeeService.update(employee);
    }

    /**
     * 删除数据
     * @param id
     */
    @DeleteMapping("/emp/{id}")
    public void delete(@PathVariable("id") Integer id){
        employeeService.delete(id);
    }
}
折叠 

17.5测试整合结果

[1]、查询所有数据(GET请求):http://localhost:8080/ssm/emp/

image

[2]、根据ID查询数据(GET请求):http://localhost:8080/ssm/emp/1

image

[3]、添加员工信息(POST请求):http://localhost:8080/ssm/emp

image

对PostMan的Body中的四个选项进行简单说明:

  • form-data:等价于http请求中的multipart/form-data,可以上传文件或者键值对,最后都会转化为一条消息,一般用来做文件的上传。
  • x-www-form-urlencoded:相当于http请求中的application/x-www-from-urlencoded,只能上传键值对,相当于将表单内的数据转换为Key-Value。
  • raw:表示的参数可以是任意格式的,可以上传text、json、xml、html等。
  • binary:相当于Content-Type:application/octet-stream,只可以上传二进制数据,通常用来上传文件,但是一次只能上传一个文件。

添加后的数据库变化:

image

[4]、修改数据(PUT请求):http://localhost:8080/ssm/emp ,这里发送的json格式的数据,因为PUT请求用key--value不好测试。

image

修改后的数据库变化:

image

[5]、删除数据(DELETE请求):http://localhost:8080/ssm/emp/4

image

image

posted @ 2022-09-04 14:46  Angelzheng  阅读(79)  评论(0编辑  收藏  举报