【造个轮子系列】之springboot自定义异常+全局异常处理

前言

公司开发模式为前后端分离,为了统一返给前端的数据,所以我们就自定义异常,并且全局处理这些异常。然后我以此为基础写了一个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">&#xe664;</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>

写在最后的话

其实全局异常处理的方法各不相同,返回前端的数据格式也是各种各样,但是万变不离其宗。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值