SpringMVC

文章目录

  • 1. 简介
  • 2. 第一个程序
  • 3. @RequestMapping
    • 3.1 功能
    • 3.2 注解位置
    • 3.3 注解属性
    • 3.4 支持ant风格的路径
    • 3.5 支持RESTFul架构的路径⭐
  • 4. SpringMVC获取请求参数
    • 4.1 原生ServletAPI获取
    • 4.2 控制器方法的形参获取
    • 4.3 @RequestParam
    • 4.4 @RequestHeader
    • 4.5 @CookieValue
    • 4.6 通过Bean获取
    • 4.7 乱码问题
  • 5. 域对象共享数据
    • 5.1 Request域
    • 5.2 Session域
    • 5.3 Application域
  • 6. SpringMVC中的视图
    • 6.1 ThymeleafView
    • 6.2 InternalResourceView
    • 6.3 RedirectView
    • 6.4 视图控制器view-controller
    • 6.5 JSP的视图解析器(了解)
  • 7.RESTFul
    • 7.1 概念
    • 7.2 模拟用户模块操作
    • 7.3 Thymeleaf的循环语法
  • 8. HttpMessageConverter
    • 8.1 @RequestBody
    • 8.2 RequestEntity
    • 8.3 @ResponseBody
      • ⭐处理json
      • ⭐处理ajax
      • @RestController
      • 响应乱码
    • 8.4 ResponseEntity
      • ⭐文件下载
      • ⭐文件上传
  • 9. 拦截器
    • 9.1 三个方法
    • 9.2 创建拦截器
    • 9.3 多个拦截器执行顺序
  • 10. 异常处理器
    • 配置文件方式
    • 注解方式
  • 11. 完全注解开发
    • 11.1 创建初始化类
    • 11.2 创建WebConfig类
    • 11.3 创建SpringConfig类
  • 12. SpringMVC执行流程
    • 12.1 常用组件
    • 12.2 DispatcherServlet初始化
    • 12.3 DispatcherServlet服务过程
    • 12.4 执行流程
  • 13. JSR303后端校验
  • 14. 异步请求
  • 15. 邮件发送
  • 16. 定时任务

1. 简介

MVC:Model View Controller ,一种软件架构思想

Model:指的是JavaBean,包括User,Service,Dao等

View:视图层值过程中的html,jsp页面

Controller:控制层,工程中的servlet,接受响应响应浏览器

SpringMVC:是Spring的一个后续产品,子项目,为表述层开发提供的一整套的解决方案(表述层:前台页面和后台Servlet)


SpringMVC特点:

  • Spring家产品,与IOC基础无缝对接
  • 基于原生的Servlet,通过强大的前端控制器DispatcherServlt,对请求和响应进行统一的处理
  • 代码清新简洁,提高开发效率
  • 内部组件化程度高,想要什么功能配置什么相应组件即可
  • 性能卓越

我们下面使用的是org.springframework:spring-webmvc:5.3.15 ,这个依赖了Spring5 中的一些核心包,SpringMVC本质就是在Spring上

依赖

<dependencies>
    <dependency>
        <groupId>junitgroupId>
        <artifactId>junitartifactId>
        <version>4.11version>
        <scope>testscope>
    dependency>

    
    <dependency>
        <groupId>org.springframeworkgroupId>
        <artifactId>spring-webmvcartifactId>
        <version>5.3.15version>
    dependency>

    
    <dependency>
        <groupId>ch.qos.logbackgroupId>
        <artifactId>logback-classicartifactId>
        <version>1.2.3version>
        <scope>testscope>
    dependency>

    
    <dependency>
        <groupId>javax.servletgroupId>
        <artifactId>javax.servlet-apiartifactId>
        <version>3.1.0version>
        <scope>providedscope>
    dependency>

    
    <dependency>
        <groupId>org.thymeleafgroupId>
        <artifactId>thymeleaf-spring5artifactId>
        <version>3.0.14.RELEASEversion>
    dependency>
dependencies>

2. 第一个程序

  1. 配置web.xml,因为我们的请求都要被SpringMVC的前端控制器 DispatcherServlt 统一处理

① 默认配置方式:通过这种配置方式,SpringMVC的配置文件默认在WEB-INF在,默认名称是[servlet-name]-servlet.xml,这样就不好了,因为一般配置文件都放在main/resources目录下,所以建议用下面的扩展配置方式


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

② 扩展配置方式:⭐ 可以通过servlet标签里面的init-param标签设置SpringMVC框架的配置文件位置和名称,通过load-on-startup标签设置前端控制器的初始化时间。

web.xml:


<servlet>
    <servlet-name>DispatcherServletservlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>

    <init-param>
        
        <param-name>contextConfigLocationparam-name>  
        
        <param-value>classpath:springMVC.xmlparam-value>    
    init-param>
    
    
    <load-on-startup>1load-on-startup>  
servlet>

<servlet-mapping>
    <servlet-name>DispatcherServletservlet-name>
    <url-pattern>/url-pattern>  
servlet-mapping>
  1. main/resources/springMVC.xml:编写SpringMVC的XML配置文件

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


<bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
    <property name="order" value="1"/> 
    <property name="characterEncoding" value="UTF-8"/> 
    <property name="templateEngine"> 
        <bean class="org.thymeleaf.spring5.SpringTemplateEngine"> 
            <property name="templateResolver">
                <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">

                      
                    <property name="prefix" value="/WEB-INF/templates/"/> 

                    
                    <property name="suffix" value=".html"/>  
                    <property name="templateMode" value="HTML5"/>
                    <property name="characterEncoding" value="UTF-8" />
                bean>
            property>
        bean>
    property>
bean>

之所以把要解析的html文件放到WEB-INF下因为这些html页面中有Thymeleaf的解析器的语法,不能让用户直接访问到(即使让用户访问到用户也看不明白),要经过解析器解析后展现给用户 。解析器如果加上前后缀,我们只需要写文件名就行了,不需要加上全路径,解析的时候会自动加上了。

Thymeleaf的解析器能解析能多种文件html,xml等,但现在最多用来解析html文件。

  1. 创建一个WEB-INF/templates/index.html文件

注意:要加入使用解析器,要引入解析器名称空间

DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">   
<head>
    <meta charset="UTF-8">
    <title>首页title>
head>
<body>
    <h1>Test页面h1>
body>
html>
  1. 创建请求控制器,访问templates下的index.html

由于DispatcherServlet对请求统一处理,而具体的请求又有不同的处理过程,所以要创建处理具体请求的类,即请求控制器,其中每一个处理请求的方法是控制器方法

@Controller  // 放到IOC容器中
public class HelloController {

    /**
     * 要访问的是 “/”  ->  /WEB-INF/templates/index.html,我们把前后缀去掉加上视图名称,方法名可以随便写的
     * @RequestMapping 创建映射关系,当请求是/时,就会来执行这个注解所标记的方法,解析器会解析返回值,加上前后缀去跳转
     * @return 返回视图名称
     */
    @RequestMapping(value = "/")  
    public String index() {
        return "index";
    }
}
  1. 配置Tomcat,直接运行就能访问到WEB-INF/templates/index.html文件

  2. 超链接访问其他目标资源,WEB-INF/templates/target.html

index.html:

<body>
    
    <a th:href="@{/target}">访问目标页面target.htmla>
body>

HelloController.java (请求控制器)

@Controller  // 放到IOC容器中
public class HelloController {
    // 访问 "/target"  ->  /WEB-INF/templates/target.html
    @RequestMapping("/target")
    public String toTarget() {
        return "target";
    }
}

最后在浏览器输入http://localhost:8080/springmvc/target,就能访问target.html资源

  1. 总结
  • 浏览器发送请求,若请求地址符合前端控制器的url-pattern(web.xml里面写的),该请求就会被前端控制器DispatcherServlet处理。

  • 前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器,将请求地址和控制器中**@RequestMapping注解的value属性值**进行匹配。

  • 若匹配成功,该注解所标识的控制器方法就是处理请求的方法。处理请求的方法需要返回一个字符串类型的视图名称,该视图名称会被视图解析器解析,加上前缀和后缀组成视图的路径,通过Thymeleaf对视图进行渲染,最终请求转发到视图所对应页面。(浏览器地址栏没变)


3. @RequestMapping


3.1 功能

请求映射,将请求和处理请求的控制器方法关联起来,建议映射关系。SpringMVC收到指定的请求就会来找在映射关系中对应的控制器方法来处理这个请求。


3.2 注解位置

  • 类上面:设置映射请求的请求路径的初始信息
  • 方法上面:设置请求的请求路径的具体信息
@Controller  
@RequestMapping("/user")          // 通常用于不同模块设置访问路径
public class HelloController {
    
    @RequestMapping("/target")    // 想要访问target.html资源,访问路径应为 .../工程路径/user/target
    public String toTarget() {
        return "target";
    }
}

如果有多个控制器,而且控制器中的控制器方法,RequestMapping中处理的请求路径是一样的,springMVC就不知道找谁处理了,就会报错。所以RequestMapping中的请求路径是唯一的。

所以我们可以在多个控制器上面加上RequestMapping注解,区分请求路径,加一层路径,从而因映射不冲突!!!


3.3 注解属性

  • value :根据请求的请求地址来匹配,String[]类型,

    可以多个@RequestMapping(value = {"/target01", "/target02"}),满足数组中其中一个就行,这个属性必须设置。

  • methed:根据请求的请求方式来进行匹配,RequestMethod[]类型,不写是不以请求方式匹配,则所有请求方式都行

    @RequestMapping(value = "/target", method = RequestMethod.GET)既要请求路径满足,请求方式也要满足!可以多个请求方式。请求不被支持则会报405错误。

    对于处理请求方式的控制器方法,SpringMVC提供了RequestMapping的派生注解,就不用特别设置method属性了

    • GetMapping :处理Get请求的映射,下面一样,里面直接写value就行了
    • PostMapping
    • PutMapping
    • DeleteMapping

    但是目前浏览器只支持Get/Post,若在form的action属性设置put/delete,则按照默认的Get请求方式,下面有解决方法

  • params:根据请求的请求参数匹配,String[]类型,可以通过四种表达式设置请求参数和请求映射的匹配关系,多个必须同时满足,参数不匹配则会报400错误。

    • "param" :请求映射的请求必须携带param请求参数

    • "!param"

    • "param=value" :请求映射的请求必须携带param请求参数并且值等于value

    • "param!=value"

    
    <a th:href="@{/target(username='admin',id=1)}">测试param参数a> 
    

  • headers:根据请求的请求头信息匹配,String[]类型,也是四种方式。请求头不匹配则会报404错误。

    header !header header=value header!=value 和上面表达意思一样

3.4 支持ant风格的路径

ant风格:类似模糊匹配

以下是在RequestMapping注解的value属性下可以用的:

:表示任意的单个字符

*:表示任意的0个或多个字符

**:表示任意的一层或多层目录,没有也行

注意:在使用**时,只能使用/**/xxx的方式

@RequestMapping("/t?mp/target")
public String toTarget() {
    return "target";
}

3.5 支持RESTFul架构的路径⭐

原始方式:/deleteUser?id=1

rest方式:/deleteUser/1

/userSpringMVCToColltronller?id=1&username=admin

-> 把请求参数以写成请求路径的方式

-> /user/springmvc/to/colltronller/1/admin

<a th:href="@{/testPath/1}">测试占位符a> <br>
@Controller  
public class HelloController {
    // /{占位符} 花括号里面就是占位符,@PathVariable("占位符")会将请求路径中的值赋给注解后面对应的形参!!!
    // 如果映射的路径中有占位符,请求路径中也必须有这个值(这层路径),例如没有后面的'/1'或者'1',就匹配不上,404
	@RequestMapping("/testPath/{id}")
    public String testPath(@PathVariable("id") Integer id) {
        System.out.println("Id = " + id);  // 1
        return "success";
    }
}


4. SpringMVC获取请求参数


4.1 原生ServletAPI获取

前端控制器DispatcherServlet,里面封装了很多数据,会根据控制器方法的形参自动注入值,我们可以直接使用

<a th:href="@{/param(id=1,username='admin')}">测试获取请求参数a> <br>
@RequestMapping("/param")
public String testParam(HttpServletRequest req) {  //会把当前请求的Request对象赋给形参,如果是rest风格就不能这样了
    String id = req.getParameter("id");
    String username = req.getParameter("username");
    return "success";
}

4.2 控制器方法的形参获取

只需要保证控制器方法里面的形参名和请求参数的name保持一致就行了,SpringMVC帮我们获取好了,自动注入

<a th:href="@{/param(id=1,username='admin')}">测试获取请求参数a> <br>
@RequestMapping("/param")
// 如果hobby是个复选框用字符串数组行。String hobby也行则多个值中间会用逗号进行拼接,例如a,b,c
public String testParam(String id, String username, String[] hobby) { 
    System.out.println(id + " " + username);
    return "success";
}

4.3 @RequestParam

上面的方法当我们的请求参数和我们的控制器方法里面的参数名不一致,就获取不到了!!

我们可以在形参前面使用 RequestParam 注解,让请求参数和我们的形参映射,这样我们就不用改前端的源代码了!

<a th:href="@{/param(id=1,user_name='admin')}">测试获取请求参数a> <br>
@RequestMapping("/param")
public String testParam(String id, @RequestParam("user_name") String username) {
    System.out.println(id + " " + username);
    return "success";
}

属性 boolean required() 默认是true,代表value对应的参数一定要传输,没有就报错400

属性String defaultValue() 默认值的意思,这个不管required是不是true还是false,当请求参数不传或是空字符串时 则使用默认值为形参赋值


4.4 @RequestHeader

让请求头信息和我们的形参映射,找到请求头对应信息赋给我们的形参

@RequestMapping("/param")
public String testParam(@RequestHeader("Host") String host) {
    System.out.println("Host请求头的信息是:" + host);
    return "success";
}

属性 boolean required()

属性 String defaultValue()


4.5 @CookieValue

让Cookie和我们的形参映射,将Cookie的key对应的信息赋给我们的形参

@RequestMapping("/param")
public String testParam(@CookieValue("JSESSIONID") String value) {
    System.out.println("JSESSIONID对应的值为:" + value);
    return "success";
}

上面这三个注解的里面的属性是一样的,都有那三个属性

属性 boolean required()

属性 String defaultValue()


4.6 通过Bean获取

当我们请求参数和我们实体类对象的属性名对应,那我们可以直接在形参位置使用这个实体类!SpringMVC自动为我们装好了

<form th:action="@{/testBean}" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    年龄:<input type="text" name="age"><br>
    <input type="submit">
form>
public class User {
    private Integer id;
    private String username;
    private String password;
    private Integer age;  // 下面省略了有参无参get/set方法
}

@Controller
public class HelloController {
    @RequestMapping("/testBean")
    public String testBean(User user) {  // 这样就不会很多参数了
        System.out.println("User = " + user); 
        return "success";
    }
}

4.7 乱码问题

以上,post请求可能会有乱码,而设置编码必须要在前端处理器DispatcherServlet获取参数之前,所以可以使用过滤器设置编码,而SpringMVC内置了许多过滤器,我们在web.xml里面配置一下就好了

Web三大组件执行顺序 ,Listener -> Filter -> Servlet,而 Listener 在项目启动时只执行一次,所以要使用 Filter


<filter>
    <filter-name>CharacterEncodingFilterfilter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
    <init-param>
        
        <param-name>encodingparam-name> 
        <param-value>UTF-8param-value>
    init-param>
    <init-param>
        
        <param-name>forceResponseEncodingparam-name>  
        <param-value>trueparam-value>
    init-param>
filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilterfilter-name>
    <url-pattern>/*url-pattern>  
filter-mapping>


5. 域对象共享数据

一共有四个域对象,pageContext -> request -> session -> application

这里不用JSP页面了,所以我们可以少一个域对象pageContext


5.1 Request域

  1. 原生的(用的少)

    @RequestMapping("/testRequest")
    public String testRequest(HttpServletRequest req) {  
        req.setAttribute("username", "admin");
        // 这个就是转发到 ...\WEB-INF\templates\success.html,所以可以在success.html获取Request域中数据
        return "success";  
    }
    

    success.html:

    <body>
        success <br>
        <p th:text="${username}">p>  
    body>
    
  2. 使用ModelAndView (SpringMVC建议用这个)

    不管什么方式在前端处理器底层都会封装为为ModelAndView!! 注意控制器方法的返回值

    /**
      * ModelAndView有Model和View的功能
      * Model主要用于向请求域共享数据
      * View主要用于设置视图,实现页面跳转
      * @return 返回值必须是ModelAndView!!
      *         返回给前端控制器解析他,不返回SpringMVC也不知道我们创建了这个ModelAndView
      */
    @RequestMapping("/testModelAndView")
    public ModelAndView testModelAndView() {
        ModelAndView modelAndView = new ModelAndView();
        // 处理模型数据 - 往Request域中存数据
        modelAndView.addObject("username", "sutong1");
        // 设置视图名称,相当于以前返回的字符串
        modelAndView.setViewName("success"); 
        return modelAndView;
    }
    
  3. 使用Model

    在控制器形参设置Model参数,返回视图名称

    @RequestMapping("/testModel")
    public String testModel(Model model) {
        model.addAttribute("username", "sutong2");
        return "success";
    }
    
  4. 使用Map

    在控制器方法形参创建一个Map,方法里往Map里面存数据就是往Request域里面存放数据,最后返回视图名称

    @RequestMapping("/testMap")
    public String testMap(Map<String, Object> map) {
        map.put("username", "sutong3");
        return "success";
    }
    
  5. 使用ModelMap

    在控制器形参设置ModelMap类型参数,返回视图名称,和第三种类似!!

    @RequestMapping("/testModelMap")
    public String testModelMap(ModelMap modelMap) {
        modelMap.addAttribute("username", "sutong4");
        return "success";
    }
    

Model, Map, ModelMap关系:

当我们打印上面三个的运行时类名是都是用一个,BindingAwareModelMap!即都是BindingAwareModelMap类型实例的

public interface Model{}
public class ModelMap extends LinkedHashMap<String, Object> {}
public class ExtendedModelMap extends ModelMap implements Model {}
public class BindingAwareModelMap extends ExtendedModelMap {}

5.2 Session域

  1. 原生的

    @RequestMapping("/testSession")
    public String testSession(HttpSession session) {
        session.setAttribute("username", "hello");
        return "success";
    }
    
    <p th:text="${session.username}"> p>   
    
  2. @SessionAttribute 注解,只能使用在类定义上(不太好用)

    作用:配置需要在session中存放的数据范围,SpringMVC将存放在model中对应的数据暂存到HttpSession中。

    @SessionAttributes(value={"user"}) // 还可以通过type选择
    @Controller
    public class UserController {
        @RequestMapping("/testSessionAttributes")
        public String testSessionAttributes(Model model){
            User user = new User("sutong","123");
            // 把model中所有数据放到request域,并且会把model中key为user的数据拷贝一份放到session域!
            model.addAttribute("user", user); 
            return "success";
        }
    }
    

5.3 Application域

  1. 原生的

    @RequestMapping("/testApplication") // 在参数上直接填ServletConfig/ServletContext会报错
    public String testApplication(HttpSession session) {  
        // 获取上下文对象很多方法,可以通过Session/Request/ServletConfig获取ServletContext !!!
        ServletContext application = session.getServletContext(); 
        application.setAttribute("username", "word");
        return "success";
    }
    
    <p th:text="${application.username}"> p>
    


6. SpringMVC中的视图

SpringMVC中的视图是个View接口,视图的作用渲染数据,将模型Model中的数据展示给用户。

视图的种类很多,默认有:

  • 转发视图 InternalResourceView
  • 重定向视图 RedirectView

当工程引入jstl 标签库的依赖(JSP里面用的),转发视图会自动转换为JstlView

若使用的视图技术为Thymeleaf,即配置了Thymeleaf的视图解析器,由此视图解析器解析之后所得到的是 ThymeleafView

创建的视图对象是什么只和我们返回的视图名称有关系!!!

返回视图名称 没有前缀就是 ThymeleafView

forward 为前缀时就是 InternalResourceView

redirect 为前缀时就是 RedirectView


6.1 ThymeleafView

当控制器方法中所设置的视图名称没有加任何前缀时,则视图名称会被我们配置的ThymeleafViewResolver视图解析器解析(是ThymeleafView类型视图,即是底层的View的运行类型),视图名称拼接视图前缀和后缀所得到的最终路径,通过转发的方式实现跳转

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

6.2 InternalResourceView

SpringMVC中默认的转发视图是InternalResourceView

当视图名称以"forward:"为前缀时,则创建InternalResourceView视图,此时的视图名称不会被我们配置的ThymeleafView视图解析器解析,而是会将前缀"forward:"去掉,剩余部分作为最终路径通过转发的方式实现跳转。

这个可以转发到另一个请求,也可以转发到一个页面(当然这个页面如果有thymeleaf语法还得要服务器解析)。

例如"forward:/testThymeleaf",“forward:/order”

@RequestMapping("/testInternalResourceView")
public String testInternalResourceView () {
    return "forward:/testThymeleaf";   // 转发到上面的那个测试,还要创建一个ThymeleafView
}

6.3 RedirectView

SpringMVC中默认的重定向视图是RedirectView

当视图名称以"redirect:"为前缀时,则创建RedirectVie视图,此时的视图名称不会被我们配置的ThymeleafView视图解析器解析,而是会将前缀"redirect:"去掉,剩余部分作为最终路径通过重定向的方式实现跳转

可以重定向一个页面,请求,项目外的路径等。

@RequestMapping("/testRedirectView")
public String testRedirectView () {
    return "redirect:/testThymeleaf";  // 重定向到上面的那个测试,还要创建一个ThymeleafView
}

6.4 视图控制器view-controller

当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称时,可以将处理器方法使用view-controller标签进行表示,就不需要写个方法了。

springMVC.xml:



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

完全注解开发:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
    }
}

6.5 JSP的视图解析器(了解)

开启工程会自动访问工程下的index.jsp,通过首页的a标签访问/WEB-INF/jsp/testJsp.jsp页面

springMVC.xml


<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
bean>

webapp/index.jsp

测试jsp解析器

/WEB-INF/jsp/testJsp.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Titletitle>
head>
<body>
    
    <h1>Jsp页面h1>
body>
html>

JspController.java

@Controller
public class JspController {
    @RequestMapping("/testJsp")
    public String testJsp() {
        // 没有配置Thymeleaf视图解析器,这个工程只会创建默认的两种视图,
        // 当没有任何前缀和加forward前缀则会被JSP的视图解析器InternalResourceViewResolver解析,
        // 创建InternalResourceView视图加上前后缀转发。
        // redirect前缀是创建RedirectView视图
        return "testJsp";   
    }
}


7.RESTFul


7.1 概念

REST:Representational state Transfer,表述层资源状态转移(表述层:前台页面和后台Servlet),一种思想(统一的规则)

通过请求路径来实现间接操作资源的目的,就是上面的rest风格的路径,让请求路径统一,而根据请求方式做不同的业务。

在HTTP协议里面,四个表述操作方式的动词:GET:获取资源,POST:新建资源,PUT:更新资源,DELETE:删除资源

REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。现在几乎都是这样的风格

操作 传统方式 REST风格
查询操作 getUserById?id=1 user/1 --> get请求方式
保存操作 saveUser user --> post请求方式
删除操作 deleteUser?id=1 user/1 --> delete请求方式
更新操作 updateUser user --> put请求方式

7.2 模拟用户模块操作

SpringMVC提供的过滤器 HiddenHttpMethodFilter可将 POST 请求转换为 DELETE ,PUT,PATCH请求

HiddenHttpMethodFilter 处理putdelete请求的条件:

  • 当前请求的请求方式必须为post

  • 当前请求必须传输请求参数_method,就会把post请求转化为参数_method的值!!

注意在web.xml中注册时,必须先注册CharacterEncodingFilter,再注册HiddenHttpMethodFilter,因为HiddenHttpMethodFilter有获取请求参数的代码,而获取了请求参数在设置编码就没用了!!

web.xml:


<filter>
    <filter-name>HiddenHttpMethodFilterfilter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilterfilter-class>
filter>
<filter-mapping>
    <filter-name>HiddenHttpMethodFilterfilter-name>
    <url-pattern>/*url-pattern> 
filter-mapping>

index.html

<a th:href="@{/user/1}">查询用户id为1的用户信息a> <br>




<form th:action="@{/user}" method="post">        
    
    <input type="hidden" name="_method" value="PUT">  
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    年龄:<input type="text" name="age"><br>
    <input type="submit" value="修改">
form>


<form th:action="@{/user/1}" method="post">
    
    <input type="hidden" name="_method" value="DELETE">  
    <input type="submit" value="删除id为1的用户">
form>

UserController.java

// 使用RestFul风格模拟用户资源的操作
@Controller
public class UserController {

    @GetMapping("/user/{id}")
    public String getOneUser(@PathVariable("id") Integer id) {
        System.out.println("查询Id为" + id + "的用户信息");
        return "success";
    }

    @PutMapping("/user")
    public String updateUser(User user) {
        System.out.println("修改了" + user + "用户信息");
        return "success";
    }

    @DeleteMapping("/user/{id}")
    public String deleteUser(@PathVariable("id") Integer id) {
        System.out.println("删除了Id为" + id + "的用户信息");
        return "success";
    }
}

7.3 Thymeleaf的循环语法

Thymeleaf的循环/细节

<table id="dataTable">
    <tr>
        <th>Idth>
        <th>Nameth>
        <th>Optionsth>
    tr>
    <tr th:each="emp : ${employeeList}">  
        <td th:text="${emp.id}">td>
        <td th:text="${emp.name}">td>
        <td>
            
            <a th:href="@{/employee/}+${emp.id}" v-on:click="deleteEmp">Deletea>  
            <a th:href="">Updatea>
        td>
    tr>
table>

<form method="post" id="deleteForm">
    <input type="hidden" name="_method" value="delete">  
form>


<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js">script>
<script type="text/javascript">
    var vm = new Vue({
        el: "#dataTable",
        methods: {
            deleteEmp: function (event) {
                var deleteForm = document.getElementById("deleteForm");
                deleteForm.action = event.target.href; // 表单根据超链接的提交地址去提交
                deleteForm.submit();     // 提交表单 get->delete
                event.preventDefault();  // 取消超链接的默认行为
            }
        }
    });
script>

<mvc:default-servlet-handler/>


<mvc:annotation-driven/>


8. HttpMessageConverter

HttpMessageConverter:报文信转换器,将请求报文转化为java对象,或将java对象转化为响应报文

两个注解

  • @RequestBody
  • @ResponseBody

两个类

  • RequestEntity
  • ResponseEntity

8.1 @RequestBody

@RequestBoby : 可以获取请求体(就是和req.getParameter类似),在控制器方法设置一个形参,使用@RequestBody进行标识,当前请求的请求体就会为形参赋值

<form th:action="@{/testRequestBody}" method="post">  
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    <input type="submit">
form>
@RequestMapping("/testRequestBody")
public String testRequestBody(@RequestBody String reqBobyStr) {
    System.out.println(reqBobyStr); // username=sutong&password=123
    return "success";
}

8.2 RequestEntity

RequestEntity:封装请求报文的一个类,在控制器方法的形参中设置该类型的形参,当前请求的请求报文就会赋值给该形参,可以通过getHeaders()获取请求头信息,通过getBody()获取请求体信息

@RequestMapping("/testRequestEntity")
public String testRequestEntity(RequestEntity<String> reqEntity){
    System.out.println(reqEntity.getHeaders()); // 请求头的所有k-v都会输出,获得特定一个可以继续.get("Host")
    System.out.println(reqEntity.getBody());    // Post才有请求体,就是参数
    return "success";
}

8.3 @ResponseBody

@ResponseBody 用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器

@RequestMapping("/testResponseBody")
public void testResponseBody(HttpServletResponse resp){
    resp.getWriter().write("haha");
}


// SpringMVC中写法!!
@RequestMapping("/testResponseBody")
@ResponseBody                          
public String testResponseBody() {
    return "haha";  // 响应的是haha这个字符串,就不是视图名称了,而是当前响应的响应体
}

⭐处理json

如果想要想要响应对象的话,要先转化为Json字符串响应给浏览器

(这里我们用的是jackson,内嵌于Spring框架方便点,好像还比Gson快点)

  1. 加入jackson依赖

  2. 开启mvc的注解驱动

  3. 在控制器方法是使用@ResponseBody标识

  4. 将Java对象直接作为控制器方法的返回值返回,jackson就会自动帮我们转化为Json格式的字符串

@RequestMapping("/testResponseBody")
@ResponseBody
public User testResponseBody() {
    // 如果返回的是一个对象,要先处理为JSON字符串
    return new User(1, "苏瞳", "123", 19);  // 挺方便的!!
}

// {"id":1,"username":"苏瞳","password":"123","age":19}

⭐处理ajax

导入axios.js或者cdn版的,来发送 ajax 请求(少用JQuery)

<div id="app">
    
    <a th:href="@{/testAxios}" v-on:click="onAxios">SpringMVC处理Ajax请求a> 
div>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js">script>
<script src="https://unpkg.com/axios/dist/axios.min.js">script>
<script type="text/javascript">
    new Vue({
        el: "#app",
        methods: {
            onAxios: function (event) {
                axios({                          // 使用axios发送Ajax请求 - get
                    method: "get",
                    url: event.target.href,
                    params: {
                        username: "admin",
                        password: "123456"
                    }
                }).then(response => {
                    alert(response.data);  // 显示服务器响应的数据
                });
                event.preventDefault();  // 阻止上面测试超链接的默认行为

                // axios.get('event.target.href', {    // 这样写也行
                //     params: {
                //         username: "admin",
                //         password: "123456"
                //     }
                // }).then(function (response) {
                //     alert(response.data);
                // });
            }
        }
    });
script>
@RequestMapping("/testAxios")
@ResponseBody
public String testAxios(String username, String password) {
    System.out.println(username + " " + password); // admin 123456
    return "hello, axios";
}

@RestController

@ResponseBody 这个注解后面是用的最多的!!

所以SpringMVC提供了一个复合注解@RestController,标识在控制器的类上面,相当于为类添加了 @Controller,并且为每一个方法都添加了@ResponseBody注解!!!


响应乱码

我们以前在Filter里面设置过请求和响应编码了,但不知道为啥直接返回中文字符串还响应乱码!!??

过滤器中已经设置了response的响应格式,但是返回值是string类型时会使用StringHttpMessageConverter这个类的对象去输出,这个类的对象默认使用的格式ISO-8859-1来输出,所以也就意味着前面你在过滤器无论怎样设置response的写入格式都会被覆盖。解决的方案有三种:

第一种是自己去重写StringHttpMessageConvert这个类,然后设置它的默认编码格式为utf-8,然后将他注册进你的springmvc中

第二种就是下面这种利用配置文件的方式

第三种可以在@RequestMapping注解加上属性produces="text/html;charset=utf-8"解决,但这个只能解决当前的控制器方法

<mvc:annotation-driven>
    <mvc:message-converters>
        
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <property name="defaultCharset" value="UTF-8" />
            <property name="supportedMediaTypes">
                <list>
                    <value>text/htmlvalue>
                    <value>text/plainvalue>
                    <value>application/jsonvalue>
                list>
            property>
        bean>
    mvc:message-converters>
mvc:annotation-driven>

8.4 ResponseEntity

ResponseEntity用于控制器方法的返回值类型,处理下载二进制文件的接口,该控制器方法的返回值就是响应到浏览器的响应报文 (相当于自定义的响应报文)


⭐文件下载

要下载文件路径:webapp/static/imgs/head.jpg ,下面的代码几乎固定的,无非就改改下载的文件名而已!!

<a th:href="@{/testDownload}">下载head.jpg图片a>
@Controller
public class FileUpAndDownController {

    @RequestMapping("/testDownload")
    public ResponseEntity<byte[]> testDownload(HttpSession session) throws IOException {
        // 1.获取要下载的文件路径
        ServletContext context = session.getServletContext(); // 获得ServletContext
        String downloadPath = context.getRealPath("/static/imgs/head.jpg"); // 文件在服务器的部署真实路径
        System.out.println(downloadPath);

        // 2.读取要下载的文件 - 就是响应体
        //  new FileInputStream(downloadPath)或者context.getResourceAsStream("/static/imgs/head.jpg"); 
        //  参数路径不同,前者要写绝对路径,后置是以部署到tomcat服务器上的项目名开始的,写绝对路径好像不行!!
        FileInputStream fis = new FileInputStream(downloadPath);
        byte[] bytes = new byte[fis.available()];
        fis.read(bytes);

        // 3.设置响应头 - 告诉客户端收到的数据是用于下载的,不是直接显示。设置下载文件的类型和服务器上传的类型一样
        HttpHeaders respHeaders = new HttpHeaders();
        respHeaders.add("Content-Disposition", "attachment;filename=head.jpg");
        respHeaders.setContentType(MediaType.valueOf(context.getMimeType(downloadPath)));

        // 4.创建ResponseEntity,返回
        ResponseEntity<byte[]> entity = new ResponseEntity<>(bytes, respHeaders, HttpStatus.OK);
        fis.close();
        return entity;
        
        // 链式编程
        /return ResponseEntity.ok().headers(respHeaders).body(bytes);
    }
}

⭐文件上传

条件:

  • 必须是**post**请求
  • encType 属性值必须为 multipart/form-data ,提交的数据以多端(每一个表单项一个数据段)的形式进行拼接,然后以二进制流的形式发送给服务器。
  • 使用 input type="file" 添加上传的文件

记得引入commons-fileupload依赖,我引入的是1.4版本,利用SpringMVC比以前学的简单多了!!

index.html

<form th:action="@{/testUpload}" method="post" enctype="multipart/form-data"> 
    请上传头像:<input type="file" name="photo"> <br>
    <input type="submit" value="提交">
form>

SpringMVC.xml

配置文件上传解析器!!!!!!✨✨✨


<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>

FileUpAndDownController.java

// 文件上传 - SpringMVC的帮我们封装好了photo信息(MultipartFile类型)
@PostMapping("/testUpload")
public String testUpload(MultipartFile photo, HttpSession session) throws IOException {
    // photo.getName() -> photo - 获取的是表单元素的name属性值
    // photo.getOriginalFilename() -> head.jpg - 获得上传的文件名

    ServletContext context = session.getServletContext();            // 获得ServletContext对象
    String uploadFileName = photo.getOriginalFilename();             // 获取上传的文件名
    String saveFolder = context.getRealPath("/WEB-INF/uploadFiles"); // 获取要上传到的文件夹
    File saveFile = new File(saveFolder);                            // 第一次上传文件目录可能不存在,判断一下
    if (!saveFile.exists()) {
        saveFile.mkdir();
    }

    String finalSavePath = saveFolder + File.separator + uploadFileName;
    photo.transferTo(Paths.get(finalSavePath)); // 传入Paths,File都行,上传到部署服务器端的文件夹中了!!
    return "success";
}

文件一般会保存到用户访问不能直接访问的目录下 。

File.separator是系统默认路径分隔符,win下是 ‘/’,注意上面这是保存到了部署的真是目录下,保存到了服务器中。

如果上面文件名出现重复则出文件覆盖,可以用UUID保证文件名的唯一性,防止覆盖。

如果目录文件过多影响读写速度,还可以使用目录生成算法分散储存。

原生的请看JavaWeb阶段写的笔记!!

使用UUID解决文件名重复:

String userUploadFileName = photo.getOriginalFilename();                // head.jpg 获取上传的文件名
String suffixName = userUploadFileName.substring(userUploadFileName.lastIndexOf('.')); // 获取文件后缀 .jpg
String realUploadFileName = UUID.randomUUID().toString() + suffixName;  // UUID + .jpg

9. 拦截器

拦截器英语:Interceptor

请求 -> Filter -> DispatcherServlet -> Interceptor -> Controller

SpringMVC中的拦截器用于拦截控制器方法的执行


9.1 三个方法

preHandle()控制器方法执行之前执行,有boolean类型的返回值表示是否拦截或放行,返回true为放行,即去调用控制器方法,返回false表示拦截,即不调用控制器方法。

postHandle()控制器方法执行之后执行

afterComplation():处理完视图和模型数据,渲染视图完毕之后执行

// 前端控制器 DispatcherServlet 里面源码!!!!
if (!mappedHandler.applyPreHandle(processedRequest, response)) {  // 调用所有拦截器的preHandle()
    return;
}
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // 调用控制器方法
if (asyncManager.isConcurrentHandlingStarted()) {
    return;
}
this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);  // 调用所有拦截器的postHandle()
....
// 里面是调用afterComplation()
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);


//-------------------------------------------processDispatchResult方法里面:
this.render(mv, request, response);          // 渲染视图
if (mappedHandler != null) {
    //调用所有拦截器的afterComplation()
    mappedHandler.triggerAfterCompletion(request, response, (Exception)null); 
}

9.2 创建拦截器

  1. 创建一个类MyInterceptor需要实现HandlerInterceptor接口
  2. SpringMVC的配置文件中进行配置
// 拦截器
@Component
public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                             Object handler) throws Exception {
        System.out.println("控制器方法执行前");
        return true;  // true代表放行
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, 
                           Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("控制器方法执行后");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                Object handler, Exception ex) throws Exception {
        System.out.println("渲染视图后!");
    }
}

<mvc:interceptors> 
    
    <bean class="com.sutong.interceptors.MyInterceptor"/> 
    
    
    <ref bean="myInterceptor"/> 
mvc:interceptors>



<mvc:interceptors>
    <mvc:interceptor>
        
        <mvc:mapping path="/**"/> 
        <mvc:exclude-mapping path="/"/>   
        <ref bean="myInterceptor"/>       
    mvc:interceptor>
mvc:interceptors>

9.3 多个拦截器执行顺序

  • 若每个拦截器的preHandle()都返回true

    此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关。

    preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行

    for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++)  // preHanders
        
    for(int i = this.interceptorList.size() - 1; i >= 0; --i)  // postHanders
        
    for(int i = this.interceptorIndex; i >= 0; --i)  // afterCompletions
    
  • 若某个拦截器的preHandle()返回了false

    preHandle()返回false和它之前的拦截器的preHandle()都会执行,**而所有的postHandle()都不执行!!**返回false的拦截器之前的拦截器的afterComplation()会执行



10. 异常处理器

SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver

实现类有:DefaultHandlerExceptionResolverSimpleMappingExceptionResolver

前者是默认的异常处理器,后者是自定义异常处理器。


配置文件方式


<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">  
        <props>
            
            <prop key="java.lang.ArithmeticException">errorprop>   
        props>
    property>
    
    
    <property name="exceptionAttribute" value="ex"/>  
bean>

注解方式

@ControllerAdvice  // 其实是Component扩展出来的注解
public class ExceptionController {
 
    // 如果控制器方法出现了一下的异常,则改注解标识的控制器方法将作为新的控制器方法使用,形参自动注入当前出现的异常
    @ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class})
    public String testException(Exception ex, Model model) {
        model.addAttribute("ex", ex); // 放到Request域 
        return "error";
    }
}


11. 完全注解开发

使用配置类,代替web.xmlSpringMVC.xml 配置文件的功能


11.1 创建初始化类

创建初始化类代替web.xml文件!!!

Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器。(Servlet容器:就是Tomcat服务器!!)

Spring提供了这个接口的实现, 即SpringServletContainerInitializer, 这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。

Spring3.2 引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展(继承)了它并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。

/**
 * web工程的初始化类 - 代替web.xml
 * 1.配置前端处理器 - DispatcherServlet
 * 2.字符编码过滤器 - CharacterEncodingFilter
 * 3.请求转换处理过滤器 - HiddenHttpMethodFilter
 */
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {

    // 获取根配置,即指定spring的配置类,父容器(配置:Services,Repositories,Datasources,TransactionManager...)
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class};
    }

    // 指定SpringMVC的配置类,子容器(配置:Controllers,ViewReslover,HandlerMapping..)
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    // 指定前端处理器DispatcherServlet的映射规则,即url-pattern
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"}; // 处理除.jsp结尾的所有请求
    }

    // 设置过滤器Filter,不需要可以不用重写这个方法,默认都是过滤所有的请求,即'/*'
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter cef = new CharacterEncodingFilter();  // 字符编码过滤器
        cef.setEncoding("UTF-8");
        cef.setForceResponseEncoding(true);

        HiddenHttpMethodFilter hhmf = new HiddenHttpMethodFilter();   // Post转Put/Delete请求过滤器
        return new Filter[]{cef, hhmf};
    }
}

11.2 创建WebConfig类

创建WebConfig类:代替 SpringMVC.xml

/**
 * 代替SpringMVC的配置文件
 * 1.扫描组件 - component-scan
 * 2.配置视图解析器Thymeleaf - ThymeleafViewResolver
 * 3.视图控制器 - view-controller
 * 4.开启MVC注解驱动,并且解决字符串响应乱码 - annotation-driven
 * 5.开放对静态资源的访问 - default-servlet-handler
 * 6.配置文件上传解析器 - CommonsMultipartResolver
 * 7.配置拦截器 - interceptors
 * 8.配置异常处理器 - 这个也可以直接用注解@ControllerAdvice
 */
@Configuration                                // 标记为是个配置类
@ComponentScan(basePackages = "com.sutong")   // 1开启注解扫描
@EnableWebMvc                                 // 4开启MVC注解驱动和开启MVC的定制功能
public class WebConfig implements WebMvcConfigurer {

    // 3视图控制器
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // addViewController是path,setViewName是视图名
        registry.addViewController("/").setViewName("index"); 
    }

    // 5开放对静态资源的访问,Override的都是实现WebMvcConfigurer接口的方法
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable(); // 默认ServletHandler可用
    }

    // 6文件上传解析器
    @Bean
    public MultipartResolver multipartResolver() {
        return new CommonsMultipartResolver();
    } 
    
    // 7拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        MyInterceptor myInterceptor = new MyInterceptor();
        registry.addInterceptor(myInterceptor).addPathPatterns("/**").excludePathPatterns("/"); // 添加
    }

    // 8异常解析器,可以用这个方法,也可用使用@Bean+@ExceptionHandler
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties prop = new Properties();
        // key是要处理的异常全类名字符串,value是发生该异常跳转的视图
        prop.setProperty("java.lang.ArithmeticException", "error");
        exceptionResolver.setExceptionMappings(prop);               
        exceptionResolver.setExceptionAttribute("ex");              // 异常信息放入Request域
        resolvers.add(exceptionResolver);                           // 放入参数的List中
    }

    
    //2Thymeleaf解析器的配置,好长啊,SpringBoot好像会简单-----------------------------------
    //配置生成模板解析器
    @Bean
    public ITemplateResolver templateResolver() {
        WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
        // ServletContextTemplateResolver需要一个ServletContext作为构造参数,
        // 可通过WebApplicationContext 的方法获得
        ServletContextTemplateResolver templateResolver = 
            new ServletContextTemplateResolver(webApplicationContext.getServletContext());
        templateResolver.setPrefix("/WEB-INF/templates/");  // 前缀
        templateResolver.setSuffix(".html");                // 后缀
        templateResolver.setCharacterEncoding("UTF-8");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        return templateResolver;
    }
    //生成模板引擎并为模板引擎注入模板解析器
    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }
    //生成视图解析器并未解析器注入模板引擎
    @Bean
    public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setOrder(1);
        viewResolver.setCharacterEncoding("UTF-8");
        viewResolver.setTemplateEngine(templateEngine);
        return viewResolver;
    }
    //------------------------------------------------------------------------------------
}


11.3 创建SpringConfig类

/**
 * 这里没用整合SSM,这个暂时可以不写
 * 要想形成父子容器,这里需要排除一些扫描包
 * (而上面那个WebConfig配置类需要配置只扫描Controller,形成互补,
 * 		只扫描记得禁用默认是扫描规则 useDefaultFilters = false )
 */
@Configuration
@ComponentScan(value = "com.sutong", excludeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
})
public class SpringConfig {
    
}


12. SpringMVC执行流程


12.1 常用组件

  • DispatcherServlet 前端控制器,框架提供
  • HandlerMapping 控制器映射器,框架提供
  • Handler 控制器,程序员开发
  • HandlerAdapter 控制器适配器,框架提供
  • ViewResolver 视图解析器,框架提供
  • View 视图,框架或视图技术提供(页面需自己写)

12.2 DispatcherServlet初始化

初始化过程:

Servlet - init() -> GenericServlet - init() -> HttpServlet null -> HttpServletBean init() - initServletBean()

-> FrameworkServlet initServletBean() - initWebApplicationContext() - createWebApplicationContext() - onRefresh()

-> DispatcherServlet onRefresh() - initStrategies()

createWebApplicationContext这个方法到整合ssm的时候可能会用到

// 里面部分代码
ConfigurableWebApplicationContext wac =
       (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass); //创建IOC容器
wac.setEnvironment(this.getEnvironment());
wac.setParent(parent);  // !!!设置父容器
String configLocation = this.getContextConfigLocation();
if (configLocation != null) {
       wac.setConfigLocation(configLocation);
}

initStrategies() 部分源码(看这初始化工作做了很多!所以要提前DispatcherServlet 初始化不要再第一次访问的时候才初始化):

protected void initStrategies(ApplicationContext context) {
    this.initMultipartResolver(context);
    this.initLocaleResolver(context);
    this.initThemeResolver(context);
    this.initHandlerMappings(context);
    this.initHandlerAdapters(context);
    this.initHandlerExceptionResolvers(context);
    this.initRequestToViewNameTranslator(context);
    this.initViewResolvers(context);
    this.initFlashMapManager(context);
}

12.3 DispatcherServlet服务过程

Servlet service() -> GenericServlet service() -> HttpServlet service() - doPost()/doGet()/doPut()/… -> HttpServletBean null

-> FrameworkServlet 重写了doPost()/doGet()/…方法 - processRequest() - doService()

-> DispatcherServlet - doService() - doDispatch() - handle()就是给我们控制器方法形参赋值,调用控制器方法,调用这个方法前后有执行拦截器的方法。

doDispatch() 部分源码:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // preHandle
    if (!mappedHandler.applyPreHandle(processedRequest, response)) {
        return;
    }
    // 执行控制器方法
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
	// postHandle
    mappedHandler.applyPostHandle(processedRequest, response, mv);     
    // afterComplation - 在processDispatchResult()里面调用triggerAfterCompletion()才是真正执行
    this.processDispatchResult(processedRequest, response,                       
                               mappedHandler, mv, (Exception)dispatchException);
}

12.4 执行流程

  1. 用户向服务器请求,请求会被前端处理器DispatcherServlet捕获
  2. DispatcherServlet对请求进行解析,得到请求资源标识符(url),判断请求url对象的映射:
    • 映射不存在
      1. 看是否配置了defalut-servlet-handler
      2. 如果没配置,则404
      3. 如果有配置,则去访问目标资源(一般是静态资源JS,CSS,HTML等),找不到目标资源还是404
    • 映射存在
      1. 根据URL调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象和对应的拦截器),最后以HandlerExecutionChain执行链对象形式返回
      2. DispatcherServlet根据获得的Handler,选择一个合适的HandlerAdapter
      3. 如果成功获取HandlerAdapter,则开始执行拦截器preHandler()方法(正向)
      4. 提取Request中的模型数据,填充Handler的入参,开始执行Handler方法,处理请求。在填充入参的过程中,SpringMVC会根据你的配置做一些额外的工作
        • HttpMessageConventer:将请求消息(JSON,XML数据)转化为一个对象/将对象转化为响应的信息
        • 数据转换:对请求消息进行数据转换,如String转化为IntergerDouble
        • 数据格式化:对请求消息进行格式化,如将字符串转化为格式化数字或格式化日期等
        • 数据验证:验证数据的有效性(长度,格式等),验证结果存储到BindingResultError
      5. Handler执行完之后,返回一个ModelAndView对象
      6. 开始执行拦截器的postHandler()方法(逆向)
      7. 根据返回的ModelAndView(如果发生异常则会HandlerExceptionResolver进行处理异常)选择一个合适的ViewResolver进行视图解析,根据ModelView来渲染视图
      8. 渲染视图后执行拦截器的afterCompletion()方法(逆向)
      9. 将渲染的结果返回给客户端


13. JSR303后端校验

1.导入hibernate-validator依赖
或者SpringBoot中引入启动器spring-boot-starter-validation,默认也是这个hibernate-validator:


<dependency>
    <groupId>org.hibernategroupId>
    <artifactId>hibernate-validatorartifactId>
    <version>7.0.2.Finalversion>
dependency>

2.Bean上加上注解(还有很多…):

public class Books {
    private Integer id;

    @Pattern(regexp = "^[a-z0-9_-]{3,16}$", message = "书名不合法")
    private String name;

    @Email(message = "邮箱不合法") // 默认的邮箱匹配规则,要和前端一样
    private String email;

    @NotNull(message = "count不能为空")
    private Integer count;

    private String detail;
}

3.Contronller层:

public class BookController {
    @Autowired
    private BookService bookService;
    
    /**
     * @param book    @Valid标注的参数代表需要校验
     * @param result  校验后的结果
     */
    @RequestMapping("/add")
    public String add(@Valid Books book, BindingResult result) {
        if (result.hasErrors()) {
            List<FieldError> errors = result.getFieldErrors();  // 获得所有错误信息
            for (FieldError error : errors) {
                System.out.print("错误的字段名:" + error.getField());
                System.out.println(" 错误信息:" + error.getDefaultMessage());
            }
            return "error";  // 一般还要保存一些错误原因等可以封装为一个Map返回..(返回一个Json字符串)..
        } 
        
        bookService.addBook(book);
        return "redirect:/book/list";
    }
}

@Validated@Valid 区别: @Validated和@Valid区别

使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同,

  1. @Valid是javax.validation里的,@Validated是@Valid 的一次封装,是Spring提供的校验机制使用。

  2. @Valid不提供分组功能,当一个实体类需要多种验证方式时,例:对于一个实体类的id来说,新增的时候是不需要的,对于更新时是必须的,对一个参数需要多种验证方式时,可通过分配不同的组达到目的。

  3. @Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上

    @Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上

  4. @Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。

    @Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。



14. 异步请求

Servlet3.0之后的规范

默认的Servlet使用异步,示例:

@WebServlet(urlPatterns = "/my", asyncSupported = true)  // 1.支持异步处理器 (Servlet3.0后)
public class MyServlet02 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
        throws ServletException, IOException {
        System.out.println("主线程:" + Thread.currentThread());
        
        // 2.开启异步模式(返回是异步的上下文对象/req.getAsyncContext())
        AsyncContext asyncContext = req.startAsync();

        // 业务逻辑进行异步处理,开启异步处理
        asyncContext.start(new Runnable() {
            @Override
            public void run() {
                System.out.println("副线程:" + Thread.currentThread());

                sayHello(); // 调用业务逻辑
                asyncContext.complete(); // 异步以及调用完了
                ServletResponse response = asyncContext.getResponse();
                try {
                    response.getWriter().write("success"); // 写东西
                } catch (IOException e) {
                    e.printStackTrace();
                }

                System.out.println("副线程:" + Thread.currentThread());
            }
        });
        
        System.out.println("主线程:" + Thread.currentThread());
    }
    
    private void sayHello() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

SpringMVC异步请求使用,示例:

import java.util.concurrent.Callable;

// 异步处理
@Controller
public class AsyncController {

    /**
     * 1.当控制器返回Callable
     * 2.SpringMVC进行异步处理,将Callable提交到TaskExecutor使用一个隔离的线程进行执行
     * 3.DispatcherServlet和所有的Filter退出web容器的线程(主线程),但Response保存打开状态
     * 4.Callable返回结果,SpringMVC将请求重新派发给容器!!恢复之前的处理
     * 5.根据Callable返回的结果,SpringMVC进入视图渲染流程/写出数据 (从收请求 -> 视图渲染)...
     *
     * 打印流程(添加了普通的拦截器):
     * preHandler.../springmvc/async01
     * 主线程开始:Thread[http-nio-8080-exec-5,5,main]
     * 主线程结束:Thread[http-nio-8080-exec-5,5,main]
     * ----DispatcherServlet和所有的Filter退出web容器的线程-----
     * 
     * ----等待Callable执行-----
     * 副线程开始:Thread[task-2,5,main]
     * 副线程结束:Thread[task-2,5,main]
     * ----等待Callable执行完成-----
     * 
     * ----再次发了一次请求-----
     * preHandle.../springmvc/async01
     * postHandle...(这个之前不用执行目标方法了,Callable的之前的返回值就是目标方法的返回值)
     * afterCompletion..
     * 
     * 上面的普通的拦截器并不能有效的拦截目标方法,
     * 要使用异步的拦截器:原生的AsyncListener,SpringMVC的AsyncHandlerInterceptor
     */
    @GetMapping("/async01")
    @ResponseBody
    public Callable<String> async01() {
        System.out.println("主线程开始:" + Thread.currentThread());

        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() {
                System.out.println("副线程开始:" + Thread.currentThread());
                sayHello(); // 业务逻辑
                System.out.println("副线程结束:" + Thread.currentThread());
                return "success";
            }
        };

        System.out.println("主线程结束:" + Thread.currentThread());
        return callable;
    }

    private void sayHello() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使用Callable 对于前台来说是和正常请求一样的,对于后台来说却可以大大增加Tomcat吞吐量。

当然有些场景, Callable并不能解决,比如说:我们访问A接口,A接口调用三方的服务,服务回调B接口,这种情况就没办法使用Callable了,这个时候可以使用DeferredResult

实际中可能没上面那么简单,要结合消息中间件等,可以下面这样(后面再详细用):

@GetMapping("async02")
@ResponseBody
public DeferredResult<String> async02() {
    DeferredResult<String> deferredResult = new DeferredResult<>(); //这个还能传入超时时间,和超出时间的Result
    // Save the deferredResult somewhere...
    return deferredResult;
}

// In some other thread
deferredResult.setResult(data);

DeferredResult 用于在不阻塞用于回答的 Tomcat HTTP 线程的情况下回答请愿书。通常是 @ResponseBody 注释方法的返回值。

DeferredResult 利用 Servlet 3.0 AsyncContext。当您需要返回结果时,它不会像其他线程那样阻塞线程。另一个很大的好处是DeferredResult 支持回调。

DeferredResult比较适合一些比较复杂的业务场景,提升性能。有个问题,当使用分布式部署,调用链走的不是同一个实例时,DeferredResult的处理有可能会出现问题。



异步任务 (对方法进行注解,以便将其异步调用)

如果在Service层使用异步,首先需要开启异步支持@EnableAsync,然后在需要异步的方法上面加@Async ,类上面添加也行,代表当前类所有方法都是异步方法,它使用的是线程池SimpleAsyncTaskExecutor,这也是Spring默认给我们提供的线程池,也可以说不是一个线程池。

  1. 异步方法建议尽量处理耗时的任务,或者是处理不希望阻断主流程的任务(比如异步记录操作日志)
  2. 尽量为@Async准备一个专门的线程池来提高效率减少开销,而不要用Spring默认的SimpleAsyncTaskExecutor,它不是一个真正的线程池~
@Service
public class MyService {
    @Async
    void doSomeWork(String s) {
        ...
    }
}

当您调用 service.doSomeWork() 时,调用将被放入执行程序的队列中以异步调用。这对于可以同时执行的任务很有用。您可以使用 Async 来执行任何类型的异步任务。


15. 邮件发送

依赖:


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-mailartifactId>
dependency>

配置:

# 邮箱发送配置
spring: 
    mail:
        username: [email protected]
        password: eayeleandankfoffbbc   # qq邮箱客户端授权码,并不是qq密码,可以防止暴露qq的密码
        host: smtp.qq.com
        properties.mail.smtp.enable: true # qq邮箱需要开启加密验证

测试:

@SpringBootTest
public class EmailTest {
    @Autowired
    JavaMailSender javaMailSender;

    @Test
    public void test01() {
        SimpleMailMessage smm = new SimpleMailMessage(); // 简单的消息邮箱
        smm.setSubject("苏瞳小号你好2"); // 主题
        smm.setText("我爱你"); // 内容
        smm.setTo("[email protected]"); // 发送给谁(我们这里是自己给自己发)
        smm.setFrom("[email protected]"); // 谁发送的
        javaMailSender.send(smm);
    }
    
    @Test
    public void test02() throws MessagingException, IOException {
        MimeMessage mm = javaMailSender.createMimeMessage(); // 复杂的邮件
        MimeMessageHelper helper = new MimeMessageHelper(mm, true, "UTF-8"); // 第二参数是否多文件
        helper.setSubject("苏瞳的文件2");
        helper.setText("

我爱你

"
, true); // 第二个参数是是否解析为HTML helper.addAttachment("1.png", new ClassPathResource("static/1.png")); // 添加附件 helper.setTo("[email protected]"); helper.setFrom("[email protected]"); javaMailSender.send(mm); } // 可以把这两个封装起来,参数:主题,正文,附件路径,收件人... }

Class.getResource(“”)获取的是相对于当前类的相对路径。

Class.getResource(“/”)获取的是classpath的根路径。

ClassLoader.getResource(“”)获取的是classpath的根路径。

spring中获取资源,四个都springframework包下的: ⭐⭐

  • FileSystemResource 从文件系统加载,比如说自己指定配置文件的全路径 (效果类似于Java中的File

  • ClassPathResource 从系统的类路径中加载 (效果类似于this.getClass().getResource(“/”).getPath();)

    在创建ClassPathResource对象时,我们可以指定是按Class的相对路径获取文件还是按ClassLoader来获取。

  • ServletContextResource 从Servlet 上下文环境中加载 (效果类似于request.getServletContext().getRealPath(“”);)

  • UrlResource 从指定的Url加载


16. 定时任务

相关接口: TaskExecutorTaskScheduler

相关注解:@EnableScheduling@Scheduled

Cron表达式(Linux中crontab差不多):

共七个占位符 * * * * * * *
第一个     一小时当中的第几秒    0-59  (Linux中没有这个秒)
第二个     一小时当中的第几分钟   0-59
第三个     一天中的第几个小时     0-23
第四个     一个月中第几天        1-31
第五个     一年中的第几月        1-12
第六个     一周中的星期几        0-7 (0,7都代表星期日)
第七个     年   		       不必须,可以省略  (Linux中也没有这个年)

*         代表任何时间,第一*则代表每分钟都要执行一次
,         代表不连续的时间,"0 8,12 * * *" 代表每天的8点0分,12点0分都会执行一次
-         代表连续的时间范围,"0 5 * * 1-6" 代表周一到周六的5点0分执行一次
*/n       代表每个多久执行一次,"*/10 4 * * *" 代表每天的4点期间,每隔10分钟执行一次(即到5点就不执行了)
?         不指定值
L 		  表示最后的意思
W 		 表示离指定日期的最近那个工作日(周一至周五).
@Service
public class ScheduledService {
    @Scheduled(cron = "0 10/5 12 * *") // 每天的12:10:00开都每隔5分钟去执行一次
    public void hello() {
        System.out.println("你被执行了");
    }
}

你可能感兴趣的:(SpringFrameWork,笔记,servlet,spring,mvc,java-ee,java)