Spring学习笔记(9)一springMVC/boot全局异常处理和参数校验

本文介绍如何在SpringBoot项目中实现全局异常处理并返回自定义错误码,同时整合参数校验功能,包括定义响应格式、错误码枚举类、业务异常类以及使用注解实现异常处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言


我们使用springboot做 Restfull API,希望能全局处理异常,返回自定义错误码。类似:

{  "code":1001, "msg":"failed ..."}

在springmvc基本思路就是定义定义全局异常处理器,返回相应的错误对象信息。其他方法如可以使用拦截器,或者filter。
我们这里使用全局异常处理器
springmvc实现全局异常一般使用两种方式:

  1. 实现接口
  2. 使用注解(比较简单)

我们先定义响应格式:

1、定义统一响应格式

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;
    }
}

2、错误码枚举类

自定义响应错误码:

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;
    }
}

3、异常的表示形式:自定义业务异常类/接口


异常一般可通过自定义异常类,或定义异常的信息接口,比如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() + ","+
                "}";
    }
}

二、实现方式一:实现接口HandlerExceptionResolver


官方推荐的是使用@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注解的增强,可以实现三个方面的功能:

  1. 全局异常处理
  2. 全局数据绑定
  3. 全局数据预处理

@ExceptionHandler:需要处理的异常,如果不传值默认处理所有异常。
@InitBinder:用来设置 WebDataBinder,WebDataBinder 用来自动绑定前台请求参数到 Model 中。
@ModelAttribute:@ModelAttribute 本来的作用是绑定键值对到 Model 里,此处是让全局的@RequestMapping 都能获得在此处设置的键值对。

1、@ExceptionHandler单独使用:

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);
    }

2、@ControllerAdvice + @ExceptionHandler方式全局

定义全局异常处理类。

  1. 通过 @ControllerAdvice 指定该类为 Controller 增强类。
  2. 通过 @ExceptionHandler 自定捕获的异常类型。
  3. 通过 @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());
    }

}

3、测试效果:

测试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>

1、常见校验注解:

注解功能
@AssertFalse可以为null,如果不为null的话必须为false
@AssertTrue可以为null,如果不为null的话必须为true
@DecimalMax设置不能超过最大值
@DecimalMin设置不能超过最小值
@Digits设置必须是数字且数字整数的位数和小数的位数必须在指定范围内
@Future日期必须在当前日期的未来
@Past日期必须在当前日期的过去
@Max最大不得超过此最大值
@Min最大不得小于此最小值
@NotNull不能为null,可以是空
@Null必须为null
@Pattern必须满足指定的正则表达式
@Size集合、数组、map等的size()值必须在指定范围内
@Email必须是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。

2、集成参数校验例子

第1步:定义实体校验:

@Data
public class User {
    @NotBlank(message = "用户名不能为空")
   private  String username;
}

第2步:定义controller测试

@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;
}

第3步:拦截参数校验

Validator校验框架返回的错误提示太臃肿了,不便于阅读,为了方便前端提示,我们需要将其简化一下。

直接修改之前定义的 RestExceptionHandler,单独拦截参数校验的三个异常:

javax.validation.ConstraintViolationExceptionorg.springframework.validation.BindExceptionorg.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);
    }
}

3、自定义参数校验

Spring Validation 提供的注解基本上够用,但是面对复杂的定义,我们还是需要自己定义相关注解来实现自动校验。比如我们验证手机号。

 第1步,创建自定义注解

/**
 * 手机号码校验注解
 *
 * @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 {};

}

第2步,自定义校验逻辑

/**
 * 手机号码校验实现
 *
 * @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) {

    }
}

第3步,在字段上增加注解:

@ApiOperation("RequestBody校验:BindException")
    @GetMapping("/validMobile")
    public Integer validMobile(@Mobile(message = "手机号不对") @RequestParam(value = "phone") String phone) {
        return 1 ;
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hguisu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值