目录
一、前言
二、Validation
1、JSR-303
2、Spring Validation
3、@Validated和@Valid的区别
三、全局异常处理
1、为何要处理异常
2、@RestControllerAdvice
3、返回自定义消息
四、全局异常原理解析
1、ControllerAdvice的加载
2、Controller接口异常如何进入exceptionHandler方法
五、总结
参考
一、前言
数据校验在业务代码中经常用到,比如前端传过来的用户名、年龄等数据,可能要求用户名非空、不能包含emoji,年龄的数值必须在1到110之间。如果不使用数据校验框架,那么就需要在业务代码中专门写一些代码用来校验数据的合法性。例如:
if (StringUitls.isBlank(userName)) {
throw new XXXException(500, "用户名不能为空");
}
if (age > 110 || age < 1) {
throw new XXXException(500, "年龄非法");
}
从上述代码可以看出,数据合法性的校验不复杂,但是嵌入到业务代码就比较凌乱,不但影响代码整洁,还需要开发自己去写校验数据合法性的代码,这种做法不够优雅,而且也容易出错产生bug。如果使用Spring的validation框架,就能够避免上述的烦恼。
二、Validation 1、JSR-303JSR是Java Specification Requests的缩写,意思是Java规范提案。它是向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,为Java平台新增API和服务。JSR对于Java来说是一个重要的标准。
JSR-303是Java EE 6中的一项子规范,称为Bean Validation。Hibernate Validator是Bean Validation的参考实现。提供了JSR-303的所有内置constraint的实现。JSR-303的常用注解如下表所示:
约束注解名称 | 约束注解说明 |
@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(value) | 被注释的元素必须符合指定的正则表达式 |
Spring Validation对Hibernate Validation进行了二次封装,在Spring MVC模块中添加了自动校验,并将校验信息封装在特定的类中。Spring Validation框架对参数的校验提供了@Validated(Spring's JSR-303规范,是JSR-303的一个变种),javax提供了@Valid。
3、@Validated和@Valid的区别@Validated和@Valid在基本验证上没太大区别,但是在分组、注解位置、嵌套验证等方便表现不太相同。
功能点 | @Validated | @Valid |
分组 | 支持分组功能,不同的分组采用不同的分组校验 | 不支持分组 |
位置 | 可以用在类、方法、方法参数上 | 在方法、构造函数、方法参数和成员属性(字段)上 |
嵌套验证 | 不支持嵌套 | 支持嵌套 |
在日常开发过程中,为了不将后端异常堆栈抛给前端页面,经常需要在controller层、service层、dao层等进行异常捕获,这样做的话代码不太美观,后期维护也不方便。而且如果有些异常没有捕获住,就可能将一些莫名其妙的异常抛给前端,用户看到此异常就非常不友好。比如用户插入数据的时候,可能他插入的数据违法了数据库的唯一性约束,那么如果没有对这种场景进行捕获并且转化成用户可读的异常,就可能会直接抛出下面异常:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1010640164' for key 'uk_user_id'
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
at com.mysql.jdbc.Util.getInstance(Util.java:408)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:936)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3978)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3914)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2530)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2683)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2495)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1903)
at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1242)
省略多余异常...
为了解决上述问题,可以将Controller层的异常信息进行统一封装处理,解决方案可以使用@RestControllerAdvice(或者ControllerAdvice) + @ExceptionHandler。用@RestControllerAdvice表示开启了全局异常处理,我们需要自定义一个方法,使用@ExceptionHandler注解修饰的方法进行统一异常处理。简单例子如下代码所示:
@RestControllerAdvice
public class UnionExceptionHandler {
@ExceptionHandler(value = Exception.class)
public String exceptionHandler(Exception e) {
System.out.println("未知异常!原因是:"+e);
return e.getMessage();
}
}
上述实例代码,对捕获的异常进行简单的二次处理,返回异常的信息。此例子较为简单,返回的错误信息也比较有限,而且不够人人性化。一般线上的响应都比较复杂,可以使用自定义的Result类型返回。例如下面代码所示:
@RestControllerAdvice
public class CustomizeResponseExceptionHandler {
@ExceptionHandler(value = CustomizeException.class)
public Result exceptionHandler(CustomizeException e) {
System.out.println("未知异常!原因是:" + e.getErrorMsg());
return Result.buildError(e.getErrorCode(), e.getErrorMsg());
}
}
public class Result implements Serializable {
private int code;
private String msg;
private T data;
private List errors;
public Result() {}
public Result(int code, String msg, T data, List errors) {
this.code = code;
this.msg = msg;
this.data = data;
this.errors = errors;
}
public static Result buildError(int code, String message) {
return new Result<>(code, msg, null, null);
}
}
2、@RestControllerAdvice
@RestControllerAdvice是Spring框架里的一个Component注解,具体代码如下所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class>[] basePackageClasses() default {};
Class>[] assignableTypes() default {};
Class extends Annotation>[] annotations() default {};
}
上述代码可以看出RestControllerAdvice只能修饰类,并且是运行时才生效。值得注意的是RestControllerAdvice类上方还有@ControllerAdvice和@ResponseBody注解修饰。如果一个类是ControllerAdvice注解修饰,想要返回json数据的话就得额外添加@ResponseBody注解。
有了@RestControllerAdvice,被@ExceptionHandler、@InitBinder和@ModelAttribute注解的方法,都会作用到@RequestMapping注解的方法上。
3、返回自定义消息在实际业务开发过程中,经常遇到数据校验的场景,比如在新增一个用户的基本信息时,需要校验这个人的基本信息的合法性。那么需要在UserDto实体类中加上一些校验注解,代码如下:
public class UserDto implements Serializable {
private static final long serialVersionUID = -8412446300622080614L;
@NotBlank(message = "用户姓名不能为空")
private String name;
@Range(min = 1, max = 110, message = "年龄必须在1到110之间")
private int age;
@NotBlank(message = "昵称不能为空")
@Pattern(regexp = "^.{2,20}$", message = "昵称2到20个字符")
private String nickname;
}
@Slf4j
@RestController
public class UserController {
@RequestMapping(value = "/user/add")
public Result addUser(@RequestBody @Validated UserDto userDto) {
boolean success = userService.addUser(userDto);
return Result.ofSuccess(success);
}
}
public class Result implements Serializable {
private int code;
private String msg;
private T data;
private List errors;
public Result() {}
public Result(int code, String msg, T data, List errors) {
this.code = code;
this.msg = msg;
this.data = data;
this.errors = errors;
}
public static Result buildError(int code, String message) {
return new Result<>(code, msg, null, null);
}
public static Result buildError(String message) {
return new Result<>(500, msg, null, null);
}
public static Result ofSuccess(T data) {
return new Result<>(data);
}
}
如果在添加用户的时候,传的参数有误,比如age传了0,那么上述代码的错误响应结果如下所示:
{
"code": 10086,
"msg": "系统异常,请稍后重试",
"errors": [
{
"name": "age",
"message": "年龄必须在1到110之间"
}
]
}
假如想将所有的人性化异常信息都放在msg里,这样前端直接报msg里的异常,用户看见就容易懂了。改造后的响应结果如下所示:
{
"code": 500,
"msg": "年龄必须在1到110之间"
}
想实现上述的要求,应该如何做呢?可以用上述提到的,用@RestControllerAdvice + @ExceptionHandler解决。代码如下:
@RestControllerAdvice
public class ControllerAdvice {
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public Result exceptionHandler(Exception exception, HttpServletRequest request, HttpServletResponse response) {
Result result = new Result<>();
StringBuilder errorBuilder = new StringBuilder();
Iterator iterator;
FieldError fieldError;
if (exception instanceof MethodArgumentNotValidException) {
iterator = ((MethodArgumentNotValidException) exception).getBindingResult().getFieldErrors().iterator();
while (iterator.hasNext()) {
fieldError = (FieldError) iterator.next();
errorBuilder.append(fieldError.getDefaultMessage()).append("\n");
}
}
return ResultUtils.buildError(errorBuilder.toString());
}
}
上述代码的@ExceptionHandler里的value是MethodArgumentNotValidException.class,这样当@Validated修饰的对象报错时,就可以被捕捉到,然后就可以组装自定义的响应格式。
四、全局异常原理解析@RestControllerAdvice是Spring4.3新增的注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并且作用到所有被@RequestMapping注解的方法上。那么为什么Controller接口抛出异常后,会自动进入@ExceptionHandler修饰的ControllerAdvice#exceptionHandler方法里呢?接下来通过源码来分析具体原理。
1、ControllerAdvice的加载上述代码可以看到,ControllerAdvice被@RestControllerAdvice修饰。ExceptionHandlerExceptionResolver类实现了InitializingBean接口,因此重写了afterPropertiesSet()方法。具体代码如下:
@Override
public void afterPropertiesSet() {
//初始化ExceptionHandlerAdvice缓存,添加ResponseBodyAdvice到responseBodyAdvice中
initExceptionHandlerAdviceCache();
if (this.argumentResolvers == null) {
List resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
//查找所有被@ControllerAdvice注解的类
List adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
...省略代码
}
/**
* 查询被ControllerAdvice注解的类,并且转化成ControllerAdviceBean列表
*/
public static List findAnnotatedBeans(ApplicationContext context) {
return Arrays.stream(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class))
.filter(name -> !ScopedProxyUtils.isScopedTarget(name))
//此处可以看到需要过滤出被ControllerAdvice注解修饰类
.filter(name -> context.findAnnotationOnBean(name, ControllerAdvice.class) != null)
.map(name -> new ControllerAdviceBean(name, context))
.collect(Collectors.toList());
}
上面的initExceptionHandlerAdviceCache会从容器中找到@ControllerAdvice注解的类,因为@RestControllerAdvice是@ControllerAdvice注解的类,因此被@RestControllerAdvice注解的类也会被扫描到。具体可以看findAnnotatedBeans方法。
当扫描出所有被@RestControllerAdvice注解的类后,放到List
public class ExceptionHandlerMethodResolver{
private final Map, Method> mappedMethods = new HashMap<>(16);
//被@ExceptionHandler注解的方法
public static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
/**
* 构造方法
*/
public ExceptionHandlerMethodResolver(Class> handlerType) {
//遍历被@ExceptionHandler注解的方法
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
for (Class extends Throwable> exceptionType : detectExceptionMappings(method)) {
addExceptionMapping(exceptionType, method);
}
}
}
/**
* 提取@ExceptionHandler里的value,然后放到result里
*/
private void detectAnnotationExceptionMappings(Method method, List> result) {
ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
Assert.state(ann != null, "No ExceptionHandler annotation");
result.addAll(Arrays.asList(ann.value()));
}
}
上面的afterPropertiesSet执行完后,就将adviceBean放到了exceptionHandlerAdviceCache中。在后续Controller接口异常的时候会用到。
2、Controller接口异常如何进入exceptionHandler方法可以从DispatcherServlet#doDispatch方法入手,具体代码如下所示:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
...省略代码
//
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
...省略代码
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
...省略代码
}
当执行Controller过程产生异常时,会进入catch方法,将异常赋值给dispatchException,具体代码是dispatchException = ex; 然后执行processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);processDispatchResult代码如下所示:
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
...省略代码
}
上述代码如果Exception不是ModelAndViewDefiningException,就会执行processHandlerException方法。processHandlerException方法会遍历所有HandlerExceptionResolver,找到一个不为空的ModelAndView,processHandlerException代码如下所示:
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
//遍历所有HandlerExceptionResolver
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
...省略代码
}
上述代码resolver.resolveException(request, response, handler, ex);最终会调用到ExceptionHandlerExceptionResolver的doResolveHandlerMethodException方法。具体代码如下所示:
@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
//从缓存中获取该异常对应的handle方法
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}
...省略代码
try {
...省略代码
else {
//执行具体的handle方法,也就是被@ExceptionHandler修饰的方法
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
}
}
...省略代码
}
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);最终会执行异常对应的处理方法,也就是上文中的exceptionHandler方法。然后exceptionHandler方法就可以根据自定义的逻辑组装返回的数据结构,可以实现将验证框架里的message返回给前端。
五、总结本文首先介绍数据校验框架的标准JSR-303,然后介绍Spring validation,接着简单对比了@Validated和@Valid的差异。对于全局异常处理使用具体的代码例子进行阐述,最后深入代码对全局异常的原理进行解析。
参考- notnull注解_参数校验注解Validated和Valid的区别,这次终于有人说清楚了 - 百度文库
- @Valid与@Validated区别_凯里欧文的博客-CSDN博客_valid和validated的区别
- @RestControllerAdvice与@ControllerAdvice的区别_假装Java大神的博客-CSDN博客
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)