我们来谈谈痛点吧。由于我的职责,我必须使用许多不同的服务(进行编辑、进行代码审查......);不同的团队通常会编写所有这些服务,每当涉及到处理错误并从服务转发错误时,有时我的眼睛就会开始流泪。让我尝试告诉您哪些代码在我看来是不可接受的错误处理以及我认为应该如何处理。
一般来说,最初问题隐藏在服务分析的缺陷中。通常,对于如何抛出错误没有参考要求。一般来说,发生这种情况有两个原因:第一个是急于开发新服务,第二个是分析师信任开发人员的经验。在这种情况下,分析师只需告诉开发人员:“好吧,稍后给我一个错误消息的示例,我将在汇合中附加它。”
让我们继续看例子;让我们看看这种方法在开发中的后果。
首先不要做的就是抛出 RuntimeException:
public Object badExample() {
throw new RuntimeException("Something wrong!");
}
展示:
调用服务或客户端将收到 500 错误,并且无法理解其请求出了什么问题。您认为在这种情况下需要多长时间才能发现问题?它会随着你的代码量成比例增长。如果你有一个方法调用链,那么在服务内部调用方法时处理此类消息就会变得更加困难。
如何改进呢?首先,让我们创建一个错误处理程序;几乎所有框架都开箱即用;在我的示例中,我将使用 Spring 框架处理程序。
例子:
@ControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(value = {RuntimeException.class})
public ResponseEntity<Object> handler(RuntimeException e) {
HttpStatus badRequest = HttpStatus.INTERNAL_SERVER_ERROR;
return new ResponseEntity<>(e.getMessage(), badRequest);
}
...
展示:
现在我们看到了消息,但是消息的格式是字符串,而不是 JSON。我们很快就会解决这个问题。
理想情况下,项目中的所有服务都应具有相同的错误消息格式。至少有两个字段是消息本身和内部整数错误代码。我想没有必要说明为什么消息文本不足以及需要多少资源来处理该字符串。
例子:
public class ExampleApiError {
private String message;
private Integer code;
private LocalDateTime dateTime;
...
我们将在错误处理程序中填充此类。但正如我所说,抛出 RuntimeException 是一种不好的做法,因此您需要创建自己的类来抛出错误,在该类的构造函数中我们将传递一条消息和一个错误代码。
public class ApiException extends RuntimeException {
private final int code;
public ApiException(String msg, int code) {
super(msg);
this.code = code;
}
...
似乎一切都更加清楚了,但即使在这里,问题也开始了;每个人都以不同的方式创建传递给类构造函数的参数。有些直接在方法中创建消息和错误代码。
例子:
public Object badExample() {
throw new ApiException("Something wrong!", 10);
}
在代码中找到这样的地方仍然很困难。嗯,首先想到的是创建常量类,一个类用于消息,第二个类用于消息代码。
例子:
public static final String SOMETHING_WRONG = "Something wrong";
public static final int SOMETHING_WRONG_CODE = 10;
public Object badExample() {
throw new ApiException(SOMETHING_WRONG, SOMETHING_WRONG_CODE);
}
当您知道错误代码在哪里时,这已经更好、更具可读性并且更容易找到。
但是,如果您没有微服务,那么将所有内容存储在一个类中可能不是一个好主意。消息开始变得越来越大,因此最好根据方法的功能来分离常量类,这些常量类将与 ProductExceptionConstant、PaymentExceptionConstant 等一起使用。
但这还不是全部。对某些人来说,这可能看起来很过时,但常量需要创建为枚举或接口。接口中的变量默认是静态的、公共的和最终的。我并不反对这种做法;最重要的是一切都应该是统一的;如果您开始通过接口进行操作,则继续这样做;无需混合。在其中一个项目中,我看到同一团队使用三种不同的常量方法。你不必这样做。)
我将再展示一个在审核过程中引起我注意的真实项目的示例。
首先,开发人员决定他返回的所有实体都将包含错误消息和错误代码,并且状态将始终为 200,在我看来,这会误导调用者。嗯,一个常量的例子。
public enum ErrorsEnum {
DEFAULT_ERROR(ErrorsConstants.DEFAULT_ERROR, "400"),
REFRESH_TOKEN_NOT_VALID(ErrorsConstants.REFRESH_TOKEN_NOT_VALID, "400.1"),
USER_NOT_FOUND(ErrorsConstants.USER_NOT_FOUND, "400.2"),
在我看来,代码 9 3 \ 4 丢失了。默认情况下,在这种情况下,您可以使用数字 Pi。
如果您对我的方法感兴趣,那就继续吧。最方便的事情是创建一个接口,它将成为每个人都有约束力的契约。
public interface ExceptionBase {
String getMsg();
Integer getCode();
}
并且需要更改异常类的构造函数。
public ApiException(ExceptionBase e) {
super(e.getMsg());
this.code = e.getCode();
}
现在,每个尝试抛出异常的人都会明白,我们必须将实现接口的对象传递给构造函数。最合适的选择是 Enum。
例子:
/**
* This class is intended for declaring exceptions that occur during order processing. code range
* 101-199.
*/
public enum OrderException implements ExceptionBase {
ORDER_NOT_FOUND("Order not found.", 101),
ORDER_CANNOT_BE_UPDATED("Order cannot be updated,", 102);
OrderException(String msg, Integer code) {
this.msg = msg;
this.code = code;
}
private final String msg;
private final Integer code;
@Override
public String getMsg() {
return msg;
}
@Override
public Integer getCode() {
return code;
}
}
使用示例:
public Object getProduct(String id) {
if (id.equals("-1")) {
throw new ApiException(OrderException.ORDER_NOT_FOUND);
}
...
服务器响应:
因此,我们有以下异常包模型。
正如您所看到的,没什么复杂的;逻辑很容易阅读。在此示例中,我在 ApiException 类中留下了一个接受字符串的构造函数,但为了可靠性,最好将其删除。
通常,代码中的大多数不一致都是由于缺乏代码检查或检查不力造成的。最常见的借口是,“这是一个临时解决方案;我们稍后会修复它”,但不,它不会那样工作;没有人会费心寻找哪里有临时解决方案,哪里有永久解决方案。事实证明,“暂时的就是永久的”。
如果您有很多相互通信的服务,那么通过制作一种错误消息格式,您将大大简化编写客户端库的工作。例如,使用Retrofit时,一旦编写了处理程序核心,您只需更改接口中的方法和接收的对象即可。
结论
错误处理是代码中非常重要的部分;它可以轻松地找到代码中的问题区域,并且还允许外部客户端了解他们在使用端点时做错了什么,因此您应该从编写项目的第一阶段就对此给予适当的关注。
示例代码在这里。
我希望读完这篇文章后你的生活会变得更轻松一些。一切成功。