SpringMVC

SprintMVC

框架学习:官方文档

重点:SprintMVC的执行流程

1.MVC框架设计

MVC是三个单词的首字母缩写,它们是Model(模型)、View(视图)和Controller(控制)。

  • Model(dao,service)

    是核心的"数据层"(Model),也就是程序需要操作的数据或信息。

  • View(jsp…)

    直接面向最终用户的"视图层"(View)。它是提供给用户的操作界面,是程序的外壳。

  • Controller(servlet)

    它负责根据用户输入的指令,选取"数据层"中的数据,然后对其进行相应的操作调用"视图层,产生最终结果。

先画出Spring MVC的示意图:

f

​ 图9-1 Spring MVC框架设计图

其中带有阿拉伯数字的说明,是MVC框架运行的流程。处理请求先到达控制器(Controller),控制器的作用是进行请求分发,这样它会根据请求的内容去访问模型层(Model);在现今互联网系统中,数据主要从数据库和NoSQL中来,而且对于数据库而言往往还存在事务的机制,为了适应这样的变化,设计者会把模型层再细分为两层,即服务层(Service)和数据访问层(DAO);当控制器获取到由模型层返回的数据后,就将数据渲染到视图中,这样就能够展现给用户了。当然这只是一个比较粗犷的说明,还有很多细节需要不断地完善。例如,如何接受请求参数、如何选择控制器、如何定位视图、视图类型等问题都需要我们进一步地阐述。

mvc要做的事

1.将url映射到java类

2.封装用户提交的数据

3.处理请求-调用model-封装响应数据

4.渲染响应数据

2.初识SpringMVC

Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就已包含在Spring Framework中。

Spring MVC一开始就定位于一个较为松散的组合,展示给用户的视图(View)、控制器返回的数据模型(Model)、定位视图的视图解析器(ViewResolver)和处理适配器(HandlerAdapter)等内容都是独立的。换句话说,通过Spring MVC很容易把后台的数据转换为各种类型的数据,以满足移动互联网数据多样化的要求。例如,Spring MVC可以十分方便地转换为目前最常用的JSON数据集,也可以转换为PDF、Excel和XML等。加之Spring MVC是基于Spring基础框架派生出来的Web框架,所以它天然就可以十分方便地整合到Spring框架中,而Spring整合Struts2还是比较繁复的。基于这些趋势,Spring MVC已经成为当前最主流的Web开发框架。学习SpringMVC,首先是学习其基于MVC的分层的思想。

优点:

  • 轻量级,简单易学

  • 高效,基于请求响应的MVC框架

  • 与Spring无缝结合

  • 约定大于配置

  • 功能强大:restful,数据验证,格式化,本地化,主题等等

与其他许多Web框架一样**,Spring MVC围绕前端控制器模式DispatcherServlet进行设计**,在该模式下,中央Servlet DispatcherServlet提供了用于请求处理的共享算法,而实际工作是由可配置的委托组件执行的。 该模型非常灵活,并支持多种工作流程。

image-20200802125954379

DispatcherServlet

SpringMVC的核心就是DispatcherServlet,DispatcherServlet实质也是一个HttpServlet。DispatcherSevlet负责将请求分发,所有的请求都有经过它来统一分发。

DispatcherServlet 与任何Servlet一样,都需要使用Java配置或在web.xml中根据Servlet规范声明和映射DispatcherServlet。

反过来,DispatcherServlet使用Spring配置发现请求映射,视图解析,异常处理等所需的委托组件。

image-20200802102523663

代码实现

1.配置DispatcherServlet 在web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

<!--  配置DispatcherServlet-->
  <servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--    关联配置文件 springmvc-servlet.xml-->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc-servlet.xml</param-value>
    </init-param>
<!--    设置启动级别-->
    <load-on-startup>1</load-on-startup>
  </servlet>

<!--  / 拦截所以请求 除了jsp-->
<!--  /* 拦截所以请求 加上jsp-->
  <servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>


</web-app>

2.配置springmvc容器springmvc-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        https://www.springframework.org/schema/mvc/spring-mvc.xsd">

<!--     处理映射器-->
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
<!--     处理适配器-->
    <bean  id="handlerAdapter" class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />

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

<!--    注册controller-->
    <bean id="/hello"  class="com.cb.Controller.MyController"/>

</beans>

3.写controller

package com.cb.Controller;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        //视图和模型
        ModelAndView model = new ModelAndView();
        //添加对象
        model.addObject("msg","hello springmvc");
        //添加视图名字
        model.setViewName("hello");

        return model;
    }
}

3.springMVC执行流程

首先,在Web服务器启动的过程中,如果在Spring Boot机制下启用Spring MVC,它就开始初始化一些重要的组件,如DispactherServlet、HandlerAdapter的实现类RequestMappingHandlerAdapter等组件对象。关于这些组件的初始化,我们可以看到spring-webmvc-xxx.jar包的属性文件DispatcherServlet.properties,它定义的对象都是在Spring MVC开始时就初始化,并且存放在Spring IoC容器中,

image-20200802130118065

image-20210630111532389

在上述代码中,中文是我加入的注释,这些组件会在Spring MVC得到初始化,所以我们并不需要太多的配置就能够开发Spring MVC程序,尤其是在Spring Boot中,更是如此,我们可以通过Spring Boot的配置来定制这些组件的初始化。下面我们一边开发一边谈它的运行流程。

其次是开发控制器(Controller)

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

	@Autowired
	private UserService userService = null;

	// 展示用户详情
	@RequestMapping("details")
	public ModelAndView details(Long id) {
		// 访问模型层得到数据
		User user = userService.getUser(id);
		// 模型和视图
		ModelAndView mv = new ModelAndView();
		// 定义模型视图
		mv.setViewName("user/details");
		// 加入数据模型
		mv.addObject("user", user);
		// 返回模型和视图
		return mv;
	}
}  

这里的注解@Controller表明这是一个控制器,然后@RequestMapping代表请求路径和控制器(或其方法)的映射关系,它会在Web服务器启动Spring MVC时,就被扫描到HandlerMapping的机制中存储,之后在用户发起请求被DispatcherServlet拦截后,通过URI和其他的条件,通过HandlerMapper机制就能找到对应的控制器(或其方法)进行响应。只是通过HandlerMapping返回的是一个HandlerExecutionChain对象,这个对象的源码如代码清单9-3所示。

代码清单9-3 HandlerExecutionChain源码

image-20210630112407922

从源码中可以看出,HandlerExecutionChain对象包含一个处理器(handler)。这里的处理器是对控制器(controller)的包装,因为我们的控制器方法可能存在参数,那么处理器就可以读入HTTP和上下文的相关参数,然后再传递给控制器方法。而在控制器执行完成返回后,处理器又可以通过配置信息对控制器的返回结果进行处理。从这段描述中可以看出,处理器包含了控制器方法的逻辑,此外还有处理器的拦截器(interceptor),这样就能够通过拦截处理器进一步地增强处理器的功能。

得到了处理器(handler),还需要去运行,但是我们有普通HTTP请求,也有按BeanName的请求,甚至是WebSocket的请求,所以它还需要一个适配器去运行HandlerExecutionChain对象包含的处理器,这就是HandlerAdapter接口定义的实现类。在代码清单9-1中,我们可以看到在Spring MVC中最常用的HandlerAdapter的实现类,这便是HttpRequestHandlerAdapter。通过请求的类型,DispatcherServlet就会找到它来执行Web请求的HandlerExecutionChain对象包含的内容,这样就能够执行我们的处理器(handler)了。只是HandlerAdapter运行HandlerExecutionChain对象这步还比较复杂,我们这里暂时不进行深入讨论,放到后面再谈。

在处理器调用控制器时,它首先通过模型层得到数据,再放入数据模型中,最后将返回模型和视图(ModelAndView)对象,这里控制器设置的视图名称设置为“user/details”,这样就走到了视图解析器(ViewResolver),去解析视图逻辑名称了。

在代码清单9-1中可以看到视图解析器(ViewResolver)的自动初始化。为了定制InternalResourceViewResolver初始化,可以在配置文件application.properties中进行配置,如代码清单9-4所示。

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

通过修改这样的配置,就能在Spring Boot的机制下定制InternalResourceViewResolver这个视图解析器的初始化,也就是在返回视图名称之后,它会以前缀(prefix)和后缀(suffix)以及视图名称组成全路径定位视图。例如,在控制器中返回的是“user/details”,那么它就会找到/WEB-INF/jsp/user/details.jsp作为视图(View)。严格地说,这一步也不是必需的,因为有些视图并不需要逻辑名称,在不需要的时候,就不再需要视图解析器工作了。关于这点,我们后面会再给出一个例子说明。

视图解析器定位到视图后,视图的作用是将数据模型(Model)渲染,这样就能够响应用户的请求。这一步就是视图将数据模型渲染(View)出来,用来展示给用户查看。按照我们控制器的返回,就是/WEB-INF/jsp/user/details.jsp作为我们的视图。

因为Spring MVC流程和组件的重要性,所以为了让读者有更好的认识,再次画出实例在Spring MVC里运行的流程图如图9-4所示

从图9-4中的阿拉伯数字就能看出其运行的流程,从而更好地知道Spring MVC运行的过程。但是有时候,我们可能需要的只是JSON数据集,因为目前前后台分离的趋势,使用JSON已经是主流的方式,正如我们之前使用的@ResponseBody标明方法一样,在后面Spring MVC会把数据转换为JSON数据集,但是这里暂时不谈@ResponseBody,因为它会采用处理器内部的机制。本节暂时不讨论处理器的内部机制,而是先用MappingJackson2JsonView转换出JSON,如代码清单9-7所示。

img

图9-4 实例在Spring MVC下的流程图

代码清单9-7 使用JSON视图

@RequestMapping("/detailsForJson")
public ModelAndView detailsForJson(Long id) {
   // 访问模型层得到数据
   User user = userService.getUser(id);
   // 模型和视图
   ModelAndView mv = new ModelAndView();
   // 生成JSON视图
   MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
   mv.setView(jsonView);
   // 加入模型
   mv.addObject("user", user);
   return mv;
}

可以看到,在控制器的方法中模型和视图(ModelAndView)中捆绑了JSON视图(Mapping- Jackson2JsonView)和数据模型(User对象),然后返回,其结果也会转变为JSON,只是需要注意的是这步与我们使用JSP作为视图是不一样的。在代码清单9-2中我们给视图设置了名称,它会根据视图解析器(InternalResourceViewResolver)的解析找到JSP,然后渲染数据到视图中,从而展示最后的结果,而这里的JSON视图是没有视图解析器的定位视图的,因为它不是一个逻辑视图,只是需要将数据模型(这里是User对象)转换为JSON而已。我们看看它如图9-5所示的流程图。

从流程图中我们可以看到并没有视图解析器,那是因为MappingJackson2JsonView是一个非逻辑视图。它并不需要视图解析器进行定位,它的作用只是将数据模型渲染为JSON数据集来响应请求。可见Spring MVC中,不是每一个步骤都是必需的,而是根据特别的需要会有不同的流程。也许更为让你关注的@ResponseBody则是一个在处理器内部机制转换的,后面我们在处理器内部机制的时候会谈到它。

图9-5 在Spring MVC流程中使用JSON视图

图9-5 在Spring MVC流程中使用JSON视图

4.定制Spring MVC的初始化

正如Spring Boot所承诺的那样,它会尽可能地配置Spring,对于Spring MVC也是如此,但是无论如何这些配置都可能满足不了我们的需要,需要进一步地对SpringMVC定制。

在Servlet 3.0规范中,web.xml再也不是一个必需的配置文件。为了适应这个规范,Spring MVC从3.1版本开始也进行了支持,也就是我们已经不再需要通过任何的XML去配置Spring MVC的运行环境,正如Spring Boot的宗旨,消除XML的繁杂配置。为了支持对于Spring MVC的配置,Spring提供了接口WebMvcConfigurer,这是一个基于Java 8的接口,所以其大部分方法都是default类型的,但是它们都是空实现,这样开发者只需要实现这个接口,重写需要自定义的方法即可,这样就很方便进行开发了。在Spring Boot中,自定义是通过配置类WebMvcAutoConfiguration定义的,它有一个静态的内部类WebMvcAutoConfigurationAdapter,通过它Spring Boot就自动配置了SpringMVC的初始化,它们之间的关系如图9-6所示。

图9-6 Spring MVC在Spring Boot中初始化的配置类图

图9-6 Spring MVC在Spring Boot中初始化的配置类图

在WebMvcAutoConfigurationAdapter类中,它会读入Spring配置Spring MVC的属性来初始化对应组件,这样便能够在一定程度上实现自定义。不过应该首先明确可以配置哪些内容,代码清单9-8所示是Spring Boot关于Spring MVC可以配置的内容。

代码清单9-8 Spring MVC可配置项

这些配置项将会被Spring Boot的机制读入,然后使用WebMvcAutoConfigurationAdapter去定制初始化。一般而言,我们只需要配置少数的选项就能够使得Spring MVC工作了。

对于这些选项,这个时候我们还可以参考图9-6那样,实现接口WebMvcConfigurer加入自己定义的方法就可以了,毕竟这个接口是Java 8的接口,其本身已经提供了default方法,对其定义的方法做了空实现。

5. 处理器映射

处理器映射正如第9章谈到的,如果Web工程使用了Spring MVC,那么它在启动阶段就会将注解@RequestMapping所配置的内容保存到处理器映射(HandlerMapping)机制中去,然后等待请求的到来,通过拦截请求信息与HandlerMapping进行匹配,找到对应的处理器(它包含控制器的逻辑),并将处理器及其拦截器保存到HandlerExecutionChain对象中,返回给DispatcherServlet,这样DispatcherServlet就可以运行它们了。从论述中可以看到,HandlerMapping的主要任务是将请求定位到具体的处理器上。

关于@RequestMapping的配置项并不多,这里通过源码来学习,如代码清单10-1所示。代码清单10-1 RequestMapping源码分析

代码清单10-1 RequestMapping源码分析

代码中对所有的配置项加入了中文说明。这里可以通过配置项value或者path来设置请求URL,从而让对应的请求映射到控制器或其方法上,在此基础上还可以通过其他配置项来缩小请求映射的范围。当然,配置项value和path也可以通过正则式来让方法匹配多个请求。但是从现实的角度来说,如果不是有必要,尽量不要这么做。因为这样请求的匹配规则就复杂了,会对后续开发造成一定的困扰。因此在能够明确场景下,都建议一个路径对应一个方法或者让正则式的匹配规则简单明了,这样就能够提高程序的可读性,以利于后续的维护和改造。

路径是必需的配置项,这里的method配置项可以限定HTTP的请求类型,这是最常用的配置项,可以区分HTTP的GET或者POST等不同的请求。只是在Spring 4.3的版本之后,为了简化method配置项的配置新增了几个注解,如@GetMapping、@PostMapping、@PatchMapping、@PutMapping和@DeleteMapping。本章只讨论@GetMapping和@PostMapping的使用,在后面的REST风格讨论时,才会讨论@PatchMapping、@PutMapping和@DeleteMapping。从名称可以看出,@GetMapping对应的是HTTP的GET方法,@PostMapping对应的是HTTP的POST方法,其他的配置项则与@RequestMapping并无太大的区别,通过它们就可以不再设置@RequestMapping的method配置项了。


6. 获取控制器参数

在第9章谈过,处理器是对控制器的包装,在处理器运行的过程中会调度控制器的方法,只是它在进入控制器方法之前会对HTTP的参数和上下文进行解析,将它们转换为控制器所需的参数。这一步是处理器首先需要做的事情,只是在大部分的情况下不需要自己去开发这一步,因为Spring MVC已经提供了大量的转换规则,通过这些规则就能非常简易地获取大部分的参数。正如之前章节一样,在大部分情况下,我们并没有太在意如何获取参数,那是因为之前的场景都比较简单,在实际的开发中可能遇到一些复杂的场景,这样参数的获取就会变得复杂起来。例如,可能前端传递一个格式化的日期参数,又如需要传递复杂的对象给控制器,这个时候就需要对Spring MVC参数的获取做进一步的学习了。

6.1 在无注解下获取参数

在没有注解的情况下,Spring MVC也可以获取参数,且参数允许为空,唯一的要求是参数名称和HTTP请求的参数名称保持一致,如代码清单10-2所示。

代码清单10-2 无注解获取参数

// HTTP GET请求
@GetMapping("/no/annotation")
@ResponseBody
public Map<String, Object> noAnnotation(Integer intVal, Long longVal, String str) {
   Map<String, Object> paramsMap = new HashMap<>();
   paramsMap.put("intVal", intVal);
   paramsMap.put("longVal", longVal);
   paramsMap.put("str", str);
   return paramsMap;
}

6.2 使用@RequestParam获取参数

上节谈到过,在无须任何注解的情况下,就要求HTTP参数和控制器方法参数名称保持一致。然而在前后台分离的趋势下,前端的命名规则可能与后端的规则不同,这时需要把前端的参数与后端对应起来。Spring MVC提供了注解@RequestParam来确定前后端参数名称的映射关系,下面用实例给予说明。在代码清单10-2中加入新的方法,如代码清单10-3所示。

代码清单10-3 使用@RequestParam获取参数

@GetMapping("/annotation")
	@ResponseBody
	public Map<String, Object> requestParam(@RequestParam("int_val") Integer intVal,
			@RequestParam("long_val") Long longVal, @RequestParam("str_val") String strVal) {
		Map<String, Object> paramsMap = new HashMap<>();
		paramsMap.put("intVal", intVal);
		paramsMap.put("longVal", longVal);
		paramsMap.put("strVal", strVal);
		return paramsMap;
	}

从代码中可以看到,在方法参数处使用了注解@RequestParam,其目的是指定HTTP参数和方法参数的映射关系,这样处理器就会按照其配置的映射关系来得到参数,然后调用控制器的方法。启动Spring Boot应用后,在浏览器地址栏输入http://localhost:8080/my/annotation?int_val=1&long_val=2&str_val=str,就能够看到请求的结果了。但如果把3个HTTP参数中的任意一个删去,就会得到异常报错的信息,因为在默认的情况下@RequestParam标注的参数是不能为空的,为了让它能够为空,可以配置其属性required为false,

6.3 传递数组

在Spring MVC中,除了可以像上面那样传递一些简单的值外,还可以传递数组。Spring MVC内部已经能够支持用逗号分隔的数组参数,下面在代码清单10-2中新增方法,

@GetMapping("/requestArray")@ResponseBodypublic Map<String, Object> requestArray(int[] intArr, Long[] longArr, String[] strArr) {   Map<String, Object> paramsMap = new HashMap<>();   paramsMap.put("intArr", intArr);   paramsMap.put("longArr", longArr);   paramsMap.put("strArr", strArr);   return paramsMap;}

方法里定义了采用数组,那么前端就需要依照一定的规则传递给这个方法,例如,输入http://localhost:8080/my/requestArray?intArr=1,2,3&longArr=4,5,6&strArr=str1,str2,str3,可以看到需要传递数组参数时,每个参数的数组元素只需要通过逗号分隔即可。

6.4 传递JSON

在当前前后端分离的趋势下,使用JSON已经是十分普遍了。对于前端的页面或者手机应用,可以通过请求后端获取JSON数据集,这样它们就能很方便地将数据渲染到视图中。有时前端也需要提交较为复杂的数据到后端,为了更好组织和提高代码的可读性,可以将数据转换为JSON数据集,通过HTTP请求体提交给后端,对此Spring MVC也提供了良好的支持。

前端需要提供,jQuery进行Ajax提交。注意到加粗的代码,它指定了提交的请求地址(url)、数据(data)、提交类型(contentType)和事后事件(success)。从脚本来看,这里先组织了一个JSON数据集,而且把提交类型也设置为了JSON类型,然后才提交到控制器。这样控制器就可以得到一个JSON数据集的请求体了。

@PostMapping("/insert")@ResponseBodypublic User insert(@RequestBody User user) {   userService.insertUser(user);   return user;}

请求到控制器的insert方法。这个方法的参数标注为@RequestBody,意味着它将接收前端提交的JSON请求体,而在JSON请求体与User类之间的属性名称是保持一致的,这样Spring MVC就会通过这层映射关系将JSON请求体转换为User对象。

6.5 通过URL传递参数

在一些网站中,提出了REST风格,这时参数往往通过URL进行传递。例如获取编号为1的用户,URL就要写为/user/1,这里的1代表的是用户编号(id)。SpringMVC对此也提供了良好的支持,可以通过处理器映射和注解@PathVariable的组合获取URL参数。首先通过处理器映射可以定位参数的位置和名称,而@PathVariable则可以通过名称来获取参数。下面演示通过URL传递参数获取用户信息的例子。在UserController中加入新的方法。

// {...}代表占位符,还可以配置参数名称	@GetMapping("/{id}")	// 响应为JSON数据集	@ResponseBody	// @PathVariable通过名称获取参数	public User get(@PathVariable("id") Long id) {		return userService.getUser(id);	}

代码中首先通过@GetMapping 指定一个URL,然后用{…}来标明参数的位置和名称。这里指定名称为id,这样Spring MVC就会根据请求去匹配这个方法。@PathVariable配置的字符串为id,它对应URL的参数声明,这样Spring就知道如何从URL中获取参数。于是请求http://localhost:8080/user/1,控制器就能够获取参数了。


6.6 获取格式化参数

在一些应用中,往往需要格式化数据,其中最为典型的当属日期和货币。例如,在一些系统中日期格式约定为yyyy-MM-dd,金额约定为货币符号和用逗号分隔,如100万美元写作$1,000,000.00等。同样地,Spring MVC也对此提供了良好的支持。对日期和数字类型的转换注解进行处理,分别是@DateTimeFormat和@NumberFormat。其中@DateTimeFormat是针对日期进行格式化的,@NumberFormat则是针对数字进行格式化的。

@PostMapping("/format/commit")@ResponseBodypublic Map<String, Object> format(      @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date date,      @NumberFormat(pattern = "#,###.##") Double number) {   Map<String, Object> dataMap = new HashMap<>();   dataMap.put("date", date);   dataMap.put("number", number);   return dataMap;}

format方法参数加粗的代码使用了注解@DateTimeFormat和@NumberFormat,它们配置了格式化所约定的格式,所以Spring会根据约定的格式把数据转换出来,这样就可以完成参数的转换。

image-20210630145857046

提交表单后,就可以看到对应的JSON数据集输出,这样就可以获取那些格式化的参数了。

在Spring Boot中,日期参数的格式化也可以不使用@DateTimeFormat,而只在配置文件application.properties中加入如下配置项即可:

spring.mvc.date-format=yyyy-MM-dd

7. 自定义参数转换规则

在10.2节中讨论了那些最常用的获取参数的方法,然而获取参数还没有那么简单。例如,可能与第三方公司合作,这个时候第三方公司会以密文的形式传递参数,或者其所定义的参数规则是现有Spring MVC所不能支持的,这时则需要通过自定义参数转换规则来满足这些特殊的要求。

如果回顾10.2节,你是否会惊讶于在Spring MVC中只需要简单地注解,甚至是不用任何注解就能够得到参数。那是因为Spring MVC提供的处理器会先以一套规则来实现参数的转换,而大部分的情况下开发者并不需要知道那些转换的细节。但是在开发自定义转换规则时,就很有必要掌握这套转换规则了。而实际上处理器的转换规则还包含控制器返回后的处理,只是这节先讨论处理器是如何获取和转换参数的内容,其他的则留到后面再讨论,到时会揭开为什么使用注解@ResponseBody标注方法后,就能够把控制器返回转变为JSON数据集的秘密。

HTTP的请求包含请求头(Header)、请求体(Body)、URL和参数等内容,服务器还包含其上下文环境和客户端交互会话(Session)机制,而这里的消息转换是指请求体的转换。下面我们讨论Spring MVC是如何从这些HTTP请求中获取参数的。

7.1 处理器获取参数逻辑

当一个请求来到时,在处理器执行的过程中,它首先会从HTTP请求和上下文环境来得到参数。如果是简易的参数它会以简单的转换器进行转换,而这些简单的转换器是Spring MVC自身已经提供了的。但是如果是转换HTTP请求体(Body),它就会调用HttpMessageConverter接口的方法对请求体的信息进行转换,首先它会先判断能否对请求体进行转换,如果可以就会将其转换为Java类型。代码清单10-10是对HttpMessageConverter接口的探讨。

image-20210630152143120

这里需要讨论的是canRead和read方法,canWrite和write方法将在后续章节讨论。回到代码清单10-6,代码中控制器方法的参数标注了@RequestBody,所以处理器会采用请求体(Body)的内容进行参数转换,而前端的请求体为JSON类型,所以首先它会调用canRead方法来确定请求体是否可读。如果判定可读后,接着就是使用read方法,将前端提交的用户JSON类型的请求体转换为控制器的用户(User)类参数,这样控制器就能够得到参数了。

上面的HttpMessageConverter接口只是将HTTP的请求体转换为对应的Java对象,而对于HTTP参数和其他内容,还没有进行讨论。例如,以性别参数来说,前端可能传递给控制器的是一个整数,而控制器参数却是一个枚举,这样就需要提供自定义的参数转换规则。

为了讨论自定义的参数规则,很有必要先了解处理器转换参数的过程。在SpringMVC中,是通过WebDataBinder机制来获取参数的,它的主要作用是解析HTTP请求的上下文,然后在控制器的调用之前转换参数并且提供验证的功能,为调用控制器方法做准备。处理器会从HTTP请求中读取数据,然后通过三种接口来进行各类参数转换,这三种接口是Converter、Formatter和GenericConverter。在SpringMVC的机制中这三种接口的实现类都采用了注册机的机制,默认的情况下SpringMVC已经在注册机内注册了许多的转换器,这样就可以实现大部分的数据类型的转换,所以在大部分的情况下无须开发者再提供转换器,这就是在上述章节中可以得到整型(Integer)、长整型(Long)、字符串(String)等各种各样参数的原因。同样地,当需要自定义转换规则时,只需要在注册机上注册自己的转换器就可以了。

实际上,WebDataBinder机制还有一个重要的功能,那就是验证转换结果。关于验证机制,后面会再讨论。有了参数的转换和验证,最终控制器就可以得到合法的参数。得到这些参数后,就可以调用控制器的方法了。为了更好地理解,图10-4所展示的是HTTP请求体(Body)的消息转换全流程图。

图10-4 Spring MVC处理器HTTP请求体转换流程图

​ 图10-4 Spring MVC处理器HTTP请求体转换流程图

这个图严格来说是请求体转换的全流程,但是有些时候Spring MVC并不会走完全流程,而是根据现实情况来处理消息的转换。根据上面的讨论,可以看到控制器的参数是处理器通过Converter、Formatter和GenericConverter这三个接口转换出来的。这里先谈谈这三个接口的不同之处。首先,Converter是一个普通的转换器,例如,有一个Integer类型的控制器参数,而从HTTP对应的为字符串,对应的Converter就会将字符串转换为Integer类型;其次,Formatter则是一个格式化转换器,类似那些日期字符串就是通过它按照约定的格式转换为日期的;最后,GenericConverter转换器则将HTTP参数转换为数组。这就是上述例子可以通过比较简单的注解就能够得到各类参数的原因。

对于数据类型转换,Spring MVC提供了一个服务机制去管理,它就是ConversionService接口。在默认的情况下,会使用这个接口的子类DefaultFormattingConversionService对象来管理这些转换类,其关系如图10-5所示。

从图10-5可以看出,Converter、Formatter和GenericConverter可以通过注册机接口进行注册,这样处理器就可以获取对应的转换器来实现参数的转换。

上面讨论的是普通Spring MVC的参数转换规则,而在Spring Boot中还提供了特殊的机制来管理这些转换器。Spring Boot的自动配置类WebMvcAutoConfiguration还定义了一个内部类WebMvcAuto- ConfigurationAdapter,代码清单10-11是它的源码。

图10-5 ConversionService转化机制设计

图10-5 ConversionService转化机制设计

image-20210630152437664

代码中加入了中文注释以利于理解,通过这个方法,可以看到在Spring Boot的初始化中,会将对应用户自定义的Converter、Formatter和GenericConverter的实现类所创建的Spring Bean自动地注册到DefaultFormattingConversionService对象中。这样对于开发者只需要自定义Converter、Formatter和GenericConverter的接口的Bean,Spring Boot就会通过这个方法将它们注册到ConversionService对象中。只是格式化Formatter接口,在实际开发中使用率比较低,所以不再论述。

7.2 一对一转换器(Converter)

Converter是一对一的转化器,也就是从一种类型转换为另外一种类型,其接口定义十分简单。

image-20210630153115028

这个接口的类型有源类型(S)和目标类型(T)两种,它们通过convert方法进行转换。例如,HTTP的类型为字符串(String)型,而控制器参数为Long型,那么就可以通过Spring内部提供的StringToNumber进行转换。假设前端要传递一个用户的信息,这个用户信息的格式是{id}-{userName}-{note},而控制器的参数是User类对象。因为这个格式比较特殊,Spring当前并没有对应的Converter进行转换,因此需要自定义转换器。这里需要的是一个从String转换为User的转换器,所以可以如代码清单10-13所示,对它进行了定义。

package com.springboot.chapter10.converter;import org.springframework.core.convert.converter.Converter;import org.springframework.stereotype.Component;import com.springboot.chapter10.pojo.User;/**** imports ****//** * 自定义字符串用户转换器 */@Componentpublic class StringToUserConverter implements Converter<String, User> {    /**     * 转换方法     */    @Override    public User convert(String userStr) {        User user = new User();        String []strArr = userStr.split("-");        Long id = Long.parseLong(strArr[0]);        String userName = strArr[1];        String note = strArr[2];        user.setId(id);        user.setUserName(userName);        user.setNote(note);        return user;    }}

这里的类标注为@Component,并且实现了Converter接口,这样Spring就会将这个类扫描并装配到IoC容器中。对于Spring Boot,之前分析过它会在初始化时把这个类自动地注册到转换机制中,所以注册这步并不需要人工再处理。这里泛型指定为String和User,这样Spring MVC就会通过HTTP的参数类型(String)和控制器的参数类型(User)进行匹配,就可以从注册机制中发现这个转换类,这样就能够将参数转换出来。

下面写一个控制器方法对其进行验证,如代码清单10-14所示。

@GetMapping("/converter")@ResponseBodypublic User getUserByConverter(User user) {   return user;}

在代码中设置断点,然后打开浏览器,在地址栏中输入http://localhost:8080/user/converter?user= 1-user_name_1-note_1便能够看到监控的数据,如图10-6所示

图10-6

从图10-6可以看出,参数已经被自定义的转换器StringToUserConverter转换出来。

7.3 GenericConverter集合和数组转换

GenericConverter是数组转换器。因为Spring MVC自身提供了一些数组转换器,需要自定义的并不多,所以这里只介绍Spring MVC自定义的数组转换器。假设需要同时新增多个用户,这样便需要传递一个用户列表(List)给控制器。此时Spring MVC会使用StringToCollectionConverter转换它,这个类实现了GenericConverter接口,并且是Spring MVC内部已经注册的数组转换器。它首先会把字符串用逗号分隔为一个个的子字符串,然后根据原类型泛型为String、目标类型泛型为User类,找到对应的Converter进行转换,将子字符串转换为User对象。上节我们已经自定义了转换器StringToUserConverter,这样它就可以发现这个转换器,从而将字符串转换为User类。这样控制器就能够得到List类型的参数,如图10-7所示。

image-20210630153612262

根据这样的场景,可以使用代码清单10-15进行验证。

@GetMapping("/list")@ResponseBodypublic List<User> list(List<User> userList) {   return userList;}

这样只要在浏览器地址栏中输入URL请求这个方法:

http://localhost:8080/user/list?userList=1-user_name_1-note_1,2-user_name_2-note_2,3-user_name_3-note_3

这里的参数使用了一个个逗号分隔,StringToCollectionConverter在处理时也就通过逗号分隔,然后通过之前自定义的转换器StringToUserConverter将其变为用户类对象,再组成为一个列表(List)传递给控制器。

8. 数据模型

上述章节只是谈到了参数的获取和转换,通过这些处理器终于可以调用控制器了。在Spring MVC流程中,控制器是业务逻辑核心内容,而控制器的核心内容之一就是对数据的处理。通过上章对Spring MVC全流程的学习,可以看到允许控制器自定义模型和视图(ModelAndView),其中模型是存放数据的地方,视图则是展示给用户。本节暂时把视图放下,先来讨论数据模型的问题。

数据模型的作用是绑定数据,为后面的视图渲染做准备。首先对Spring MVC使用的模型接口和类设计进行探讨,如图10-10所示。

image-20210630165933954

从图10-10可以看到,在类ModelAndView中存在一个ModelMap类型的属性,ModelMap继承了LinkedHashMap类,所以它具备Map接口的一切特性,除此之外它还可以增加数据属性。在Spring MVC的应用中,如果在控制器方法的参数中使用ModelAndView、Model或者ModelMap作为参数类型,Spring MVC会自动创建数据模型对象,如代码清单10-22所示。

package com.springboot.chapter10.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.ui.ModelMap;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.servlet.ModelAndView;import com.springboot.chapter10.pojo.User;import com.springboot.chapter10.service.UserService;/****imports****/@RequestMapping("/data")@Controllerpublic class DataModelController {    // 注入用户服务类    @Autowired    private UserService userService = null;        // 测试Model接口    @GetMapping("/model")    public String useModel(Long id, Model model) {        User user = userService.getUser(id);        model.addAttribute("user", user);        // 这里返回字符串,在Spring MVC中,会自动创建ModelAndView且绑定名称        return "data/user";    }        // 测试modelMap类    @GetMapping("/modelMap")    public ModelAndView useModelMap(Long id, ModelMap modelMap) {        User user = userService.getUser(id);        ModelAndView mv = new ModelAndView();        // 设置视图名称        mv.setViewName("data/user");        // 设置数据模型,此处modelMap并没有和mv绑定,这步系统会自动处理        modelMap.put("user", user);        return mv;    }        // 测试ModelAndView    @GetMapping("/mav")    public ModelAndView useModelAndView(Long id, ModelAndView mv) {        User user = userService.getUser(id);        // 设置数据模型        mv.addObject("user", user);        // 设置视图名称        mv.setViewName("data/user");        return mv;    }}

从这段代码中可以看出Spring MVC还是比较智能的。例如,useModel方法里,只是返回一个字符串,Spring MVC会自动生成对应的视图,并且绑定数据模型。又如,useModelMap方法,返回了ModelAndView对象,但是它没有绑定ModelMap对象,Spring MVC又会自动地绑定它。

上述数据对象,无论使用哪一个都是允许的。只是它们都是渲染同一个JSP视图,且该视图逻辑名称为/data/user,这样通过InternalResourceViewResolver的定位,它就会找到/WEB-INF/jsp/data/user.jsp作为视图,然后将数据渲染到这个JSP上。这个JSP的内容如代码清单10-23所示。

9. 视图和视图解析器

视图是渲染数据模型展示给用户的组件,在Spring MVC中又分为逻辑视图和非逻辑视图。逻辑视图是需要视图解析器(ViewResolver)进行进一步定位的。例如,之前的例子所返回的字符串之所以能找到对应的JSP,就是因为使用了逻辑视图,经由视图解析器的定位后,才能找到视图将数据模型进行渲染展示给用户查看。对于非逻辑视图,则并不需要进一步地定位视图的位置,它只需要直接将数据模型渲染出来即可。例如,代码清单9-7中的MappingJackson2JsonView视图就是这样的情况。在实际的工作中视图解析器InternalResourceViewResolver是比较常用的,前面的章节也一直在使用它,相信读者对其已经比较熟悉了。其他的视图解析器使用得已经不多,基于实用的原则,这里就不再详细介绍其他视图解析器的用法。本节主要的任务是讨论Spring MVC中视图的使用,在使用视图之前,需要先了解在Spring MVC中视图是怎么设计的。

9.1 视图设计

对于视图,除了JSON和JSP视图之外,还有其他类型的视图,如Excel、PDF等。虽然视图具有多样性,但是它们都会实现Spring MVC定义的视图接口View,其源码如代码清单10-24所示。

代码清单10-24 Spring MVC视图接口定义

image-20210630170656671

在这段代码中有两个方法,其中getContentType方法是获取HTTP响应类型的,它可以返回的类型是文本、JSON数据集或者文件等,而render方法则是将数据模型渲染到视图的,这是视图的核心方法,所以有必要进一步地讨论它。在它的参数中,model是数据模型,实际就是从控制器(或者由处理器自动绑定)返回的数据模型,这样render方法就可以把它渲染出来。渲染视图是比较复杂的过程,为了简化视图渲染的开发,在Spring MVC中已经给开发者提供了许多开发好的视图类,所以在大部分的情况下并不需要自己开发自己的视图。Spring MVC所提供的视图接口和类如图10-11所示。

image-20210630171012628

注意,图10-11中只画出了常用的视图类,并非所有的视图。从图10-11可以看出,在Spring MVC中已经开发好了各种各样的视图,所以在大部分情况下,只需要定义如何将数据模型渲染到视图中展示给用户即可。例如,之前看到的MappingJackson2JsonView视图,因为它不是逻辑视图,所以并不需要使用视图解析器(ViewResolver)去定位视图,它会将数据模型渲染为JSON数据集展示给用户查看;而常用的视图JstlView,则是一个逻辑视图,于是可以在控制器返回一个字符串,使用视图解析器去定位对应的JSP文件,就能够找到对应的JSP文件,将数据模型传递进入,JstlView就会将数据模型渲染,展示数据给用户。对于PDF和Excel视图等类型的视图,它们只需要接收数据模型,然后通过自定义的渲染即可。为了说明视图的使用方法,下一节将介绍如何使用PDF视图——AbstractPdfView。

10. 文件上传

Spring MVC对文件上传提供了良好的支持,而在Spring Boot中可以更为简单地配置文件上传所需的内容。为了更好地理解Spring Boot的配置,首先从Spring MVC的机制谈起。

10.1  Spring MVC对文件上传的支持

首先,DispatcherServlet会使用适配器模式,将HttpServletRequest接口对象转换为MultipartHttp ServletRequest对象。MultipartHttpServletRequest接口扩展了HttpServletRequest接口的所有方法,而且定义了一些操作文件的方法,这样通过这些方法就可以实现对上传文件的操作。

下面先探讨HttpServletRequest和MultipartHttpServletRequest的关系,如图10-13所示。

image-20210630172724413

这里对于文件上传的场景,Spring MVC会将HttpServletRequest对象转化为MultipartHttpServlet- Request对象。从MultipartHttpServletRequest接口的定义看,它存在许多的方法用来处理文件,这样在Spring MVC中操作文件就十分便捷。

只是在使用Spring MVC上传文件时,还需要配置MultipartHttpServletRequest,这个任务是通过MultipartResolver接口实现的。对于MultipartResolver接口,它又存在两个实现类,这两个实现类分别是StandardServletMultipartResolver和CommonsMultipartResolver,可以使用它们中的任意一个来实现文件上传。在默认的情况下Spring推荐使用的是StandardServletMultipartResolver,因为它只需要依赖于Servlet API提供的包,而对于CommonsMultipartResolver,则需要依赖于Apache提供的第三方包来实现,这显然没有StandardServletMultipartResolver来得实在。从实用的角度来说,因为Spring 3.1之后已经能够支持StandardServletMultipartResolver,所以CommonsMultipartResolver已经渐渐被废弃了,因此这里不再对其进行介绍。它们的关系如图10-14所示。

image-20210630172754158

在Spring Boot的机制内,如果你没有自定义MultipartResolver对象,那么自动配置的机制会为你自动创建MultipartResolver对象,实际为StandardServletMultipartResolver,所以你并不需要自己去创建它。为了更加灵活,Spring Boot会提供代码清单10-30所示的配置项。

image-20210630172918353

根据这些配置,Spring Boot会自动生成StandardServletMultipartResolver对象,这样就能够对上传的文件进行配置。对于文件的上传可以使用Servlet API提供的Part接口或者Spring MVC提供的MultipartFile接口作为参数。其实无论使用哪个类都是允许的,只是我更加推荐使用的是Part,因为毕竟MultipartFile是SpringMVC提供的第三方包才能进行支持的,后续版本变化的概率略大一些。

10.2 开发文件上传功能

开发Spring Boot下的MVC上传,首先需要配置代码清单10-30中的配置项,如代码清单10-31所示。

代码清单10-31 Spring MVC上传文件配置

# 指定默认上传的文件夹spring.servlet.multipart.location=e:/springboot# 限制单个文件最大大小,这里设置为5Mspring.servlet.multipart.max-file-size=5242880# 限制所有文件最大大小,这里设置为20Mspring.servlet.multipart.max-request-size=20MB 

这里定义了上传的目标文件夹为e:/springboot,并且指定单个文件最大为5MB,所有文件最大为20MB。为了测试文件的上传,需要创建JSP文件,其内容如代码清单10-32所示。

请注意,这里的表单声明为multipart/form-data,如果没有这个声明,Spring MVC就会解析文件请求出错,从而导致上传文件失败。有了这个JSP文件,下面开发文件上传控制器(这个控制器将包括使用HttpServletRequest、MultipartFile和Part参数)来完成文件上传,如代码清单10-33所示。

package com.springboot.chapter10.controller;import java.io.File;import java.util.HashMap;import java.util.Map;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.Part;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.multipart.MultipartFile;import org.springframework.web.multipart.MultipartHttpServletRequest;/**** imports ****/@Controller@RequestMapping("/file")public class FileController {    /**     * 打开文件上传请求页面     * @return 指向JSP的字符串     */    @GetMapping("/upload/page")    public String uploadPage() {        return "/file/upload";    }        // 使用HttpServletRequest作为参数    @PostMapping("/upload/request")    @ResponseBody    public Map<String, Object> uploadRequest(HttpServletRequest request) {        boolean flag = false;        MultipartHttpServletRequest mreq = null;        // 强制转换为MultipartHttpServletRequest接口对象        if (request instanceof MultipartHttpServletRequest) {            mreq = (MultipartHttpServletRequest) request;        } else {            return dealResultMap(false, "上传失败");        }        // 获取MultipartFile文件信息        MultipartFile mf = mreq.getFile("file");        // 获取源文件名称        String fileName = mf.getOriginalFilename();        File file = new File(fileName);        try {            // 保存文件            mf.transferTo(file);        } catch (Exception e) {            e.printStackTrace();            return dealResultMap(false, "上传失败");        }         return dealResultMap(true, "上传成功");    }        // 使用Spring MVC的MultipartFile类作为参数    @PostMapping("/upload/multipart")    @ResponseBody    public Map<String, Object> uploadMultipartFile(MultipartFile file) {        String fileName = file.getOriginalFilename();        File dest = new File(fileName);        try {            file.transferTo(dest);        } catch (Exception e) {            e.printStackTrace();            return dealResultMap(false, "上传失败");        }         return dealResultMap(true, "上传成功");    }        @PostMapping("/upload/part")    @ResponseBody    public Map<String, Object> uploadPart(Part file) {        // 获取提交文件名称        String fileName = file.getSubmittedFileName();        try {            // 写入文件            file.write(fileName);        } catch (Exception e) {            e.printStackTrace();            return dealResultMap(false, "上传失败");        }         return dealResultMap(true, "上传成功");    }        // 处理上传文件结果    private Map<String, Object> dealResultMap(boolean success, String msg) {        Map<String, Object> result = new HashMap<String, Object>();        result.put("success", success);        result.put("msg", msg);        return result;    }}

代码中uploadPage方法用来映射上传文件的JSP,所以只需要请求它便能够打开上传文件的页面。uploadRequest方法则将HttpServletRequest对象传递,从之前的分析可知,在调用控制器之前,DispatcherServlet会将其转换为MultipartHttpServletRequest对象,所以方法中使用了强制转换,从而得到MultipartHttpServletRequest对象,然后获取MultipartFile对象,接着使用MultipartFile对象的getOriginalFilename方法就可以得到上传的文件名,而通过它的transferTo方法,就可以将文件保存到对应的路径中。uploadMultipartFile则是直接使用MultipartFile对象获取上传的文件,从而进行操作。uploadPart方法是使用Servlet的API,可以使用其write方法直接写入文件,这也是我推荐的方式。

11. 拦截器

在第9章中,谈到过当请求来到DispatcherServlet时,它会根据HandlerMapping的机制找到处理器,这样就会返回一个HandlerExecutionChain对象,这个对象包含处理器和拦截器。这里的拦截器会对处理器进行拦截,这样通过拦截器就可以增强处理器的功能,这节的内容就是对它的使用。

11.1 拦截器的设计

首先所有的拦截器都需要实现HandlerInterceptor接口,该接口定义如代码清单10-34所示。

代码清单10-34 HandlerInterceptor源码

image-20210630174118201

上面代码的中文注释是我加入的。除了需要知道拦截器各个方法的作用外,还需要知道这些方法执行的流程,如图10-15所示。

image-20210630174134951

从图10-15可以看出,其流程描述如下。

  • 执行preHandle方法,该方法会返回一个布尔值。如果为false,则结束所有流程;如果为true,则执行下一步。
  • 执行处理器逻辑,它包含控制器的功能。
  • 执行postHandle方法。
  • 执行视图解析和视图渲染。
  • 执行afterCompletion方法。因为这个接口是Java 8的接口,所以3个方法都被声明为default,并且提供了空实现。当我们需要自己定义方法的时候,只需要实现HandlerInterceptor,覆盖其对应的方法即可。

11.2 开发拦截器

从上一节的论述中知道,可以实现HandlerInterceptor接口即可。下面先实现一个简单的拦截器,如代码清单10-35所示。

代码清单10-35 自定义简单拦截器

package com.springboot.chapter10.interceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;public class Interceptor1 implements HandlerInterceptor {    @Override    public boolean preHandle(HttpServletRequest request,             HttpServletResponse response, Object handler)            throws Exception {        System.out.println("处理器前方法");        // 返回true,不会拦截后续的处理        return 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("处理器完成方法");    }}

这里的代码实现了HandlerInterceptor,然后按照自己的需要重写了3个具体的拦截器方法。在这些方法中都打印了一些信息,这样就可以定位拦截器方法的执行顺序。其中这里的preHandle方法返回的是true,后续测试时,有兴趣的读者可以将其修改为返回false,再观察其执行的顺序。有了这个拦截器,Spring MVC并不会发现它,它还需要进行注册才能够拦截处理器,为此需要在配置文件中实现WebMvcConfigurer接口,最后覆盖其addInterceptors方法进行注册拦截器,如代码清单10-36所示。

@Overridepublic void addInterceptors(InterceptorRegistry registry) {	// 注册拦截器到Spring MVC机制,然后它会返回一个拦截器注册	InterceptorRegistration ir = registry.addInterceptor(new Interceptor1());	//  指定拦截匹配模式,限制拦截器拦截请求	ir.addPathPatterns("/interceptor/*");}

这里通过实现WebMvcConfigurer接口,重写其中的addInterceptors方法,进而加入自定义拦截器——Interceptor1,然后指定其拦截的模式,所以它只会拦截与正则式“/interceptor/*”匹配的请求。这里还需要创建对应的请求方法,为此新建控制器来实现,如代码清单10-37所示。

package com.springboot.chapter10.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;/**** imports ****/@Controller@RequestMapping("/interceptor")public class InterceptorController {   @GetMapping("/start")   public String start() {      System.out.println("执行处理器逻辑");      return "/welcome";   }}

Jsp 打印 视图渲染

后台打印的日志。

image-20210630175150312

显然处理器被拦截器拦截了,这里需要注意的是拦截器方法的执行顺序。有兴趣的读者可以把拦截器的preHandle方法返回修改为false,或者让控制器抛出异常,然后重新测试,从而进一步掌握整个拦截器的流程。这些都非常容易做到,就不再赘述了。

11.3 多个拦截器的顺序

上一节讨论了拦截器,而实际上拦截器可能还不止一个。那么在多个拦截器环境中,它的各个方法执行的顺序是怎么样的呢?

image-20210630175440606

这个结果是责任链模式的规则,对于处理器前方法采用先注册先执行,而处理器后方法和完成方法则是先注册后执行的规则。只是上述仅测试了处理器前(preHandle)方法返回为true的场景,在某些时候还可能返回为false,这个时候又如何呢?为此,可以将MulitiInterceptor2的preHandle方法修改返回为false,然后再进行测试,其日志如下:

image-20210630175458890

从上面的日志可以看出,处理器前(preHandle)方法会执行,但是一旦返回false,则后续的拦截器、处理器和所有拦截器的处理器后(postHandle)方法都不会被执行。完成方法afterCompletion则不一样,它只会执行返回true的拦截器的完成方法,而且顺序是先注册后执行。

12. 国际化

在一些企业的生产实践中,客户或者员工来自各地,甚至是在不同的国家办公,所以对时间和语言的需求会各自不同。例如,我国需要的是简体中文,而美国需要的则是美国英文。为了让不同的人在各自熟悉语言和文化的环境下办理业务,就需要对系统进行国际化。Spring MVC对此提供了良好的支持,本节我们学习这方面的知识。

12.1 国际化消息源

对于国际化,Spring MVC提供了国际化消息源机制,那就是MessageSource接口体系。它的作用是装载国际化消息,其设计如图10-16所示。

这里在大部分的情况下,是使用JDK的ResourceBundle处理国际化信息的,为此这里主要使用ResourceBundleMessageSource这个国际化消息源。

为了更方便地使用Spring MVC的国际化,Spring Boot提供了代码清单10-41所示的配置项,使得开发者能够以最快的速度配置国际化。

代码清单10-41 国际化配置项

image-20210630211116027

image-20210630211258356

这些配置项在大部分的情况下都不需要配置,只需要配置几项常用的即可快速地启动国际化的消息的读入。例如,如果我们需要设置中国简体中文和美国英文的国际化消息,可以把两个属性(properties)文件放置在resources目录,只是这里要求有3个文件,且文件名分别为messages.properties、messages_zh_CN.properties和messages_us_US.properties。注意,messages.propertiess是默认的国际化文件,如果没有这个文件,则Spring MVC将不再启用国际化的消息机制;messages_zh_CN.properties则表示简体中文的国际化消息,对于messages_us_US.properties则是美国的国际化消息。注意,这里配置文件的名称都是以messages开头,那是因为在默认的情况下国际化的默认选项spring.messages.basename的值也为messages,这样就可以不配置它。如果配置文件不是以messages开头,那么就需要按照自己的需要对它进行配置。通过Spring Boot这样简单的配置,Spring MVC的国际化消息机制就能够读取国际化的消息文件。

12.2 国际化解析器

对于国际化,还需要确定用户是使用哪个国际区域。为此,Spring MVC也提供了LocaleResolver接口来确定用户的国际化区域。同样地,实现这个接口的也有一系列的接口和类,它们代表了不同的方法。下面先介绍它们的机制。

  • AcceptHeaderLocaleResolver:使用浏览器头请求的信息去实现国际化区域。这个一般来说不常用,因为设置浏览器头是一个比较专业的设置,对用户不友好。而Spring MVC和Spring Boot都选择它作为默认的国际化解析器,因为这符合大部分计算机用户的选择。
  • FixedLocaleResolver:固定的国际化区域。只能选择一种,不能变化,所以用处不大,后续不再讨论它的使用。
  • CookieLocaleResolver:将国际化区域信息设置在浏览器Cookie中,这样使得系统可以从Cookie中读取国际化信息来确定用户的国际化区域。但是对于用户来说,他可以禁止浏览器使用Cookie,这样就会读取Cookie失败了,失败后会使用默认的国际化区域。默认的国际化区域会从浏览器头请求读出,也可以通过服务端由开发者配置。
  • SessionLocaleResolver:类似于CookieLocaleResolver,只是将国际化信息设置在Session中,这样就能读取Session中的信息去确定用户的国际化区域。这也是最常用的让用户选择国际化的手段。

这几个类和相关接口的关系如图10-17所示。

image-20210630211441407

从图10-17可以看到4个国际化解析器的具体实现类,它们都通过不同的继承路径实现了LocaleResolver接口,从而使用不同的策略去确定国际化区域。

Spring Boot中提供了两个简单的配置项,以方便开发者能够以最快的速度配置国际化解析器。这两个配置项如代码清单10-42所示。

代码清单10-42 Spring Boot国际化选项

# 指定国际化区域,可以覆盖"Accept-Language" 头信息#spring.mvc.locale=#国际化解析器,可以选择:fixed、accept-header#fixed代表固定的国际化,accept-header代表读取浏览器的"Accept-Language"头信息#spring.mvc.locale-resolver=accept-header

显然可以通过这些配置快速启用FixedLocaleResolver和AcceptHeaderLocaleResolver两种解析器,在默认的情况下Spring Boot会使用AcceptHeaderLocaleResolver确定国际化区域。如果只是希望采用浏览器请求头确定国际化区域,那么配置AcceptHeaderLocaleResolver就可以了,无须任何的开发。如果希望指定固定的国际化区域,而无须改变,那么也可以将其配置为FixedLocaleResolver,并且指定固定的国际化区域,同样也无须任何开发,这也是比较方便的。但是有时候我们希望的是能让用户更加灵活地指定国际化区域,这时就可能使用CookieLocaleResolver或者SessionLocaleResolver。这是能够让用户指定国际化区域的方式,下节将以SessionLocaleResolver作为例子讲解它们的使用。

12.3 国际化实例——SessionLocaleResolver

上节讲述国际化的消息源的读取和几个国际化的解析器的使用。本节主要是使用实例来讲解国际化的使用,采用的是在实际工作中使用得最多的SessionLocaleResolver。

先设置国际化消息源的配置项。下面使用国际化消息源文件为international.properties,在application.properties文件中增加代码清单10-43所示的配置内容。

代码清单10-43 配置国际化消息

# 文件编码spring.messages.encoding=UTF-8# 国际化文件基础名称spring.messages.basename=international# 国际化消息缓存有效时间(单位秒),超时将重新载入spring.messages.cache-duration=3600

请注意,配置项spring.messages.basename的值为international,这个配置项的默认值为messages,这就意味着我们的国际化消息的配置文件名称为international.properties、international_zh_CN.properties和international_en_US.properties,并且将其放入到resources文件夹中。其中international.properties是必不可少的,否则Spring Boot将不会生成国际化消息机制;对于Spring MVC,它是默认的国际化消息源,也就是不能确定国际化或者国际化消息源查找失败,就会从采用这个文件的消息源来提供国际化消息。

配置项spring.messages.cache-duration则表示缓存过期的时间,也就是超过3600 s(1 h)后就会过期,从而国际化消息系统会重新读入这些国际化文件以达到更新的效果。如果不配置这项表示永不过期,这样就不会重新读入国际化文件。为了测试,这里还要创建三个国际化属性文件,如代码清单10-44所示。

image-20210630211656497

这样Spring MVC就会读入这些国际化消息文件。接着需要创建国际化解析器,这里主要是SessionLocaleResolver,只是这并不能使用Spring Boot的配置完成。我们在开发前,需要先了解它的机制。在Spring MVC中,它提供了一个拦截器LocaleChangeInterceptor,可以在处理器前处理相关的逻辑,也就是拦截器的preHandle方法的作用。这个拦截器可以拦截一个请求参数,通过这个参数可以确定其国际化信息,并且把国际化信息保存到Session中,其流程如图10-18所示。

image-20210630211718962

可以看到,LocaleChangeInterceptor拦截器可以通过请求参数来确定国际化,同时把请求参数保存到Session中,这样后续就可以从Session中读取国际化的消息。只是这里还需要确定请求参数的名称,于是可以在Spring Boot启动中增加代码清单10-45所示的内容。

// 国际化拦截器	private LocaleChangeInterceptor lci = null;	// 国际化解析器,请注意这个Bean Name要为localeResolver	@Bean(name = "localeResolver")	public LocaleResolver initLocaleResolver() {		SessionLocaleResolver slr = new SessionLocaleResolver();		// 默认国际化区域		slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);		return slr;	}	// 创建国际化拦截器	@Bean	public LocaleChangeInterceptor localeChangeInterceptor() {		if (lci != null) {			return lci;		}		lci = new LocaleChangeInterceptor();		// 设置参数名		lci.setParamName("language");		return lci;	}	// 给处理器增加国际化拦截器	@Override	public void addInterceptors(InterceptorRegistry registry) {		// 拦截器在执行处理器前方法(preHandle)将请求的国际区域根据参数修改为对应的区域		registry.addInterceptor(localeChangeInterceptor());	}

其中,localeResolver方法创建了一个国际化拦截器。它需要注意两点:首先需要保证其Bean Name为"localeResolver",这是Spring MVC中的约定,否则系统就不会感知这个解析器;其次这里设置了默认的国际化区域为简体中文,也就是说,当参数为空或者为失效的时候,就使用这个默认的国际化规则。

localeChangeInterceptor方法则是创建国际化拦截器。这里需要注意的是设置了一个名称为language的参数,也就是拦截器将读取HTTP请求为language的参数,用以设置国际化参数,这样可以通过这个参数的变化来设置用户的国际化区域。

addInterceptors方法则是将拦截器LocaleChangeInterceptor添加到Spring MVC拦截器的机制中,让它能够拦截处理器,这样就能够实现修改国际化区域的作用。

上述已经处理完了国际化的内容,接下来需要了解的是在控制器和视图中如何获取和使用国际化区域的消息。首先是控制器,如代码清单10-46所示。

代码清单10-46 国际化控制器

package com.springboot.chapter10.controller;import java.util.Locale;import javax.servlet.http.HttpServletRequest;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.MessageSource;import org.springframework.context.i18n.LocaleContextHolder;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;/**** imports ****/@Controller@RequestMapping("/i18n")public class I18nController {   // 注入国际化消息接口对象   @Autowired   private MessageSource messageSource;   // 后台获取国际化信息和打开国际化视图   @GetMapping("/page")   public String page(HttpServletRequest request) {      // 后台获取国际化区域      Locale locale = LocaleContextHolder.getLocale();      // 获取国际化消息      String msg = messageSource.getMessage("msg", null, locale);      System.out.println("msg = " + msg);      // 返回视图      return "i18n/internationalization";   }}

上述代码中注入了国际化消息源接口对象,它是通过代码清单10-43的配置来读入国际化消息配置文件时创建的。加粗部分是展示如何在后台中获取国际化区域和国际化消息。最后控制器方法会返回一个字符串,它将指向视图。这样就可以开发JSP来展现国际化的消息,如代码清单10-47所示。

代码清单10-47 视图国际化(/WEB-INF/jsp/i18n/internationalization.jsp)

注意加粗的代码,这里的链接是通过language参数去修改国际化的,它与拦截器所定义的参数名称保持一致,所以它会被拦截,用来确定国际化区域,这样就能够实现国际化。启动Spring Boot后,图10-19和图10-20是我测试国际化的结果。

image-20210630212304286

从图10-20可以看出,视图的国际化已经可以通过language参数进行转换。但是应注意的是,国际化参数已经保存在Session中,所以即使没有这个参数,也会从Session中读取来设置国际化区域。

13. Spring MVC拾遗

Spring MVC的内容比较多也比较杂,上面介绍了常用的内容,但是还有一些比较烦琐且常用的知识需要介绍,故本节命名为“拾遗”。

13.1 @ResponseBody转换为JSON的秘密

一直以来,当想把某个控制器的返回转变为JSON数据集时,只需要在方法上标注@ResponseBody注解即可,那么Spring MVC是如何做到的呢?回到10.3.1节中,在进入控制器方法前,当遇到标注的@ResponseBody后,处理器就会记录这个方法的响应类型为JSON数据集。当执行完控制器返回后,处理器会启用结果解析器(ResultResolver)去解析这个结果,它会去轮询注册给Spring MVC的HttpMessageConverter接口的实现类。因为MappingJackson2HttpMessageConverter这个实现类已经被Spring MVC所注册,加上Spring MVC将控制器的结果类型标明为JSON,所以就匹配上了,于是通过它就在处理器内部把结果转换为了JSON。当然有时候会轮询不到匹配的HttpMessageConverter,那么它就会交由Spring MVC后续流程去处理。如果控制器返回结果被MappingJackson2HttpMessageConverter进行了转换,那么后续的模型和视图(ModelAndView)就返回null,这样视图解析器和视图渲染将不再被执行,其流程如图10-21所示

image-20210630213204994

14. 重定向

重定向(Redirect)就是通过各种方法将各种网络请求重新定个方向转到其他位置。这里继续使用代码清单10-23的JSP视图,这里需要完成插入一个新的用户信息到数据库,而插入之后需要通过该JSP视图展现给请求者。假设原本就存在一个showUser方法通过这个JSP视图来显示用户,这样我们希望的是插入用户之后,就使用这个showUser方法来展示用户,这样旧的功能就能够重用了。下面来完成这个功能,如代码清单10-48所示。

	// 显示用户	@GetMapping("/show")	public String showUser(Long id, Model model) {	    User user = userService.getUser(id);	    model.addAttribute("user", user);	    return "data/user";	}	// 使用字符串指定跳转	@GetMapping("/redirect1")	public String redirect1(String userName, String note) {	    User user = new User();	    user.setNote(note);	    user.setUserName(userName);	    // 插入数据库后,回填user的id	    userService.insertUser(user);	    return "redirect:/user/show?id=" + user.getId();	}	// 使用模型和视图指定跳转	@GetMapping("/redirect2")	public ModelAndView redirect2(String userName, String note) {	    User user = new User();	    user.setNote(note);	    user.setUserName(userName);	    userService.insertUser(user);	    ModelAndView mv = new ModelAndView();	    mv.setViewName("redirect:/user/show?id=" + user.getId());	    return mv;	}

代码中的showUser方法查询用户信息后,绑定到数据模型中,然后返回一个字符串,它指向JSP视图,这样视图就能够把数据模型的数据渲染出来。redirect1方法是先新增用户数据,而新增用户数据库会返回用户编号(id),然后通过以“redirect:”开头的字符串,然后后续的字符串指向shouUser方法请求的URL,并且将id作为参数传递,这样就能够调用这个请求。在redirect2方法中,类似于redirect1方法,先插入用户,但它是将视图名称转换为redirect1中返回的字符串,这样Spring MVC也可以执行重定向。

这里使用一个参数id传递给showUser方法,redirect1和redirect2方法已经包含了user对象的全部信息,而在showUser方法中却要重新查询一次,这样显然不合理。如果要将User对象直接传递给showUser方法,这在URL层面是完成不了的,好在Spring MVC也考虑了这样的场景。它提供了RedirectAttributes,这是一个扩展了ModelMap的接口,它有一个addFlashAttribute方法,这个方法可以保存需要传递给重定位的数据,改写代码清单10-48中的代码,改后代码如代码清单10-49所示。

代码清单10-49 重定向传递Java对象

// 显示用户	// 参数user直接从数据模型RedirectAttributes对象中取出	@RequestMapping("/showUser")	public String showUser(User user, Model model) {	    System.out.println(user.getId());	    return "data/user";	}	// 使用字符串指定跳转	@RequestMapping("/redirect1")	public String redirect1(String userName, String note, RedirectAttributes ra) {	    User user = new User();	    user.setNote(note);	    user.setUserName(userName);	    userService.insertUser(user);	    // 保存需要传递给重定向的对象	    ra.addFlashAttribute("user", user);	    return "redirect:/user/showUser";	}	// 使用模型和视图指定跳转	@RequestMapping("/redirect2")	public ModelAndView redirect2(String userName, String note,	        RedirectAttributes ra) {	    User user = new User();	    user.setNote(note);	    user.setUserName(userName);	    userService.insertUser(user);	    // 保存需要传递给重定向的对象	    ra.addFlashAttribute("user", user);	    ModelAndView mv = new ModelAndView();	    mv.setViewName("redirect:/user/showUser");	    return mv;	}

上述代码给方法中添加了RedirectAttributes对象参数,然后将redirect1和redirect2方法中插入的用户信息通过addFlashAttribute方法保存起来,再执行重定向到showUser方法中,并且再将user对象传递,那它又是如何做到的呢?

首先,被addFlashAttribute方法保存的参数,在控制器执行完成后,会被保存到Session对象中。当执行重定向时,在进入重定向前首先把Session中的参数取出,用以填充重定向方法的参数和数据模型,之后删除Session中的数据,然后就可以调用重定向方法,并将对象传递给重定向的方法。其流程如图10-22所示。

image-20210630213838667

15. 操作会话对象

在Web应用中,操作会话(HttpSession)对象是十分普遍的,对此Spring MVC也提供了支持。主要是两个注解用来操作HttpSession对象,它们是@SessionAttribute和@SessionAttributes。其中,@SessionAttribute应用于参数,它的作用是将HttpSession中的属性读出,赋予控制器的参数;@SessionAttributes则只能用于类的注解,它会将相关数据模型的属性保存到Session中。下面举例说明。首先,在webapp目录下创建一个JSP文件,它用于让HttpSession记录属性然后进行转发,如代码清单10-50所示。

代码清单10-50 测试操作HttpSession(/session.jsp)

image-20210630214528587

这样请求这个JSP文件时,服务器就会用HttpSession对象记录名称为id的信息,然后跳转到/session/test上,接着就要编写控制器处理这个请求,如代码清单10-51所示。

代码清单10-51 使用注解@SessionAttribute和@SessionAttributes

image-20210630214647950

这里的控制器标注了注解@SessionAttributes,并且指定了名称和类型,值得注意的是它们是“或者”的关系,也就是当Spring MVC中数据模型的属性满足名称或者类型时,它就会将属性保存到Session中。对于test方法,首先是使用注解@SessionAttribute读出HttpSession保存的id参数,然后保存一个名称为id_new的Long型参数,按照@SessionAttributes配置的类型,它将在控制器执行后被保存到Session中。接着根据id查询出用户,然后用名称user保存用户信息,按照@SessionAttributes配置的名称,它也将在控制器执行后被保存到Session中。最后返回一个指向视图的字符串,指向一个新的JSP视图。为了测试@SessionAttributes的配置,可以在这个视图中获取HttpSession保存的数据,如代码清单10-52所示。

代码清单10-52 测试@SessionAttributes视图(/WEB-INF/jsp/session/test.jsp)

image-20210630214824743

加粗的代码便是从Session中获取数据,然后展示在视图中。这样我们只要启动Spring Boot应用,然后请求http://localhost:8080/session.jsp,就可以得到验证了,如图10-23所示。

image-20210630214841439

16. 给控制器增加通知

在Spring AOP中,可以通过通知来增强Bean的功能。同样地,Spring MVC也可以给控制器增加通知,于是在控制器方法的前后和异常发生时去执行不同的处理。这里涉及4个注解,它们是@ControllerAdvice、@InitBinder、@ExceptionHandler和@ModelAttribute。这里需要注意的是它们的作用和执行的顺序。

  • @ControllerAdvice:定义一个控制器的通知类,允许定义一些关于增强控制器的各类通知和限定增强哪些控制器功能等。
  • @InitBinder:定义控制器参数绑定规则,如转换规则、格式化等,它会在参数转换之前执行。
  • @ExceptionHandler:定义控制器发生异常后的操作。一般来说,发生异常后,可以跳转到指定的友好页面,以避免用户使用的不友好。
  • @ModelAttribute: 可以在控制器方法执行之前,对数据模型进行操作。
 Source code recreated from a .class file by IntelliJ IDEA// (powered by FernFlower decompiler)//package com.springboot.chapter10.controller.advice;import java.text.SimpleDateFormat;import java.util.Date;import org.springframework.beans.propertyeditors.CustomDateEditor;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.WebDataBinder;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.InitBinder;import org.springframework.web.bind.annotation.ModelAttribute;@ControllerAdvice(    basePackages = {"com.springboot.chapter10.controller.advice.test.*"},    annotations = {Controller.class})public class MyControllerAdvice {    public MyControllerAdvice() {    }    @InitBinder    public void initDataBinder(WebDataBinder binder) {        CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false);        binder.registerCustomEditor(Date.class, dateEditor);    }    @ModelAttribute    public void projectModel(Model model) {        model.addAttribute("project_name", "chapter10");    }    @ExceptionHandler({Exception.class})    public String exception(Model model, Exception ex) {        model.addAttribute("exception_message", ex.getMessage());        return "exception";    }}

请注意加粗的注解,下面阐述它们的作用。

  • @ControllerAdvice标明这是一个控制器通知类,这个注解也标注了@Controller,所以它会在Spring IoC启动中自动扫描和装配。它的配置项basePackages配置的是包名限制,也就是符合该配置的包的控制器才会被这个控制器通知所拦截,而annotations的配置项则是在原有包名限定的基础上再添加被标注为@Controller的类才会被拦截。
  • @InitBinder是一个在控制器参数转换前被执行的代码。这里的WebDataBinder参数对象是Spring MVC会自动生成的参数,这里定义了日期(Date)类型的参数,采用了限定格式“yyyy-MM-dd”,则不再需要加入@DateTimeFormat对格式再进行指定,直接采用“yyyy-MM-dd”格式传递日期参数即可。
  • @ModelAttribute是一个数据模型的注解。它在执行控制器方法前被执行,代码中增加了一个工程名称(project_name)的字符串,因此在控制器方法中可以获取它。
  • @ExceptionHandler的配置项为Exception,它可以拦截所有控制器发生的异常。这里的Exception参数是Spring MVC执行控制器发生异常时传递的,而在方法中,给数据模型添加了异常信息,然后返回一个字符串exception,这个字符就指向了对应的JSP视图。

为了测试这个控制器通知,需要开发新的控制器和字符串exception指向的JSP视图。先来完成控制器,如代码清单10-54所示。

代码清单10-54 测试控制器通知

 Source code recreated from a .class file by IntelliJ IDEA// (powered by FernFlower decompiler)//package com.springboot.chapter10.controller.advice.test;import java.util.Date;import org.apache.tools.ant.util.DateUtils;import org.springframework.stereotype.Controller;import org.springframework.ui.ModelMap;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;@Controller@RequestMapping({"/advice"})public class AdviceController {    public AdviceController() {    }    @GetMapping({"/test"})    public String test(Date date, ModelMap modelMap) {        System.out.println(modelMap.get("project_name"));        System.out.println(DateUtils.format(date, "yyyy-MM-dd"));        throw new RuntimeException("异常了,跳转到控制器通知的异常信息里");    }}

这个控制器所在的包正好是控制器通知(MyControllerAdvice )所指定的包,它标注的@Controller也是通知指定的注解,这样控制器通知就可以拦截这个控制器。这样它会先执行其标注了@InitBinder和@ModelAttribute的两个方法。因为标注@InitBinder的方法设定了日期格式为“yyyy-MM-dd”,所以控制器方法的日期参数并没有加入格式的限定。而标注@ModelAttribute的方法在数据模型中设置了新的属性,所以这里的控制器也能从数据模型中获取数据。控制器方法最后抛出了异常,这样就会让MyControllerAdvice 标注@ExceptionHandler的方法被触发,并且会将异常消息传递给它。为了让异常通知被展示,需要一个JSP视图,如代码清单10-55所示。

代码清单10-55 展示异常页面(/WEB-INF/jsp/exception.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>    <h3><td>${exception_message}</td></h3></body></html>

通过加粗的代码,就将控制器异常通知所绑定的异常消息渲染到JSP中。这样凡是控制器发生异常,就能够通过对应的异常页面给渲染出来,从而避免系统对使用者的不友好,提高网站的友好度。

通过上面的代码,整个流程就开发完成了。如图10-24所示是对其进行测试的结果。

image-20210630220652899

17. 获取请求头参数

在HTTP请求中,有些网站会利用请求头的数据进行身份验证,所以有时在控制器中还需要拿到请求头的数据。在Spring MVC中可以通过注解@RequestHeader进行获取。下面先编写一个JSP,让它通过JavaScript携带请求头对后端控制器发出请求,如代码清单10-56所示。

代码清单10-56 带请求头的HTTP请求

<%@ page pageEncoding="UTF-8"%><!DOCTYPE html><html><head><meta charset="UTF-8"><title>获取请求头参数</title><!-- 加载Query文件--><script src="https://code.jquery.com/jquery-3.2.0.js"></script><script type="text/javascript">$.post({    url : "./user",    // 设置请求头参数    headers : {id : '1'},    // 成功后的方法    success : function(user) {        if (user == null || user.id == null) {            alert("获取失败");            return;        }        // 弹出请求返回的用户信息        alert("id=" + user.id +", user_name="        		+user.userName+", note="+ user.note);    }});</script></head><body></body>

代码中使用脚本对控制器发出了请求,而加粗的代码则是设置了请求头,是一个键为id而值为1的请求头,这样这个请求头也会发送到控制器中。那么控制器该怎么取到这个请求头参数呢?其实也是十分简单的,使用注解@RequestHeader就可以了。下面在UserController中加入代码清单10-57所示的代码。

代码清单10-57 使用@RequestHeader接收请求头参数

@GetMapping("/header/page")public String headerPage() {    return "header";}@PostMapping("/header/user")@ResponseBody// 通过@RequestHeader接收请求头参数public User headerUser(@RequestHeader("id") Long id) {    User user = userService.getUser(id);    return user;}

代码中headerPage方法是请求代码清单10-56的JSP页面。headerUser方法中的参数id则是使用注解@RequestHeader(“id”),它代表从请求头中获取键为id的参数,这样就能从请求头中获取参数。在浏览器地址栏中输入http://localhost:8080/user/header/page就可以看到结果,如图10-25所示。

image-20210630220955273

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值