通过上篇文章,我们学习了spring mvc的一些常用知识,比如定义一个Controller、处理请求、获取请求参数、获取请求头参数、获取Cookie数据、获取Request、Session作用域数据、使用提前初始化数据、视图控制器实习无逻辑页面跳转、重定向与请求转发、放行静态资源等。那么这一章我们来进一步学习spring mvc的进阶知识点。

进阶知识

高级请求映射

URI变量占位符

URI模式匹配允许我们在URI中添加占位符,进而传递参数,用法:

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable("petId") Long petId) {
    // ...
}

还可以用在类级别的@RequestMapping URI上,这样所有handler方法都可以获得这个参数:

@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

    @GetMapping("/pets/{petId}")
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}

URI通配符

SpringMVC在URI中出现以下几种通配符,如:

  • ?: 匹配一个字符(不能匹配零个字符)
  • *: 匹配零个或多个字符
  • **:匹配零个或多个路径段(如: /test/query/test/query/2)

示例代码:

//@RequestMapping("/test/pattern/*")    //可以匹配: /test/pattern/ /test/pattern/aa /test/pattern/bb ...  不能匹配:/test/pattern /test/pattern/aa/bb
//@RequestMapping("/test/pattern/?")  //可以匹配:/test/pattern/a /test/pattern/b ... 不能匹配:/test/pattern/ /test/pattern/aa ...
@RequestMapping("/test/pattern/**")  //可以匹配:/test/pattern /test/pattern/ /test/pattern/aa /test/pattern/aa/bb ...
public String testPattern() {
    System.out.println("testPattern");
    return "main";
}

注: 通配符的方式旨在通过模糊匹配,让方法能处理更多的请求URI,不支持将模糊匹配的部分作为参数注入到handler方法的参数中,如需获取请参见下面的正则表达式匹配

URI变量正则表达式匹配

除了变量占位符、通配符之外,SpringMVC还支持以正则的方式匹配URI,并且可以将正则匹配到的部分作为参数注入到handler方法参数中

语法: {varName:regex}

示例:
要匹配/spring-web-3.0.5.jar

@GetMapping("/test/reg_pattern/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") //匹配 /test/reg_pattern/spring-web-3.0.5.jar
//    @RequestMapping("/test/reg_pattern/{path:.*}")    //匹配 /test/reg_pattern/任意长度字符
public String testRegPattern(@PathVariable(name = "path", required = false) String path,
                             @PathVariable(name="name", required = false) String name,
                             @PathVariable(name="version", required = false) String version,
                             @PathVariable(name="ext", required = false) String ext) {
    System.out.println("testRegPattern, path: " + path);
    System.out.println("testRegPattern, name: " + name+", version: " + version+", ext: " + ext);
    return "main";
}

其他不常用的匹配

按照request mimeType匹配
@PostMapping(path = "/pets", consumes = "application/json") 
public void addPet(@RequestBody Pet pet) {
    // ...
}

只有请求是application/json类型,并且请求地址是/pets才能匹配到此方法处理

按照请求参数匹配
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") 
public void findPet(@PathVariable String petId) {
    // ...
}

只有请求参数中包含myParam这个参数,并且参数值等于myValue,并且请求地址也满足,才能匹配此方法处理

按照请求头参数匹配
@GetMapping(path = "/pets", headers = "myHeader=myValue") 
public void findPet(@PathVariable String petId) {
    // ...
}

只有请求头中包含myParam这个参数,并且参数值等于myValue,并且请求地址也满足,才能匹配此方法处理

文件上传

SpringMVC提供了一个MultipartResolver接口用于支持文件上传。 MultipartResolver有两个实现类分别基于apache file-upload包和Serlvet3.0 API方式的文件上传。他们是:

  • CommonsMultipartResolver (2.5)
  • StandardServletMultipartResolver (3.0)

2.5方式文件上传配置

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

支持"maxUploadSize", "maxInMemorySize" and "defaultEncoding"三个参数

注意: apache file-upload相关的依赖需要自己另外添加进来

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

3.0方式文件上传配置

在web.xml的DispatcherServlet的配置中添加一行:<multipart-config />

<servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/DispatcherServlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    <!-- 开启文件上传支持 -->
        <multipart-config />
    </servlet>
    <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

在beans中配置

<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver" />

文件上传主方法

无论是2.5还是3.0的方式在Controller中处理文件上传的方法是一样的,示例:

/**
 * 文件上传2.5和3.0版本
 *
 * @author bennett
 */

@Controller
public class TestFileUpload {

    /**
     * 文件上传2.5版本
     *
     * @param file
     * @return
     */
    @ResponseBody
    @PostMapping("/upload")
    public String upload(@RequestParam("myFile") MultipartFile file) {
        // 不建议使用file.isEmpty()判断是否有上传文件,因为在有些浏览器中没有上传文件,则文件大小是-1
        // 但是file.isEmpty()判断依据是file.getSize == 0,因此这种情况下使用getSize() > 0会更好
        if (file.getSize() > 0){
            return uploadFile(file);
        }
        return null;
    }

    /**
     * 文件上传3.0版本
     *
     * @param file
     * @return
     */
    @ResponseBody
    @PostMapping("/upload3")
    public String upload3(@RequestParam("myFile") MultipartFile file) {
        if (file.getSize() > 0){
            return uploadFile(file);
        }
        return null;
    }

    /**
     * 文件上传实现的主方法
     *
     * @param file
     * @return
     */
    private String uploadFile(MultipartFile file) {
        // 文件存放路径
        String filePath = "D:/";
        /* 也可以使用项目的根路径下的upload文件夹(方法参数加上HttpServletRequest request)
        // String filePath = request.getServletContext().getRealPath("/upload/");
        // 没有则创建文件夹
        File fileDir = new File(filePath);
        if (!fileDir.exists()) {
            fileDir.mkdirs();
        } */
        // 文件的原始名,如:xxx.jpg
        String fileName = file.getOriginalFilename();
        // 截出文件的类型
        String fileType = fileName.substring(fileName.lastIndexOf("."));
        // 获取当前的日期
        String dateTime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        // 文件名后拼接上当前日期
        String fileSupplementName = "_" + dateTime;
        fileName = fileName.substring(0, fileName.lastIndexOf(".")) + fileSupplementName;
        // 全新的文件名
        fileName = fileName + fileType;
        try {
            file.transferTo(new File(filePath + fileName));
            return "上传成功";
        } catch (IOException e) {
            e.printStackTrace();
            return "上传失败!";
        }
    }
}

消息转换器(MessageConverter)

处理响应内容乱码

我们可以通过配置一个专门处理返回内容是文本信息的MessageConverter

<mvc:annotation-driven>
        <mvc:message-converters>
            <!-- 处理响应中文内容乱码 -->
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="defaultCharset" value="UTF-8" />
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html</value>
                        <value>application/json</value>
                    </list>
                </property>
            </bean>
        </mvc:message-converters>
</mvc:annotation-driven>

返回JSON数据

使用Jackson

SpringMVC默认使用Jackson将我们返回的对象类型转换成JSON格式数据,我们只需要做好以下两步:

1、添加Jackson依赖

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

2、在dispatcher-servlet.xml中开启MVC注解支持

<!-- <mvc:annotation-driven /> -->
<!-- 如果不返回带有日期类型的json字符串,那么直接使用上面的开启注解支持就够了,但是如果有日期类型的就需要添加一个消息转换器来处理日期类型的格式,否则直接使用日期对应的毫秒数 -->
<mvc:annotation-driven>
	<mvc:message-converters>
    	<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="objectMapper">
                    <bean class="com.fasterxml.jackson.databind.ObjectMapper">
                        <property name="dateFormat">
                            <bean class="java.text.SimpleDateFormat">
                                <constructor-arg type="java.lang.String" value="yyyy-MM-dd:HH:mm:ss"/>
                            </bean>
                        </property>
                    </bean>
                </property>
            </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

使用FastJSON

除了使用默认的Jackson来处理JSON,我们还可以用其他的JSON库,比如国内的FastJSON:

  1. 添加FastJSON依赖:
<dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>fastjson</artifactId>
     <version>1.2.57</version>
</dependency>
  1. 在dispatcher-servlet.xml中配置消息转换器:
<mvc:message-converters>
    <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
        <property name="defaultCharset" value="UTF-8" />
        <property name="fastJsonConfig">
            <!-- 设置转换JSON的参数 -->
            <bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
                <property name="dateFormat" value="yyyy-MM-dd HH:mm:ss" />
            </bean>
        </property>
        <property name="supportedMediaTypes">
        <!-- 指定转换完JSON后返回的响应头和编码,添加text/html是为了处理在IE下application/json会弹出下载框问题 -->
            <list>
                <!--<value>text/html;charset=UTF-8</value>-->                
                <value>application/json;charset=UTF-8</value>
            </list>
        </property>
    </bean>
</mvc:message-converters>

使用方法: 在Controller中可以通过直接在类上添加@RestController或者在方法上添加@ResponseBody注解, 然后返回任意Java类型即可(包括自定义类型)
示例:

@ResponseBody
@RequestMapping("/getStudent")
public Student getStudent(@RequestParam("id") Integer id) {
    return studentService.findStudentById(id);
}

提供RestFul服务

我们可以通过以下两种方式提供RestFul的服务

  • @RestController注解替换Controller类上的@Controller注解,这样该类里面所有的RequestMapping方法都会强制输出返回的内容本身给调用方
  • 在需要提供RestFul的RequestMapping方法上添加@ResonpseBody注解,这样会强制此方法输出返回的内容本身给调用方。

此外,SpringMVC还给我们提供了专用的RestFul相关注解GetMappingPostMappingPutMappingDeleteMappingPatchMapping用于简化@RequestMapping注解, 比如@GetMapping注解就相当于@RequestMapping(method=RequestMethod.GET)

拦截器

配置方法

下面给出配置的示例,具体根据自己的要求配置拦截路径

<!-- 拦截器 -->
<mvc:interceptors>
    <!-- 通过<bean>的形式添加interceptor,默认拦截所有请求 -->
    <!-- <bean class="com.ssm.sms.interceptor.VerifyInterceptor"/> -->
    
    <!-- 通过<mvc:interceptor>配置,可以进一步拦截哪些路径和不拦截哪些路径 -->
    <mvc:interceptor>
        <!-- 拦截以/message开头的请求 -->
        <mvc:mapping path="/message/**"/>
        <!-- 不拦截以/test开头的请求 -->
        <mvc:exclude-mapping path="/test" />
        <bean class="com.ssm.sms.interceptor.VerifyInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

拦截时机

  • preHandler:执行handler之前。权力比较大,和Spring AOP中的环绕通知很相似,都是可以影响到后续方法的执行。

  • postHandler:执行handler之后。权力比较小,和Spring AOP中的After通知相似,只能获取到一些信息,但不能对后续的进行造成影响。

  • afterCompletion:请求完成,返回客户端之前。和postHandler一样的功能,区别在于比postHandler的时机更晚,但是同样不能影响后续操作,只能记录一些信息。

拦截器示例

下面给出拦截器的示例:

/**
 * 校验用户的状态和消息返回的错误信息
 *
 * @author bennett
 */

@Slf4j
public class VerifyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        User user = (User) request.getSession().getAttribute("user");

        // 用户的session信息是否过期
        if (user == null){
            response.setContentType("text/html");
            response.getWriter().println("<script>alert('对不起,你的登录过期,请重新登录!');window.location.href='/login'</script>");
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("after");
    }

}