上一节我们讲到SpringBoot的包扫描机制
现在开始讲解SpringBoot的异常处理机制。
回顾下之前发表过一个java文章,简单讲解过java的异常处理
现在开始讲解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的 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~";
}
}
此时我们执行,会进入全局异常类。
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异常报错处理");
}
}
再次执行代码
进入专用的异常处理方法。 当有特定处理异常类时,全局异常则不进行。
下面我们思考一个问题:我们没有必要保留全局异常 ?
答案:必须保留, 异常除了我们自己定义的异常还有很多种未知异常
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~";
}
}
但我们需要注意,此时我们只是改了返回的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;
}
}
成功完成SpringBoot异常机制。
如何Properties文件编码乱码问题?
File>>>Settings
如果不生效,就重新输入 Properties 文件的中文
下面我们思考一个问题?如果传入的参数错误,导致报错,这种报错SpringBoot是否存在一种参数校验机制呢?