Spring参数校验和全局异常处理

Spring参数校验和全局异常处理,第1张

目录

一、前言

二、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-303

        JSR是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)

被注释的元素必须符合指定的正则表达式

2、Spring Validation

        Spring Validation对Hibernate Validation进行了二次封装,在Spring MVC模块中添加了自动校验,并将校验信息封装在特定的类中。Spring Validation框架对参数的校验提供了@Validated(Spring's JSR-303规范,是JSR-303的一个变种),javax提供了@Valid。

3、@Validated和@Valid的区别

        @Validated和@Valid在基本验证上没太大区别,但是在分组、注解位置、嵌套验证等方便表现不太相同。

功能点

@Validated

@Valid

分组

支持分组功能,不同的分组采用不同的分组校验

不支持分组

位置

可以用在类、方法、方法参数上

在方法、构造函数、方法参数和成员属性(字段)上

嵌套验证

不支持嵌套

支持嵌套

三、全局异常处理 1、为何要处理异常

        在日常开发过程中,为了不将后端异常堆栈抛给前端页面,经常需要在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[] 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 adviceBeans中,遍历adviceBeans,将这些ControllerAdviceBean放到exceptionHandlerAdviceCache,exceptionHandlerAdviceCache的类型是Map。放exceptionHandlerAdviceCache过程中,会扫描每个带有@ExceptionHandler注解的方法,ExceptionHandler的value可能有多个,然后以exceptionType为key,method为value放到mappedMethods中。具体方法在ExceptionHandlerMethodResolver构造函数里,具体代码如下所示:

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 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的差异。对于全局异常处理使用具体的代码例子进行阐述,最后深入代码对全局异常的原理进行解析。

参考
  1. notnull注解_参数校验注解Validated和Valid的区别,这次终于有人说清楚了 - 百度文库
  2. @Valid与@Validated区别_凯里欧文的博客-CSDN博客_valid和validated的区别
  3. @RestControllerAdvice与@ControllerAdvice的区别_假装Java大神的博客-CSDN博客

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/876876.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存