Spring Boot有不少很是好的特性,能够帮助咱们更快速的完成开发工做。今天和你们聊聊Spring boot的全局异常处理。
java
问题
一、spring boot中怎么进行全局异常处理?
二、为何个人404异常捕获不到?
三、常见的http请求异常,能统一封装成json返回吗?程序员
实战说明
项目依赖包:web
<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>
接口声明:spring
@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!";
}
}
自定义返回码枚举类:json
/**
* @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);
}
}
自定义业务异常:tomcat
/**
* 自定义业务异常
* @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());
}
}
一、使用@ControllerAdvice注解声明全局异常处理类
二、使用@ExceptionHandler指定要捕捉什么异常,这里会优先捕捉子级异常,当没有匹配到子级异常时,才会去匹配父级异常。好比同时声明了@ExceptionHandler( BusinessException.class )和@ExceptionHandler(Exception.class )方法进行异常处理,当抛出BusinessException异常时,只会被@ExceptionHandler( BusinessException.class )注解的方法捕获到。
三、经过@ResponseBody注解控制返回json格式数据。微信
重启项目,再次请求,结果以下。
说明咱们配置的BusinessException异常的全局捕获成功,也是按照咱们定义的异常码返回的JSON格式数据。

404异常捕捉
假设咱们去请求项目下一个不存在的url,会出现什么样的返回结果呢?
请求链路:http://localhost:8080/test
咱们会发现,返回的是一个404的异常页面,关键是后台居然没有打印任何异常日志。

首先,添加参数,控制异常抛出:
#出现错误时, 直接抛出异常
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对象不就行了,可是这里比较难获取本来的HttpStatus,不推荐。
因此,建议你们尽可能谨慎使用@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必定要配置为"/",
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的属性来定义路径。
总结
一、经过@ControllerAdvice、@ExceptionHandler、@ResponseBody三个注解的组合使用,实现全局异常处理。
二、经过配置spring.mvc.throw-exception-if-no-handler-found=true,控制404异常抛出
三、经过继承ResponseEntityExceptionHandler类,能够利用重写实现404异常的自定义格式返回
四、自定义业务异常和统一的接口返回数据格式,将CommonResp、ResultCode、BusinessException很好的结合使用。
五、404异常致使tomcat版本号泄露问题的解决
六、全局异常处理拦截不到404请求的缘由分析
不要老是抱怨平时工做的内容没有什么技术含量,不少小的功能特性,你真的掌握了吗?
点赞,关注,共勉,作一个真正的程序员。

更多精彩,关注我吧。

本文分享自微信公众号 - 跟着老万学java(douzhe_2019)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。