SpringBoot异常处理的方法
关于SpringMVC框架统一处理异常,需要自定义方法来处理,该方法:
- 【注解】必须添加
@ExceptionHandler
注解; - 【访问权限】应该使用
public
权限; - 【返回值类型】与处理请求的方法的设计思路完全相同,即:如果需要转发或重定向,在使用
@Controller
注解的情况下,使用String
作为返回值类型即可,则返回的字符串就是视图名,如果需要重定向,则返回的字符串必须以redirect:
作为前缀并拼接目标路径,如果需要响应正文,在使用@Controller
的情况下,当前处理异常的方法还需要添加@ResponseBody
,或直接将@Controller
替换为@RestController
,则方法之前就不必添加@ResponseBody
了,如果需要响应的正文是JSON格式的,则需要添加jackson
依赖并将返回值设计为自定义类型; - 【方法名称】自定义;
- 【参数列表】必须添加1个异常类型的参数,表示SpringMVC捕获并用于调用当前方法的异常对象,为了保证调用时不出错,该参数类型对于即将抛出的异常来说,“只能大不能小”!另外,不可以像处理请求的方法一样随心所欲的添加参数,但是,可选择性的添加
HttpServletRequest
、HttpServletResponse
类型的参数;
例如,可以在UserController
中添加:
@ExceptionHandler
public R handleException(Throwable e) {
if (e instanceof InviteCodeException) {
return R.failure(2).setMessage("InviteCodeException");
} else if (e instanceof PhoneDuplicateException) {
return R.failure(3).setMessage("PhoneDuplicateException");
} else if (e instanceof InsertException) {
return R.failure(4).setMessage("InsertException");
} else {
return R.failure(998).setMessage("未知错误!");
}
}
关于@ExceptionHandler
注解,其源代码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
/**
* Exceptions handled by the annotated method. If empty, will default to any
* exceptions listed in the method argument list.
*/
Class<? extends Throwable>[] value() default {};
}
通过以上源代码可以看到:@ExceptionHandler
注解存在value
属性,是默认属性(在配置时,不需要显式的声明属性名称,可以直接在注解中填写参数值),其类型是“异常类的数组”,表示“添加了注解的方法将处理的异常类型,如果没有配置该属性值,将按照方法的参数列表中的异常进行处理”。简单来说“当需要指定需要处理的某些异常种类时,可以在@EXceptionHandler
注解中添加参数,或者将处理异常的方法的参数指定为某种异常,而其它的异常将不会被当前方法所处理!”。
另外,以上处理异常的方法,只作用于当前控制器类!如果需要将处理异常的方法作用于整个项目的任何控制类中的请求,可以:
- 将处理异常的方法定义在控制器类的基类中;
- 将处理异常的方法定义在任意类中,并在该类的声明之前添加
@ControllerAdvice
或@RestControllerAdvice
注解;
@RestControllerAdvice
=@ControllerAdvice
+@ResponseBody
所以,可以在controller
子包中创建GlobalExceptionHandler
类,在类的声明之前添加@RestControllerAdvice
,并将处理异常的方法移动到该类中:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public R handleException(Throwable e) {
if (e instanceof InviteCodeException) {
return R.failure(2).setMessage("InviteCodeException");
} else if (e instanceof PhoneDuplicateException) {
return R.failure(3).setMessage("PhoneDuplicateException");
} else if (e instanceof InsertException) {
return R.failure(4).setMessage("InsertException");
} else {
return R.failure(998).setMessage("未知错误!");
}
}
}
一旦使用了“统一处理异常”的机制,也就意味了同一种异常,无论在哪种情景下被抛出,都会是相同的处理方式,按照以上代码,处理异常时,封装的“错误描述信息”也是完全相同的!这是极为不合理的,关于“错误信息描述”,应该是由抛出异常的那一方进行描述,而不是处理异常的一方进行描述!
所以,应该在“注册”的业务中,抛出异常时进行描述:
@Override
public void regStudent(StudentRegisterDTO studentRegisterDTO) {
// ...
// 原有其它代码
// ...
if (classInfo == null) {
throw new InviteCodeException("注册失败!邀请码错误!");
}
// ...
// 原有其它代码
// ...
if (result != null) {
throw new PhoneDuplicateException("注册失败!手机号码已经被占用!");
}
// ...
// 原有其它代码
// ...
if (rows != 1) {
throw new InsertException("注册失败!服务器忙,请稍后再次尝试!");
}
}
然后,在处理异常时,调用异常对象的getMessage()
方法就可以获取以上抛出时封装的异常描述信息:
@ExceptionHandler(ServiceException.class)
public R handleException(Throwable e) {
if (e instanceof InviteCodeException) {
return R.failure(2).setMessage(e.getMessage());
} else if (e instanceof PhoneDuplicateException) {
return R.failure(3).setMessage(e.getMessage());
} else if (e instanceof InsertException) {
return R.failure(4).setMessage(e.getMessage());
} else {
return R.failure(998).setMessage("未知错误!");
}
}
由于以上处理异常时,每次处理时都使用的相同的做法,所以,还可以将R
类再次调整,以便于管理代码!
在R
类中添加新的方法:
public static R failure(Integer failureState, Throwable e) {
return new R().setState(failureState).setMessage(e.getMessage());
}
另外,关于创建R
对象时使用到的状态码(state),其编号应该是有一定规律的,不应该就是1 > 2 > 3 ……这样编号!同时,这些编号应该使用静态常量来表示,以增加程序代码的可阅读性!
最终,R
类应该调整为:
package cn.tedu.straw.commons.vo;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 响应到客户端的JSON数据的封装类
*/
@Data
@Accessors(chain = true)
public class R<T> {
/**
* 响应状态码
*/
private Integer state;
/**
* 出错时的错误提示信息
*/
private String message;
/**
* 成功时响应给客户端的数据
*/
private T data;
/**
* 操作成功
*
* @return 状态码已经标记为“成功”的对象
*/
public static R ok() {
return new R().setState(State.SUCCESS);
}
/**
* 操作失败
*
* @param failureState 操作失败的状态码,取值推荐使用#link{R.State}
* @param e 操作失败时抛出并被捕获的异常对象
* @return 已经封装了操作失败的状态码、错误描述信息的对象
* @see R.State
*/
public static R failure(Integer failureState, Throwable e) {
return new R().setState(failureState).setMessage(e.getMessage());
}
/**
* 状态码
*/
public static interface State {
/**
* 成功
*/
int SUCCESS = 2000;
/**
* 邀请码错误
*/
int ERR_INVITE_CODE = 4000;
/**
* 手机号码冲突
*/
int ERR_PHONE_DUPLICATE = 4001;
/**
* 插入数据失败
*/
int ERR_INSERT_FAIL = 4002;
/**
* 未知错误
*/
int ERR_UNKNOWN = 9000;
}
}
关于处理异常的代码:
package cn.tedu.straw.api.controller;
import cn.tedu.straw.api.ex.InsertException;
import cn.tedu.straw.api.ex.InviteCodeException;
import cn.tedu.straw.api.ex.PhoneDuplicateException;
import cn.tedu.straw.api.ex.ServiceException;
import cn.tedu.straw.commons.vo.R;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public R handleException(Throwable e) {
if (e instanceof InviteCodeException) {
return R.failure(R.State.ERR_INVITE_CODE, e);
} else if (e instanceof PhoneDuplicateException) {
return R.failure(R.State.ERR_PHONE_DUPLICATE, e);
} else if (e instanceof InsertException) {
return R.failure(R.State.ERR_INSERT_FAIL, e);
} else {
return R.failure(R.State.ERR_UNKNOWN, e);
}
}
}
关于控制器的代码:
package cn.tedu.straw.api.controller;
import cn.tedu.straw.api.dto.StudentRegisterDTO;
import cn.tedu.straw.api.service.IUserService;
import cn.tedu.straw.commons.vo.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* </p>
*
* @author tedu.cn
* @since 2020-08-11
*/
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@Autowired
private IUserService userService;
// http://localhost:8080/api/v1/users/student/register?phone=13100131001&password=1234&inviteCode=JSD1912-876840
@RequestMapping("/student/register")
public R regStudent(StudentRegisterDTO studentRegisterDTO) {
userService.regStudent(studentRegisterDTO);
return R.ok();
}
}
最后,还应该在application.properties
中添加:
# 将响应的JSON数据设置为“不为null”时显示,反之,为null的属性将不会出现在JSON数据中
spring.jackson.default-property-inclusion=non_null