springmvc4 (2) 高级操作

274 阅读7分钟

1. 参数转换

概念: 处理器适配器中可配置自定义类型转换器,用于将请求参数类型转成自定义类型:

  • 开发转换器类:实现 o.s.c.c.c.Converter<A, B> 接口并重写 convert()
    • 该转换器仅在动作方法使用B类型变量接收A类型请求参数时生效。
  • 配置转换器类:主配文件中管理 o.s.f.s.FormattingConversionServiceFactoryBean
    • 注入 converters:使用 <set> + <bean> 注入1或N个转换器类全名。
    • 使用 <mvc:annotation-driven />conversion-service 引用转换器类。
  • 使用转换注解:主配文件中管理 o.s.f.s.FormattingConversionServiceFactoryBean
    • 无需注入 converters,否则转换注解失效,仍使用转换器类方式。
    • 使用 @DateTimeFormat(pattern="") 标记动作方法参数或实体类属性。
    • 异常信息存放在 BindingResult 参数中,其位置必须紧跟爆发异常的那个实体类参数。

源码: /springmvc4/

  • res: spring/springmvc.xml
    <mvc:annotation-driven />
    <bean  id="converter-list"class="org.springframework.format.support.FormattingConversionServiceFactoryBean"/>

    <!--配置自定义转换器类-->
    <!--
    <mvc:annotation-driven conversion-service="converter-list"/>

    <bean id="converter-list" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="com.yap.converter.StringToDateConverter"/>
            </set>
        </property>
    </bean>
    -->
  • src: c.y.converter.StringToDateConverter
package com.yap.converter;

import org.springframework.core.convert.converter.Converter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author yap
 */
public class StringToDateConverter implements Converter<String, Date> {

    @Override
    public Date convert(String source) {
        System.out.println("convert()..." + source);
        Date date = null;
        try {
            date = new SimpleDateFormat("yyyy-MM-dd").parse(source);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}
  • src: c.y.controller.ConverterController
package com.yap.controller;

import com.yap.pojo.Student;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Date;

/**
 * @author yap
 */
@RequestMapping("/api/converter")
@Controller
public class ConverterController {

    @ResponseBody
    @RequestMapping("string-to-date")
    public String stringToDate(Date date, String name) {
        System.out.println(date);
        System.out.println(name);
        return "success";
    }

    @ResponseBody
    @RequestMapping("date-time-format")
    public String dateTimeFormat(Student student, BindingResult result) {

        if (result.hasErrors()) {
            System.out.println("爆发了" + result.getErrorCount() + "个异常!");
            for (ObjectError e : result.getAllErrors()) {
                System.out.println("爆发异常的对象是:" + e.getObjectName());
                System.out.println("具体异常的内容为:" + e.getDefaultMessage());
            }
        }
        System.out.println(student);
        return "success";
    }

}
  • src: c.y.pojo.Student
package com.yap.pojo;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;

import java.io.Serializable;
import java.util.Date;

/**
 * @author yap
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {

    @JsonIgnore
    private static final long serialVersionUID = 1L;

    @JsonProperty("primary-key")
    private Integer id;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String name;
    
	<!--DateTimeFormat注解测试转换器类-->
    @DateTimeFormat(pattern="MM-dd yyyy")
    @JsonFormat(pattern="yyyy-MM-dd hh:mm:ss", locale="zh", timezone="UTC")
    private Date birthday;
}
  • psm: api/converter/string-to-date
  • psm: api/converter/date-time-format

2. 参数校验

概念: HibernateValidator是JSR303的拓展校验工具,用于校验请求参数是否满足指定格式:

  • 添加依赖:hibernate-validator
  • 主配中添加 <mvc:annotation-driven /> 以驱动 o.s.v.b.LocalValidatorFactoryBean 校验类。
  • 开发实体类:为属性标记HibernateValidator校验注解,多注解优先级从上到下:
    • @Null/@NotNull:属性必须为/不为null。
    • @AssertTrue/@AssertFalse:属性必须为true/false。
    • @Min(18)/@Max(18):属性必须是最小/大为18的整数。
    • @DecimalMin(3.5)/@DecimalMax(3.5):属性必须是最小/大为3.5的浮点数。
    • @Size(2, 9):属性的必须是2到9之间的一个数。
    • @Past/@Future:属性必须是一个过去/将来的日期。
    • @Pattern(value):属性必须符合指定的正则表达式。
    • @Email:属性必须是合法的电子邮箱地址,JSR303拓展注解。
    • @Length:字符串属性的长度必须在指定的范围内,JSR303拓展注解。
    • @NotEmpty:字符串/集合/数组属性必须不为null且长度不为0,JSR303拓展注解。
    • @Range:属性值必须在指定的范围内,JSR303拓展注解。
  • 开发动作类:对动作方法实体类参数标记 @Valid 以对其启用HibernateValidator校验。
    • 异常信息存放在 BindingResult 参数中,其位置必须紧跟爆发异常的那个实体类参数。
  • psm: api/valid/hibernate-validator 源码: /springmvc4/
  • res: pom.xml
    <!--hibernate-validator-->
    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.0.13.Final</version>
    </dependency>

  • src: c.y.pojo.Teacher
package com.yap.pojo;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.*;
import java.io.Serializable;
import java.util.Date;
import java.util.List;

/**
 * @author yap
 */
@Data
public class Teacher implements Serializable {

    @Null
    private Integer id;

    @NotNull
    @Email
    private String email;

    @NotNull
    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birth;


    @NotEmpty
    private List list;

    @Length
    private String a ;
} 
  • src: c.y.controller.HibernateValidatorController
package com.yap.controller;

import com.yap.pojo.Teacher;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.validation.Valid;

/**
 * @Author Yap
 */
@RequestMapping("/api/valid/")
@Controller
public class HibernateValidatorController {

    @ResponseBody
    @RequestMapping("hibernate-validator")
    public String hibernateValid(@Valid Teacher teacher, BindingResult result) {

        if (result.hasErrors()) {
            System.out.println("爆发了" + result.getErrorCount() + "个异常!");
            for (ObjectError e : result.getAllErrors()) {
                System.out.println("爆发异常的对象是:" + e.getObjectName());
                System.out.println("具体异常的内容为:" + e.getDefaultMessage());
            }
        }
        System.out.println(teacher);
        return "ok";
    }
}

3. 文件上传

流程: 添加依赖 commons-fileupload

  • 主配中管理 o.s.w.m.c.CommonsMultipartResolverid 固定为 multipartResolver
    • 注入 maxUploadSize 以配置上传文件大小限制,单位字节,默认-1无限制。
    • 注入 maxInMemorySize 以配置内存最大值使用容量,单位字节。
    • 注入 defaultEncoding 以配置文件编码。
  • 动作方法使用 @RequestParam("file") MultipartFile/MultipartFile[] 接收流文件:
    • file.getOriginalFilename():获取文件客户端真实名字。
    • file.transferTo(File desc):将文件对象上传到指定位置。
  • 视图层表单添加 enctype=multipart/form-data 属性并以POST方式提交。
  • psm: api/file/upload:在 Body/form data 中KEY框末尾选择 File 并POST方式提交。

源码: /springmvc4/

  • res: pom.xml
    <!--commons-fileupload-->
    <dependency>
      <groupId>commons-fileupload</groupId>
      <artifactId>commons-fileupload</artifactId>
      <version>1.3.3</version>
    </dependency>

  • res: spring/springmvc.xml
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize" value="1073741824"/>
        <property name="maxInMemorySize" value="40960"/>
        <property name="defaultEncoding" value="UTF-8"/>
    </bean>

  • src: c.y.controller.FileController
package com.yap.controller;

import org.apache.commons.io.FileUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.multipart.MultipartFile;

import javax.validation.constraints.NotNull;
import java.io.File;
import java.io.IOException;

/**
 * @author yap
 */
@Controller
@RequestMapping("/api/file")
public class FileController {

    @ResponseBody
    @RequestMapping("upload")
    public String upload(@NotNull @RequestParam("avatar") MultipartFile avatar) throws IOException {
        File desc = new File("F:\\upload" + System.currentTimeMillis() + avatar.getOriginalFilename());
        if (!desc.getParentFile().exists()) {
            System.out.println(desc.getParentFile().mkdirs());
        }
        avatar.transferTo(desc);
        return "success";
    }
}
  • web: view/upload.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
    <base href="/springmvc4/">
</head>
<body>
<section>
    <form action="api/file/upload" method="post" enctype="multipart/form-data">
        <label><input type="file" name="avatar"></label>
        <button type="submit">上传头像</button>
    </form>
</section>
</body>
</html>

4. 文件下载

流程: 文件下载动作方法需要 new ResponseEntity<byte[]>(字节数组,响应头,http状态码) 并返回:

  • FileUtils.readFileToByteArray(file):获取文件字节数组。
  • new HttpHeaders():获取响应头对象。
    • setContentDispositionFormData("attachment", filename):设置以附件形式下载文件及附件名。
    • setContentType(MediaType.APPLICATION_OCTET_STREAM):设置响应类型为8进制字节流。
  • HttpStatus.CREATED:http状态码201表示请求已成功,且创建了一个新资源并响应回客户端。
  • psm: api/file/download:在 Send 处选择 Send and Download源码: /springmvc4/
  • src: c.y.controller.FileController

    @RequestMapping("download")
    public ResponseEntity<byte[]> download(String file) throws IOException {
        byte[] bytes = FileUtils.readFileToByteArray(new File("F:\\upload\\" + file));
        HttpHeaders headers = new HttpHeaders();
        headers.setContentDispositionFormData("attachment", file);
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        return new ResponseEntity<>(bytes, headers, HttpStatus.CREATED);
    }

5. 拦截器类

概念: 拦截器用于拦截请求和响应,多个拦截器可组成拦截器链:

  • 开发拦截器类:实现 HandlerInterceptor 接口,并重写其中的三个方法:
    • preHandle():拦截请求,返回true表示放行,返回false会终止程序,请求结束。
    • postHandle():拦截响应,该方法可以对 ModelAndView 进行操作。
    • afterCompletion():渲染视图后执行。
  • 在主配中使用 <mvc:interceptors >/<mvc:interceptor > 配置拦截器类,默认拦截所有请求:
    • <mvc:mapping path=""> 用于指定拦截的请求。
    • <mvc:exclude-mapping path=""> 用于指定不拦截的请求。
    • 二者同时存在时,取交集。

源码: /springmvc4/

  • res: spring/springmvc.xml
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/api/interceptor/test"/>
            <bean class="com.yap.interceptor.InterceptorA"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/api/interceptor/test"/>
            <bean class="com.yap.interceptor.InterceptorB"/>
        </mvc:interceptor>
    </mvc:interceptors>
  • src: c.y.interceptor.InterceptorA
package com.yap.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

/**
 * @author yap
 */
public class InterceptorA implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        System.out.println("InterceptorA:preHandle()...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest req, HttpServletResponse resp,Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("InterceptorA:postHandle()...");
    }

    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) throws Exception {
        System.out.println("InterceptorA:afterCompletion()...");
    }
}
  • src: c.y.interceptor.InterceptorB
package com.yap.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

/**
 * @author yap
 */
public class InterceptorB implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
        System.out.println("InterceptorB:preHandle()...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest req, HttpServletResponse resp,Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("InterceptorB:postHandle()...");
    }

    @Override
    public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex) throws Exception {
        System.out.println("InterceptorB:afterCompletion()...");
    }
}
  • src: c.y.controller.InterceptorController
package com.yap.controller;

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

/**
 * @author yap
 */
@Controller
@RequestMapping("/api/interceptor")
public class InterceptorController {

    @ResponseBody
    @RequestMapping("test")
    public String test() {
        System.out.println("test()...");
        return "success";
    }
}

6. 异常处理

概念: HandlerExceptionResolver 是spring异常处理的顶级接口,其每个实现类都是种异常处理方式:

  • ExceptionHandlerExceptionResolver:使用注解方式捕获和处理异常:
    • 开发异常处理方法,有且仅有一个异常类型参数,不能设置其他参数。
    • 对方法标记 @ExceptionHandler 并使用 value 指定捕获的异常类,缺省表示捕获全部异常。
    • 该方法可以移动到 @ControllerAdvice 标记的类中以上升作用范围,该类需要被spring扫描。
  • ResponseStatusExcpetionResovler:使用注解方式自定义异常页面:
    • 开发异常页面方法,标记 @ResponseStatus 并使用 value/reason 设置http状态码/异常原因。
    • 该方法可以移动到 @ResponseStatus 标记的异常子类中以上升作用范围,该类需要被spring扫描。
  • SimpleMappingExceptionResolver:通过XML配置来处理异常:
    • 主配文件中管理 o.s.w.s.h.SimpleMappingExceptionResolver 类。
    • 注入 exceptionAttribute 以指定存储异常信息的变量名,默认 exception,存于请求域。
    • 注入 exceptionMappings 以指定捕获哪些异常及对应跳转页面,页面值使用视图解析器的前后缀。

源码: /springmvc4/

  • src: c.y.util.ExceptionUtil

package com.yap.util;

import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author yap
 */
@Component
@ControllerAdvice
public class ExceptionUtil {

	@ResponseBody
	@ExceptionHandler({ArithmeticException.class, ArrayIndexOutOfBoundsException.class})
	public String exceptionHandler(Exception e) {
		System.out.println("ExceptionUtil.exceptionHandler()..." + e);
		return "error";
	}
}
  • src: c.y.util.ExceptionUtil
package com.yap.util;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
 * @author yap
 */
@Component
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "MyNotFoundException: 页面走丢了!")
public class MyNotFoundException extends Exception {
}
  • src: c.y.controller.ExceptionController
package com.yap.controller;

import com.yap.util.MyNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
 * @author yap
 */
@RequestMapping("/api/exception")
@Controller
public class ExceptionController {

    @ResponseBody
    @RequestMapping("exception-handler-test")
    public String exceptionHandlerTest(Integer num) {
        System.out.println("test()...");
        if (num == 0) {
            throw new ArithmeticException();
        }
        if (num == 1) {
            throw new ArrayIndexOutOfBoundsException();
        }
        return "success";
    }

    @ResponseBody
    @ExceptionHandler(ArithmeticException.class)
    public String exceptionHandler(Exception e) {
        System.out.println("ExceptionController.exceptionHandler()..." + e);
        return "error";
    }

    @RequestMapping("response-status-test")
    public String responseStatusTest(Integer num) {
        System.out.println("responseStatusTest()...");
        return num == 0 ? "forward:response-status" : "success";
    }

    @RequestMapping("response-status")
    @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "ExceptionController: 页面走丢了!")
    public void responseStatus() {
        System.out.println("responseStatus()...");
    }

    @ResponseBody
    @RequestMapping("my-not-found-exception-test")
    public String myNotFoundExceptionTest(Integer num) throws MyNotFoundException {
        System.out.println("myNotFoundExceptionTest()...");
        if (num == 0) {
            throw new MyNotFoundException();
        }
        return "success";
    }

   <!--xml方式配置-->
    @ResponseBody
    @RequestMapping("xml-exception-test")
    public String xmlExceptionTest(Integer num) {
        System.out.println("xmlExceptionTest()...");
        if (num == 0) {
            throw new NullPointerException();
        }
        return "success";
    }
}
  • res: spring/springmvc.xml
 <!--XML配置-->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionAttribute" value="ex" />
        <property name="exceptionMappings">
            <props>
                <prop key="java.lang.NullPointerException">forward:/view/error.jsp</prop>
            </props>
        </property>
    </bean>
  • web: view/error.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<section>
    <p>异常信息: <%=request.getAttribute("ex")%>
</section>
</body>
</html>

7. 国际化属性

概念: ResourceBundleMessageSource 类用于在响应阶段加载和读取国际化属性文件:

  • 在classpath下创建属性文件 基名_语言_地区.properties
  • 在主配文件中管理 o.s.c.s.ResourceBundleMessageSourceid 固定为 messageSource
    • 注入 basenames 以设置属性文件基名。
    • 注入 defaultEncoding 以设置编码。
  • 在JSP中使用spring标签读取国际化文件:
    • <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
    • <spring:message code=""> 读取国际化属性文件中的key值。
  • cli: api/i18n/testF12 - 搜索 语言 - 将 英语(美国) 移到顶部。 源码: /springmvc4/
  • res: spring/springmvc.xml
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames" value="i18n"/>
        <property name="defaultEncoding" value="utf-8"/>
    </bean>

  • res: i18n_zh_CN.properties
title=请登录您的账号
username=账号
password=密码
submit=登录
  • res: i18n_en_US.properties
title=plz login your account!
username=username
password=password
submit=login
  • src: c.y.controller.I18nController
package com.yap.controller;

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

/**
 * @author yap
 */
@Controller
@RequestMapping("/api/i18n")
public class I18nController {

    @RequestMapping("test")
    public String test() {
        System.out.println("test()...");
        return "forward:/view/i18n.jsp";
    }
}
  • web: view/i18n.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--重点--%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1><spring:message code="title"/></h1>
<form action="">
    <label><spring:message code="username"/></label>
    <label><input type="text"/></label><br/>
    <label><spring:message code="password"/></label>
    <label><input type="password"/></label><br/>
    <button><spring:message code="submit"/></button>
</form>
</body>
</html>