SprintBoot系列(五):异常处理机制

上一节我们讲到SpringBoot的包扫描机制

http://www.fcors.com/%e6%8a%80%e6%9c%af%e4%b8%8e%e6%a1%86%e6%9e%b6/sprintboot%e7%b3%bb%e5%88%97%ef%bc%88%e5%9b%9b%ef%bc%89%ef%bc%9a%e5%8c%85%e6%89%ab%e6%8f%8f%e4%b8%8e%e8%87%aa%e5%8a%a8%e8%a3%85%e9%85%8d/

现在开始讲解SpringBoot的异常处理机制。

回顾下之前发表过一个java文章,简单讲解过java的异常处理

https://www.fcors.com/%e6%8a%80%e6%9c%af%e4%b8%8e%e6%a1%86%e6%9e%b6/java%e5%bc%82%e5%b8%b8%e5%a4%84%e7%90%86/

现在开始讲解SpringBoot的异常处理机制

在Java报错一般分为资源不存在、参数错误、内部错误等,如果我们把这些报错内容和错误堆栈显示出来,对于前端访问者来说及其不友好。为了避免这种问题,我将对SpringBoot的异常处理机制进行讲解。

前端开发一般是通过api获得json进行前后端交互。因此统一Json格式就显得十分重要。下面我建议此格式

 {
     code:10001,
     message:xxx,
     request:GET url
 }

引入request的话,我们在排查错误的时候,可以更容易获取前端提交的data,减少沟通成本。

结合上文讲到的IOC思想,把code和message通过文件配置的方式,可以让错误提醒有以下优势

  • code码的统一,避免重复
  • 错误提醒统一维护,避免重复
  • 可更好地扩展多语言管理

Java的异常可能出现在controller、Server、Model层,所以需要一个全局拦截错误,然后再处理Exception。

如何全局拦截异常,下面先做一个简单的demo

1、全局异常简单demo

1.1、 创建全局异常处理类

1.1.1、在项目创建一个package,命名为core

1.1.2、在core中创建一个全局异常处理类GolbalExceptionAdvice

引入注解:@ControllerAdvice

package com.fcors.fcors.core;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class GolbalExceptionAdvice {


    @ExceptionHandler(value=Exception.class)
    public void handleException(HttpServletRequest req,Exception e){
        System.out.println("全局异常报错处理");
    }
}

1.1.3、执行代码层,两种定义异常的方式

method

package com.fcors.fcors.api.v1;

import com.fcors.fcors.sample.IConnect;
import com.fcors.fcors.sample.ISkill;
import com.fcors.fcors.sample.database.MySql;
import com.fcors.fcors.sample.hero.Diana;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/api/")
public class TestController {
    @Autowired
    private ISkill iskill;
    @GetMapping("/test")
    public String test2() throws Exception {
        iskill.q();
        throw new Exception("这是一个自定义错误");
//        return "Hello Fox is testing~";
    }

}

try-catch

package com.fcors.fcors.api.v1;

import com.fcors.fcors.sample.IConnect;
import com.fcors.fcors.sample.ISkill;
import com.fcors.fcors.sample.database.MySql;
import com.fcors.fcors.sample.hero.Diana;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/api/")
public class TestController {
    @Autowired
    private ISkill iskill;
    @GetMapping("/test")
    public String test2()  {
        iskill.q();
        try {
            throw new Exception("这是一个自定义错误");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "Hello Fox is testing~";
    }

}
JAVA、基础技术、技术与框架SprintBoot系列(五):异常处理机制插图
断点后调试,能进入

Java的 Throwable 分为: 环境异常Error和代码异常 Exception

Exception如果从编译和代码层面可分为:

  • Exception (可理解成: CheckException ) 检测异常,必须处理不然无法build
  • RuntimeException 运行时异常

如果我们定义了一个异常处理类extends Exception。结果就是检测异常

如果我们定义了一个异常处理类extends RuntimeException 。结果就是运行异常

那怎么更好的理解Exception和 RuntimeException ?

如果异常是能处理的,那就使用checkExption。 例如除法的分母不能为0;读取文件或者调用某个类的方法。

如果异常在编译时并不明确的,就使用RuntimeExption。例如: 例如查数据,返回是空,或者Sql错误;

从异常类型来分:已知异常和未知异常

已知异常是指:开发者思考了的异常,例如除法的分母不能为0,在代码处throw new RuntimeException(“已知异常”)

未知异常:开发者未思考的异常,例如除法的分母不能为0,并没有代码报错。

下面我们开始讲解如何自定义一个专用异常处理类,例如处于Http的webapi的

2、 自定义一个http的Exception

2.1、创建一个 HttpException并继承 RuntimException

2.1.1、在项目下创建一个package,命名为exception

2.1.2、创建HttpException 处理类(服务器异常)

因为该错误在编译时并不确定,所以使用继承RuntimeException

package com.fcors.fcors.exception;

public class HttpException extends RuntimeException{
    protected Integer code;
    protected Integer httpStatusCode = 500;
    public Integer getCode() {
        return code;
    }

    public Integer getHttpStatusCode() {
        return httpStatusCode;
    }
}

2.2、创建具体的异常处理类

2.2.1、创建一个 NotFoundException(404没找到)

package com.fcors.fcors.exception;

public class NotFoundException extends HttpException{
    public NotFoundException(int code){
        this.httpStatusCode=404;
        this.code=code;
    }
}

2.2.2、创建一个 ForbiddenException (403权限不足)

package com.fcors.fcors.exception;

public class ForbiddenException extends HttpException{
    public ForbiddenException(int code){
        this.httpStatusCode=403;
        this.code=code;
    }
}

2.3、代码调用层,使用这个专用自定义异常

package com.fcors.fcors.api.v1;

import com.fcors.fcors.exception.NotFoundException;
import com.fcors.fcors.sample.IConnect;
import com.fcors.fcors.sample.ISkill;
import com.fcors.fcors.sample.database.MySql;
import com.fcors.fcors.sample.hero.Diana;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/api/")
public class TestController {
    @Autowired
    private ISkill iskill;
    @GetMapping("/test")
    public String test2() throws NotFoundException {
        iskill.q();
        throw new NotFoundException(10001);
//        return "Hello Fox is testing~";
    }

}

此时我们执行,会进入全局异常类。

JAVA、基础技术、技术与框架SprintBoot系列(五):异常处理机制插图1

2.4、定义异常处理具体逻辑

在demo1的GlobalException中追加逻辑

package com.fcors.fcors.core;

import com.fcors.fcors.exception.HttpException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class GolbalExceptionAdvice {


    @ExceptionHandler(value=Exception.class)
    public void handleException(HttpServletRequest req,Exception e){
        System.out.println("全局异常报错处理");
    }
    @ExceptionHandler(value=HttpException.class)
    public void handelHttpException(HttpServletRequest req, HttpException e){
        System.out.println("专用HttpException异常报错处理");

    }
}

再次执行代码

JAVA、基础技术、技术与框架SprintBoot系列(五):异常处理机制插图2

进入专用的异常处理方法。 当有特定处理异常类时,全局异常则不进行。

下面我们思考一个问题:我们没有必要保留全局异常 ?

答案:必须保留, 异常除了我们自己定义的异常还有很多种未知异常

3、异常返回信息转换成Json

3.1、 UnifyResponse类

在core下面创建一个UnifyResponse类

如果一个类需要被序列化,就要设置一个get方法,例如demo2中的“HttpException”。如果 UnifyResponse 没有使用getter会报错。

package com.fcors.fcors.core;

public class UnifyResponse {
    private int code;
    private String message;
    private String request;
    
    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public String getRequest() {
        return request;
    }



    public  UnifyResponse(int code,String message,String request){
        this.code=code;
        this.message=message;
        this.request=request;
    }
}

3.2、修改GlobalExceptionAdvice

修改handleException方法

package com.fcors.fcors.core;

import com.fcors.fcors.exception.HttpException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class GolbalExceptionAdvice {


    @ExceptionHandler(value=Exception.class)
    @ResponseBody
    public UnifyResponse handleException(HttpServletRequest req, Exception e){
        System.out.println("全局异常报错处理");
        UnifyResponse message = new UnifyResponse(999,"测试错误","url");
        return message;
    }
    @ExceptionHandler(value=HttpException.class)
    public void handelHttpException(HttpServletRequest req, HttpException e){
        System.out.println("专用HttpException异常报错处理");

    }
}

3.3、在代码执行层抛出RunTimeException错误

package com.fcors.fcors.api.v1;

import com.fcors.fcors.exception.NotFoundException;
import com.fcors.fcors.sample.IConnect;
import com.fcors.fcors.sample.ISkill;
import com.fcors.fcors.sample.database.MySql;
import com.fcors.fcors.sample.hero.Diana;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/api/")
public class TestController {
    @Autowired
    private ISkill iskill;
    @GetMapping("/test")
    public String test2() throws RuntimeException {
        iskill.q();
        throw new RuntimeException("测试");
//        return "Hello Fox is testing~";
    }

}
JAVA、基础技术、技术与框架SprintBoot系列(五):异常处理机制插图3

但我们需要注意,此时我们只是改了返回的code,并没有修改实际http的返回状态码。

4、如何修改Http的返回状态

4.1.1、注解方式 @ResponseStatus

@ResponseStatus(code=HttpStatus.iNTERNAL_SERVER_ERROR)

修改GlobalExceptionAdvice

package com.fcors.fcors.core;

import com.fcors.fcors.exception.HttpException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class GolbalExceptionAdvice {


    @ExceptionHandler(value=Exception.class)
    @ResponseBody
    @ResponseStatus(code= HttpStatus.INTERNAL_SERVER_ERROR)
    public UnifyResponse handleException(HttpServletRequest req, Exception e){
        System.out.println("全局异常报错处理");
        UnifyResponse message = new UnifyResponse(999,"测试错误","url");
        return message;
    }
    @ExceptionHandler(value=HttpException.class)
    public void handelHttpException(HttpServletRequest req, HttpException e){
        System.out.println("专用HttpException异常报错处理");

    }
}

这个方式存在一些缺点,例如我们不能根据不同的结果,返回不同的状态码;我们不能返回专属状态码。如何解决呢?我们可以尝试方法二

4.1.2、 根据不同的异常结果返回不同的状态码,这样就不能写在@ResponseStatus中

通过修改GlobalExceptionAdvice中的handleHttpException

package com.fcors.fcors.core;

import com.fcors.fcors.exception.HttpException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class GolbalExceptionAdvice {


    @ExceptionHandler(value=Exception.class)
    @ResponseBody
    @ResponseStatus(code= HttpStatus.INTERNAL_SERVER_ERROR)
    public UnifyResponse handleException(HttpServletRequest req, Exception e){
        System.out.println("全局异常报错处理");
        UnifyResponse message = new UnifyResponse(999,"测试错误","url");
        return message;
    }
    @ExceptionHandler(value=HttpException.class)
    public ResponseEntity handelHttpException(HttpServletRequest req, HttpException e){
        String requestUrl = req.getRequestURI();
        String method = req.getMethod();

        UnifyResponse message = new UnifyResponse(e.getCode(),"errorMsg",method+" "+requestUrl);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpStatus httpStatus = HttpStatus.resolve(e.getHttpStatusCode());
        ResponseEntity<UnifyResponse> r = new ResponseEntity<>(message,headers,httpStatus);
        System.out.println("专用HttpException异常报错处理");
        return r;



    }
}

4.2、修改执行的方法层

package com.fcors.fcors.api.v1;

import com.fcors.fcors.exception.ForbiddenException;
import com.fcors.fcors.exception.NotFoundException;
import com.fcors.fcors.sample.IConnect;
import com.fcors.fcors.sample.ISkill;
import com.fcors.fcors.sample.database.MySql;
import com.fcors.fcors.sample.hero.Diana;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1/api/")
public class TestController {
    @Autowired
    private ISkill iskill;
    @GetMapping("/test")
    public String test2() throws RuntimeException {
        iskill.q();
        throw new ForbiddenException(10001);
//        return "Hello Fox is testing~";
    }

}

5、把错误code和message写到配置文件中

5.1、 创建配置文件

在resources下创建一个文件加config ,并创建(file)文件exception-code.properties

命名方式: [项目名].codes[10001]=”xxxx”

先回忆一下,之前是如何引入配置文件中的参数

通过@Value 读取配置。

但因为这个是动态的,所以 不通过 @Value方式

5.2、创建一个 ExceptionCodeConfiguration

在core下创建一个package[configuration] , 创建一个类ExceptionCodeConfiguration

package com.fcors.fcors.core.configuration;


import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@ConfigurationProperties(prefix="fcors")
@PropertySource(value="classpath:config/exception-code.properties")
@Component
public class ExceptionCodeConfiguration {
    private Map<Integer,String> codes = new HashMap<>();
    public Map<Integer, String> getCodes() {
        return codes;
    }

    public void setCodes(Map<Integer, String> codes) {
        this.codes = codes;
    }

    public String getMessage(int code){
        String message = codes.get(code);
        return message;
    }



}

5.3、 修改 GlobalExceptionAdvice中的handleHttpException

package com.fcors.fcors.core;

import com.fcors.fcors.core.configuration.ExceptionCodeConfiguration;
import com.fcors.fcors.exception.HttpException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class GolbalExceptionAdvice {
    @Autowired
    private ExceptionCodeConfiguration codeConfiguration;

    @ExceptionHandler(value=Exception.class)
    @ResponseBody
    @ResponseStatus(code= HttpStatus.INTERNAL_SERVER_ERROR)
    public UnifyResponse handleException(HttpServletRequest req, Exception e){
        System.out.println("全局异常报错处理");
        UnifyResponse message = new UnifyResponse(999,"测试错误","url");
        return message;
    }
    @ExceptionHandler(value=HttpException.class)
    public ResponseEntity handelHttpException(HttpServletRequest req, HttpException e){
        String requestUrl = req.getRequestURI();
        String method = req.getMethod();

        UnifyResponse message = new UnifyResponse(e.getCode(),codeConfiguration.getMessage(e.getCode()),method+" "+requestUrl);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpStatus httpStatus = HttpStatus.resolve(e.getHttpStatusCode());
        ResponseEntity<UnifyResponse> r = new ResponseEntity<>(message,headers,httpStatus);
        System.out.println("专用HttpException异常报错处理");
        return r;



    }
}
JAVA、基础技术、技术与框架SprintBoot系列(五):异常处理机制插图4

成功完成SpringBoot异常机制。

如何Properties文件编码乱码问题?

File>>>Settings

JAVA、基础技术、技术与框架SprintBoot系列(五):异常处理机制插图5

如果不生效,就重新输入 Properties 文件的中文

下面我们思考一个问题?如果传入的参数错误,导致报错,这种报错SpringBoot是否存在一种参数校验机制呢?

http://www.fcors.com/%e6%8a%80%e6%9c%af%e4%b8%8e%e6%a1%86%e6%9e%b6/sprintboot%e7%b3%bb%e5%88%97%ef%bc%88%e5%85%ad%ef%bc%89%ef%bc%9a%e5%8f%82%e6%95%b0%e6%a0%a1%e9%aa%8c%e6%9c%ba%e5%88%b6/