SpringBoot + Vue前后端分离开发:全局异常处理及统一结果封装
文章目录
- SpringBoot + Vue前后端分离开发:全局异常处理及统一结果封装
- 前后端分离开发中的异常处理
- 统一结果封装
- 后端全局异常捕获及处理
- 前端axios后置拦截器
前后端分离开发中的异常处理
在我们的前后端分离项目中,有时候不可避免发生后端服务报错抛出异常的情况,如果不配置全局异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端,然后再由前端给用户返回能够使其理解的报错信息,而不是抛一个java exception给用户。
后端给前端返回错误信息,前端需要能接收到此错误信息并告知用户,也即前端也需要进行异常处理,在Vue项目中,前端都是用axios向后端发送请求,前端需要配置全局axios拦截器,拦截后端给我们返回的http response,判断请求是否成功,失败的话则返回相应错误信息
在上述前后端交互的过程中,有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。否则后端返回的数据五花八门,前端也无法判断请求是否成功了
统一结果封装
统一结果封装是由前后端协商共同确定的,对于不同的项目可能封装类不同。
在此我们用一个简单的统一结果封装类来演示这个前后端交互的重要环节。一般来说,统一结果封装类里面有几个要素必要的:
- 请求是否成功,可用code表示(如200表示成功,400表示异常)
- 请求结果消息message
- 请求结果数据data
这里我们用一个Result类来表示统一结果封装类,那么Result类里肯定有以下三个属性了:
Result里应该有哪些方法呢?请求只有成功和失败两种情况,Result里应定义静态方法,用以生成Result对象返回给前端,参数即为Result对象的3个属性,于是我们定义以下两个方法:
光有这两个方法还不够,因为这两个方法并不是特别方便使用。对于大部分请求成功的情况,code就是200,请求成功的message其实并不重要,data才是前端需要的,也就是说,在大部分请求成功的情况下,我们只要把data传给前端就行,创建Result的方法,参数只要一个,那就是data,code直接为200,message为操作成功就行。于是我们定义一个succ的重载方法,能让我们更方便的使用结果封装:
对于大部分请求失败的情况也是一样,一般请求失败时,后端都不会向前端返回data,请求失败时异常的报错信息message才是前端需要的,code也可以直接定义为400,因为400代表http中的bad request,即请求失败。于是我们定义一个fail的重载方法:
此时,统一结果封装类我们就定义完成了,它的完整代码如下:
@Data public class Result implements Serializable { private int code; private String msg; private Object data; public static Result succ(Object data) { return succ(200, "操作成功", data); } public static Result fail(String msg) { return fail(400, msg, null); } public static Result succ (int code, String msg, Object data) { Result result = new Result(); result.setCode(code); result.setMsg(msg); result.setData(data); return result; } public static Result fail (int code, String msg, Object data) { Result result = new Result(); result.setCode(code); result.setMsg(msg); result.setData(data); return result; } }
需注意的是,Result类实现了Serializable接口,是为了可序列化,能转成JSON等进行网络传输,传到前端
后端全局异常捕获及处理
我们可以定义一个叫GlobalExceptionHandler的类来捕获和处理全局异常,该类需要使用@RestControllerAdvice注解
@RestControllerAdvice注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。@RestControllerAdvice 是组件注解,他使得其实现类能够被classpath扫描自动发现。@RestControllerAdvice注解主要配合@ExceptionHandler使用,统一处理异常情况。
即@RestControllerAdvice注解定义全局控制器异常处理
我们可以在该类中定义多个重载的handler方法,用于捕获并处理异常,handler的方法参数为各类不同的异常,handler方法需使用@ExceptionHandler注解,该注解的元素value能指定捕获的是哪类异常 ,如@ExceptionHandler(value = RuntimeException.class)
表示捕获运行时异常。用@ExceptionHandler注解标记的这个异常的处理,是全局的,所有与value值相同类型的异常,都会跑到这个地方处理。
handler方法还需使用@ResponseStatus注解,可以通过@ResponseStatus注解和自定义异常配合使用,来返回自定义响应状态码和自定义错误信息给客户端。即@ResponseStatus注解配合@ExceptionHandler注解使用,这里我们主要是需要用它来设置不同异常所对应的http状态码。@ResponseStatus注解中有两个参数,value属性设置异常的状态码,reaseon是异常的描述。
GlobalExceptionHandler全局异常处理类的完整代码例子如下:
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = RuntimeException.class) public Result handler(RuntimeException e) { log.error("运行时异常:----------------{}", e.getMessage()); return Result.fail(e.getMessage()); } @ResponseStatus(HttpStatus.FORBIDDEN) @ExceptionHandler(value = AccessDeniedException.class) public Result handler(AccessDeniedException e) { log.info("security权限不足:----------------{}", e.getMessage()); return Result.fail("权限不足"); } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = MethodArgumentNotValidException.class) public Result handler(MethodArgumentNotValidException e) { log.info("实体校验异常:----------------{}", e.getMessage()); BindingResult bindingResult = e.getBindingResult(); ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get(); return Result.fail(objectError.getDefaultMessage()); } @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = IllegalArgumentException.class) public Result handler(IllegalArgumentException e) { log.error("Assert异常:----------------{}", e.getMessage()); return Result.fail(e.getMessage()); } }
由于项目中不同的异常众多,我们也可以自定义异常类,并在GlobalExceptionHandler中添加对自定义异常的捕获,举个例子,我们可以自定义一个登陆验证码异常:
public class CaptchaException extends AuthenticationException { public CaptchaException(String msg) { super(msg); } }
然后在全局异常处理中加入对该自定义异常的处理:
@ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(value = CaptchaException.class) public Result handler(CaptchaException e) { log.error("登录验证码异常:----------------{}", e.getMessage()); return Result.fail(e.getMessage()); }
注意,Java中的异常Exception都是Throwable的子类,Throwable中定义了getMessage()方法,用以返回异常的具体信息:
例子中的MethodArgumentNotValidException继承了BindException,这个异常是spring实体校验中定义的,该异常有一个BindingResult属性,BindingResult用在实体类校验信息返回结果绑定,BindingResult类继承了spring validation中的Errors类,Errors类用于存储并公开有关特定对象的数据绑定和验证错误的信息。bindingResult.hasErrors()可判断是否校验通过,Errors类中有getAllErrors()方法,用于获取全局和Field字段中的所有错误。ObjectError类用于封装object error
前端axios后置拦截器
在Vue的src目录下创建axios.js文件,用以配置axios,前端使用axios后置拦截器拦截http response,获取后端返回的信息
axios.js的完整代码示例如下:
import axios from "axios"; import router from "./router"; import Element from "element-ui" axios.defaults.baseURL = "http://localhost:8082" const request = axios.create({ timeout: 5000, headers: { 'Content-Type': "application/json; charset=utf-8" } }) // 后置拦截 request.interceptors.response.use(response => { console.log("response ->" + response) // 这里是response的拦截 let res = response.data if (res.code === 200) { return response } else { Element.Message.error(!res.msg ? '系统异常' : res.msg) // 拒绝流程继续往下走 return Promise.reject(response.data.msg) } }, // 如果业务中出现异常 error => { // 先看后端返没返报错信息,返了就用后端的 if (error.response.data) { error.message = error.response.data.msg } // 401没权限 if (error.response.status === 401) { router.push("/login") } Element.Message.error(error.message, {duration:3000}) return Promise.reject(error) } ) export default request
后置拦截器中有response => {...}
和error => {...}
,分别表示对响应数据和错误请求的处理,对于响应数据,若其code为200,说明请求成功,则返回response。否则进入异常处理,这里用element-UI来弹出错误信息,若响应数据的msg为空,则错误信息为系统异常,否则错误信息就是响应数据中的异常信息,调用Promise.reject拒绝后续流程。对于错误请求的处理也是类似的