SpringBoot中Spring MVC开发

67 篇文章 0 订阅
SpringBoot中Spring MVC开发
本小记学习目标
    1、认识Spring MVC
    2、Spring MVC开发实例
    3、Spring MVC的深入开发
    4、Spring MVC其它补充说明
一、认识Spring MVC
    Spring MVC一开始定位于一个较为松散的组合,展示给到用户的视图(View)、控制器返回的数据模型(Model)、定位视图的视图解析器(ViewResolver)和处理适配器(HandlerAdapter)等内容都是独立的。
    通过Spring MVC可以很轻轻地把后台数据转换为各种类型的数据,以此来满足互联网数据的多样性。它已经成为了当前最主流的Web开发框架。
Spring MVC的框架设计
    Spring MVC示意图
示意图说明:
    1、处理请求首先到达控制器Controller
    2、控制器进行请求分发,根据请求的内容去访问模型层Model
    3、一般设计者会把模型层再细分为业务层(Service)和数据访问层(Dao)
    4、当控制器获取到由模型层返回的数据后,则把数据渲染到视图中,这样就可以展现给到用户了
Spring MVC运行流程及相关组件
    Spring MVC的流程是围绕DispatcherServlet而工作的,它是基础,在这个之上它还有其它的组件协同完成任务。
Spring MVC的流程与组件示意图
二、Spring MVC的简单开发实例
新增一个Maven项目SpringBoot_SpringMVC
在pom.xml中添加相关的依赖,修改后的pom.xml文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.xiaoxie</groupId>
    <artifactId>SpringBoot_SpringMVC</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- AOP依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- Web开发包依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- jstl,jsp的依赖包 -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <!--<scope>provided</scope>-->
        </dependency>
        <!-- 属性文件依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Mysql依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- Druid依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!-- mybatis的依赖引入 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!-- redis依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- redis客户端驱动jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
    </dependencies>
    <!-- 引入相关插件 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
在project Structure中为Web资源新增目,新增的目录在/src/main/webapp
在resources/application.properties文件中添加如下内容
spring.mvc.view.prefix=/jsp/
spring.mvc.view.suffix=.jsp
#durid相关的datasource的配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=org.gjt.mm.mysql.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=root
#连接池的属性
spring.datasource.druid.initial-size=15
spring.datasource.druid.max-active=100
spring.datasource.druid.min-idle=15
spring.datasource.druid.max-wait=60000
spring.datasource.druid.keep-alive=true
#数据源默认的隔离级别"读写提交"
spring.datasource.druid.default-transaction-isolation=2
#Mybatis配置
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.xiaoxie.pojo
#Redis配置
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=2000ms
#配置Redis服务器
spring.redis.port=6379
spring.redis.host=127.0.0.1
spring.redis.password=xiaoxie
#Redis连接超时时间,单位毫秒
spring.redis.timeout=1000ms
#Redis缓存管理器配置
spring.cache.type=REDIS
spring.cache.cache-names=redisCache
其中配置可以看到
spring.mvc.view.prefix=/jsp/
spring.mvc.view.suffix=.jsp
上面两包为MVC视图解析的路径及件的后缀名
#durid相关的datasource的配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=org.gjt.mm.mysql.Driver
spring.datasource.url=jdbc: mysql://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=root
#连接池的属性
spring.datasource.druid.initial-size=15
spring.datasource.druid.max-active=100
spring.datasource.druid.min-idle=15
spring.datasource.druid.max-wait=60000
spring.datasource.druid.keep-alive=true
#数据源默认的隔离级别"读写提交"
spring.datasource.druid.default-transaction-isolation=2
上面的这些配置表示由于使用了Druid需要配置据源的相关内容
#Mybatis配置
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.xiaoxie.pojo
上面的配置是我们需要使用mybatis访问数据库进行的配置
#Redis配置
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=2000ms
#配置Redis服务器
spring.redis.port=6379
spring.redis.host=127.0.0.1
spring.redis.password=xiaoxie
#Redis连接超时时间,单位毫秒
spring.redis.timeout=1000ms
#Redis缓存管理器配置
spring.cache.type=REDIS
spring.cache.cache-names=redisCache
上面这些表示的是Redis及Redis缓存相关的配置
从上面的配置中我们可以看到我们的实体类需要在com.xiaoxie.pojo当中,我们在这个包下新增一个User类,并把这个类给定别外为user
package com.xiaoxie.pojo;
import org.apache.ibatis.type.Alias;
@Alias("user")
public class User {
    private Long id;
    private String userName;
    private String note;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getNote() {
        return note;
    }
    public void setNote(String note) {
        this.note = note;
    }
}
定义Mybatis的Dao接口,com.xiaoxie.dao.UserDao
package com.xiaoxie.dao;
import com.xiaoxie.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Mapper
public interface UserDao {
    /**
     * 获取单个用户
     * @param id
     * @return
     */
    User getUser(Long id);
    /**
     * 查找用户
     * @param userName
     * @param note
     * @return
     */
    List<User> findUsers(@Param("userName") String userName, @Param("note") String note);
}
注意,上面的注解@Mapper
根据配置文件中的信息我们在resources/mybatis/mapper下新增mapper的配置文件:userMapper.xml,注意这里的配置保持与Dao接口相对应
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxie.dao.UserDao">
    <select id="getUser" parameterType="long" resultType="user">
        select id,user_name as userName,note from t_user where id=#{id}
    </select>
    <select id="findUsers" resultType="user">
        select id,user_name as userName,note from t_user
        <where>
            <if test="userName != null">
                and user_name = #{userName}
            </if>
            <if test="note != null">
                and note = #{note}
            </if>
        </where>
    </select>
</mapper>
新增service接口:com.xiaoxie.service.UserService
package com.xiaoxie.service;
import com.xiaoxie.pojo.User;
import java.util.List;
public interface UserService {
    /**
     * 获取单个用户
     * @param id
     * @return
     */
    User getUser(Long id);
    /**
     * 查找用户
     * @param userName
     * @param note
     * @return
     */
    List<User> findUsers(String userName, String note);
}
新增Service的实现类:com.xiaoxie.service.impl.UserServiceImpl
package com.xiaoxie.service.impl;
import com.xiaoxie.dao.UserDao;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
    //数据交互的Dao注入
    @Autowired
    private UserDao userDao = null;
    @Override
    @Transactional
    public User getUser(Long id) {
        return userDao.getUser(id);
    }
    @Override
    public List<User> findUsers(String userName, String note) {
        return userDao.findUsers(userName,note);
    }
}
新增controller类:com.xiaoxie.controller.UserController
package com.xiaoxie.controller;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import java.util.List;
@Controller
@RequestMapping("/user")
public class UserController {
    //注入用户服务类
    @Autowired
    private UserService userService = null;
    @RequestMapping("/table")
    public ModelAndView table(){
        //访问模型层得到数据
        List<User> userList = userService.findUsers(null,null);
        //模型和视图
        ModelAndView mv = new ModelAndView();
        //定义模型视图
        mv.setViewName("user/table");
        //加入数据模型
        mv.addObject("userList",userList);
        //返回模型和视图
        return mv;
    }
    @RequestMapping("/list")
    @ResponseBody
    public List<User> list(@RequestParam(value = "userName",required = false) String userName,
                           @RequestParam(value = "note",required = false) String note){
        /*if(userName == null || "".equals(userName.trim())){
            userName = null;
        }
        if(note == null || "".equals(note.trim())){
            note = null;
        }*/
        //访问模型层得到数据
        List<User> userList = userService.findUsers(userName,note);
        return userList;
    }
}
对于控制器说明:
    对于控制器,首先要指定请求分发,这个任务由注解@RequestMapping完成,这个注解可以标注在类或者方法上,当一个类被标注的时候,所有关于它的请求都需要在@RequestMapping定义的url下。当方法被标注后,可以定义分部url。这样就可以让请求的url找到对应的路径。配置了扫描路径后,Spring MVC扫描机制则可以将其扫描,并装载为HandlerMapping。
    在上面的这个控制器类中有两个方法
        table方法,会查询所有用户,它是没有查询条件的会查询所有记录,当查询出记录后,创建模型和视图(ModelAndView)同时设置视图名称为"user/table"以及把查询到的记录绑定到模型和视图中,最后返回模型和视图。根据application.properties中的配置我们可知它会把webapp/jsp/user/table.jsp作为视图来渲染数据。
        list方法,这个方法中有一个注解@ResponseBody,这样Spring MVC会把结果转换为JSON。然后获取参数,使用注解@RequestParam,通过指定参数名称使用HTTP请求的参数和方法的参数进行绑定,这个注解的默认规则是参数不可以为空,为了保证参数可以为空则添加属性required=false
在webapp/jsp/user/ 下新增table.jsp页面
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML>
<HEAD>
    <TITLE>用户列表</TITLE>
    <link rel="stylesheet" type="text/css" href="../../css/easyui.css">
    <link rel="stylesheet" type="text/css" href="../../css/icon.css">
    <link rel="stylesheet" type="text/css" href="../../css/demo.css">
    <script type="text/javascript" src="../../js/jquery.min.js"></script>
    <script type="text/javascript" src="../../js/jquery.easyui.min.js"></script>
    <script type="text/javascript">
        //定义事件方法
        function onSearch() {
            //指定请求路径
            var opts = $("#dg").datagrid("options");
            opts.url = "./list";
            //获取查询参数
            var userName = $("#userName").val();
            var note = $("#note").val();
            //对参数进行组织
            var params = {};
            if(userName != null && userName.trim() != ''){
                params.userName = userName;
            }
            if(note != null && note.trim() != ''){
                params.note = note;
            }
            //重新载入表格数据
            $("#dg").datagrid("load",params);
        }
    </script>
</HEAD>
<BODY>
    <div style="margin: 20px 0;"></div>
    <div class="easyui-layout" style="width:100%;height:350px;">
        <div data-options="region:'north'" style="height:50px">
            <form id="searchForm" method="post">
                <table>
                    <tr>
                        <td>用户名称:</td>
                        <td>
                            <input id="userName" name="userName" class="easyui-textbox" data-options="prompt:'输入用户名称...'" style="width: 100%;height: 32px;">
                        </td>
                        <td>备注:</td>
                        <td>
                            <input id="note" name="note" class="easyui-textbox" data-options="prompt:'输入备注...'" style="width: 100%;height: 32px;">
                        </td>
                        <td>
                            <a href="#" class="easyui-linkbutton" style="width: 80px;" οnclick="onSearch()">查询</a>
                        </td>
                    </tr>
                </table>
            </form>
        </div>
        <div data-options="region:'center',title:'用户列表',iconCls:'icon-ok'">
            <table id="dg" class="easyui-datagrid",data-options="border:false,SingleSelect:true,fit:true,fitColumns:true">
                <thead>
                    <tr>
                        <th data-options="field:'id'" width="80">编号</th>
                        <th data-options="field:'userName'" width="100">用户名称</th>
                        <th data-options="field:'note'" width="80">备注</th>
                    </tr>
                </thead>
                <tbody>
                    <!-- 使用forEache渲染数据模型 -->
                    <c:forEach items="${userList}" var="user">
                        <tr>
                            <td>${user.id}</td>
                            <td>${user.userName}</td>
                            <td>${user.note}</td>
                        </tr>
                    </c:forEach>
                </tbody>
            </table>
        </div>
    </div>
</BODY>
</HTML>
从这里可以看到我们使用了EasyUI来作为界面(我们需要下载EasyUI相应的开发包并把相应的css、js、images文件复制到工程当中去)
三、Spring MVC深入开发
1、处理器映射
如果Web工程中使用了Spring MVC,那么它在启动阶段则会把注解@RequestMapping所配置的内容保存到处理器映射(HandlerMapping)机制中去,然后则等待请求的到来,通过拦截器请求信息与HandlerMapping进行匹配,找到对应的处理器(包含控制器的逻辑),并把处理器及拦截器保存到HandlerExecutionChain对象中,返回给到DispatcherServlet,这样DispatcherServlet就可以运行它们了。
HandlerMapping的主要任务是:把请求定位到具体的处理器上
@RequestMapping的源码
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
   String name() default "";    //配置请求映射名称
   @AliasFor("path")
   String[] value() default {};    //通过路径映射
   @AliasFor("value")
   String[] path() default {};    //通过路径映射回path配置项
   RequestMethod[] method() default {};    //限定只响应HTTP请求,GET、POST、HEAD、OPIONS、PUT、TRACE……,默认情况是可以响应所有请求类型
   String[] params() default {};    //当存在对应的HTTP参数时才响应请求
   String[] headers() default {};    //限定请求头存在对应的参数时才响应
   String[] consumes() default {};    //限定HTTP请求体提交类型,比如:application/json,text/html
   String[] produces() default {};    //限定返回的内容类型,当HTTP请求头中的Accept类型中包含指定类型时返回
}
从上面可以看到,可以通过配置value或path来设置请求url,从而让对应的请求映射到控制器或其方法上,在这个基础上还可以通过其它的配置缩小请求映射的范围。
一般来说一个路径对应一个方法而不要过于依赖于正则去配置,这样可以提高程序的可读性,便于后续的维护和改造。
2、获取控制器参数
处理器是对控制器的包装,在处理器运行的过程中会调度控制器的方法,只是它在进入控制器方法之前会对HTTP参数和上下文进行解析,把它们转换为控制器所需要的参数。
获取控制器参数是处理器首先要做的事情,只是这个过程Spring MVC已帮我们做了大量转换规则,通过默认的这些规则则可以非常简易地获取大部分的参数。但有时候我们面对的场景要相对复杂一些,这样我们参数的获取也会变得相对复杂。
在无解的情况下获取参数
在没有注解的情况下,Spring MVC可以获取参数,而且允许参数为空,这里要保证参数名与HTTP请求的参数名保持一致。
新增一个controller,com.xiaoxie.controller.TestController
package com.xiaoxie.controller;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Controller
public class TestController {
    
    @GetMapping("/test/noAnnotation")   //get请求
    @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;
    }
}
新增这么一个方法后,在浏览器中访问:http://localhost:8080/test.noAnnotation?intVal=1&longVal=10&str=str
浏览器展示的json如下:
{"str":"str","intVal":1,"longVal":10}
使用@RequestParam获取参数
在没有注解的情况下,要求HTTP参数名和控制器方法参数名保持一致。然而现在一般是做的前后台分离,前端的命名规则可能与后端的不一致,这时就需要把前后端的参数对应起来。
Spring MVC提供了@RequestParam来确定前后端的参数名称的映射关系。
在TestController类中新增方法如下
@GetMapping("/test/annotation")
@ResponseBody
public Map<String,Object> annotation(
        /**把前端的参数与方法的参数进行对应*/
        @RequestParam("int_val") Integer intVal,
        @RequestParam("long_val") Integer 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参数和方法参数的映射关系,这样处理器会按配置的映射关系得到参数,然后调用控制器的方法。
在浏览器访问如下地址:http://localhost:8080/test/annotation?int_val=1&long_val=10&str_val
浏览器展示的json如下:
{"intVal":1,"strVal":"","longVal":10}
当使用了上面这种方式进行映射,这三个参数前端都要传过来,如上面的链接str_val没有值也需要保留不然会产生异常报错,原因是这个注解中有一个required的属性,这个属性默认的值是true。
如果我们明确把str_val这个参数的@RequestParam添加,required = false,则表示str_val是可以不传的
通过上面的改造后访问如下地址:http://localhost:8080/test/annotation?int_val=1&long_val=10
浏览器展示的json如下:
{"intVal":1,"strVal":null,"longVal":10}
把上面的方法改造为如下:
@GetMapping("/test/annotation")
@ResponseBody
public Map<String,Object> annotation(
        /**把前端的参数与方法的参数进行对应*/
        @RequestParam("int_val") Integer intVal,
        @RequestParam(value = "long_val",defaultValue = "1000") Integer longVal,
        @RequestParam(value = "str_val",required = false,defaultValue = "default_str") String strVal
){
    Map<String,Object> paramsMap = new HashMap<>();
    paramsMap.put("intVal",intVal);
    paramsMap.put("longVal",longVal);
    paramsMap.put("strVal",strVal);
    return paramsMap;
}
这里有两个点:
属性说明:
required = false    表示这个参数可以不传
defaultValue = ""    表示转入默认值是多少,这里要注意如果接收参数是非字符串的会有数据类的转换过程要保证默认值字符串可以转换为对应的参数类型
通过上面的改造后,访问地址:http://localhost:8080/test/annotation?int_val=1&long_val
浏览器展示的json如下:
{"intVal":1,"strVal":"default_str","longVal":1000}
传递数组
Spring MVC内部支持使用逗号分隔的数组参数
在TestController类中新增如下方法
@GetMapping("/test/Array")
@ResponseBody
public Map<String,Object> array(
        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/test/Array?intArr=1,2,3&longArr=10,20,30&strArr=str1,str2,str3
浏览器中展示json如下
{"intArr":[1,2,3],"strArr":["str1","str2","str3"],"longArr":[10,20,30]}
注意:对于传递数组,如果某个参数没有传递是不会报错的,只是在浏览器中展示这个数据时为null,比如把上面的访问修改为如下:http://localhost:8080/test/Array?intArr=1,2,3&longArr=10,20,30
上面的链接浏览器展示的json如下:
{"intArr":[1,2,3],"strArr":null,"longArr":[10,20,30]}
传递JSON
前端的应用通过请求后端的接口获取JSON数据集,然后它们可以方便地把数据渲染到视图当中,有时候前端也会把数据转换为JSON,通过HTTP请求体提交给后端。
在webapp/js目录下,把jquery的js文件导入
在webapp/jsp/user下新增一个add.jsp页面
<%@ page pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>新增用户</title>
        <!-- 引入Jquery -->
        <script src="../../js/jquery-3.2.1.min.js"></script>
        <script type="text/javascript">
            $(document).ready(function(){
                $("#submit").click(function () {
                    var userName=$("#userName").val();
                    var note = $("#note").val();
                    if($.trim(userName)==''){
                        alert("用户名称不能为空!");
                        return;
                    }
                    var params = {
                        userName:userName,
                        note:note
                    };
                    $.post({
                        url:"./insert",
                        //设定转递参数类型为json
                        contentType:"application/json",
                        //把json转为字符串
                        data:JSON.stringify(params),
                        //如果成功
                        success:function(result){
                            if(result == null || result.id == null){
                                alert("插入失败");
                                return;
                            }
                            alert("插入成功!" + JSON.stringify(result));
                        }
                    });
                });
            });
        </script>
    </head>
    <body>
        <div style="margin: 20px 0;"></div>
        <form id="insertForm">
            <table>
                <tr>
                    <td>用户名称:</td>
                    <td><input id="userName" name="userName"></td>
                </tr>
                <tr>
                    <td>备注</td>
                    <td><input id="note" name="note"></td>
                </tr>
                <tr>
                    <td></td>
                    <td align="right"><input id="submit" type="button" value="提交"/></td>
                </tr>
            </table>
        </form>
    </body>
</html>
这个jsp页面做了一个简单的表单,使用jQuery进行Ajax提交。
对于这里的Ajax的提交设置如下:
    提交的类型(contentType)为application/json
    提交的数据(data),通过JSON.stringify()方法把参数转为JSON
    请求的地址(url)设定为./insert
    success设置了提交成功之后的事后事件
对于这个JSP表单页面(add.jsp),在controller中指定一个方法来打开它
/**
* 打开新增用户的请求页面
*/
@GetMapping("/add")
public String add(){
    return "/user/add";
}
它返回一个字符串,通过ViewResolver就可以找到对应的页面,比如上面返回的是/user/add,表示要找到的视图是:/jsp/user/add.jsp
controller中新一个方法来处理上面的Ajax的请求
/**
* 新增用户
*/
@PostMapping("/insert")
@ResponseBody
public User insert(@RequestBody User user){
    userService.insertUser(user);
    return user;
}
这里的参数指定的@RequestBody注解,这个则表示它会提收前端提交的JSON请求体,在JSON请求体中的属性与User类的属性名称是一致的,这样Spring MVC就可以通过映射关系把JSON请求体转换为User对象。
通过URL转递参数
通过URL传递参数则是使用REST风格。比如我们要查询用户ID为28的用户,url可以类似写为:/user/28,这里的28就是用户的id。
要实现上述的方式可以通过处理器映射和注解@PathVariable的组合获取URL参数。先通过处理器映射定位参数的位置和名称,而@PathVariable则可以通过名称来获取参数。
在controller中新增如下方法
/**
* 根据ID来获取用户,{...}来表示在路径中的占位符
* @param id
* @return
*/
@GetMapping("/get/{id}")
@ResponseBody
public User get(@PathVariable("id") Long id){
   return userService.getUser(id);
}
通过@GetMapping指定一个URL,用{...}标明参数的位置和名称。
通过@PathVariable配置的字符串为id,它对应URL的参数说明
格式化参数
有一些应用中往往需要格式化数据,最典型的格式化数据是:日期、数值
如:日期格式化约定为    yyyy-MM-dd
    数值格式化约定为    #,###.##
在Spring MVC中对日期和数字类型的转换注解进行处理,分别使用注解:@DateTimeFormat、@NumberFormat
新增一个JSP表单,在webapp/jsp/format/formatter.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>格式化</title>
</head>
<body>
    <form action="./commit" method="post">
        <table>
            <tr>
                <td>日期(yyyy-MM-dd)</td>
                <td><input type="text" name="date" value="2000-10-31"/></td>
            </tr>
            <tr>
                <td>金额(#,###.##)</td>
                <td><input type="text" name="number" value="1,234,567.89"></td>
            </tr>
            <tr>
                <td colspan="2" align="right"><input type="submit" value="提交"></td>
            </tr>
        </table>
    </form>
</body>
</html>
在controller中新增方法来打开formatter.jsp视图
@GetMapping("/test/format")
public String showFormat(){
    return "/format/formatter";
}
在controller中新增方法来接收formatter.jsp页面的数据提交
@PostMapping("/test/commit")
@ResponseBody
public Map<String,Object> format(
        @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) Date date,
        @NumberFormat(pattern = "#,###.##") Double number
){
    Map<String,Object> map = new HashMap<>();
    System.out.println("date:" + date);
    map.put("date",date);
    map.put("number",number);
    return map;
}
在上面的方法中使用了@DateTimeFormat和@NumberFormat进行格式化的约定,Spring会根据这个约定的格式把数据转换出来。
3、自定义参数转换
处理器获取参数逻辑
    当请求到来时,在处理器执行的过程中,它先会从HTTP请求和上下文环境来得到参数,如果是简单的参数,它会以Spring MVC自身提供的转换器进行转换,如果要转换HTTP请求体(Body),会调用HttpMessageConverter接口的方法对请求体的信息进行转换,它先判断是否对请求体进行转换,如果可以则会把它转换为Java类型。
HttpMessageConverter接口源码
package org.springframework.http.converter;
import java.io.IOException;
import java.util.List;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
public interface HttpMessageConverter<T> {
   boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);    //是否可读,clazz为Java类型,mdeiaType为Http请求类型
   boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);    //判断clazz类型是否可以转换为mediaType类型,clazz为java类型,mediaType为http响应类型
   List<MediaType> getSupportedMediaTypes();    //可支持的媒体类型列表
   T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
         throws IOException, HttpMessageNotReadableException;    //当canRead验证通过后,读Http请求信息
   void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
         throws IOException, HttpMessageNotWritableException;    //当canWrite方法验证通过后,写入响应
}
处理器转换参数的过程
    Spring MVC中,通过WebDataBinder机制来获取参数的,它的主要作用是解析HTTP请求上下文,然后在控制器的调用之前转换参数并提供验证的功能,为调用控制器方法做准备。处理器会从HTTP请求中读取数据,然后通过三种接口来进行各类参数转换,三种接口分别是:Converter、Formatter、GenericConverter
    Spring MVC中这三种接口的实现类都采用了注册机的机制,默认情况下Spring MVC也已经在注册机内注册了很多转换器,这样就可以默认实现大多数的数据类型转换,当需要自定义的转换规则的时候,则需要在注册机上注册自己的转换器。
    WebDataBinder的另一个重要功能,就是验证转换结果。
    通过参数的转换和验证,最终控制器可以得到合法的参数,有了参数后就可以调用控制器方法了。
Sprng MVC处理器Http请求体转换流程
上图是请求体转换的全流程,但是Spring MVC会根据实际情况来处理消息的转换并不一定会走完全流程。
控制器的参数是片是器通过Converter、Formatter、GenericConverter这三个接口转换出来的。
Converter:一个普通的转换器
Formatter:格式化转换器
GenericConverter:把Http参数转换为数组
对于数据类型转换,Spring MVC提供了一个服务机制去管理,这个服务机制是一个ConversionService接口,默认的情况下会使用它的子类DefaultFormattingConversionService对象来管理这些转换类。
一对一转换器Converter
Converter接口源码如下:
package org.springframework.core.convert.converter;
import org.springframework.lang.Nullable;
@FunctionalInterface
public interface Converter<S, T> {
   @Nullable
   T convert(S source);
}
convert方法则是把源类型S转为目标类型T
如:Http的类型为String,控制器的类型为Long,则可以通过Spring内部的StringToNumber<T extends Number>进行转换。
如果我们要把前端传的一个用户信息转为User对象,这个时候则需要自定义一个转换器了
新增一个converter,com.xiaoxie.converter.StringToUserConverter
package com.xiaoxie.converter;
import com.xiaoxie.pojo.User;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
@Component
public class StringToUserConverter implements Converter<String,User> {
    @Override
    public User convert(String s) {
        User user = new User();
        String[] strArr = s.split("-");
        Long id = Long.parseLong(strArr[0]);
        String userName = strArr[1];
        String note = strArr[2];
        if("null".equals(note)){
            note = "";
        }
        user.setId(id);
        user.setUserName(userName);
        user.setNote(note);
        return user;
    }
}
这个类标注为@Component,并且实现了Converter接口,这样Spring会把这个类扫描并装配到IoC容器中。
新增一个controller类:com.xiaoxie.controller.ConverterController
package com.xiaoxie.controller;
import com.xiaoxie.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@Controller
public class ConverterController {
    @GetMapping("/StringToUser")
    @ResponseBody
    public User getUserByConverter(User user){
        return user;
    }
}
运行Spring Boot,访问:http://localhost:8080/StringToUser?user=1-关羽-null
浏览器展示json如下:
{"id":1,"userName":"关羽","note":""}
通过上的的实例可以看到通过前端转过来的字符串“1-关羽-null”,确实通过StringToUserConverter类转换为了User对象。
GenericConverter集合和数组转换
Spring MVC中有一个转换器StringToCollectionConverter转换器,这个类实现了GenericConverter接口,并且是Spring MVC内部已注册的数组转换器。它先会把字符串使用逗号进行分隔为子字符串,然后再根据源类型与目标类型找到对应的Converter进行转换。
ConverterController类中新增方法
@GetMapping("/StringToUserList")
@ResponseBody
public List<User> list(List<User> userList){
    return userList;
}
访问地址:http://localhost:8080/StringToUserList?userList=1-关羽-null,2-张飞-null
浏览器展示json如下:
[{"id":1,"userName":"关羽","note":""},{"id":2,"userName":"张飞","note":""}]
数据验证
JSR-303验证
它是通过注解的方式进行的,在对应的POJO的属性上加入相关的注解。
新增一个pojo类:com.xiaoxie.pojo.ValidatorPOJO
package com.xiaoxie.pojo;
import org.hibernate.validator.constraints.Range;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.*;
import java.util.Date;
public class ValidatorPOJO {
    @NotNull(message = "id不能为空")    //非空判断
    private Long id;
    @Future(message = "需要一个将来日期")   //只允许是一个将来日期
    //@Past //这个表示只允许一个过去的日期
    @DateTimeFormat(pattern = "yyyy-MM-dd") //日期格式化
    @NotNull    //不允许为空
    private Date date;
    @NotNull    //不允许为空
    @DecimalMax(value = "100.00",message = "最大值为100.00")   //最大值为100.00
    @DecimalMin(value = "0.00",message = "最小值为0.00")  //最小值为0.00
    private Double score = null;
    @NotNull
    @Min(value = 0,message="最小值为0") //最小值为0
    @Max(value = 150,message = "最大值为150")   //最大值为150
    private Integer age;
    @Range(min = 1,max = 10,message = "范围为1到10")    //限定范围
    private Long grade;
    @Email(message = "邮箱格式错误")  //邮箱验证
    private String email;
    @Size(min=1,max=20,message = "字符串长度在1到20之间")
    private String city;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Date getDate() {
        return date;
    }
    public void setDate(Date date) {
        this.date = date;
    }
    public Double getScore() {
        return score;
    }
    public void setScore(Double score) {
        this.score = score;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public Long getGrade() {
        return grade;
    }
    public void setGrade(Long grade) {
        this.grade = grade;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
}
新增一个jsp页面(webapp/jsp/validator/pojo.jsp),然后使用JSON数据请求发送对象给到控制器
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>JSR-303验证</title>
    <!-- 引入Jquery -->
    <script src="../../js/jquery-3.2.1.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function(){
            //请求验证的POJO
            var pojo = {
                id:null,
                date:'2020-01-01',
                score:100.01,
                age:-1,
                grade:11,
                email:'email',
                city:'长沙'
            }
            $.post({
                url:"./validate",
                contentType:"application/json", //传递的参数类型为JSON
                data:JSON.stringify(pojo),
                success:function(retult){
                    //alert(JSON.stringify(retult));
                }
            });
        });
    </script>
</head>
<body>
</body>
</html>
有了上面的jsp页面,则在打开这个页面时,它会通过Ajax请深求到对应的方法,然后在生成对象时对传过来的值进行验证。
新增controller:com.xiaoxie.controller.ValidateController
package com.xiaoxie.controller;
import com.xiaoxie.pojo.ValidatorPOJO;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
public class ValidateController {
    @GetMapping("/validate/page")
    public String page(){
        return "/validator/pojo";
    }
    @RequestMapping(value = "/validate/validate")
    @ResponseBody
    public Map<String,Object> validate(
            @Valid @RequestBody ValidatorPOJO vp,Errors errors
            ){
        Map<String,Object> errMap = new HashMap<>();
        //获取错误列表
        List<ObjectError> oes = errors.getAllErrors();
        for (ObjectError oe:oes) {
            String key = null;
            String msg = null;
            if(oe instanceof FieldError){
                FieldError fe = (FieldError) oe;
                key = fe.getField();    //错误验证字段名称
            }else{
                key = oe.getObjectName();    //验证对象名称
            }
            //错误信息
            msg = oe.getDefaultMessage();
            errMap.put(key,msg);
        }
        return errMap;
    }
}
page方法用来定位到指定的pojo.jsp
validate方未能用来接收pojo.jsp页面过来的Ajax请求
使用@RequestBody代表着接收一个JSON参数,这样Spring就会获取页面通过Ajax提交的JSON请求,@Valid表示启动验证机,这样Spring会启用JSR-303验证机制进行验证。
JSR-303对pojo的属性进行了验证,但是对于一些复杂的业务逻辑进行验证则不能通过JSR-303进行了,这个时候则需要使用Spring的验证机制。
参数验证机制
为了更加灵活地提供验证机制,Spring提供了自己的验证机制,在参数转换时,Spring MVC存在WebDataBinder要机制进行管理,在默认情况下Spring会自己动地根据上下文通过注册了的转换器转换出控制器所需要的参数,实际上在WebDataBinder中除了可以注册转换器外,还可以注册验证器。
Spring控制器中,它允许使用注解@InitBinder,这个注解的作用是允许在进入控制器方法前修改WebDataBinder机制。
关于Spring MVC的验证机制,它定义了一个接口Validator,其源码如下:
package org.springframework.validation;
import org.springframework.lang.Nullable;
public interface Validator {
   boolean supports(Class<?> clazz);    //判断当前验证器是否支持Class类型的验证
   void validate(@Nullable Object target, Errors errors); //如果support返回true,则这个方法执行验证逻辑
}
这个接口中有两个方法,supports方法参数需要验证pojo。如果这个方法返回true,则Spring会使用当前验证器的的validation方法验证pojo,validation方法包含需要的target对象和错误对象error,其中target是参数绑定后的POJO,这样就可以通过这个参数对象进行业务逻辑的自己定义验证。如果发现错误,则可以保存到errors对象中,然后返回给控制器。
新增一个验证器com.xiaoxie.validator.UserValidator
package com.xiaoxie.validator;
import com.xiaoxie.pojo.User;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
public class UserValidator implements Validator {
    //控制这个验证器只支持User类验证
    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.equals(User.class);
    }
    /**
     * 验证逻辑
     * @param target
     * @param errors
     */
    @Override
    public void validate(Object target, Errors errors) {
        if(target == null){
            errors.rejectValue("",null,"用户不能为空");
            return;
        }
        User user = (User) target;
        if(StringUtils.isEmpty(user.getUserName())){
            errors.rejectValue("userName",null,"用户名不能为空");
        }
    }
}
有了上面的验证器Spring还不会自动启用它,因为还没有绑定给WebDataBinder机制。在Spring MVC中提供了一个注解@InitBinder,它的作用是在执行控制器方法前,让处理器先执行由它标注的方法
在UserController中添加如下两个方法
//调用控制器前会先执行这个方法
@InitBinder
public void initBinder(WebDataBinder binder){
    //绑定验证器
    binder.setValidator(new UserValidator());
    //设定日期格式
    binder.registerCustomEditor(Date.class,
            new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"),false));
}
@GetMapping("/validate")
@ResponseBody
public Map<String,Object> validator(@Valid User user,Errors errors,Date date){
    Map<String,Object> map = new HashMap<>();
    map.put("user",user);
    map.put("date",date);
    //判断是否有错误
    if(errors.hasErrors()){
        //获取所有错误
        List<ObjectError> oes = errors.getAllErrors();
        for(ObjectError oe : oes){
            if(oe instanceof FieldError){
                //字段错误
                FieldError fe = (FieldError) oe;
                map.put(fe.getField(),fe.getDefaultMessage());
            } else {
                //对象错误
                map.put(oe.getObjectName(),oe.getDefaultMessage());
            }
        }
    }
    return map;
}
注意:这里的validator的参数,第一个是User,它使用了@Valid,这个时候它会去遍历到自定义听验证器UserValidator,第二个参数是Errors,它接收验证器返回的错误,它必须在User参数后,不能把它放在Date参数后面。
启动springBoot后,访问地址:http://localhost:8080/user/validate?user=1--备注信息&date=2020-01-01
浏览器展示的JSON数据如下:
{"date":"2019-12-31T16:00:00.000+0000","userName":"用户名不能为空","user":{"id":1,"userName":"","note":"备注信息"}}
数据模型
Spring MVC流程中,控制器是业务逻辑核心内容,而控制器的核心内容之一就是对数据的处理。
Spring MVCk中可以让控制器自定义模型和视图ModelAndView。其中模型是存放数据的地方,视图是向用户展示数据的地方。
ModelAndView中存在一个ModelMap类型的属性,ModelMap继承了LinkedHashMap类,所以它具备Map接口的一切特性。
如果在Spring MVC应用的控制器方法的参数中使用ModelAndView、Model、ModelMap作为参数类型,会自动创建数据模型对象。
新增一个controller类:com.xiaoxie.controller.DataModelController
package com.xiaoxie.controller;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.UserService;
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.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
@RequestMapping("/data")
@Controller
public 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.put("user",user);
        return mv;
    }
    /*ModelAndView*/
    @GetMapping("/modelAndView")
    public ModelAndView useModelAndView(Long id,ModelAndView mv){
        User user = userService.getUser(id);
        //设加置模型数据
        mv.addObject("user",user);
        //设置视图名称
        mv.setViewName("data/user");
        return mv;
    }
}
根据设置的视图在,新增JSP页面:webapp/jsp/data/user.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>用户信息</title>
</head>
<body>
    <table>
        <tr>
            <td>编号</td>
            <td>${user.id}</td>
        </tr>
        <tr>
            <td>用户名</td>
            <td>${user.userName}</td>
        </tr>
        <tr>
            <td>备注</td>
            <td>${user.note}</td>
        </tr>
    </table>
</body>
</html> 
浏览器请求上面的三个controller方法对应的地址都会把视图映射到user.jsp
视图与视图解析器
视图是渲染数据模型展示给用户的组件,Spring MVC当中把它又分为逻辑视图和非逻辑视图,其中逻辑视图是需要视图解析器进行进一步的定位的。而非逻辑视图则不需要进一步进行定位视图位置,只需要直接把数据模型渲染出来就可以了。
对于视图来说,除了JSON、JSP之外,还有其他类型的视图,如:Excel、PDF……,视图是多种多样的,但是它们都会实现Spring MVC定义的视图接口View
View接口的源码如下
package org.springframework.web.servlet;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
public interface View {
   String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";    //响应状态属性
   String PATH_VARIABLES = View.class.getName() + ".pathVariables";    //路径变量
   String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";    //选择内容类型
    //响应类型
   @Nullable
   default String getContentType() {
      return null;
   }
    
    //渲染方法
   void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
         throws Exception;
}
从上面的源码中可以看到,它有两个方法
getContentType():它是用来获取Http响应类型的,它可以返回的类型是文本、JSON数据集或文件等
render():把数据模型渲染到视图,它是视图的核心方法
render方法的参数说明
    model:数据模型,实际就是从控制器(或由处理器自动绑定)返回的数据模型
    request:http请求
    response:http响应
Spring MVC当中已经开发好了各种各样的视图,在大部分的情况下,只需要定义如何把数据模型渲染到视图中展示给用户即可。
展现PDF视图
对于像PDF、Excel等类型的视图,它们只需要接收数据模型,然后通过自定义的渲染即可。
PDF视图,它的视图抽象类是:AbstractPdfView,其源码如下:
package org.springframework.web.servlet.view.document;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.PageSize;
import com.lowagie.text.pdf.PdfWriter;
import org.springframework.web.servlet.view.AbstractView;
public abstract class AbstractPdfView extends AbstractView {
   public AbstractPdfView() {
      setContentType("application/pdf");
   }
   @Override
   protected boolean generatesDownloadContent() {
      return true;
   }
   @Override
   protected final void renderMergedOutputModel(
         Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
      // IE workaround: write into byte array first.
      ByteArrayOutputStream baos = createTemporaryOutputStream();
      // Apply preferences and build metadata.
      Document document = newDocument();
      PdfWriter writer = newWriter(document, baos);
      prepareWriter(model, writer, request);
      buildPdfMetadata(model, document, request);
      // Build PDF document.
      document.open();
      buildPdfDocument(model, document, writer, request, response);
      document.close();
      // Flush to HTTP response.
      writeToResponse(response, baos);
   }
   protected Document newDocument() {
      return new Document(PageSize.A4);
   }
   protected PdfWriter newWriter(Document document, OutputStream os) throws DocumentException {
      return PdfWriter.getInstance(document, os);
   }
   protected void prepareWriter(Map<String, Object> model, PdfWriter writer, HttpServletRequest request)
         throws DocumentException {
      writer.setViewerPreferences(getViewerPreferences());
   }
   protected int getViewerPreferences() {
      return PdfWriter.ALLOW_PRINTING | PdfWriter.PageLayoutSinglePage;
   }
   protected void buildPdfMetadata(Map<String, Object> model, Document document, HttpServletRequest request) {
   }
   protected abstract void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer,
         HttpServletRequest request, HttpServletResponse response) throws Exception;
}
其中有一个抽象方法是需要自行实现的
实现了buildPdfDocument抽象方法则可以把数据模型渲染为PDF。
这个方法中的参数
    model:数据模型对象
    document:与pdf文档相关的文档对象
    writer:与pdf文档相关参数
    request:http请求
    response:http响应
通过这个方法可以重写可以定制PDF格式和数据渲染
我们向pom.xml中添加如下依赖
<!-- PDF相关依赖 -->
    <dependency>
        <groupId>org.xhtmlrenderer</groupId>
        <artifactId>core-renderer</artifactId>
        <version>R8</version>
    </dependency>
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itextpdf</artifactId>
        <version>5.5.12</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.itextpdf/itext-asian -->
    <dependency>
        <groupId>com.itextpdf</groupId>
        <artifactId>itext-asian</artifactId>
        <version>5.2.0</version>
    </dependency>
pdf文档写渲染数据时默认是不支持中文的,为了支持中文,我们需要把中文字体加载到项目中,在resources下添加目录font,并在其中拷贝中文字体文件STFANGSO.TTF
在controller中(UserController类)新增一个展示PDF文档的方法
@GetMapping("/export/pdf")
public ModelAndView exportPdf(String userName,String note){
    //查询用户列表
    List<User> userList = userService.findUsers(userName,note);
    ModelAndView mv = new ModelAndView();
    mv.addObject("userList",userList);
    View view = new AbstractPdfView() {
        @Override
        protected void buildPdfDocument(Map<String, Object> model, Document document, PdfWriter writer, HttpServletRequest request, HttpServletResponse response) throws Exception {
            try{
                document.setPageSize(PageSize.A4);  //A4纸张
                document.addTitle("用户信息");  //标题
                document.add(new Chunk("\n"));  //换行
                PdfPTable table = new PdfPTable(3); //3列
                //单元格
                PdfPCell cell = null;
                //字体定义
                Font hfont = FontFactory.getFont("font/STFANGSO.TTF", BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED);
                //Font font = new Font();
                hfont.setColor(Color.BLUE);
                hfont.setStyle(Font.BOLD);
                //标题
                cell = new PdfPCell(new Paragraph("id",hfont));
                //居中
                cell.setHorizontalAlignment(1);
                //把单元格加入到表格中
                table.addCell(cell);
                cell = new PdfPCell(new Paragraph("用户名",hfont));
                cell.setHorizontalAlignment(1);
                table.addCell(cell);
                cell = new PdfPCell(new Paragraph("备注",hfont));
                cell.setHorizontalAlignment(1);
                table.addCell(cell);
                Font bfont = FontFactory.getFont("font/STFANGSO.TTF", BaseFont.IDENTITY_H,BaseFont.NOT_EMBEDDED);
                //Font font = new Font();
                bfont.setColor(Color.BLACK);
                hfont.setStyle(Font.NORMAL);
                //获取数据模型中的用户列表数据
                List<User> userList1 = (List<User>) model.get("userList");
                for(User user:userList1){
                    document.add(new Chunk("\n"));
                    cell = new PdfPCell(new Paragraph(user.getId() + "",bfont));
                    cell.setHorizontalAlignment(1); //居中
                    table.addCell(cell);
                    cell = new PdfPCell(new Paragraph(user.getUserName(),bfont));
                    cell.setHorizontalAlignment(1); //居中
                    table .addCell(cell);
                    String note = user.getNote() == null?"":user.getNote();
                    cell = new PdfPCell(new Paragraph(note,bfont));
                    cell.setHorizontalAlignment(1); //居中
                    table .addCell(cell);
                }
                //在文档中添加表格
                document.add(table);
            }catch(DocumentException e){
                e.printStackTrace();
            }
        }
    };
    //设置视图
    mv.setView(view);
    return mv;
}
启动Spring Boot,访问地址:http://localhost:8080/user/export/pdf
浏览器展示的效果如下:
文件上传
Spring MVC对文件上传的支持
DispatcherServlet会使用适配器模式,把HttpServletRequest接口对象转为MultipartHttpServletRequest对象,它扩展了HttpServletRequest接口的方法同时提供了一些操作文件的方法。
在使用Spring MVC上传文件时,还需要配置MultipartHttpServletRequest,这个是通过MultipartResolver接口实现的。对于这个接口它存在两个实现类:StandardServletMultipartResolver、CommonsMultipartResolver。
可以通过上面的实现类去实现文件上传,建议使用StandardServletMultipartResolver。
在Spring Boot的机制内,如果没有自定义MultipartResolver对象,自动配置的机制会为你自动创建MulitpartResolver对象,实际对象为StandardServletMultipartResolver。所以这里是不需要自行去创建的,在Spring Boot中在application.properties中进行相应的属性配置即可。
Spring MVC上传文件相关属性配置
#指定上传文件的文件夹位置
spring.servlet.multipart.location=D:/IdeaProjects/SpringBoot_SpringMVC/src/main/resources/uploads
#单个文件上传大小限制
spring.servlet.multipart.max-file-size=5MB
#所有文件的上传大小限制
spring.servlet.multipart.max-request-size=20MB
编辑一个jsp页面用来提交上传的文件(webapp/jsp/file/upload.jsp)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>文件上传</title>
</head>
<body>
    <form method="post" action="./part" enctype="multipart/form-data">
        <input type="file" name="file" value="选择上传文件"/>
        <input type="submit" value="提交"/>
    </form>
</body>
</html>
注意:<form>表单中声明为multipart/form-data,如果不做这个声明,Spring MVC会解析文件请求出错。
新增一个controller类:com.xiaoxie.controller.FileController
package com.xiaoxie.controller;
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;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("/file")
public class FileController {
    @GetMapping("/uploadPage")
    public String uploadPage(){
        return "/file/upload";
    }
    //使用HttpServletRequest作为参数
    @PostMapping("/request")
    @ResponseBody
    public Map<String,Object> uploadRequest(HttpServletRequest request){
        Map<String,Object> map = new HashMap<>();
        MultipartHttpServletRequest multipartHttpServletRequest = null;
        //强制转换请求类型
        if(request instanceof MultipartHttpServletRequest){
            multipartHttpServletRequest = (MultipartHttpServletRequest) request;
        } else {
            map.put("success",false);
            map.put("message","上传失败");
            return map;
        }
        //获取MultipartFile文件信息
        MultipartFile mf = multipartHttpServletRequest.getFile("file");
        //获得源文件名
        String fileName = mf.getOriginalFilename();
        File file = new File(fileName);
        try{
            //保存文件
            mf.transferTo(file);
        } catch (Exception e){
            e.printStackTrace();
            map.put("success",false);
            map.put("message","保存文件失败");
            return map;
        }
        map.put("success",true);
        map.put("message","上传文件成功");
        return map;
    }
    //使用Spring MVC的MultipartFile作为参数
    @PostMapping("/multipart")
    @ResponseBody
    public Map<String,Object> uploadMultipartFile(MultipartFile file){
        Map<String,Object> map = new HashMap<>();
        String fileName = file.getOriginalFilename();
        File dest = new File(fileName);
        try{
            file.transferTo(dest);
        } catch (Exception e){
            e.printStackTrace();
            map.put("success",false);
            map.put("message","上传文件失败");
            return map;
        }
        map.put("success",true);
        map.put("message","上传文件成功");
        return map;
    }
    //使用Servlet的Part作为参数
    @PostMapping("/part")
    @ResponseBody
    public Map<String,Object> uploadPart(Part file){
        Map<String,Object> map = new HashMap<>();
        //获取提交文件名
        String fileName = file.getSubmittedFileName();
        try{
            //写入文件
            file.write(fileName);
        } catch(Exception e){
            e.printStackTrace();
            map.put("success",false);
            map.put("message","上传文件失败");
            return map;
        }
        map.put("success",true);
        map.put("message","上传文件成功");
        return map;
    }
}
其中uploadPage方法,它是用来定位到上传文件那个jsp文件。
uploadRequest方法则是用HttpServletRequest对象传递,在调用控制器之间,DispatcherServlet会把它转为MultipartHttpServleRequest对象,在方法中使用了强制转换,从而可以得到MultipartHttpServletRequest对象,然后通过它来获取MultipartFile对象,通过获取到的MultipartFile对象调用getOriginalFilename方法获取上传的文件名,通过它的transferTo方法,把文件保存到配置的路径中去。
uploadMultipartFile方法则直接使用MultipartFile对象获取上传文件
uploadPart方法则使用Servlet的API,使用getSubmittedFileName方法获取到源文件名称,然后通过write方法直接写入文件。(推荐使用这种方法)
拦截器
当请求到了DispatcherServlet时,它会根据HandlerMapping的机制找到处理器,会返回一个HandlerExecutionChain对象,这个对象包含处理器和拦截器,拦截器会对处理器进行拦截,从而可以达到增强处理器的功能的目的。
所有的拦截器需要实现HandlerInterceptor接口,其源码如下:
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.method.HandlerMethod;
public interface HandlerInterceptor {
    //处理器执行前方法
   default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
         throws Exception {
      return true;
   }
    //处理器处理后方法
   default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
         @Nullable ModelAndView modelAndView) throws Exception {
   }
    //处理完成后方法
   default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
         @Nullable Exception ex) throws Exception {
   }
}
执行步骤说明:
    1、perHandler方法:它会返回一个布尔值。如果为false,则结束流程,否则可以执行后续流程
    2、在执行perHandler方法返回true后,则接下来执行处理器逻辑,这里就包含了控制器的功能
    3、postHandler方法:当处理器执行完成后会执行这个方法
    4、执行视图解析和视图渲染
    5、执行afterCompletion方法
上面接口中的三个方法都被修饰为default,提供了空实现,如果需要自定义方法则只需要实现HandlerInterceptor,覆盖其对应的方法就可以了。
自定义拦截器:com.xiaoxie.interceptor.MyInterceptor1
package com.xiaoxie.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor1 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("处理器前方法:preHandle" );
        return true;    //这里返回true,不会拦截后续的处理
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("处理器后方法:postHandle");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("处理器完成方法:afterCompletion");
    }
}
上面这个类自定义了拦截器,接下来为了让MVC发现它,还需要对这进行注册。
新增一个配置类:com.xiaoxie.config.InterceptorConfig
package com.xiaoxie.config;
import com.xiaoxie.interceptor.MyInterceptor1;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer{
    //注册拦截器到MVC
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        InterceptorRegistration interceptorRegistration = registry.addInterceptor(new MyInterceptor1());
        //指定拦截匹配模式
        interceptorRegistration.addPathPatterns("/interceptor/*");
    }
}
这里通过实现WebMvcConfigurer接口,并重写addInterceptors方法把拦截器注册到MVC
新增一个控制器:com.xiaoxie.controller.InterceptorController
package com.xiaoxie.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/interceptor")
public class InterceptorController {
    @GetMapping("/test")
    public String test(){
        System.out.println("执行处理器逻辑");
        return "/test";
    }
}
这里控制器的请求路径为:/interceptor/test,根据拦截器的注册内容中所指定的拦截匹配模式可以看到这个请求是会被拦截的,这个控制器会打开一个页面:test.jsp
test.jsp页面(webapp/jsp/test.jsp)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>测试Interceptor</title>
</head>
<body>
    <h1>
        <%
            System.out.println("视图渲染");
            out.print("Interceptor测试");
        %>
    </h1>
</body>
</html>
启动Spring Boot当访问http://localhost:8080/interceptor/test时,在控制台可以看到打印结果如下:
处理器前方法:preHandle
执行处理器逻辑
处理器后方法:postHandle
视图渲染
处理器完成方法:afterCompletion
上面我们看到了一个拦截对处理器进行拦截时的执行情况,当存在多个拦截器的时候执行情况又是如何呢?
自定义三个拦截器分别为:MulInterceptor1、MulInterceptor2、MulInterceptor3
package com.xiaoxie.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** MulInterceptor1 */
public class MulInterceptor1 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(this.getClass().getSimpleName() + ":处理器前方法(preHandle)");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(this.getClass().getSimpleName() + ":处理器后方法(postHandle)");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(this.getClass().getSimpleName() + ":处理器完成方法(afterCompletion)");
    }
}
package com.xiaoxie.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** MulInterceptor2 */
public class MulInterceptor2 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(this.getClass().getSimpleName() + ":处理器前方法(preHandle)");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(this.getClass().getSimpleName() + ":处理器后方法(postHandle)");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(this.getClass().getSimpleName() + ":处理器完成方法(afterCompletion)");
    }
}
package com.xiaoxie.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** MulInterceptor3 */
public class MulInterceptor3 implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(this.getClass().getSimpleName() + ":处理器前方法(preHandle)");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(this.getClass().getSimpleName() + ":处理器后方法(postHandle)");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(this.getClass().getSimpleName() + ":处理器完成方法(afterCompletion)");
    }
}
把上面这三个拦截器进行注册,修改com.xiaoxie.config.InterceptorConfig
package com.xiaoxie.config;
import com.xiaoxie.interceptor.MulInterceptor1;
import com.xiaoxie.interceptor.MulInterceptor2;
import com.xiaoxie.interceptor.MulInterceptor3;
import com.xiaoxie.interceptor.MyInterceptor1;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer{
    //注册拦截器到MVC
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //三个拦截器的注册
        registry.addInterceptor(new MulInterceptor1()).addPathPatterns("/interceptor/*");
        registry.addInterceptor(new MulInterceptor2()).addPathPatterns("/interceptor/*");
        registry.addInterceptor(new MulInterceptor3()).addPathPatterns("/interceptor/*");
    }
}
启动Spring Boot,访问:http://localhost:8080/interceptor/test
在控制台打印结果如下:
MulInterceptor1:处理器前方法(preHandle)
MulInterceptor2:处理器前方法(preHandle)
MulInterceptor3:处理器前方法(preHandle)
执行处理器逻辑
MulInterceptor3:处理器后方法(postHandle)
MulInterceptor2:处理器后方法(postHandle)
MulInterceptor1:处理器后方法(postHandle)
视图渲染
MulInterceptor3:处理器完成方法(afterCompletion)
MulInterceptor2:处理器完成方法(afterCompletion)
MulInterceptor1:处理器完成方法(afterCompletion)
从这个运行的结果可以看出来,对于处理器前方法采用先注册先执行,而处理器后方法和完成方法则是先注册后执行。
国际化
对于国际化,Spring MVC提供了国际化消息源机制,它是MessageSource接口体系。
在Spring Boot中为了方便使用Spring MVC的国际化,提供了一些配置项,这样开发过程中可以快速配置国际化。
Spring.messages.always-use-message-format=false    //这个默认是false,设置国际化消息是否总是使用格式化
spring.messages.basename=messages    //设置国际化属性名称,如果存在多个则使用
逗号分隔,默认为messages
spring.messages.cache-duration=    //指定国际化消息缓存时间,默认为永不过期,如果设置为0则每次都重新加载
spring.messages.encoding=UTF-8    //国际化消息编码
spring.messages.fallback-to-system-locale=true    //如果没有找到特定区域设置文件,则设置是否返回系统区域设置
spring.messages.use-code-as-default-message=false    //是否使用消息编码作为默认的响应消息
当我们需要设置中文简体、美国英文国际化消息,我们需要有三个属性文件(properties)放到resources目录
messages.properties    //这个是默认的国际化文件,如果没有这个文件则Spring MVC不会启用国际化消息机制
messages_zh_CN.properties    //表示简体中文的国际化消息
messages_en_US.properties    //表示美国英文的国际化消息
这里的messages开头的原因是spring.messages.basename的默认配置是:messages,如果国际化的属性文件发messages开头则上面的这个配置直接用Spring MVC的默认配置即可,不用再单独指定。
在使用国际化时,需要确定用户是使用哪个国际区域。Spring MVC提供了LocaleResolver接口来确定用户的国际化区域。实现这个接口的也有一系列的接口和类
    AcceptHeaderLocaleResolver:使用浏览器头请求的信息去实现国际化区域。Spring MVC和Spring Boot都使用它作为默认的国际化解析器
    FixedLocaleResolver:固定国际化区域。只能先择一种,不可以变化。
    CookieLocaleResolver:把国际化区域信息设置到浏览器Cookie当中。Cookie是可以被禁止的,如果被禁止后读取Cookie则会失败,这个时候使用默认国际化区域,默认国际化区域可以从浏览器头请求读出,也可以在服务端进行配置。
    SessionLocaleResolver:把国际化的信息设置到Session中,读取Session中的信息去确定国际化区域。
在application.properties中新增如下配置:
#配置国际化消息
#文件编码
spring.messages.encoding=UTF-8
#国际化文件基础名称
spring.messages.basename=international
#国际化消息缓存有效时间(单位:秒)
spring.messages.cache-duration=3600s
在resuources目录下新增三个国际化消息的属性文件
international.properties
msg=Spring MVC 国际化
international_zh_CN.properties
msg=Spring MVC 国际化
international_en_US.properties
msg=Spring MVC internationalization
创建国际化解析器配置:com.xiaoxie.config.ResolverConfig
Spring MVC中提供了一个拦截器LocaleChangeInterceptor,可以在处理器前处理相关逻辑(preHandle方法),可以拦截一个请求参数,通过这个参数来确定国际化信息,并把国际化信息保存到Session中。后续则可以从Session中读取国际化的消息。这里的请求参数需要进行配置。
package com.xiaoxie.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;
@Configuration
public class ResolverConfig implements WebMvcConfigurer {
    //国际化拦截器
    private LocaleChangeInterceptor lci = null;
    //国际化解析器,名称为localeResolver,这个名称是Spring MVC中约定的不可以更改
    @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) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}
新增控制器:com.xiaoxie.controller.I18nController
package com.xiaoxie.controller;
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;
import javax.servlet.http.HttpServletRequest;
import java.util.Locale;
@Controller
@RequestMapping("/i18n")
public class I18Controller {
    //国际化消息接口对象
    @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/international";
    }
}
新增视图international.jsp,文件位置:webapp/jsp/i18n/international.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="mvc" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head>
    <title>Spring MVC国际化</title>
</head>
<body>
    <a href="./page?language=zh_CN">简体中文</a>
    <a href="./page?language=en_US">美国英文</a>
    <h2>
        <spring:message code="msg"/>
    </h2>
    <!-- 当前国际化区域 -->
    Locale:${pageContext.response.locale}
</body>
</html>
四、Spring MVC其它补充说明
@ResponseBody注解的控器方法返回转JSON
在进入到控制器方法方遇到标注@ResponseBody,处理器会记这个方法的响应类型为JSON数据集。当执行完控制器后,处理器会启用解析器(ResultResolver)去解析这个结果。会去轮询看注册给Spring MVC的HttpMessageConverter接口的实现类。轮询到MappingJackson2HttpMessageConverter这个实现类,加之控制器对结果的标注为JSON则两者匹配上,就会使用它在内部把结果转换为JSON。如果被上面说的这个实现类进行了转换则后续的模型和视图(ModelAndView)会返回null,这样的话视图解析器和视图渲染就不再被执行了。
重定向的处理方式
重定向Redirect,就是通过各种方法把网络请求重新转到其它位置。
    方法一:在返回的字符串内容以“redirect:”开头,如:redirect:/user/show?id=1
    方法二:如果使用ModelAndView时,在在返回ModelAndView前,setViewName时指定的字符串以“redirect:”开头
    方法三:在控制器方法中添加RedirectAttributes对象参数,然后在方法中使用addFlashAttribute来保存重定向时绑定的属性值。
        在addFlashAttribute方法保存的参数,在控制器执行完成后,会被保存到Session对象中,当执行重定向时,在进入重定向前先把Session中的参数取出,用来填充重定向方法的参数和数据模型,之后删除Session中的数据,然后则可以调用重定向方法,并把对象传递给重定向的方法。
操作会话对象Session
操作会话HttpSesson对象,在Spring MVC中主要由两个注解来操作
    @SessionAttribute    用于参数,把HttpSession中的属性读出来,赋予控制器的参数
    @SessionAttributes    用于类的注解,它会把相关数据模型的属性保存到Session中
如下:
    @SessionAttribtes(names = {"user"},types = Long.class) 它只能用于类上,其中names和types两者是或者的关系,只要两者之一满足就会把属性保存到Session中。
    @SessionAttribute("id")    它用于参数上,表示读取Session中id参数值。
给控制器增加通知
Spring AOP中,可以通过通知来增强Bean功能,Spring MVC中则可以给控制器增加通知。
给控制器增加通知涉及到四个注解
    @ControllerAdvice    定义一个控制器的通知类,允许定义一些关于增强控制器的各类通知和限定增强哪些控制器功能……
    @InitBinder    定义控制器参数绑定规则,它会在参数转换之前执行
    @ExceptionHandler    定义控制器发生异常后执行操作,一般来说可以在发生异常后跳转到异常页面,保证友好性
    @ModelAttribute    可以在控制器方法执行前,对数据模型进行操作
自定义一个注解com.xiaoxie.annotation.MyAnnotation
package com.xiaoxie.annotation;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface MyAnnotation {
}
定义一个控制器的通知类:com.xiaoxie.advice.MyControllerAdvice
package com.xiaoxie.advice;
import com.xiaoxie.annotation.MyAnnotation;
import org.springframework.beans.propertyeditors.CustomDateEditor;
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;
import java.text.SimpleDateFormat;
import java.util.Date;
@ControllerAdvice(
        //指定拦截的包
        basePackages = {"com.xiaoxie.controller.*"},
        //限定被标注的类,这里使用到了自定义的注解
        annotations = MyAnnotation.class
)
public class MyControllerAdvice {
    //绑定格式化、参数转换规则和增加验证器等
    @InitBinder
    public void initDataBinder(WebDataBinder binder){
        //自定义日期编辑器,限定格式为yyyy-MM-dd,并且参数不可以为空
        CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"),
                false);
        //注册自定义日期编辑器
        binder.registerCustomEditor(Date.class,dateEditor);
    }
    //在执行控制器之前先执行,可以用来初始化数据模型
    @ModelAttribute
    public void projectModel(Model model){
        model.addAttribute("name","Spring MVC");
    }
    //异常处理,被拦截的控制器方法发生异常时有相应的视图响应
    @ExceptionHandler(value = Exception.class)
    public String exceptionHandler(Model model,Exception e){
        //数据模型增加异常消息
        model.addAttribute("exception_message",e.getMessage());
        //返回视图
        return "exception";
    }
}
新增一个conroller:com.xiaoxie.controller.AdviceController
package com.xiaoxie.controller;
import com.xiaoxie.annotation.MyAnnotation;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Date;
@Controller
@MyAnnotation
@RequestMapping("/advice")
public class AdviceController {
    @GetMapping("/test")
    public String test(Date date, ModelMap modelMap){
        //从模型中获取数据
        System.out.println(modelMap.get("name"));
        //打印日期参数
        System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(date));
        //抛出异常
        throw new RuntimeException("异常了~~~");
    }
}
当控制器发生异常时会返回到exception.jsp这个视图,新增jsp页面:webapp/jsp/excepton.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>异常提示</title>
</head>
<body>
    <h2>${exception_message}</h2>
</body>
</html>
启动Spring boot,访问:http://localhost:8080/advice/test?date=2020-01-01 09:00:00
在控制台中会打印如下信息
Spring MVC
2020-01-01
浏览器会展示视图exception.jsp的信息,并显示exception_message绑定的信息:异常了~~
获取请求头参数
在Http请求中,有些网站会利用请求头的数据来进行身份验证,因而有时也需要从请求头中获取数据。
在Spring MVC中可以通过注解@RequestHeader来进行获取。
新增一个jsp页面,在页面中使用ajax方式设置请求头的参数
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>获取请求头参数</title>
    <script src="../js/jquery-3.2.1.min.js"></script>
    <script type="text/javascript">
        $.post({
            url:"./getdata",
            //设置请求头参数
            headers:{id:'1'},
            //成功后的方法
            success:function(data){
                if(data == null){
                    alert("获取失败");
                    return;
                }
                //打印响应数据
                alert(JSON.stringify(data));
            }
        });
    </script>
</head>
<body>
</body>
</html>
新增一个controller,com.xiaoxie.controller.HeaderController
package com.xiaoxie.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("/header")
public class HeaderController {
    @GetMapping("/page")
    public String page(){
        return "header";
    }
    @PostMapping("/getdata")
    @ResponseBody
    public Map<String,Object> getData(@RequestHeader("id") String id){
        Map<String,Object> map = new HashMap<>();
        if("1".equals(id)){
            map.put("success",true);
            map.put("message","验证成功!");
            return map;
        } else {
            return null;
        }
    }
}
启动Spring Boot 访问:http://localhost:8080/header/page
这个时候页面会弹框,弹框中提示内容如下 :
{"success":true,"message":"验证成功!"}
从这个弹框提示可以看出我们对请求头中的信息已经正常获取到了。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值