SpringMVC进阶使用


Web MVC相关

根据官网文档介绍总结

BindingResult

BindingResult作为控制层方法的参数时,它保存了该方法BindingResult之前的参数绑定的结果,和HttpServletRequest对象一样,来自SpringMVC“内部”,暂且称之为“内部参数”,其它需要通过请求参数去转换成java bean的参数称之为“外来参数”。

例:有5个mvc处理器(控制层的方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Controller
public class xxxController() {

@RequestMapping("/url0")
public xxx method0(User user,Dept dept) {
// ...
}

@RequestMapping("/url1")
public xxx bindMethod1(User user,BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String , String> map = new HashMap<>();
// 遍历获取检验不通过的属性名以及抛出对应异常的message
bindingResult.getFieldErrors().forEach( (item) -> {
String message = item.getDefaultMessage();
String field = item.getField();
map.put( field , message );
});
}
// ...
}
@RequestMapping("/url3")
public xxx bindMethod2(User user,Dept dept,BindingResult bindingResult) {
// ...
}
@RequestMapping("/url3")
public xxx bindMethod3(User user,BindingResult bindingResult,Dept dept) {
// ...
}

@RequestMapping("/url4")
public xxx integerMethod5(User user,Integer size) {
// ...
}
}

上面的代码中,method0都熟悉了,前端参数直接绑定,找不到的参数则直接为默认值,而当参数转换错误时,即前端传入了非数字的字符串, 而后端要转换成Integer类型,如{id : 123abc}要转换到User对象的id属性,此时mvc会抛出绑定参数相关的异常(BindException ,该异常可获取到BindingResult对象),此时前端便会收到400状态码的应答。

而在bindMethod1中,BindingResult对像则保存了mvc绑定user对象的结果:有无错误、错误的属性field和错误信息message等;因为处理器显式声明了BindingResult对像作为形参,所以上述由绑定异常导致的400错误不会发生,继续进入处理器方法执行业务。

  • 在bindMethod2和bindMethod3中,处理器需要绑定2个bean到实参,经验证执行过程如下:
  1. 从左到右先绑定User,若绑定失败则抛出参数绑定相关的异常

  2. 绑定User通过,绑定Dept失败,进入控制层方法,BindingResult保存id绑定结果

  3. 绑定都通过,进入控制层方法,BindingResult保存Dept绑定结果

    故而得出:BindingResult保存的绑定结果是处理器(控制层的方法)最后一个bean参数的绑定结果【一对一绑定】

  • 在integerMethod5中,有一个普通数据类型的形参,如果对其转换失败时,mvc则直接抛出方法参数类型匹配异常(MethodArgumentTypeMismatchException),注意不是绑定异常。

稍加思考不难得出,mvc处理器参数处理的简单流程:

mvc绑定流程

当我们需要关心客户端传入bean参数的对错时,可以在处理器上使用BindingResult对象,不关心则不需要,直接让mvc自动返回400错误。一般情况下,很少使用BindingResult对象,BindingResult更多的是在参数绑定相关异常中作为绑定结果信息的载体,通过BindingResult先打一下mvc的基础。

@ControllerAdvice

顾名思义,控制器增强,原理是AOP,结合控制层能干的事和AOP方可理解其作用,不多BB。

1
2
3
4
@ControllerAdvice(
basePackageClasses = {xxxController.class})// 只切入指定的类
public class ExceptionHandlerAdvice {
}

@InitBinder参数绑定

PropertyEditor

先了解这个,PropertyEditor的使用和原理

@InitBinder可以实现参数绑定,将String类型的数据转换成对应的java对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
@Controller
public class InitBinderController {

@RequestMapping("/initBinder/date")
@ResponseBody
public String test(Date date) {

String str;
if (date == null) {

str = "date is null";
}else {

str = date.toString();
}
log.info(str);
return str;
}

@RequestMapping("/initBinder/bigDecimal")
@ResponseBody
public String test2(@RequestParam("bigDecimal") BigDecimal number) {

String str;
if (number == null) {

str = "number is null";
}else {

str = number.toString();
}
log.info(str);
return str;
}

@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
// 注册自定义的 大小数转换器
binder.registerCustomEditor(BigDecimal.class,new BigDecimalEditor());
}

WebDataBinder实现了PropertyEditorRegistry接口,所以只需要注册CustomDateEditor解析器和自定义的大小数转换器(下见代码)就可以了。

自定义参数转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @author WindShadow
* @version 2020/9/20.
* 大小数转换器
*/
public class BigDecimalEditor extends PropertyEditorSupport {

@Override
public void setAsText(String text) throws IllegalArgumentException {

BigDecimal bigDecimal;
try {
bigDecimal = BigDecimal.valueOf(Double.valueOf(text));
}catch (Exception e) {

throw new IllegalArgumentException(e);
}
setValue(bigDecimal);
}
}

简而言之@InitBinder:控制层方法参数映射,String => Java Object(一对一)

@ControllerAdvice + @InitBinder = 被增强的Controller都拥有共同的数据绑定器

更详细的参数绑定原理的博客:https://www.cnblogs.com/yourbatman/p/11218694.html

@ExceptionHandler异常处理

几种异常处理方式

先大致了解一下百度上的3中异常处理方式

可得:大致有以下几种异常处理方式

  1. 手动try-catch,自己返回对应视图或数据【原始操作,颗粒细,代码臃肿,分支多】
  2. Controller + @ExceptionHandler【只能在当前控制器中处理异常】
  3. BaseController + @ExceptionHandler + 继承【类似 2,但因为继承,具有统一异常处理能力】
  4. 实现 HandlerExceptionResolver 接口【可实现统一异常处理】
  5. @ControllerAdvice + @ExceptionHandler 【AOP原理,可实现统一异常处理】

@ExceptionHandler支持的参数和返回值类型

意思你在@ExceptionHandler的异常处理方法下的操作,类似在@RequestMapping方法下的操作,但是仅限于文档上指明的方法参数类型和返回值。和@RequestMapping一样,返回String依旧默认是视图名称,方法加上@ResponseBody就是json形式返回。

注意:方法参数为map等具有往视图添加数据的类型时,方法被调用时会报错

优先级

既然在SpringMVC中有多种处理异常的方式,那么就存在一个优先级的问题:

当发生异常的时候,SpringMVC会如下处理:

(1)SpringMVC会先从配置文件找异常解析器HandlerExceptionResolver

(2)如果找到了异常异常解析器,那么接下来就会判断该异常解析器能否处理当前发生的异常

(3)如果可以处理的话,那么就进行处理,然后给前台返回对应的异常视图

(4)如果没有找到对应的异常解析器或者是找到的异常解析器不能处理当前的异常的时候,就看当前的Controller中有没有提供对应的异常处理器,如果提供了就由Controller自己进行处理并返回对应的视图

(5)如果配置文件里面没有定义对应的异常解析器,而当前Controller中也没有定义的话,就看有没有全局ControllerAdvice提供的全局异常处理器,如果没有那么该异常就会被抛出来

@Valid数据校验

前提了解:JSR303数据校验

JSR303注解小细节

设定一个pojo,下面的字段中,如果前端没有传过来email字段,也就是得到User对象的email属性为null,这是允许的,等同于【选填,格式必须为邮箱】,如果在@Email上再加入@NotNull,即叠加校验,等同于【必填,格式必须为邮箱】,叠加校验校验顺序从上往下

1
2
3
4
5
6
7
8
9
10
11
12
13
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
private Long id;
@NotNull // 该字段不能为空
private String name;
private String address;
// @NotNull // 注解可叠加使用
@Email // 该字段必须是邮箱格式
private String email;
}

控制层写法

参数 + BindingResult对象

1
2
3
4
5
6
7
8
9
10
11
12
13
 @RequestMapping("/url")
public xxx validTest1(@Valid User user,BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String , String> map = new HashMap<>();
// 遍历获取检验不通过的属性名以及抛出对应异常的message
bindingResult.getFieldErrors().forEach( (item) -> {
String message = item.getDefaultMessage();
String field = item.getField();
map.put( field , message );
});
}
// …
}

@Valid 开启校验功能,紧跟在校验的Bean后添加一个BindingResult,BindingResult会封装前面Bean的校验结果,可见参数校验结果是被视为 参数校验结果的子集。这种处理器的写法缺点很明显,每个需要参数校验的地方都用 BindingResult 获取校验结果。冷门操作。

结合@ExceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 @RequestMapping("/url")
public xxx validTest1(@Valid User user) {
// 正常业务...
}

@ResponseStatus(value = HttpStatus.BAD_GATEWAY,reason = "bind异常")// 以此状态码作为最终响应状态码,若不配置,处理完异常后以正常的200响应
@ExceptionHandler(value = {
MethodArgumentNotValidException.class,// @RequestBody 参数映射验证失败抛出此类型异常
BindException.class,// get、post等参数自动绑定到bean的请求方式时,验证失败抛出此类型异常
ConstraintViolationException.class// @RequestParam 参数映射验证失败抛出此类型异常
})
public xxx validErrEx(Exception e) {
if (e instanceof MethodArgumentNotValidException) {
log.info("MethodArgumentNotValidException");
BindingResult bindingResult = ((MethodArgumentNotValidException)e).getBindingResult();
// ...
} else if (e instanceof BindException) {
log.info("BindException");
BindingResult bindingResult = ((BindException)e).getBindingResult();
// ...
}else if (e instanceof ConstraintViolationException) {
log.info("ConstraintViolationException");
Set<ConstraintViolation<?>> constraintViolationSet =((ConstraintViolationException)e).getConstraintViolations();
// ...
}else {
// @ExceptionHandler注解限定了异常类型,此句不可能被执行到
throw new RuntimeException("未知异常");
}
// ...
}

数据校验不起作用的写法

1
2
3
4
5
6
7
8
9
10
 @RequestMapping("/url")
public xxx validTest1(@Valid User user, @Email String emaill) {

// ...
}
@RequestMapping("/url")
public xxx validTest2(@Valid User user, @Valid @Email String email) {

// ...
}

直接验证基本数据类型是无效的;可以这样理解:@Valid作用于bean上,表示对此bean开启JSR303数据检验,bean中所有被JSR303注解作用的属性都会被检验。

多参数验证顺序

1
2
3
4
5
@RequestMapping("/url")
public xxx validTest1(@Valid User user, @Valid Dept dept) {

// ...
}

和参数绑定一样,验证顺序从左到右,由此可见参数校验属于参数绑定的子过程

@Validated数据校验

@Valid是java的注解(javax.validation包下),@Validated是Spring的注解,Spring对@Validated的功能支持包含了@Valid的支持,即在Spring环境中,@Valid能干的@Validated都能干,所以在控制层的数据校验时,也可用@Validated代替@Valid。另外Spring对@Validated提供了分组校验

1
2
3
public @interface Validated {
Class<?>[] value() default {};
}

分组校验

场景:User bean在新增操作时,id要为空,更新操作时id不能为空。@Valid没有分组功能,@Validated拥有分组功能。

建立分组类

1
2
3
4
5
public class Group {
public interface Insert {}
public interface Update {}
}

User bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
// 校验时分组
@Null(groups = {Group.Insert.class})
@NotNull(groups = {Group.Insert.class})
private Long id;
private String name;
private Integer age;
private Double money;
private String gender;
@Email
private String email;
}

控制层写法

1
2
3
4
@RequestMapping("/url")
public xxx addUser(@Validated(Group.Insert.class) User user) {
// 正常业务...
}

自定义校验注解

实现类似上述@Email @Null的检验注解,以行数据逻辑删除属性为例

行数据类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RowData {

public static final Integer DELETED = 1;
public static final Integer NOT_DELETED = 0;
@NotNull
@MyDeletedCodeValidated
private Integer deleted;

public Integer getDeleted() {
return deleted;
}

public void setDeleted(Integer deleted) {
this.deleted = deleted;
}

@Override
public String toString() {
return "RowData{" +
"deleted=" + deleted +
'}';
}
}

校验注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({FIELD, ANNOTATION_TYPE, PARAMETER})
/**
* 表示处理的这个注解的类是哪一个,可以是多个
*/
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyDeletedCodeValidated {

String message() default "";
/**
* 下面的这俩是必须的
*/
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

处理检验注解的类,必须实现ConstraintValidator接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyConstraintValidator implements ConstraintValidator<MyDeletedCodeValidated,Integer> {

private String message;

@Override
public void initialize(MyDeletedCodeValidated constraintAnnotation) {

this.message = constraintAnnotation.message();
}

@Override
public boolean isValid(Integer code, ConstraintValidatorContext context) {

if (!RowData.DELETED.equals(code) && !RowData.NOT_DELETED.equals(code)) {

// 禁用默认的消息模板
context.disableDefaultConstraintViolation();
// 设置自己的消息模板
context.buildConstraintViolationWithTemplate("".equals(message) ? "删除代号只能为0或1" : message)
.addConstraintViolation();
//不合法,不通过
return false;
}else {
return true;
}
}
}

控制层直接使用

1
2
3
4
5
6
  @RequestMapping("/rowData")
@ResponseBody
public String rowDataValidated(@Validated RowData rowData) {

return rowData == null ? "null" : rowData.toString();
}

自定义校验器

定义一个Gender类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Gender {

public static final int MAN = 1;
public static final int WOMAN = 0;

private Integer code;

public Gender() {
}

public Gender(Integer code) {
this.code = code;
}

public Integer getCode() {
return code;
}

public void setCode(Integer code) {
this.code = code;
}
}

实现对应的校验器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 对{@link Gender}类的校验器
* @author WindShadow
* @version 2021-10-21.
*/
public class GenderValidator implements Validator {

@Override
public boolean supports(Class<?> clazz) {
return Gender.class.equals(clazz);
}

@Override
public void validate(Object target, Errors errors) {

ValidationUtils.rejectIfEmpty(errors,"code","code is empty");
Gender gender = (Gender) target;
if (Gender.MAN != gender.getCode() && Gender.WOMAN != gender.getCode()) {

errors.reject(String.format("性别代码只能为%s = 男,%s = 女",1,0));
}
}
}

控制层注册

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping("/gender")
@ResponseBody
public String genderValidated(@Validated @RequestBody Gender gender) {

return gender == null ? "null" : gender.toString();
}
@InitBinder("gender")
public void initBinder(WebDataBinder binder) {

binder.addValidators(new GenderValidator());
}

@MatrixVariable 矩阵变量

RFC3986定义了在URI中包含name-value的规范 。

使用@MatrixVariable可以方便的进行多条件组合查询

开启矩阵变量功能

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 矩阵变量配置,关键类UrlPathHelper
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {

UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);// 必须 false
configurer.setUrlPathHelper(urlPathHelper);
}
}

xml中

1
<mvc:annotation-driven enable-matrix-variables="true"/>

官方例子:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#mvc-ann-matrix-variables

  1. handler入参为Map<String,String> 和 Map<String,List

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Controller
    public class MatrixController {

    // /matrix/user/userls;id=100;name=ls;age=18;address=nj/com/alibaba;id=1001;address=hz;dept=dept001,dept002
    // /matrix/user/userls;id=100;name=ls;age=18;address=nj/com/alibaba;id=1001;address=hz;dept=dept001;dept=dept002
    @RequestMapping("/matrix/user/{user}/com/{com}")
    @ResponseBody
    public String matrix1(@PathVariable("user") String user, @PathVariable("com") String com,
    @MatrixVariable(pathVar = "user") Map<String,String> userInfo,
    @MatrixVariable(pathVar = "com") Map<String,List<String>> comInfo) {

    return user + "\n" + com + "\n" + JSON.toJSONString(userInfo) + "\n" + JSON.toJSONString(comInfo);
    }

    返回结果:

    1
    2
    3
    4
    userls
    alibaba
    {"id":"100","name":"ls","age":"18","address":"nj"}
    {"id":["1001"],"address":["hz"],"dept":["dept001","dept002"]}
  2. handler入参为普通简单类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Controller
    public class MatrixController {
    // /matrix2/user/userls;id=100;name=ls;age=18;address=nj
    @RequestMapping("/matrix2/user/{user}")
    @ResponseBody
    public String matrix2(
    @MatrixVariable(pathVar = "user",name = "name") String name,
    @MatrixVariable(pathVar = "user",name = "age") Integer age,
    @MatrixVariable(pathVar = "user",name = "id") Integer id,
    @MatrixVariable(pathVar = "user",name = "address") String address) {
    return name + "\n" + age + "\n" + id + "\n" + address;
    }
    }

    返回结果:

    1
    2
    3
    4
    ls
    18
    100
    nj
  3. URL + ? + 参数

    对比 2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Controller
    public class MatrixController {
    // /matrix2/user/userls;id=100;name=ls;age=18;address=nj?car=lbjn
    @RequestMapping("/matrix3/user/{user}")
    @ResponseBody
    public String matrix3(
    @MatrixVariable(pathVar = "user",name = "name") String name,
    @MatrixVariable(pathVar = "user",name = "age") Integer age,
    @MatrixVariable(pathVar = "user",name = "id") Integer id,
    @MatrixVariable(pathVar = "user",name = "address") String address,
    @RequestParam String car) {
    return name + "\n" + age + "\n" + id + "\n" + address + "\n" + car;
    }
    }

    返回结果:

    1
    2
    3
    4
    5
    ls
    18
    100
    nj
    lbjn

注意点:

  1. 显然的,如url = /matrix/user/userls;id=100;name=ls;age=18;address=nj/com/alibaba;id=1001;address=hz;dept=dept001,dept002。分号开始的地方当作一个矩阵,遇到一些非矩阵符号(如 / 或 ? ,它们是正常url里有特别意义的字符)时,矩阵视为结束。
  2. URL格式
    • dept=dp01;dept=dp02 等价于 dept=dp01,dp02
    • /matrix3/user/userls;?car=lbjn 此种格式绑定到 @PathVariable(“user”) String user 参数时,user值为 userls;
    • /matrix3/user/userls;age=18;?car=lbjn/matrix3/user/userls;age=18?car=lbjn 这两种格式的差别在 ? 之前的分号,但是他们都能正常绑定到参数。即 age 值为 18 而不是 18;
  3. 根据 1 2可知,如url = /matrix3/user/userls;?car=lbjn处理器有@PathVariable(“user”) String user。矩阵user并不被认为存在,所以矩阵参数开始的字符是分号 。这一点不注意可能会被误导。
  4. @MatrixVariable(pathVar = “user”,name = “name”)中,显然 pathVar 代表参数所在的矩阵,只有一个矩阵时也可省略
  5. @MatrixVariable(required = false)表示此参数可以不存在,即required属性控制参数是否必填
  6. @MatrixVariable(defaultValue = “value”),通过defaultValue属性表示缺省值
  7. 使用@MatrixVariable后,传入分号作为参数值时需要转义编码啥的,具体转义规则啥的看RFC3986来吧,目前百度上也没看到有个完美可行的说法,可能大多数人矩阵变量用的不是也别多,个人感觉挺方便的

@ModelAttribute

@ModelAttribute注解用于将方法的参数或方法的返回值绑定到指定的模型属性上,并返回给Web视图。被@ModelAttribute注解注释的方法会在此controller每个handler方法(处理器)执行前被执行,因此对于一个controller映射多个URL的用法来说,要谨慎使用。

  1. 例A:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Controller
    public class ModelAttributeController {

    @ModelAttribute
    public void modelVoid(Model model) {// 返回值为void时,入参没有 Model类型会报错

    model.addAttribute("void","voidString");
    }
    @ModelAttribute("@ModelAttributeKey")
    public String model() {

    return "@ModelAttributeValue";
    }
    @RequestMapping("/modelAttribute")
    public String show(Model model) {

    log.info("modelData: {}", JSON.toJSON(model.asMap()));
    return "show";
    }
    }

    打印结果:

    1
    modelData: {"@ModelAttributeKey":"@ModelAttributeValue","void":"voidString"}
  2. 例B

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Controller
    public class ModelAttributeController {

    @ModelAttribute
    public void modelVoid(Model model) {// 返回值为void时,入参没有 Model类型会报错

    model.addAttribute("void","voidString");
    }
    @RequestMapping(value = "/modelAttribute3")
    public String show3(@ModelAttribute("void") String voidString) {

    log.info("voidString: {}",voidString);
    return "show";
    }
    }

    打印结果:

    1
    voidString: voidString
  3. 例C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Controller
    public class ModelAttributeController {

    // 此时 user 来自前端 User必须有无参构造
    @RequestMapping(value = "/addUser")
    public String show3(@ModelAttribute("void") User user) {

    log.info("user: {}",user);
    return "show";
    }
    }
  4. 例D

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Controller
    public class ModelAttributeController {
    // 此时,@RequestMapping内的value表示视图名称,即 show
    // 请求此url后,后端将渲染 show 的视图页面并返回,该视图的 model有键值对 attributeName=hi
    @RequestMapping(value = "/show")
    @ModelAttribute("attributeName")
    public String show() {
    return "hi";
    }
    }

@RequestAttribute

参照@ModelAttribute的使用,只不过数据放的域不同,此时数据存放于request域,即 request.setAttribute()

@SessionAttribute

参照@ModelAttribute的使用,只不过数据放的域不同,此时数据存放于session域,即 session.setAttribute()