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格式返回结果不符。
声明全局异常处理:
/** * @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格式数据。
404异常捕捉
假设我们去请求一个不存在的项目下一个不存在的url,会出现什么样的返回结果呢?
请求链路:http://localhost:8080/test
我们会发现,返回的是一个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的状态码也和提示信息中的吻合。
这里提一点注意事项,在全局异常处理类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
分析:
这是由于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的异常处理,有更深的了解。
里面默认对如下异常进行了捕捉处理。
核心处理流程:
可以发现,默认的实现中,返回结构都是为空。
这就是我们在继承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); } }
如果只是简单继承,不封装返回值的话,请求结果如下:
定义server.servlet.context-path后,异常捕获失败
新增server.servlet.context-path属性,让servlet拦截所有与/tax匹配的请求
server.servlet.context-path=/tax
请求如下链接:http://localhost:8080/testGet
分析:
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异常,会泄露中间件的版本信息。
在很多安全级别比较高的项目中,由于需要进行安全扫描,如果发现中间件的版本信息,就容易针对性的进行攻击,是一个非常常见的中间件版本信息泄露的安全漏洞问题。
经研究发现,该问题是由于引入了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请求的原因分析