前言
工作中发现一个定律,如果总是习惯别人帮忙做事的结果是自己不会做事了。一直以来,spring帮我解决了程序运行中的各种问题,我只要关心我的业务逻辑,设计好我的业务代码,返回正确的结果即可。直到遇到了400。
spring返回400的时候通常没有任何错误提示,当然也通常是参数不匹配。这在参数少的情况下还可以一眼看穿,但当参数很大是,排除参数也很麻烦,更何况,既然错误了,为什么指出来原因呢。好吧,springmvc把这个权力交给了用户自己。话不多说了,来一起看看详细的介绍吧。
springmvc异常处理
最开始的时候也想过自己拦截会出异常的method来进行异常处理,但显然不需要这么做。spring提供了内嵌的以及全局的异常处理方法,基本可以满足我的需求了。
1. 内嵌异常处理
如果只是这个controller的异常做单独处理,那么就适合绑定这个controller本身的异常。
具体做法是使用注解@ExceptionHandler.
在这个controller中添加一个方法,并添加上述注解,并指明要拦截的异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@RequestMapping (value = "saveOrUpdate" , method = RequestMethod.POST)
CodeMsg result = null ;
try {
result = orderService.saveOrUpdate(order);
} catch (Exception e) {
logger.error( "save failed." , e);
return this .renderString(response, CodeMsg.error(e.getMessage()));
}
return this .renderString(response, result);
}
@ResponseBody
@ResponseStatus (HttpStatus.BAD_REQUEST)
@ExceptionHandler (HttpMessageNotReadableException. class )
public CodeMsg messageNotReadable(HttpMessageNotReadableException exception, HttpServletResponse response){
LOGGER.error( "请求参数不匹配。" , exception);
return CodeMsg.error(exception.getMessage());
}
|
这里saveOrUpdate是我们想要拦截一样的请求,而messageNotReadable则是处理异常的代码。
@ExceptionHandler(HttpMessageNotReadableException.class)表示我要拦截何种异常。在这里,由于springmvc默认采用jackson作为json序列化工具,当反序列化失败的时候就会抛出HttpMessageNotReadableException异常。具体如下:
1
2
3
4
5
|
{
"code" : 1 ,
"msg" : "Could not read JSON: Failed to parse Date value '2017-03-' (format: \"yyyy-MM-dd HH:mm:ss\"): Unparseable date: \"2017-03-\" (through reference chain: com.test.modules.order.entity.Order[\"serveTime\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Failed to parse Date value '2017-03-' (format: \"yyyy-MM-dd HH:mm:ss\"): Unparseable date: \"2017-03-\" (through reference chain: com.test.modules.order.entity.Order[\"serveTime\"])" ,
"data" : ""
}
|
这是个典型的jackson反序列化失败异常,也是造成我遇见过的400原因最多的。通常是日期格式不对。
另外, @ResponseStatus(HttpStatus.BAD_REQUEST)
这个注解是为了标识这个方法返回值的HttpStatus code。我设置为400,当然也可以自定义成其他的。
2. 批量异常处理
看到大多数资料写的是全局异常处理,我觉得对我来说批量更合适些,因为我只是希望部分controller被拦截而不是全部。
springmvc提供了@ControllerAdvice来做批量拦截。
第一次看到注释这么少的源码,忍不住多读几遍。
1
|
Indicates the annotated class assists a "Controller" .
|
表示这个注解是服务于Controller的。
1
|
Serves as a specialization of { @link Component @Component }, allowing for implementation classes to be autodetected through classpath scanning.
|
用来当做特殊的Component注解,允许使用者扫描发现所有的classpath。
1
2
3
|
It is typically used to define { @link ExceptionHandler @ExceptionHandler },
* { @link InitBinder @InitBinder }, and { @link ModelAttribute @ModelAttribute }
* methods that apply to all { @link RequestMapping @RequestMapping } methods.
|
典型的应用是用来定义xxxx.
1
2
3
4
5
|
One of { @link #annotations()}, { @link #basePackageClasses()},
* { @link #basePackages()} or its alias { @link #value()}
* may be specified to define specific subsets of Controllers
* to assist. When multiple selectors are applied, OR logic is applied -
* meaning selected Controllers should match at least one selector.
|
这几个参数指定了扫描范围。
1
2
3
|
the default behavior (i.e. if used without any selector),
* the { @code @ControllerAdvice } annotated class will
* assist all known Controllers.
|
默认扫描所有的已知的的Controllers。
1
2
|
Note that those checks are done at runtime, so adding many attributes and using
* multiple strategies may have negative impacts (complexity, performance).
|
注意这个检查是在运行时做的,所以注意性能问题,不要放太多的参数。
说的如此清楚,以至于用法如此简单。
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
|
@ResponseBody
@ControllerAdvice ( "com.api" )
public class ApiExceptionHandler extends BaseClientController {
private static final Logger LOGGER = LoggerFactory.getLogger(ApiExceptionHandler. class );
/**
*
* @param exception UnexpectedTypeException
* @param response
* @return
*/
@ResponseStatus (HttpStatus.BAD_REQUEST)
@ExceptionHandler (UnexpectedTypeException. class )
public CodeMsg unexpectedType(UnexpectedTypeException exception, HttpServletResponse response){
LOGGER.error( "校验方法太多,不确定合适的校验方法。" , exception);
return CodeMsg.error(exception.getMessage());
}
@ResponseStatus (HttpStatus.BAD_REQUEST)
@ExceptionHandler (HttpMessageNotReadableException. class )
public CodeMsg messageNotReadable(HttpMessageNotReadableException exception, HttpServletResponse response){
LOGGER.error( "请求参数不匹配,request的json格式不正确" , exception);
return CodeMsg.error(exception.getMessage());
}
@ResponseStatus (HttpStatus.BAD_REQUEST)
@ExceptionHandler (Exception. class )
public CodeMsg ex(MethodArgumentNotValidException exception, HttpServletResponse response){
LOGGER.error( "请求参数不合法。" , exception);
BindingResult bindingResult = exception.getBindingResult();
String msg = "校验失败" ;
return new CodeMsg(CodeMsgConstant.error, msg, getErrors(bindingResult));
}
private Map<String, String> getErrors(BindingResult result) {
Map<String, String> map = new HashMap<>();
List<FieldError> list = result.getFieldErrors();
for (FieldError error : list) {
map.put(error.getField(), error.getDefaultMessage());
}
return map;
}
}
|
3. Hibernate-validate
使用参数校验如果不catch异常就会返回400. 所以这个也要规范一下。
3.1 引入hibernate-validate
1
2
3
4
5
|
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version> 5.0 . 2 .Final</version>
</dependency>
|
1
2
3
4
5
|
<mvc:annotation-driven validator= "validator" />
<bean id= "validator" class = "org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" >
<property name= "providerClass" value= "org.hibernate.validator.HibernateValidator" />
<property name= "validationMessageSource" ref= "messageSource" />
</bean>
|
3.2 使用
在实体类字段上标注要求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class AlipayRequest {
@NotEmpty
private String out_trade_no;
private String subject;
@DecimalMin (value = "0.01" , message = "费用最少不能小于0.01" )
@DecimalMax (value = "100000000.00" , message = "费用最大不能超过100000000" )
private String total_fee;
/**
* 订单类型
*/
@NotEmpty (message = "订单类型不能为空" )
private String business_type;
//....
}
|
controller里添加@Valid
1
2
3
4
5
|
@RequestMapping (value = "sign" , method = RequestMethod.POST)
public String sign( @Valid @RequestBody AlipayRequest params
){
....
}
|
4.错误处理
前面已经提到,如果不做处理的结果就是400,415. 这个对应Exception是MethodArgumentNotValidException,也是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@ResponseStatus (HttpStatus.BAD_REQUEST)
@ExceptionHandler (Exception. class )
public CodeMsg ex(MethodArgumentNotValidException exception, HttpServletResponse response){
LOGGER.error( "请求参数不合法。" , exception);
BindingResult bindingResult = exception.getBindingResult();
String msg = "校验失败" ;
return new CodeMsg(CodeMsgConstant.error, msg, getErrors(bindingResult));
}
private Map<String, String> getErrors(BindingResult result) {
Map<String, String> map = new HashMap<>();
List<FieldError> list = result.getFieldErrors();
for (FieldError error : list) {
map.put(error.getField(), error.getDefaultMessage());
}
return map;
}
|
返回结果:
1
2
3
4
5
6
7
8
|
{
"code" : 1 ,
"msg" : "校验失败" ,
"data" : {
"out_trade_no" : "不能为空" ,
"business_type" : "订单类型不能为空"
}
}
|
大概有这么几个限制注解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/**
* Bean Validation 中内置的 constraint
* @Null 被注释的元素必须为 null
* @NotNull 被注释的元素必须不为 null
* @AssertTrue 被注释的元素必须为 true
* @AssertFalse 被注释的元素必须为 false +
* @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
* @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
* @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
* @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
* @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
* @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
* @Past 被注释的元素必须是一个过去的日期
* @Future 被注释的元素必须是一个将来的日期
* @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
* Hibernate Validator 附加的 constraint
* @NotBlank(message =) 验证字符串非null,且长度必须大于0
* @Email 被注释的元素必须是电子邮箱地址
* @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
* @NotEmpty 被注释的字符串的必须非空
* @Range(min=,max=,message=) 被注释的元素必须在合适的范围内
*/
|
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。
原文链接:https://ryan-miao.github.io/2017/05/20/spring400/