同意!但是不是最佳实践还有待商榷,我这里给出自己一直使用的用异常控制流程的方案,分享讨论一下吧。
由于JAVA只能有一个返回值,但有时候一个service方法除了返回结果外还真的需要有一些附加信息,比如用户非法操作时要中断流程并给出错误信息。如果你不想在service方法中充满着各种大多时候都无用的ResultBean或者让人看见就头大的Map,异常的确值得去尝试一下。
异常最大的优势就是可以中断方法并返回附加信息,方便统一管理,使你的代码更简洁。
缺点就是性能,至于差多少还没测试过,希望有人可以去测一下。
下面开始主题内容:
1.定义异常;
我理解的业务异常是指用户非法操作(如注册用户名重复)需要中断操作并给用户返回合理信息的异常。定义如下:
先定义一个继承自RuntimeException的异常,主要在最后一个构造方法,后面两个值为false,false。意思就是不调用fillIStackTrace()方法和不添加suppressException;因为我们主要关注错误信息,并不在意栈轨迹,所以禁用他们用来提高性能。当然也可以通过复写特定方法来实现,我只是觉得用构造方法更简单。
public class UnFillStackTraceException extends RuntimeException {
private static final long serialVersionUID = -3181827538683088424L;
public UnFillStackTraceException() {
this(null, null);
}
public UnFillStackTraceException(String message) {
this(message, null);
}
public UnFillStackTraceException(Throwable cause) {
this(null, cause);
}
public UnFillStackTraceException(String message, Throwable cause) {
super(message, cause, false, false);
}
}
接下来定义业务异常:
public class APIException extends UnFillStackTraceException {
private static final long serialVersionUID = -1043498038361659805L;
private final StatusCode statusCode;
public APIException(StatusCode statusCode) {
this.statusCode = statusCode;
}
public APIException(StatusCode statusCode, String message) {
super(message);
this.statusCode = statusCode;
}
public StatusCode getStatusCode() {
return this.statusCode;
}
@Override
public String getMessage() {
return StringUtils.defaultIfBlank(super.getMessage(), statusCode.defaultMessage);
}
}
很简单,上面的看一下就明白。因为我们说了业务异常必须是明确,可以给用户提示的错误,所以要构造APIException必须设置相应的StatusCode。
2. StatusCode的设计
业务异常之中肯定要包含相应的错误信息,一般用代码来表示,代码设计的方式有好多种,这里我采用的方案是:基于HttpStatusCode的基础上扩展三位。好处就是可以和HTTP状态码相互转换,因为我们前台返回的时候都是基于http状态码的。
代码如下:
public enum StatusCode {
/**
* 服务器未知异常
*/
ERROR(500000, "服务器异常"),
//授权异常
DISABLE_ACCOUNT(401001, "账户已被冻结"),
INVALID_TOKEN(401002, "无效的身份凭证"),
EXPIRED_TOKEN(401003, "身份凭证已过期"),
NO_PERMISSION(401004, "无权限进行该操作"),
BAD_CREDENTIALS(401005, "密码错误"),
ILLEGAL_OPERATION(400001, "非法操作"),
NOT_FOUND(404000,"访问的资源不存在"),
INVALID_PARAM(422001, "参数无效");
public final int code;
public final String defaultMessage;
StatusCode(int code, String defaultMessage) {
this.code = code;
this.defaultMessage = defaultMessage;
}
public int getHttpStatusCode(){
return convertToHttpStatus(this);
}
public static StatusCode valueOf(int code) {
for (StatusCode value : StatusCode.values()) {
if (value.code == code) {
return value;
}
}
throw new IllegalArgumentException("没有符合'" + code + "'的StatusCode");
}
public static int convertToHttpStatus(StatusCode statusCode) {
return statusCode.code / 1000;
}
public static int convertToHttpStatus(int code) {
return convertToHttpStatus(valueOf(code));
}
}
3. 捕获异常
这一步基本上没啥说的,统一用ControllerAdvice捕获就行了。
@RestControllerAdvice
public class ControllerExceptionHandler {
public static final Logger log = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ExceptionHandler(APIException.class)
public ResponseEntity handleBusinessException(APIException apiException){
return ResponseEntity.status(apiException.getStatusCode().getHttpStatusCode())
.body(new ErrorBody(apiException));
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorBody handleUnknownException(Exception e){
log.error("服务器未知异常",e);
return new ErrorBody(StatusCode.ERROR);
}
}
4. 使用
定义好这些东西后就可愉快的使用了,Service中简单的代码如下:
@Override
@Transactional(rollbackFor = Exception.class)
public SecurityAccount registerAccount(AccountRegister register) {
accountRepository.findUserByUsername(register.getUsername()).ifPresent(eac->{
throw new InvalidParamException("用户名:" + register.getUsername() + "已被使用");
});
Account account = BeanUtil.copyBean(register,Account.class);
if (StringUtils.isNotBlank(register.getPassword())){
account.setPassword(passwordEncoder.encode(register.getPassword()));
} else {
throw new InvalidParamException("密码不能为空");
}
account.setCreateTime(LocalDateTime.now());
accountRepository.save(account);
return convertToSecurity(account);
}
InvalidParamException是继承自ApiException并在构造函数中设置好状态码,方便使用。
Controller层代码:
Controller层没有对返回结果再做封装,因为大多时候根本没必要。尽量利用http状态码即可,对前端使用很舒服。
如果发生错误,再统一返回ErrorBody,里面有错误码和详细信息,供前端展示。对于有复杂业务的操作,如不能简单的使用成功或者失败来表示的,就自己再针对业务和前端协商专门定义即可。
@PostMapping
public long registerAccount(@Validated @RequestBody AccountRegister accountRegister,
BindingResult result) {
checkBindingResult(result);
return accountService.registerAccount(accountRegister).getId();
}
使用http状态码返回错误后,前端使用相当舒服,不用再为业务异常捕获一次,为http错误再捕获一次,统一进catch即可:
$http.post("system/accounts", this.editInfo).then(res => {
this.$message.success("操作成功");
this.cancelDialog();
this.loadData();
}).catch(reason => {
if (!reason.handled) {
this.$message.error(reason.response.data.message);
}
})
这些只是我个人习惯中总结下来的实践,并非最佳实践。放在这里供大家讨论一下,希望能多指出不足,一起学习改进。另外说一句,知乎的电脑端编辑器好难用,,好像有不少bug啊。