前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring学习笔记(9)一springMVC/boot全局异常处理和参数校验

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

作者头像
黄规速
发布2022-04-14 16:08:29
7790
发布2022-04-14 16:08:29
举报
文章被收录于专栏:架构师成长之路

一、前言

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

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

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

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

我们先定义响应格式:

1、定义统一响应格式

代码语言:javascript
复制
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、错误码枚举类

自定义响应错误码:

代码语言:javascript
复制
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之类,然后通过一个统一的异常类进行封装。 可以定义一个接口,该接口主要是方便后面的异常处理工具类实现

代码语言:javascript
复制
public interface BaseErrors {
    String getCode();
    String getMsg();
}

BusinessException:

代码语言:javascript
复制
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格式:

代码语言:javascript
复制
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.6.5</version>
</dependency>

实现接口HandlerExceptionResolver:

代码语言:javascript
复制
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;
    }
}

抛出异常:

代码语言:javascript
复制
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)、这种使用方式,代码侵入性高。

代码语言:javascript
复制
    @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方式。该方式可以全局处理异常,处理逻辑灵活,最为推荐。

代码语言:javascript
复制
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:

代码语言:javascript
复制
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抛出异常:

代码语言:javascript
复制
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步:定义实体校验:

代码语言:javascript
复制
@Data
public class User {
    @NotBlank(message = "用户名不能为空")
   private  String username;
}

第2步:定义controller测试

代码语言:javascript
复制
@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

代码语言:javascript
复制
@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步,创建自定义注解

代码语言:javascript
复制
/**
 * 手机号码校验注解
 *
 * @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步,自定义校验逻辑

代码语言:javascript
复制
/**
 * 手机号码校验实现
 *
 * @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步,在字段上增加注解:

代码语言:javascript
复制
@ApiOperation("RequestBody校验:BindException")
    @GetMapping("/validMobile")
    public Integer validMobile(@Mobile(message = "手机号不对") @RequestParam(value = "phone") String phone) {
        return 1 ;
    }
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/03/14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
    • 1、定义统一响应格式
      • 2、错误码枚举类
        • 3、异常的表示形式:自定义业务异常类/接口
    • 二、实现方式一:实现接口HandlerExceptionResolver
    • 三、实现方式二:使用注解
      • 1、@ExceptionHandler单独使用:
        • 2、@ControllerAdvice + @ExceptionHandler方式全局
          • 3、测试效果:
          • 四、集成参数校验
            • 1、常见校验注解:
              • 2、集成参数校验例子
                • 第1步:定义实体校验:
                • 第2步:定义controller测试
                • 第3步:拦截参数校验
              • 3、自定义参数校验
                • 第1步,创建自定义注解
                • 第2步,自定义校验逻辑
                • 第3步,在字段上增加注解:
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档