Spring异常处理 ExceptionHandler的使用
通常一个web程序在运行过程中,由于用户的操作不当,或者程序的bug,有大量需要处理的异常。其中有些异常是需要暴露给用户的,比如登陆超时,权限不足等等。可以通过弹出提示信息的方式告诉用户出了什么错误。
而这就表示在程序中需要一个机制,去处理这些异常,将程序的异常转换为用户可读的异常。而且最重要的,是要将这个机制统一,提供统一的异常处理。因为我设计这个结构的主要目的是为了简化代码。
在探寻spring的异常处理机制的时候,我分别使用了三种方式。三种方式都是使用的@ExceptionHandler注解。
当一个Controller中有方法加了@ExceptionHandler之后,这个Controller其他方法中没有捕获的异常就会以参数的形式传入加了@ExceptionHandler注解的那个方法中。
首先需要为自己的系统设计一个自定义的异常类,通过它来传递状态码。
/** * Created by 47. * 自定义异常 */ public class SystemException extends RuntimeException{ private String code;//状态码 public SystemException(String message, String code) { super(message); this.code = code; } public String getCode() { return code; } }
第一种思路,设计一个基类。
/** * Created by 47. * 处理异常的类,需要处理异常的Controller直接继承这个类 */ public class BaseController { /** * 处理Controller抛出的异常 * @param e 异常实例 * @return Controller层的返回值 */ @ExceptionHandler @ResponseBody public Object expHandler(Exception e){ if(e instanceof SystemException){ SystemException ex= (SystemException) e; return WebResult.buildResult().status(ex.getCode()) .msg(ex.getMessage()); }else{ e.printStackTrace(); return WebResult.buildResult().status(Config.FAIL) .msg("系统错误"); } } }
之后所有需要异常处理的Controller都继承这个类,从而获取到异常处理的方法。
虽然这种方式可以解决问题,但是极其不灵活,因为动用了继承机制就只为获取一个默认的方法,这显然是不好的。
第二种思路,将这个基类变为接口,提供此方法的默认实现(也就是接口中的default方法,java8开始支持接口方法的默认实现)
/** * Created by 47. * 接口形式的异常处理 */ public interface DataExceptionSolver { @ExceptionHandler @ResponseBody default Object exceptionHandler(Exception e){ try { throw e; } catch (SystemException systemException) { systemException.printStackTrace(); return WebResult.buildResult().status(systemException.getCode()) .msg(systemException.getMessage()); } catch (Exception e1){ e1.printStackTrace(); return WebResult.buildResult().status(Config.FAIL) .msg("系统错误"); } } }
这种方式虽然没有占用继承,但是也不是很优雅,因为几乎所有的Controller都需要进行异常处理,于是我每个Controller都需要去写implement DataExceptionSolver,这显然不是我真正想要的。况且这种方式依赖java8才有的语法,这是一个很大的局限。
第三种思路,使用加强Controller做全局异常处理。
所谓加强Controller就是@ControllerAdvice注解,有这个注解的类中的方法的某些注解会应用到所有的Controller里,其中就包括@ExceptionHandler注解。
于是可以写一个全局的异常处理类:
/** * Created by 47 * 全局异常处理,捕获所有Controller中抛出的异常。 */ @ControllerAdvice public class GlobalExceptionHandler { //处理自定义的异常 @ExceptionHandler(SystemException.class) @ResponseBody public Object customHandler(SystemException e){ e.printStackTrace(); return WebResult.buildResult().status(e.getCode()).msg(e.getMessage()); } //其他未处理的异常 @ExceptionHandler(Exception.class) @ResponseBody public Object exceptionHandler(Exception e){ e.printStackTrace(); return WebResult.buildResult().status(Config.FAIL).msg("系统错误"); }
}
或者:
@ControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(Exception.class) @ResponseBody public Result dbExceptionHandler(Exception e){ logger.error("GlobalExceptionHandler: {}", e); return ResultBuilder.failure(null).msg(e.getMessage()).build(); } }
这个类中只处理了两个异常,但是已经满足了大部分需要,如果还有需要特殊处理的地方,可以再加上处理的方法就行了。第三种实现方式是目前我知道的最优雅的方式了。
如此,我们现在的Controller中的方法就可以很简洁了,比如处理登陆的逻辑就可以这样简单的写:
/** * Created by 47 * 账号 */ @RestController @RequestMapping("passport") public class PassportController { PassportService passportService; @RequestMapping("login") public Object doLogin(HttpSession session, String username, String password){ User user = passportService.doLogin(username, password); session.setAttribute("user", user); return WebResult.buildResult().redirectUrl("/student/index"); } }
而在passprotService的doLogin方法中,可能会抛出用户名或密码错误等异常,然后就会交由GlobalExceptionHandler去处理,直接返回异常信息给前端,然后前端也不需要关心是否返回了异常,因为这些都已经定义好了。
前端js代码只需要这样写:
//登陆 AJAX.POST("/passport/login", { username:name, password:psw })
一个异常在其中流转的过程为:
比如doLogin方法抛出了自定义异常,其code为:FAIL,message为:用户名或密码错误,由于在controller的方法中没有捕获这个异常,所以会将异常抛给GlobalExceptionHandler,然后GlobalExceptionHandler通过WebResult将状态码和提示信息返回给前端,前端通过默认的处理函数,弹框提示用户“用户名或密码错误”。而对于这样的一次交互,我们根本不用编写异常处理部分的逻辑。
到这里,代码已经简洁了很多,而且重用性大大提高。