Home Tags Posts tagged with "ControllerAdvice"

ControllerAdvice

在项目开发中,异常处理往往是一个容易被忽略的点,我们可能遇到程序异常就进行捕获,这样可能会产生大量的tyr-catch语句,使得项目结构复杂化,并且由于异常出现的位置常常是跨模块的,如果我们没有对异常进行统一管理,那么当程序遇到异常时,能提供给我们调试的信息就不清晰

我们处理了异常,却没有得到更好的效果,因此为了解决这个问题,我查阅了相关资料,并决心根据自己的项目经验进行总结

在此篇文章中你可以看到:

  1. 对枚举类型的使用
  2. Springboot项目中对异常的统一处理
  3. 自定义数据传输格式
  4. Result风格的CRUD
  5. Postman的使用

 

正文:

SpringBoot处理异常的几种方式:

  1. 使用@ControllerAdvice和@ExceptionHandler处理全局异常
  2. 使用@ExceptionHandler处理Controller级别的异常
  3. ResponseStatusException

本文采用第一种,使用@ControllerAdvice开启全局异常的捕获,自定义一个方法使用@ExceptionHandler注解然后定义捕获异常的类型即可对这些捕获的异常进行统一处理

示例:

上述示例中,我们对捕获的异常进行简单的二次处理,返回异常的信息,这种方式虽然能让我们知道异常的原因,但在很多情况下来说还是不够人性化

因此我们可以通过自定义的异常类及枚举类型来实现我们想要的数据;

项目依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.4.4</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.yang</groupId>
   <artifactId>helloworld</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>helloworld</name>
   <description>Demo project for Spring Boot</description>
   <properties>
      <java.version>11</java.version>
   </properties>
   <dependencies>
      <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
      <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava</artifactId>
         <version>21.0</version>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>

      <dependency>
         <groupId>org.thymeleaf.extras</groupId>
         <artifactId>thymeleaf-extras-java8time</artifactId>
      </dependency>

      <dependency>
         <groupId>org.thymeleaf</groupId>
         <artifactId>thymeleaf-spring5</artifactId>
      </dependency>
        <dependency>
            <groupId>org.mortbay.jetty</groupId>
            <artifactId>jetty</artifactId>
            <version>6.1.25</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
      <dependency>
         <groupId>com.alibaba</groupId>
         <artifactId>fastjson</artifactId>
         <version>1.2.58</version>
      </dependency>
   </dependencies>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>

</project>

 

自定义基础接口类

首先定义一个基础的接口类,自定义的错误描述枚举类型需要实现该接口

public interface BaseErrorInfoInterface {
    /** 错误码*/
    String getResultCode();
   
   /** 错误描述*/
    String getResultMsg();
}

自定义枚举类型,实现该接口(用于错误描述)
public enum CommonEnum implements BaseErrorInfoInterface {
   // 数据操作错误定义
   SUCCESS("200", "成功!"), 
   BODY_NOT_MATCH("400","请求的数据格式不符!"),
   SIGNATURE_NOT_MATCH("401","请求的数字签名不匹配!"),
   NOT_FOUND("404", "未找到该资源!"), 
   INTERNAL_SERVER_ERROR("500", "服务器内部错误!"),
   SERVER_BUSY("503","服务器正忙,请稍后再试!")
   ;

   /** 错误码 */
   private String resultCode;

   /** 错误描述 */
   private String resultMsg;

   CommonEnum(String resultCode, String resultMsg) {
      this.resultCode = resultCode;
      this.resultMsg = resultMsg;
   }

   @Override
   public String getResultCode() {
      return resultCode;
   }

   @Override
   public String getResultMsg() {
      return resultMsg;
   }

}

自定义异常类
自定义一个异常类,用于处理我们程序中所出现的异常
public class BizException extends RuntimeException {

   private static final long serialVersionUID = 1L;

   /**
    * 错误码
    */
   protected String errorCode;
   /**
    * 错误信息
    */
   protected String errorMsg;

   public BizException() {
      super();
   }

   public BizException(BaseErrorInfoInterface errorInfoInterface) {
      super(errorInfoInterface.getResultCode());
      this.errorCode = errorInfoInterface.getResultCode();
      this.errorMsg = errorInfoInterface.getResultMsg();
   }
   
   public BizException(BaseErrorInfoInterface errorInfoInterface, Throwable cause) {
      super(errorInfoInterface.getResultCode(), cause);
      this.errorCode = errorInfoInterface.getResultCode();
      this.errorMsg = errorInfoInterface.getResultMsg();
   }
   
   public BizException(String errorMsg) {
      super(errorMsg);
      this.errorMsg = errorMsg;
   }
   
   public BizException(String errorCode, String errorMsg) {
      super(errorCode);
      this.errorCode = errorCode;
      this.errorMsg = errorMsg;
   }

   public BizException(String errorCode, String errorMsg, Throwable cause) {
      super(errorCode, cause);
      this.errorCode = errorCode;
      this.errorMsg = errorMsg;
   }
   

   public String getErrorCode() {
      return errorCode;
   }

   public void setErrorCode(String errorCode) {
      this.errorCode = errorCode;
   }

   public String getErrorMsg() {
      return errorMsg;
   }

   public void setErrorMsg(String errorMsg) {
      this.errorMsg = errorMsg;
   }

   public String getMessage() {
      return errorMsg;
   }

   /**
    */
   @Override
   public Throwable fillInStackTrace() {
      return this;
   }
}

自定义数据格式
定义数据的传输格式(返回给前端的格式)
public class ResultBody {
   /**
    * 响应代码
    */
   private String code;

   /**
    * 响应消息
    */
   private String message;

   /**
    * 响应结果
    */
   private Object result;

   public ResultBody() {
   }

   public ResultBody(BaseErrorInfoInterface errorInfo) {
      this.code = errorInfo.getResultCode();
      this.message = errorInfo.getResultMsg();
   }

   public String getCode() {
      return code;
   }

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

   public String getMessage() {
      return message;
   }

   public void setMessage(String message) {
      this.message = message;
   }

   public Object getResult() {
      return result;
   }

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

   /**
    * 成功
    * 
    * @return
    */
   public static ResultBody success() {
      return success(null);
   }

   /**
    * 成功
    * @param data
    * @return
    */
   public static ResultBody success(Object data) {
      ResultBody rb = new ResultBody();
      rb.setCode(CommonEnum.SUCCESS.getResultCode());
      rb.setMessage(CommonEnum.SUCCESS.getResultMsg());
      rb.setResult(data);
      return rb;
   }

   /**
    * 失败
    */
   public static ResultBody error(BaseErrorInfoInterface errorInfo) {
      ResultBody rb = new ResultBody();
      rb.setCode(errorInfo.getResultCode());
      rb.setMessage(errorInfo.getResultMsg());
      rb.setResult(null);
      return rb;
   }

   /**
    * 失败
    */
   public static ResultBody error(String code, String message) {
      ResultBody rb = new ResultBody();
      rb.setCode(code);
      rb.setMessage(message);
      rb.setResult(null);
      return rb;
   }

   /**
    * 失败
    */
   public static ResultBody error( String message) {
      ResultBody rb = new ResultBody();
      rb.setCode("-1");
      rb.setMessage(message);
      rb.setResult(null);
      return rb;
   }

   @Override
   public String toString() {
      return JSONObject.toJSONString(this);
   }

}

自定义全局异常处理类
编写自定义全局异常处理类,程序中遇到异常了进行抛出,抛出之后总要捕获吧,此类即为捕获全局抛出的异常

/**
 * 
* @Title: GlobalExceptionHandler
* @Description: 全局异常处理
如果使用@RestControllerAdvice 注解
则会将返回的数据类型转换成JSON格式
 */
@ControllerAdvice
public class GlobalExceptionHandler {
   private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
   
   /**
    * 处理自定义的业务异常
    * @param req
    * @param e
    * @return
    */
    @ExceptionHandler(value = BizException.class)  
    @ResponseBody  
   public  ResultBody bizExceptionHandler(HttpServletRequest req, BizException e){
       logger.error("发生业务异常!原因是:{}",e.getErrorMsg());
       return ResultBody.error(e.getErrorCode(),e.getErrorMsg());
    }

   /**
    * 处理空指针的异常
    * @param req
    * @param e
    * @return
    */
   @ExceptionHandler(value =NullPointerException.class)
   @ResponseBody
   public ResultBody exceptionHandler(HttpServletRequest req, NullPointerException e){
      logger.error("发生空指针异常!原因是:",e);
      return ResultBody.error(CommonEnum.BODY_NOT_MATCH);
   }


    /**
        * 处理其他异常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value =Exception.class)
   @ResponseBody
   public ResultBody exceptionHandler(HttpServletRequest req, Exception e){
       logger.error("未知异常!原因是:",e);
           return ResultBody.error(CommonEnum.INTERNAL_SERVER_ERROR);
    }
}

自定义一个实体类
User实体类,目的是供Controller操作并制造异常,模拟程序出现的异常

/**
 * 
* Title: User
* Description:用户pojo类
 */
public class User {
   /** 编号*/
   private Long id;
   /** 姓名*/
   private String name;
   /** 年龄*/
   private Integer age;
   /**  
    * 获取编号  
    * @return  id  
    */
   public Long getId() {
      return id;
   }
   /**  
    * 设置编号  
    * @param Long id  
    */
   public void setId(Long id) {
      this.id = id;
   }
   /**  
    * 获取姓名  
    * @return  name  
    */
   public String getName() {
      return name;
   }
   /**  
    * 设置姓名  
    * @param String name  
    */
   public void setName(String name) {
      this.name = name;
   }
   /**  
    * 获取年龄  
    * @return  age  
    */
   public Integer getAge() {
      return age;
   }
   /**  
    * 设置年龄  
    * @param Integer age  
    */
   public void setAge(Integer age) {
      this.age = age;
   }
}

Controller控制层
该层采用Result风格实现CRUD功能,在该类我们故意制造异常不捕获,让其抛出被我们的全局异常捕获类GlobalExceptionHandler捕获进行处理
这些异常有自定义的异常抛出,也有空指针的异常抛出,也有不可预知的异常抛出

* @Title: UserRestController
* @Description:
* 用户控制层
 */
@RestController
@RequestMapping(value = "/api")
public class UserRestController {

   @PostMapping("/user")
    public boolean insert(@RequestBody(required = false) User user) {
       System.out.println("开始新增...");
       //如果姓名为空就手动抛出一个自定义的异常!
        if(user.getName()==null){
            throw  new BizException("-1","用户姓名不能为空!");
        }
        return true;
    }

    @PutMapping("/user")
    public boolean update(@RequestBody(required = false) User user) {
       System.out.println("开始更新...");
       //这里故意造成一个空指针的异常,并且不进行处理
        String str=null;
        str.equals("111");
        return true;
    }

    @DeleteMapping("/user")
    public boolean delete(@RequestBody(required = false) User user)  {
        System.out.println("开始删除...");
        //这里故意造成一个异常,并且不进行处理
        Integer.parseInt("abc123");
        return true;
    }

    @GetMapping("/user")
    public List<User> findByUser(User user) {
       System.out.println("开始查询123...");
        List<User> userList =new ArrayList<>();
        User user2=new User();
        user2.setId(1L);
        user2.setName("xuwujing");
        user2.setAge(18);
        userList.add(user2);
        return userList;
    } 
}

主程序入口
/**
 * 
* @Title: App
* @Description:
* 主程序入口
 */

@SpringBootApplication
public class App 
{
    public static void main( String[] args )
    {
      SpringApplication.run(App.class, args);
      System.out.println("程序正在运行...");
    }
}

接下来使用Postman进行接口测试

首先进行查询,使用GET 方式进行请求。

然后我们再来测试下自定义的异常是否能够被正确的捕获并处理。

使用POST方式进行请求

POST http://localhost:8080/api/user

Body参数为:

{“id”:1,”age”:18}

返回参数为:

{“code”:”-1″,”message”:”用户姓名不能为空!”,”result”:null}

示例图:
在这里插入图片描述
可以看出将我们抛出的异常进行数据封装,然后将异常返回出来。

然后我们再来测试下空指针异常是否能够被正确的捕获并处理。在自定义全局异常中,我们除了定义空指针的异常处理,也定义最高级别之一的Exception异常,那么这里发生了空指针异常之后,它是回优先使用哪一个呢?这里我们来测试下。

使用PUT方式进行请求。

PUT http://localhost:8080/api/user

Body参数为:

{“id”:1,”age”:18}

返回参数为:

{“code”:”400″,”message”:”请求的数据格式不符!”,”result”:null}

示例图:

我们可以看到这里的的确是返回空指针的异常护理,可以得出全局异常处理优先处理子类的异常。

那么我们在来试试未指定其异常的处理,看该异常是否能够被捕获。

使用DELETE方式进行请求

DELETE http://localhost:8080/api/user

Body参数为:

{“id”:1}

返回参数为:

{“code”:”500″,”message”:”服务器内部错误!”,”result”:null}

在这里插入图片描述

这里可以看到它使用了我们在自定义全局异常处理类中的Exception异常处理的方法。
到这里,测试就结束了。顺便再说一下,自义定全局异常处理除了可以处理上述的数据格式之外,也可以处理页面的跳转,只需在新增的异常方法的返回处理上填写该跳转的路径并不使用ResponseBody 注解即可。 细心的同学也许发现了在GlobalExceptionHandler类中使用的是ControllerAdvice注解,而非RestControllerAdvice注解,如果是用的RestControllerAdvice注解,它会将数据自动转换成JSON格式,这种于ControllerRestController类似,所以我们在使用全局异常处理的之后可以进行灵活的选择处理。