Spring Boot之全局异常处理:404异常为何捕获不到?

简介: Spring Boot之全局异常处理:404异常为何捕获不到?

Spring Boot有很多非常好的特性,可以帮助我们更快速的完成开发工作。今天和大家聊聊Spring boot的全局异常处理


问题


1、spring boot中怎么进行全局异常处理?

2、为什么我的404异常捕获不到?

3、常见的http请求异常,能统一封装成json返回吗?


实战说明


项目依赖包:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>


接口声明:

@SpringBootApplication
@RestController
public class ErrorApplication {
    public static void main(String[] args) {
        SpringApplication.run(ErrorApplication.class, args);
    }
    @GetMapping("/hello")
    public String hello(){
        return "hello laowan!";
    }
    @GetMapping("/testGet")
    public String testGet(String name) throws Exception {
        if (name==null) {
           throw new BusinessException(ResultCode.PAPAM_IS_BLANK);
        }
        return "laowan!";
    }
    @PostMapping("/testPost")
    public String testPost(){
        return "post laowan!";
    }
}


自定义返回码枚举类:

/**
 * @program: error
 * @description:返回状态码
 * @author: wanli
 * @create: 2020-05-09 22:03
 **/
@Getter
public enum ResultCode {
    /*成功状态吗*/
    SUCCESS(1,"成功"),
    /*系统异常:4001-1999*/
    SYS_ERROR(4000,"系统异常,请稍后重试"),
    /*参数错误:1001-1999*/
     PAPAM_IS_INVALID(1001,"参数无效"),
     PAPAM_IS_BLANK(1002,"参数为空"),
     PAPAM_TYPE_BIND_ERROR(1003,"参数类型错误"),
     PAPAM_NOT_COMPLETE(1003,"参数缺失"),
    /*用户错误:2001-2999*/
    USER_NOT_LOGGED_IN(2001,"用户未登录,请登录后重试"),
    USER_LOGIN_ERROR(2002,"账号不存在或密码错误"),
    USER_ACCOUNT_FORBIDDERN(2003,"账号已被禁用"),
    USER_NOT_EXIST(2004,"用户不存在"),
    USER_HAS_EXISTED(2005,"账号已存在")
    ;
    //状态码
    private Integer code;
    //提示信息
    private String message;
    ResultCode(Integer code,String message){
        this.code = code;
        this.message = message;
    }
}


通用返回类:

/**
 * 通用返回响应
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
public class CommonResp<T> {
    private Integer code;
    private String message;
    private T data;
    public CommonResp(ResultCode resultCode) {
        this.code=resultCode.getCode();
        this.message=resultCode.getMessage();
    }
    public CommonResp(ResultCode resultCode, T data) {
        this.code=resultCode.getCode();
        this.message=resultCode.getMessage();
        this.data = data;
    }
    public CommonResp(Integer code,String message) {
        this.code=code;
        this.message=message;
    }
    public static <T> CommonResp create(ResultCode resultCode) {
        return new CommonResp( resultCode);
    }
    public static <T> CommonResp getErrorResult(String message) {
        return new CommonResp(-1,message);
    }
    public static <T> CommonResp create(ResultCode resultCode, T data) {
        return new CommonResp( resultCode,data);
    }
}


自定义业务异常:

/**
 * 自定义业务异常
 * @program: error
 * @description:
 * @author: wanli
 * @create: 2020-05-09 21:49
 **/
@Getter
public class BusinessException extends  Exception{
    private ResultCode resultCode;
    public BusinessException(){}
    public BusinessException(ResultCode resultCode){
        super(resultCode.getMessage());
        this.resultCode = resultCode;
    }
    public BusinessException(String message){
        super(message);
    }
}


如果我们不进行异常处理,直接抛出BusinessException异常的话,请求接口如下:

请求链接:http://localhost:8080/testGet

返回结果如下,是一个异常提示页面,显然和我们现在主流的前后端分离,统一采用json格式返回结果不符。

37.png


声明全局异常处理:

/**
 * @ClassName: GlobalExceptionHandler
 * @Description: 异常处理
 * @date: 2017年6月6日 下午2:12:08
 */
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler{
    /**
     * 业务异常处理
     * @param e
     * @return
     * @throws Exception
     */
    @ResponseBody
    @ExceptionHandler( BusinessException.class )
    public CommonResp handleBusinessException (BusinessException e ) throws Exception {
        log.error("BusinessException error", e);
        return CommonResp.create(e.getResultCode());
    }
}


1、使用@ControllerAdvice注解声明全局异常处理类

2、使用@ExceptionHandler指定要捕捉什么异常,这里会优先捕捉子级异常,当没有匹配到子级异常时,才会去匹配父级异常。比如同时声明了@ExceptionHandler( BusinessException.class )和@ExceptionHandler(Exception.class )方法进行异常处理,当抛出BusinessException异常时,只会被@ExceptionHandler( BusinessException.class )注解的方法捕获到。

3、通过@ResponseBody注解控制返回json格式数据。


重启项目,再次请求,结果如下。

说明我们配置的BusinessException异常的全局捕获成功,也是按照我们定义的异常码返回的JSON格式数据。

36.png


404异常捕捉


假设我们去请求一个不存在的项目下一个不存在的url,会出现什么样的返回结果呢?

请求链路:http://localhost:8080/test

35.png

我们会发现,返回的是一个404的异常页面,关键是后台竟然没有打印任何异常日志。


那么针对这类不是经由请求接口里面抛出的异常,我们怎么去捕捉,并封装成json格式进行返回呢?


首先,添加参数,控制异常抛出:


#出现错误时, 直接抛出异常
spring.mvc.throw-exception-if-no-handler-found=true
#不要为我们工程中的资源文件建立映射
spring.resources.add-mappings=false


然后继承ResponseEntityExceptionHandler,封装异常处理

@ControllerAdvice
@Slf4j
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
    public RestResponseEntityExceptionHandler() {
        super();
    }
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        log.error(ex.getMessage(),ex);
        if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
            request.setAttribute("javax.servlet.error.exception", ex, 0);
        }
        return new ResponseEntity( new CommonResp(status.value(),ex.getMessage()), headers, status);
    }
 }

再次请求,发现404异常捕获成功,并返回json异常提示。


请求的HttpStatus的状态码也和提示信息中的吻合。

34.png


这里提一点注意事项,在全局异常处理类GlobalExceptionHandler中,尽量不要为了方便,直接对Exception异常进行捕获处理,会影响返回结果的HttpStatus。

我们演示一下:

/**
 * 统一异常处理
 * @param e
 * @return
 * @throws Exception
 */
@ResponseBody
@ExceptionHandler( Exception.class )
public CommonResp handleException (Exception e){
  log.error( "Exception error", e );
  return  CommonResp.getErrorResult(e.getMessage());
}


然后再次请求http://localhost:8080/test

33.png

32.png


分析:

这是由于RestResponseEntityExceptionHandler类先对异常处理,返回ResponseEntity,由于ResponseEntity中的HttpStatus是一个异常码,异常会紧接着被我们自定义的GlobalExceptionHandler类中的@ExceptionHandler( Exception.class )捕获,这里由于返回的是一个封装的CommonResp对象,而不是一个ResponseEntity对象,默认就相当于把异常捕捉封装处理了,虽然返回的结果数据是json数据,异常提示也正确,但是原本HttpStatu为404的请求竟然变成了200成功请求,显然不是我们想要的。


有人可能会说,我在@ExceptionHandler( Exception.class )方法里面,也封装返回一个ResponseEntity对象不就好了,但是这里比较难获取原本的HttpStatu,不推荐。


所以,建议大家尽量谨慎使用@ExceptionHandler( Exception.class)去进行异常处理,而是针对具体的异常进行特定处理。


最后,推荐大家看看ResponseEntityExceptionHandler类的源码,会对Spring Boot中对ResponseEntity的异常处理,有更深的了解。

里面默认对如下异常进行了捕捉处理。

31.png

核心处理流程:

30.png

可以发现,默认的实现中,返回结构都是为空。

29.png

这就是我们在继承ResponseEntityExceptionHandler类后,重写handleExceptionInternal类的原因:

@ControllerAdvice
@Slf4j
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
    public RestResponseEntityExceptionHandler() {
        super();
    }
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        log.error(ex.getMessage(),ex);
        if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
            request.setAttribute("javax.servlet.error.exception", ex, 0);
        }
        //通过HttpStatus返回码和异常名称封装返回结果
        return new ResponseEntity( new CommonResp(status.value(),ex.getMessage()), headers, status);
    }
}

如果只是简单继承,不封装返回值的话,请求结果如下:

28.png


定义server.servlet.context-path后,异常捕获失败


新增server.servlet.context-path属性,让servlet拦截所有与/tax匹配的请求


server.servlet.context-path=/tax

请求如下链接:http://localhost:8080/testGet

27.png

分析:

server.servlet.context-path默认为"/",即servlet拦截tomcat下的所有请求。

如果配置为server.servlet.context-path=/tax,那么tomcat只会将请求路径匹配的请求转发到项目中。

这也是很多人疑惑,为什么已经在spring boot项目中配置了全局异常处理,

但是当前请求localhost:8080/testGet时,404异常请求没有被项目中配置的全局异常处理捕获。

因为请求根本没有进你的项目中,而且直接被tomcat处理了,所以明明请求报404失败,但是你的工程下没有任何异常日志提示,全局异常处理也没有生效。


可以想想下以前使用单独的web服务器部署项目,如果你的请求路径没有和server.servlet.context-path匹配的话,请求根本就没有进入你的项目中。


所以,如果希望对进入tomcat的所有请求都进行处理的话,server.servlet.context-path一定要配置为"/",

这样你才能在代码中,对相关异常做处理,不然,就是直接tomcat返回的默认异常页面了。


404异常抛出tomcat版本信息问题


有时候我们会发现,经由tomcat直接抛出的404异常,会泄露中间件的版本信息。

26.png

在很多安全级别比较高的项目中,由于需要进行安全扫描,如果发现中间件的版本信息,就容易针对性的进行攻击,是一个非常常见的中间件版本信息泄露的安全漏洞问题。


经研究发现,该问题是由于引入了spring-boot-devtools包导致的。

解决办法有2种,

方法一:简单暴力的去除spring-boot-devtools包依赖。

方法二:通过设置scope为provided,使该包只在测试时有效,编译打包时自动过滤该jar包依赖。


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>provided</scope>
    <optional>true</optional>
</dependency>


给大家复习下Maven的scope属性的作用:

1.compile:默认值 他表示被依赖项目需要参与当前项目的编译,还有后续的测试,运行周期也参与其中,是一个比较强的依赖。打包的时候通常需要包含进去


2.test:依赖项目仅仅参与测试相关的工作,包括测试代码的编译和执行,不会被打包,例如:junit


3.runtime:表示被依赖项目无需参与项目的编译,不过后期的测试和运行周期需要其参与。与compile相比,跳过了编译而已。例如JDBC驱动,适用运行和测试阶段


4.provided:打包的时候可以不用包进去,别的设施会提供。事实上该依赖理论上可以参与编译,测试,运行等周期。相当于compile,但是打包阶段做了exclude操作


5.system:从参与度来说,和provided相同,不过被依赖项不会从maven仓库下载,而是从本地文件系统拿。需要添加systemPath的属性来定义路径。


总结


1、通过@ControllerAdvice、@ExceptionHandler、@ResponseBody三个注解的组合使用,实现全局异常处理。

2、通过配置spring.mvc.throw-exception-if-no-handler-found=true,控制404异常抛出

3、通过继承ResponseEntityExceptionHandler类,可以利用重写实现404异常的自定义格式返回

4、自定义业务异常和统一的接口返回数据格式,将CommonResp、ResultCode、BusinessException很好的结合使用。

5、404异常导致tomcat版本号泄露问题的解决

6、全局异常处理拦截不到404请求的原因分析

目录
相关文章
|
3月前
|
JSON Java 数据库
第08课:Spring Boot中的全局异常处理
第08课:Spring Boot中的全局异常处理
504 0
|
7月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
本文介绍了在Spring Boot项目中如何通过创建`GlobalExceptionHandler`类来全局处理系统异常。通过使用`@ControllerAdvice`注解,可以拦截项目中的各种异常,并结合`@ExceptionHandler`注解针对特定异常(如参数缺失、空指针等)进行定制化处理。文中详细展示了处理参数缺失异常和空指针异常的示例代码,并说明了通过拦截`Exception`父类实现统一异常处理的方法。虽然拦截`Exception`可一劳永逸,但为便于问题排查,建议优先处理常见异常,最后再兜底处理未知异常,确保返回给调用方的信息友好且明确。
874 0
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
|
9月前
|
XML Java 应用服务中间件
Spring Boot 两种部署到服务器的方式
本文介绍了Spring Boot项目的两种部署方式:jar包和war包。Jar包方式使用内置Tomcat,只需配置JDK 1.8及以上环境,通过`nohup java -jar`命令后台运行,并开放服务器端口即可访问。War包则需将项目打包后放入外部Tomcat的webapps目录,修改启动类继承`SpringBootServletInitializer`并调整pom.xml中的打包类型为war,最后启动Tomcat访问应用。两者各有优劣,jar包更简单便捷,而war包适合传统部署场景。需要注意的是,war包部署时,内置Tomcat的端口配置不会生效。
2199 17
Spring Boot 两种部署到服务器的方式
|
7月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
302 0
|
7月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——定义返回的统一 json 结构
本课主要讲解Spring Boot中的全局异常处理方法。在项目开发中,各层操作难免会遇到各种异常,若逐一处理将导致代码耦合度高、维护困难。因此,需将异常处理从业务逻辑中分离,实现统一管理与友好反馈。本文通过定义一个简化的JsonResult类(含状态码code和消息msg),结合全局异常拦截器,展示如何封装并返回标准化的JSON响应,从而提升代码质量和用户体验。
151 0
|
7月前
|
Java 数据库 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——指定项目配置文件
在实际项目中,开发环境和生产环境的配置往往不同。为简化配置切换,可通过创建 `application-dev.yml` 和 `application-pro.yml` 分别管理开发与生产环境配置,如设置不同端口(8001/8002)。在 `application.yml` 中使用 `spring.profiles.active` 指定加载的配置文件,实现环境快速切换。本节还介绍了通过配置类读取参数的方法,适用于微服务场景,提升代码可维护性。课程源码可从 [Gitee](https://gitee.com/eson15/springboot_study) 下载。
261 0
|
11月前
|
开发框架 Java UED
如何使用 Spring Boot 实现异常处理
如何使用 Spring Boot 实现异常处理
429 2
|
11月前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
574 2
|
11月前
|
Dubbo Java 应用服务中间件
深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案
本文深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案。通过检查GraalVM版本兼容性、配置反射列表、使用代理类、检查配置文件、禁用不支持的功能、查看日志文件、使用GraalVM诊断工具和调整GraalVM配置等步骤,帮助开发者快速定位并解决问题,确保服务的正常运行。
352 1
深入实践springboot实战 蓄势待发 我不是雷锋 我是知识搬运工
springboot,说白了就是一个集合了功能的大类库,包括springMVC,spring,spring data,spring security等等,并且提供了很多和可以和其他常用框架,插件完美整合的接口(只能说是一些常用框架,基本在github上能排上名次的都有完美整合,但如果是自己写的一个框架就无法实现快速整合)。