前言
公司开发模式为前后端分离,为了统一返给前端的数据,所以我们就自定义异常,并且全局处理这些异常。然后我以此为基础写了一个springboot的starter,又添加了一些其他功能,在此作为记录。
一、自定义异常
首先,为了统一返回数据,就要写个类封装一下返回的数据,正确的时候把数据放进去,错误的时候就要返回错误信息,为了方便管理,我们就需要把这些错误信息放进枚举里。但是我们是要造个轮子,让别人使用,所以就要写个接口。我已经牢牢记住这一点了,我好几个老师都这么教我的,哈哈。
我们规定返回的数据格式为:
{"code":1001,"msg":"成功","data":null}
那么,就写个接口吧
public interface ExceptionEnum {
void setCode(Integer code);
void setMsg(String msg);
Integer getCode();
String getMsg();
}
其次,就是写个类继承RuntimeException,添加一个code属性和一些构造方法。
public class MyException extends RuntimeException {
private static final long serialVersionUID = 5579492723835964916L;
private Integer code;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public MyException( Integer code,String message) {
super(message);
this.code = code;
}
public MyException( Integer code,String message, Throwable cause) {
super(message, cause);
this.code = code;
}
/**2019年4月28日 下午6:21:38 添加*/
public MyException(ExceptionEnum exceptionEnum) {
super(exceptionEnum.getMsg());
Integer code = exceptionEnum.getCode();
this.code = code;
}
/**2019年4月28日 下午6:21:38 添加*/
public MyException(ExceptionEnum exceptionEnum,Throwable cause) {
super(exceptionEnum.getMsg(),cause);
Integer code = exceptionEnum.getCode();
this.code = code;
}
}
二、封装返回数据
上面也说了,需要写个类,封装返回的数据。
public class ResultBO<T> implements Serializable {
private static final long serialVersionUID = -4006037402528281189L;
/** 错误码 */
private Integer code;
/** 提示信息 */
private String msg;
/**
* 返回的具体内容
*/
private T data;
public ResultBO(Integer code,String msg,T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "ResultBO [code=" + code + ", msg=" + msg + ", data=" + data + "]";
}
}
三、返回数据工具类
为了方便构造封装的类,我们需要写一个工具类。成功时调用success方法,出现错误时调用error方法,传入错误信息枚举。
public class ResultTool {
private ResultTool(){}
/**
* 成功时调用,调用者不需要数据
* <br> 2018年4月18日上午11:24:15
*/
public static ResultBO<Object> success(){
return success(null);
}
/**
* 成功时调用,并返回 data数据
* <br>2018年4月18日上午11:22:40
*/
public static ResultBO<Object> success(Object data){
return success("成功",data);
}
/**
* 成功时调用,向调用者返回提示信息,并返回指定格式的数据
*/
public static ResultBO<Object> success(String msg,Object data){
return new ResultBO<Object>(0, msg, data);
}
/**
* 失败时调用<br>
* (传入参数为null时,会自动设置 code为-1,msg为"未定义异常")<br>
* 2018年4月18日上午11:27:45
*/
public static ResultBO<Object> error(ExceptionEnum en){
if(en ==null){
return new ResultBO<>(-1, "未定义异常", null);
}
return new ResultBO<>(en.getCode(), en.getMsg(), null);
}
public static ResultBO<Object> error(Integer code,String msg){
if(code == null){
code = -1;
}
if(msg == null){
msg = "未定义异常";
}
msg = msg.trim();
return new ResultBO<Object>(code, msg, null);
}
public static ResultBO<Object> error(Integer code,String msg,Object cause){
if(code == null){
code = -1;
}
if(msg == null){
msg = "未定义异常";
}
msg = msg.trim();
return new ResultBO<Object>(code, msg,cause);
}
}
四、全局异常处理
前面的都是准备工作,接下来才是重点。
首先写一个配置类,配置异常处理的一些可能会变得属性。
enablePrintStackTrace 是当出现异常时,是否打印异常信息
requestType 是请求的类型,不分离,分离还有两种都有。
errorViewName 是在不分离的时候发生异常跳转的异常页面。
@ConfigurationProperties(prefix="spring.exception")
public class ErrorProperties {
/**
* 启用错误信息打印
*/
private boolean enablePrintStackTrace = true;
/**
* 异常处理时,请求的类型
*/
private RequestType requestType = RequestType.DEFAULT;
/**
* 当请求不为ajax时,错误页面的路径
*/
private String errorViewName = "error/err";
public boolean isEnablePrintStackTrace() {
return enablePrintStackTrace;
}
public void setEnablePrintStackTrace(boolean enablePrintStackTrace) {
this.enablePrintStackTrace = enablePrintStackTrace;
}
public RequestType getRequestType() {
return requestType;
}
public void setRequestType(RequestType requestType) {
this.requestType = requestType;
}
public String getErrorViewName() {
return errorViewName;
}
public void setErrorViewName(String errorViewName) {
this.errorViewName = errorViewName;
}
public enum RequestType{
/**
* 默认前后端不分离 模式
*/
DEFAULT(1,"default"),
/**
* 前后端分离模式
*/
AJAX(2,"ajax"),
/**
* 两种模式
*/
ALL(3,"all");
private Integer type;
private String msg;
public Integer getType() {
return type;
}
public String getMsg() {
return msg;
}
private RequestType(Integer type, String msg) {
this.type = type;
this.msg = msg;
}
}
}
其次,我们需要@ControllerAdvice这个注解来完成全局异常的处理。在这里,我处理了三种情况,前后端不分离模式、前后端分离模式、两种模式共存,不同的请求方式会响应不同的结果。
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
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.servlet.ModelAndView;
import work.waynelee.exception.properties.ErrorProperties;
/**
* 异常拦截器
* @author lwq
* 2019年1月15日 下午1:14:21
*/
@ControllerAdvice
@Order(2)
public class MyExceptionHandler {
@Autowired
private ErrorProperties errorProperties;
/**
* 全局异常处理,根据请求头数据的不同,判断是否为ajax请求,做出相应的处理
* @param e
* @param request
* @return
*/
@ExceptionHandler(value=Exception.class)
@ResponseBody
public Object exceptionHandler(Exception e,HttpServletRequest request){
Integer type = errorProperties.getRequestType().getType();
//如果是前后端不分离模式
if (Objects.equals(type,1)) {
return resoleDefaultException(e, request);
//如果是前后端分离的模式
}else if (Objects.equals(type,2)) {
return resoleAjaxException(e);
//两种模式都有
}else{
return resoleAllException(e, request);
}
}
/**
* 前后端不分离模式时处理异常
* @param e
* @param request
* @return
*/
public ModelAndView resoleDefaultException(Exception e,HttpServletRequest request){
boolean enablePrintStackTrace = errorProperties.isEnablePrintStackTrace();
String url = request.getRequestURL().toString();
ModelAndView mav = new ModelAndView();
mav.setViewName(errorProperties.getErrorViewName());
mav.addObject("url", url);
if(e instanceof MyException){
MyException em = (MyException) e;
mav.addObject("code",em.getCode());
mav.addObject("msg", em.getMessage());
}else if(e instanceof NullPointerException){
if (enablePrintStackTrace) {
e.printStackTrace();
}
mav.addObject("msg", "空指针异常");
}else{
if (enablePrintStackTrace) {
e.printStackTrace();
}
mav.addObject("msg", e.getMessage());
}
mav.addObject("stackTrace",e.getStackTrace()[0]);
return mav;
}
/**
* 前后端分离模式下处理异常信息
* @param e
* @return
*/
public ResultBO<Object> resoleAjaxException(Exception e){
boolean enablePrintStackTrace = errorProperties.isEnablePrintStackTrace();
//判定是否是自己定义的异常
if(e instanceof MyException){
MyException em = (MyException) e;
return ResultTool.error(em.getCode(),em.getMessage());
//判断是否为空指针异常
}else if(e instanceof NullPointerException){
if (enablePrintStackTrace) {
e.printStackTrace();
}
return ResultTool.error(2,"空指针异常");
}
if (enablePrintStackTrace) {
e.printStackTrace();
}
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
//e.printStackTrace(pw);
pw.flush();
sw.flush();
String result = sw.toString();
String regEx = "Caused by:(.*)";
Pattern pat = Pattern.compile(regEx);
Matcher mat = pat.matcher(result);
boolean rs = mat.find();
if (rs) {
return ResultTool.error(2,e.getClass().getName()+" : "+e.getMessage(),mat.group(1));
}else{
//返回系统异常
return ResultTool.error(2,e.getClass().getName()+" : "+e.getMessage(),e.getStackTrace()[0]);
}
}
/**
* 两种模式都有的情况下处理异常
* @param e
* @param request
* @return
*/
public Object resoleAllException(Exception e,HttpServletRequest request){
String url = request.getRequestURL().toString();
//判断是否为ajax请求
String acceptHeader = request.getHeader("Accept");
String xRequestedWith = request.getHeader("x-requested-with");
String referer = request.getHeader("Referer");
String postmanToken = request.getHeader("postman-token");
//如果是ajax请求
if ((acceptHeader != null && acceptHeader.contains("application/json"))
|| xRequestedWith != null && "XMLHttpRequest".equals(xRequestedWith)
|| referer != null && !url.equalsIgnoreCase(referer)
|| postmanToken != null) {
return resoleAjaxException(e);
}else{
return resoleDefaultException(e, request);
}
}
}
五、自动配置
会写springboot starter的都知道,最后我们都需要一个AutoConfiguration来完成你所写的Bean的实例化以及一些其他配置。
@Configuration
@EnableConfigurationProperties(value={ErrorProperties.class})
public class ExceptionAutoConfiguration {
@Bean
public MyExceptionHandler myExceptionHandler(){
MyExceptionHandler myExceptionHandler = new MyExceptionHandler();
return myExceptionHandler;
}
}
@EnableConfigurationProperties就是启用自己写的配置类。
这样还没完,之前在【造个轮子系列】之自定义验证码的 springboot starter 里也说过。我们还需要配置点东西,让spring找到这个配置类。
但是这次我们不写配置文件,我们用注解。最主要的就是@Import这个注解,意思就是:唉!spring,你要用ExceptionAutoConfiguration这个类来完成我想要的。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({ExceptionAutoConfiguration.class})
public @interface EnableExceptionHandling {
}
六、使用
使用起来也是非常简单,首先在启动类上使用这个注解。
@ServletComponentScan
@SpringBootApplication
@EnableExceptionHandling
public class BlogApplication extends SpringBootServletInitializer{
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(BlogApplication.class);
}
}
其次,自定义一个枚举实现上面的那个接口。
public enum LwqExceptionEnum implements ExceptionEnum{
/**
* code : 101 <br>
* msg : 成功
*/
SUCCESS(101,"成功"),
/**
* code : 102 <br>
* msg : 失败
*/
FAIL(102,"失败")
;
private Integer code;
private String msg;
private LwqExceptionEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
前后端分离模式下在controller层返回ResultBO即可
@GetMapping("/article")
public ResultBO<Object> listClassify()throws Exception{
if ("有错误") {
return ResultTool.error(LwqExceptionEnum.FAIL);
}
return ResultTool.success();
}
前后端不分离的话可以不做任何处理,会跳转到默认的错误页面。如果你觉得默认不太好看,也可以自定义页面。在templates下新建error文件夹,新建html文件。文件夹和错误页面的名字都可以在配置文件中修改。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta name="viewport" content="width=device-width" />
<meta name="author" content="www.waynelee.work" />
<meta name="robots" content="all" />
<title>出错了</title>
<link rel="icon" href="https://img.waynelee.work/blog/lee.ico" type="image/x-icon" />
<link rel="stylesheet" href="../../../layui/css/layui.css" />
<link rel="stylesheet" href="../../../css/admin.css" />
</head>
<body>
<div class="layui-fluid">
<div class="layadmin-tips">
<i class="layui-icon"></i>
<div class="layui-text">
<div th:switch = "${status != null}">
<h1 th:case = "true" th:text="${status}"></h1>
<div th:case = "false" style="text-align:left">
<h2>服务器出现异常</h2><br />
<h3>url:<span th:text="${url}"></span></h3>
<h3>code:<span th:text="${code}"></span></h3>
</div>
</div>
</div>
</div>
</div>
<script src="../../layui/layui.js"></script>
</body>
</html>
写在最后的话
其实全局异常处理的方法各不相同,返回前端的数据格式也是各种各样,但是万变不离其宗。