java spring 业务异常 ExceptionController (使用@RestControllerAdvice)

2023-01-10 15:47:26

目录

1. Exception自定义基类

2. 异常捕获的Controller

3. 原理剖析

3.1 @RestControllerAdvice + @ControllerAdvice

3.2 @RestController 与 @Controller的区别

3.3 @ResponseBody 注解的意思

3.4 @ControllerAdvice的其他用法

3.4.1 @InitBinder

3.4.2 @ModelAttribute

3.5 HttpMessageConverter / InternalResourceViewResolver


 

1. Exception自定义基类

自定义的异常需要继承RuntimeException类、

然后需要去重写:

public RuntimeException(String message) {
    super(message);
}

那么一个简单的自定义业务异常代码如下:

public class ResultException extends RuntimeException {
    public ResultException(RespCode respCode, String result){
        super(FastJsonUtils.toJsonString(new RespResult(respCode, result)));
    }
    public ResultException(int code, String msg, String result){
        super(FastJsonUtils.toJsonString(new RespResult(code, msg, result)));
    }
}

其实就是定义一个重写RuntimeException 的返回字符串方法,而且因为前端好多时候需要Json的字符串。这样返回前端还可以接受。

对于转json的Utils的code:

public class FastJsonUtils {
    public static final String toJsonString(Object object){
        if(null == object){
            return null;
        }
        return JSON.toJSONString(object,
                SerializerFeature.PrettyFormat,
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteNullStringAsEmpty,
                SerializerFeature.WriteNullNumberAsZero,
                SerializerFeature.WriteNullBooleanAsFalse,
                SerializerFeature.WriteNullListAsEmpty);
    }
}

2. 异常捕获的Controller

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;


@Slf4j
@RestControllerAdvice
public class ExceptionController {
    /*算数异常类:ArithmeticException
    空指针异常类型:NullPointerException
    类型强制转换类型:ClassCastException
    数组负下标异常:NegativeArrayException
    数组下标越界异常:ArrayIndexOutOfBoundsException
    违背安全原则异常:SecturityException
    文件已结束异常:EOFException
    文件未找到异常:FileNotFoundException
    字符串转换为数字异常:NumberFormatException
    操作数据库异常:SQLException
    输入输出异常:IOException
    方法未找到异常:NoSuchMethodException
    下标越界异常:IndexOutOfBoundsExecption
    系统异常:SystemException
    创建一个大小为负数的数组错误异常:NegativeArraySizeException
    数据格式异常:NumberFormatException
    安全异常:SecurityException
    不支持的操作异常:UnsupportedOperationException
    网络操作在主线程异常:NetworkOnMainThreadException  
    请求状态异常: IllegalStateException (extends RuntimeException , 父类:IllegalComponentStateException 在不合理或不正确时间内唤醒一方法时出现的异常信息。换句话说,即 Java 环境或 Java 应用不满足请求操作)
    网络请求异常:HttpHostConnectException
    子线程Thread更新UI view 异常:ViewRootImpl$CalledFromWrongThreadException
    证书不匹配的主机名异常: SSLExceptionero
    反射Method.invoke(obj, args...)方法抛出异常:InvocationTargetException
    EventBus使用异常:EventBusException
    非法参数异常:IllegalArgumentException
    参数不能小于0异常:ZeroException*/

    @ExceptionHandler(value = {ResultException.class})
    @ResponseBody
    public String resultException(HttpServletRequest req, Exception e) {
        return e.getMessage();
    }

    @ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class, ClassCastException.class,
            ArrayIndexOutOfBoundsException.class, FileNotFoundException.class, NumberFormatException.class,
            SQLException.class, IOException.class, RuntimeException.class})
    @ResponseBody
    public RespResult defaultErrorHandler(HttpServletRequest req, Exception e) {
        RespResult respResult = new RespResult(RespCode.UNKNOWN.getCode(), RespCode.UNKNOWN.getMsg(), null);
        log.error("[ExceptionController] 异常 xxxRequestId = {}; message = {}; 响应接口 = {}", req.getAttribute("xxxRequestId"),
                e.getMessage(), JSON.toJSONString(respResult));
        return respResult;
    }

//    http状态码 = 400
    @ExceptionHandler(value = {MissingRequestHeaderException.class})
    @ResponseBody
    public RespResult missingRequestHeaderException(HttpServletRequest req, Exception e) {
        RespResult respResult = new RespResult(RespCode.ERROR_2_.getCode(), RespCode.ERROR_2_.getMsg(), null);
        log.error("[ExceptionController] 异常 xxxRequestId = {}; message = {}; 响应接口 = {}", req.getAttribute("xxxRequestId"),
                e.getMessage(), JSON.toJSONString(respResult));
        return respResult;
    }

//    http状态码 = 415
    @ExceptionHandler(value = {HttpMediaTypeNotSupportedException.class})
    @ResponseBody
    public RespResult httpMediaTypeNotSupportedException(HttpServletRequest req, Exception e) {
        RespResult respResult = new RespResult(RespCode.ERROR_3_.getCode(), RespCode.ERROR_3_.getMsg(), null);
        log.error("[ExceptionController] 异常 xxxRequestId = {}; message = {}; 响应接口 = {}", req.getAttribute("xxxRequestId"),
                e.getMessage(), JSON.toJSONString(respResult));
        return respResult;
    }

    @ExceptionHandler(value = {ConstraintViolationException.class})
    @ResponseBody
    public RespResult constraintViolationException(HttpServletRequest req, Exception e) {
        return new RespResult(RespCode.ERROR_4_, null);
    }

    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    @ResponseBody
    public RespResult methodArgumentNotValidException(HttpServletRequest req, MethodArgumentNotValidException e) {
        return new RespResult(RespCode.ERROR_4_.getCode(), e.getBindingResult().getFieldError().getDefaultMessage(),null);
    }

    @ExceptionHandler(value = {Exception.class})
    @ResponseBody
    public RespResult defaultErrorHandler1(HttpServletRequest req, Exception e) {
        log.error("[ExceptionController] 异常 uuid = {}; message = {}", req.getAttribute("uuid"), e.getMessage());
        return new RespResult(RespCode.UNKNOWN.getCode(), RespCode.UNKNOWN.getMsg(), null);
    }


}

自定义的返回基类:就是个简单的POJO

public class RespResult<T> {

    private int code;

    private String msg;

    private T result;

    public RespResult() {
    }

    public RespResult(int code, String msg, T result) {
        this.code = code;
        this.msg = msg;
        this.result = result;
    }

    public RespResult(RespCode respCode, T result) {
        this.code = respCode.getCode();
        this.msg = respCode.getMsg();
        this.result = result;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getResult() {
        return result;
    }

    public void setResult(T result) {
        this.result = result;
    }
}

3. 原理剖析

3.1 @RestControllerAdvice + @ControllerAdvice

因为异常类中加入了这个注解,所以所有通过Controller进去的请求发生了异常都会被捕获。

首先是@RestControllerAdvice 源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.web.bind.annotation;

import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

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

这里面的核心是@ControllerAdvice

@ControllerAdvice源码:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.web.bind.annotation;

import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] assignableTypes() default {};

    Class<? extends Annotation>[] annotations() default {};
}

在Spring 3.2中,新增了@ControllerAdvice、@RestControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping、@PostMapping, @GetMapping注解中。

所以在我的业务使用中,使用 @ControllerAdvice + @ExceptionHandler 来处理异常。

其实这个注解应该说是Spring的注解,所以不管是MVC还是spring boot 都可以来进行使用这个注解来实现对Exception的Handler

================================================================================================

但是这个注解远远不止这点用途。

@ControllerAdvice是在类上声明的注解,其用法主要有三点:

结合方法型注解@ExceptionHandler,用于捕获Controller中抛出的指定类型的异常,从而达到不同类型的异常区别处理的目的;
结合方法型注解@InitBinder,用于request中自定义参数解析方式进行注册,从而达到自定义指定格式参数的目的;
结合方法型注解@ModelAttribute,表示其标注的方法将会在目标Controller方法执行之前执行。

@ControllerAdvice的用法基本是将其声明在某个bean上,然后在该bean的方法上使用其他的注解来指定不同的织入逻辑。不过这里@ControllerAdvice并不是使用AOP的方式来织入业务逻辑的,而是Spring内置对其各个逻辑的织入方式进行了内置支持。本文将对@ControllerAdvice的这三种使用方式分别进行讲解。

  如下是我们使用@ExceptionHandler捕获RuntimeException异常的例子:

@ControllerAdvice(basePackages = "mvc")
public class SpringControllerAdvice {
  @ExceptionHandler(RuntimeException.class)
  public ModelAndView runtimeException(RuntimeException e) {
    e.printStackTrace();
    return new ModelAndView("error");
  }
}

那么这个和@RestControllerAdvice有什么区别,从源码中可以看出来其实@RestControllerAdvice这个是被@ControllerAdvice修饰的。但是@RestController的时候还会被@ResponseBody修饰。

注解@ControllerAdvice的类可以拥有@ExceptionHandler, @InitBinder或 @ModelAttribute注解的方法,并且这些方法会被应用到控制器类层次的所有@RequestMapping方法上。

@RestControllerAdvice 类似于 @RestController 与 @Controller的区别

3.2 @RestController 与 @Controller的区别

@RestController注解相当于@ResponseBody + @Controller合在一起的作用。

@RestControllerAdvice这个是被@ControllerAdvice修饰的。但是@RestController的时候还会被@ResponseBody修饰。

都表示这个类可以接受Http请求,

不同:@Controller标识一个Spring类是Spring MVC controller处理器

 @RestController是@Controller和@ResponseBody的结合体,两个标注合并起来的作用。

所以以往都是这样用:

@Controller  
@ResponseBody  
public class MyController { }  
  
@RestController  
public class MyController { } 

3.3 @ResponseBody 注解的意思

所以看一下这个注解到底是什么意思呢?

可以简单的理解为这个注解表名Controller是返回的数据,而不是找对应的页面URI地址~

返回数据是什么格式的,取决于你的request的header中规定是什么格式的

也就是Content-Type所指定的比如说application/json

如果只是使用@RestController注解Controller,则Controller中的方法无法返回jsp页面,或者html,配置的视图解析器 InternalResourceViewResolver不起作用,返回的内容就是Return 里的内容。(返回数据啦)

 如果需要返回到指定页面,则需要用 @Controller配合视图解析器InternalResourceViewResolver才行。

所以该注解:通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。

3.4 @ControllerAdvice的其他用法

PS:可以结合异常@ExceptionHandler,自定义返回得浏览器Code

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //自定义浏览器返回状态码

3.4.1 @InitBinder

该注解的主要作用是绑定一些自定义的参数。一般情况下我们使用的参数通过@RequestParam,@RequestBody或者@ModelAttribute等注解就可以进行绑定了,但对于一些特殊类型参数,比如Date,它们的绑定Spring是没有提供直接的支持的,我们只能为其声明一个转换器,将request中字符串类型的参数通过转换器转换为Date类型的参数,从而供给@RequestMapping标注的方法使用。如下是@InitBinder的声明:

PS:貌似我都是用String来接收,然后自己去service中在处理Date = =

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InitBinder {
    // 这里value参数用于指定需要绑定的参数名称,如果不指定,则会对所有的参数进行适配,
    // 只有是其指定的类型的参数才会被转换
	String[] value() default {};
}

 如下是使用@InitBinder注册Date类型参数转换器的实现:


@ControllerAdvice(basePackages = "mvc")
public class SpringControllerAdvice {
  @InitBinder
  public void globalInitBinder(WebDataBinder binder) {
    binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
  }
}

 

@Controller
@RequestMapping("/user")
public class UserController {
 
  @Autowired
  private UserService userService;
 
  @RequestMapping(value = "/detail", method = RequestMethod.GET)
  public ModelAndView detail(@RequestParam("id") long id, Date date) {
    System.out.println(date);
    ModelAndView view = new ModelAndView("user");
    User user = userService.detail(id);
    view.addObject("user", user);
    return view;
  }
}

那么亲自试一试呗~

实验结果就是,如果想用date类型接受的话,是会抛出异常的!

异常如下:

JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2019-07-15 10:22:99": not a valid representation (error: Failed to parse Date value '2019-07-15 10:22:99': Cannot parse date "2019-07-15 10:22:99": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2019-07-15 10:22:99": not a valid representation (error: Failed to parse Date value '2019-07-15 10:22:99': Cannot parse date "2019-07-15 10:22:99": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null))

 那么加上这个@InitBinder来处理时间!

但是,经过测试,其实这个WebDataBinder 其实进来的都是Header中的内容+拼接的参数(不支持Json)

@Component
public class BaseController {
  @InitBinder
  public void initBinder(WebDataBinder dataBinder) {
    dataBinder.registerCustomEditor(
        Date.class,
        new PropertyEditorSupport() {
          @Override
          public void setAsText(String value) {
            try {
              setValue(new SimpleDateFormat("yyyy-MM-DD HH:mm:ss").parse(value));
            } catch (ParseException e) {
              setValue(null);
            }
          }
       
        });
  }
}

 而且网传,加上这个在controller中就可以把用类来接受的参数进行转换,但是测试之后并不能将body中的JSON入参进入这里面进行转换。

  @InitBinder
    public void initDateFormate(WebDataBinder dataBinder) {
        dataBinder.addCustomFormatter(new DateFormatter("yyyy-MM-dd HH:mm:ss"));
    }

                                                                                           代码3.4.1-code1

对于json的入参应该用

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss", locale = "zh", timezone = "GMT+8")

就可以将String的json入参转成了Date,而且也可以用第自定义序列化和反序列工具来实现(可以自行百度)
 

但是到现在有点跑题,那么@InitBinder 到底可以对日期进行格式刷吗?

猜测是只能对RequestParam那种类型进行操作,实际在测试一下~

确实是这样,也就是只能对加上上面的代码3.4.1-code1 , 这个代码需要对每个controller都加上,而且只能对requestParam来使用,不能对JSON用

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

对于这个@InitBinder 就如它的名字,初始化数据绑定器。

会对所有的接口都先进入这个数据绑定器,但是这个数据绑定器并不能对json进行处理,只能对Header和RequestParam来进行处理!

   /**
     * 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
     * @param binder
     */
    @InitBinder
    public void initWebBinder(WebDataBinder binder){
        //对日期的统一处理
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
        //添加对数据的校验
        //binder.setValidator();
    }

 

3.4.2 @ModelAttribute

/**
     * 把值绑定到Model中,使全局@RequestMapping可以获取到该值
     * @param model
     */
    @ModelAttribute
    public void addAttribute(Model model) {
        model.addAttribute("attribute",  "The Attribute");
    }

 

这个有点像 定义一个抽象的BaseCtrl 然后将里面塞入xxxRquestId 或者类似的访问属性,然后扔到线程池里面;

但是request中放入属性可能是进入接口之后才做的;至少我们的代码是这样;

比如我们定义的BaseCtrl,将代码简单的脱敏了~

@Slf4j
@Component
public abstract class BaseCtrl {


    protected String setGlobalRequestId(HttpServletRequest request, String xxxRequestId) {

        if (StringUtils.isBlank(xxxRequestId)) {
            xxxRequestId= Tools.getUUID();
        }
        request.setAttribute("xxxRequestId", xxxRequestId);
        return xxxRequestId;
    }
}

 

看一下ModelAttribute

https://www.jianshu.com/p/cf9acf314a4c?utm_source=oschina-app

 

3.5 HttpMessageConverter / InternalResourceViewResolver

====之后补一下一次MVC的请求流程=====

 

参考:

https://blog.csdn.net/zxfryp909012366/article/details/82955259 

https://yq.aliyun.com/articles/647428

  • 作者:pmdream
  • 原文链接:https://blog.csdn.net/pmdream/article/details/95969267
    更新时间:2023-01-10 15:47:26