我们使用springboot做 Restfull API,希望能全局处理异常,返回自定义错误码。类似:
{ "code":1001, "msg":"failed ..."}
在springmvc基本思路就是定义定义全局异常处理器,返回相应的错误对象信息。其他方法如可以使用拦截器,或者filter。 我们这里使用全局异常处理器 springmvc实现全局异常一般使用两种方式:
我们先定义响应格式:
package com.demo.springmvc.response;
public class ApiResponse {
private String code;
private String msg;
private Object data;
public ApiResponse() {
}
public ApiResponse(ResponseCode responseCode) {
this.code = responseCode.getCode();
this.msg = responseCode.getMsg();
}
public static ApiResponse getInstance(ResponseCode responseCode) {
return new ApiResponse(responseCode);
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return this.data;
}
public void setData(Object data) {
this.data = data;
}
}
自定义响应错误码:
package com.demo.springmvc.response;
/**
* Created by huangguisu on 2020/7/9.
*/
public enum ResponseCode {
SUCCESS("0", "success"),//成功
UNKNOWN_ERROR("999", "unkonwn error"),//未知错误
TOKEN_ERROR("1001", "token error"),//token错误
TEST1_ERROR("1002", "test1 error"),//test1错误
TEST2_ERROR("1003", "test2 error");//test2错误
/**
* 结果码
*/
private String code;
/**
* 结果码描述
*/
private String msg;
ResponseCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
异常一般可通过自定义异常类,或定义异常的信息接口,比如code,msg之类,然后通过一个统一的异常类进行封装。 可以定义一个接口,该接口主要是方便后面的异常处理工具类实现
public interface BaseErrors {
String getCode();
String getMsg();
}
BusinessException:
package com.demo.springmvc.exception;
import com.demo.springmvc.response.ResponseCode;
/**
* 自定义业务异常
* Created by huangguisu on 2019/7/9.
*/
public class BusinessException extends Exception implements BaseErrors{
private String code;
private String msg;
private ResponseCode responseCode;
public BusinessException(ResponseCode responseCode) {
super(responseCode.getMsg());
this.code = responseCode.getCode();
this.msg = responseCode.getMsg();
this.responseCode = responseCode;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public ResponseCode getResponseCode() {
return responseCode;
}
public void setResponseCode(ResponseCode responseCode) {
this.responseCode = responseCode;
}
@Override
public String getMessage() {
return "{" +
"\"code\":" + this.getCode() + ","+
"\"msg\":" + this.getMsg() + ","+
"}";
}
}
官方推荐的是使用@ExceptionHandler注解去捕获固定的异常,我们这只是为了演示,使用MappingJackson2JsonView类需要添加依赖才能实现返回响应json格式:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.5</version>
</dependency>
实现接口HandlerExceptionResolver:
package com.demo.springmvc.exception;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.HandlerExceptionResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import com.demo.springmvc.response.*;
@Component
public class ExceptionResolver implements HandlerExceptionResolver{
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView());
//使用自定义BusinessException
if (e instanceof BusinessException){
modelAndView.addObject("code",((BusinessException) e).getCode());
modelAndView.addObject("msg",((BusinessException) e).getMsg());
}else {
//未知的异常
modelAndView.addObject("code", ResponseCode.UNKNOWN_ERROR.getCode());
modelAndView.addObject("msg",ResponseCode.UNKNOWN_ERROR.getMsg());
}
return modelAndView;
}
}
抛出异常:
package com.demo.springmvc.controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.demo.springmvc.exception.BusinessException;
import com.demo.springmvc.response.ResponseCode;
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/")
@ResponseBody
public String index() {
return "Hello world";
}
@RequestMapping("/exception")
@ResponseBody
public String testException() throws Exception{
ResponseCode responseCode = ResponseCode.TOKEN_ERROR;
throw new BusinessException(responseCode);
}
}
部署后测试效果:
在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中.
@ControllerAdvice:
使用 @ControllerAdvice注解 的类的方法可以使用 @ExceptionHandler、 @InitBinder、 @ModelAttribute 注解到方法上,这对所有注解了 @RequestMapping 的控制器内的方法都有效。
@RestControllerAdvice
是@RestController
注解的增强,可以实现三个方面的功能:
@ExceptionHandler:需要处理的异常,如果不传值默认处理所有异常。 @InitBinder:用来设置 WebDataBinder,WebDataBinder 用来自动绑定前台请求参数到 Model 中。 @ModelAttribute:@ModelAttribute 本来的作用是绑定键值对到 Model 里,此处是让全局的@RequestMapping 都能获得在此处设置的键值对。
1)、@ExceptionHandler单独使用,必须和要处理的方法在一个Controller类里面。这种配置方式处理的优先级最高,可以返回多种类型数据。 2)、可以处理多类异常,如果不指定@ExceptionHandler的value,就处理所有Exception。 3)、这种使用方式,代码侵入性高。
@RequestMapping("/exception1")
@ResponseBody
@ExceptionHandler({BusinessException.class})
public String exception()throws Exception {
throw new BusinessException(ResponseCode.TEST1_ERROR);
}
@RequestMapping("/exception2")
@ResponseBody
@ExceptionHandler(value={Exception.class,BusinessException.class})
public String exception2() throws Exception{
throw new BusinessException(ResponseCode.TEST2_ERROR);
}
定义全局异常处理类。
@ControllerAdvice
指定该类为 Controller
增强类。@ExceptionHandler
自定捕获的异常类型。@ResponseBody
返回 json
到前端。这种配置方式可以在全局范围内处理异常,优先级仅次于单独使用@ExceptionHandler方式。该方式可以全局处理异常,处理逻辑灵活,最为推荐。
package com.demo.springmvc.exception;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.demo.springmvc.response.*;
@ControllerAdvice
public class CommonException {
@ExceptionHandler(Exception.class)
@ResponseBody
public ApiResponse handleException(Exception e) {
return ApiResponse.getInstance(ResponseCode.UNKNOWN_ERROR);
}
/**
* 处理业务异常
*
* @param e 业务异常
* @return apiResponse
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public ApiResponse handleOpdRuntimeException(BusinessException e) {
return ApiResponse.getInstance(e.getResponseCode());
}
}
测试controller:
package com.demo.springmvc.controller;
import com.demo.springmvc.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.demo.springmvc.exception.BusinessException;
import com.demo.springmvc.response.ResponseCode;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
TestService testService;
@RequestMapping("/exception")
@ResponseBody
public String testException() throws Exception{
ResponseCode responseCode = ResponseCode.TOKEN_ERROR;
throw new BusinessException(responseCode);
}
@RequestMapping("/exception1")
@ResponseBody
//@ExceptionHandler({BusinessException.class})
public String exception()throws Exception {
testService.testException1();
return "";
}
@RequestMapping("/exception2")
@ResponseBody
//@ExceptionHandler(value={Exception.class,BusinessException.class})
public String exception2() throws Exception{
testService.testException2();
return "";
}
}
service抛出异常:
package com.demo.springmvc.service;
import com.demo.springmvc.exception.BusinessException;
import com.demo.springmvc.response.ResponseCode;
import org.springframework.stereotype.Service;
/**
* Created by huangguisu on 2020/7/9.
*/
@Service
public class TestService {
public void testException1(){
throw new BusinessException(ResponseCode.TEST1_ERROR);
}
public void testException2(){
throw new BusinessException(ResponseCode.TEST2_ERROR);
}
}
如果springmvc项目测试不生效: 1:检查包扫描路径是否对:<context:component-scan base-package="com.demo" /> 2 2:是否添加激活注解模式:<mvc:annotation-driven/>
为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,校验用户名密码是否为空,校验邮件、手机号码格式是否准确。靠代码对接口参数一个个校验的话就太繁琐了,代码可读性极差。
Validator框架就是为了解决开发人员在开发的时候少写代码,提升开发效率;Validator专门用来进行接口参数校验,例如常见的必填校验,email格式校验,用户名必须位于6到12之间 等等
Validator校验框架遵循了JSR-303验证规范(参数校验规范), JSR是 Java Specification Requests
的缩写。
从
springboot-2.3
开始,校验包被独立成了一个starter
组件,所以需要引入validation,而springboot-2.3
之前的版本不需要。 <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-validation</artifactid> </dependency>
注解 | 功能 |
---|---|
@AssertFalse | 可以为null,如果不为null的话必须为false |
@AssertTrue | 可以为null,如果不为null的话必须为true |
@DecimalMax | 设置不能超过最大值 |
@DecimalMin | 设置不能超过最小值 |
@Digits | 设置必须是数字且数字整数的位数和小数的位数必须在指定范围内 |
@Future | 日期必须在当前日期的未来 |
@Past | 日期必须在当前日期的过去 |
@Max | 最大不得超过此最大值 |
@Min | 最大不得小于此最小值 |
@NotNull | 不能为null,可以是空 |
@Null | 必须为null |
@Pattern | 必须满足指定的正则表达式 |
@Size | 集合、数组、map等的size()值必须在指定范围内 |
必须是email格式 | |
@Length | 长度必须在指定范围内 |
@NotBlank | 字符串不能为null,字符串trim()后也不能等于“” |
@NotEmpty | 不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“” |
@Range | 值必须在指定范围内 |
@URL | 必须是一个URL |
注:此表格只是简单的对注解功能的说明,并没有对每一个注解的属性进行说明;
@NotNull:主要用在基本数据类型上(int,Integer,Double),不能为null,但是可以试empty(""," "," "); @NotEmpty: 主要用在集合类上,不能为空,而且长度必须大于0(" "," "); @NotBlank: 只能用在String字符串类型上,而且调用trim()后,即去除两边的空白字符后长度必须大于0。
@Data
public class User {
@NotBlank(message = "用户名不能为空")
private String username;
}
@RestController
@Validated
public class ValidateController {
@ApiOperation("单参数校验:ConstraintViolationException")
@GetMapping("/validParam")
public Integer testNotBlank(@NotBlank(message = "userid不能为空" ) @RequestParam(value = "userid") String userid) {
return 1 ;
}
@ApiOperation("RequestBody校验:MethodArgumentNotValidException")
@PostMapping("/validObj")
public Integer testObject(@Validated @RequestBody User user) {
return 1 ;
}
@ApiOperation("RequestBody校验:BindException")
@PostMapping("/validForm")
public Integer testFrom(@Validated User user) {
return 1 ;
}
直接使用@NotBlank不生效,需要在类加上注解@Validated 如果还不生效,在实体类加上@Valid注解才生效。如果是嵌套的对象: 在外层的对象引用添加@Valid注解,如: @Valid private NamespaceDoc namespaceDoc; @Data @Valid public class BuildIndexDto { @NotNull(message = "操作类型不能为空") private OperationTypeEnum opType; @Valid private NamespaceDoc namespaceDoc; }
Validator
校验框架返回的错误提示太臃肿了,不便于阅读,为了方便前端提示,我们需要将其简化一下。
直接修改之前定义的 RestExceptionHandler
,单独拦截参数校验的三个异常:
javax.validation.ConstraintViolationException
,org.springframework.validation.BindException
,org.springframework.web.bind.MethodArgumentNotValidException
@ControllerAdvice
public class ApplicationExceptionHandler {
private static Logger logger = LoggerFactory.getLogger(ApplicationExceptionHandler.class);
/**
* 处理简单GET单个参数校验
* #@ResponseStatus(HttpStatus.BAD_REQUEST) //注释掉原因:统一使用通用格式
* @param e
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public RestVO handleValidationException(ConstraintViolationException e) {
for (ConstraintViolation<?> s : e.getConstraintViolations()) {
return new RestVO(SystemErrorCode.INVALID_PARAMETER.getCode(), s.getMessage());
}
return new RestVO(SystemErrorCode.INVALID_PARAMETER);
}
/**
* 处理参数BeanValidation异常
* #@ResponseStatus(HttpStatus.BAD_REQUEST) //注释掉原因:统一使用通用格式
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public RestVO handleValidationBodyException(MethodArgumentNotValidException e) {
for (ObjectError s : e.getBindingResult().getAllErrors()) {
return new RestVO(SystemErrorCode.INVALID_PARAMETER.getCode(), s.getDefaultMessage(), "BeanValidation " + s.getObjectName() + "校验异常");
}
return new RestVO(SystemErrorCode.INVALID_PARAMETER);
}
/**
* 处理From实体类校验
*
* @param e
* @return
*/
@ExceptionHandler(BindException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public RestVO handleValidationBeanException(BindException e) {
for (ObjectError s : e.getAllErrors()) {
String msg = s.getDefaultMessage();
//指定不合法formatter会进入该异常
if (msg.contains("IllegalArgumentException")) {
return new RestVO(SystemErrorCode.INVALID_PARAMETER.getCode(), msg.substring(msg.lastIndexOf(":") + 1));
}
return new RestVO(SystemErrorCode.INVALID_PARAMETER.getCode(), s.getDefaultMessage());
}
return new RestVO(SystemErrorCode.INVALID_PARAMETER);
}
}
Spring Validation 提供的注解基本上够用,但是面对复杂的定义,我们还是需要自己定义相关注解来实现自动校验。比如我们验证手机号。
/**
* 手机号码校验注解
*
* @author huangguisu
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {MobileValidator.class}) //标明由哪个类执行校验逻辑
public @interface Mobile {
String regexp() default "";
String message() default "手机号码格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
/**
* 手机号码校验实现
*
* @author huangguisu
*/
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private static Pattern pattern = Pattern.compile("^0?(13[0-9]|14[0-9]|15[0-9]|16[0-9]|17[0-9]|18[0-9]|19[0-9])[0-9]{8}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
Matcher m = pattern.matcher(value);
return m.matches();
}
@Override
public void initialize(Mobile constraintAnnotation) {
}
}
@ApiOperation("RequestBody校验:BindException")
@GetMapping("/validMobile")
public Integer validMobile(@Mobile(message = "手机号不对") @RequestParam(value = "phone") String phone) {
return 1 ;
}