Try/Catch 一切并重新抛出业务异常
Try/Catch everything and rethrow Business Exceptions
想象一些代码:
public void doSomething(Object object){
try {
if (object == null)
throw new BusinessException("Object was null");
try {
// do logic actions
} catch (Exception e) {
throw new BusinessException("Something went wrong doing logic", e)
}
try {
// do some IO actions
} catch (Exception e) {
throw new BusinessException("Something went wrong doing IO.", e)
}
} catch(Exception e){
throw new BusinessException("Something went wrong in doSomething.", e)
}
}
BusinessException
是 RuntimeException
的扩展。我的经理和另一位高级工程师告诉我,BusinessException
是唯一应该抛出的异常,每个方法都应该像上面的方法一样设计以确保这一点。任何时候出现问题,他们都希望抛出相同的 BusinessException
。
他们的想法是,他们希望从用户那里“抽象”掉逻辑异常,而只向用户提供“业务异常”。我的经理不希望我们只捕获特定的异常,例如IOException
他们希望始终 catch(Exception)
以确保没有遗漏任何东西。
我不明白他们所说的这种“抽象”。我很确定没有任何东西被“抽象”掉,一个异常只是被封装(或屏蔽)在一个新的异常中。
撇开语义不谈,我发现这确实很奇怪,而且我正在努力理解他们认为这种冗长的异常处理所提供的价值。我不难想象这会使调试变得更加困难。如果抛出任何业务异常,它会立即被另一个 catch 块捕获并重新包装到一个新的异常中,从而使堆栈跟踪和潜在的调试工作复杂化。
拥有如此多的异常实例化似乎也是一个性能问题。
此外,这是一个 spring 引导应用程序,我们已经有一个 ResponseEntityExceptionHandler
@ControllerAdvice
public class MyAppResponseEntityExceptionHandler extends ResponseEntityExceptionHandler{
@ExceptionHandler(value = { IllegalArgumentException.class })
protected ResponseEntity<Object> handleConflict(IllegalArgumentException ex, WebRequest request) {
String bodyOfResponse = "Check the arguments";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request);
}
// Several more for different exception types...
}
这只是“各自为政”的情况还是客观上存在问题?
这就像,最终用户不知道如何处理异常,所以通用异常会更好。
您可以为 diff 类型的操作编写 diff 自定义异常,例如数据库调用、api 调用和 return 调用者只有一种类型的异常。
即您可以像这样定义自定义异常。
public class BusinessException extends RuntimeException {
private final ErrorCodes errorCode;
private final Object body;
public BusinessException(String message, ErrorCodes errorCode) {
super(message);
this.errorCode = errorCode;
this.body = null;
}
public BusinessException(String message, ErrorCodes errorCode, Object body) {
super(message);
this.errorCode = errorCode;
this.body = body;
}
}
其中 ErrorCodes 是将包含诸如 InternalError、EntityNotFound、Unauthorised 等错误代码的枚举。
现在您可以使用此自定义异常,您将捕获应用程序中的任何异常并抛出此异常并附上正确的错误消息和错误代码。
类似这样。
throw new BusinessException("Error while fetching some api data.", INTERNAL_SERVER_ERROR);
或
throw new ServiceException("User is not authorised to perform operation.", UNAUTHORIZED);
首先,永远不建议使用通用的 catch
块来捕获 Throwable
或 Exception
的实例,除非它存在于 catch 块链中,因为示例:
public void doSomething() {
try {
// do some database stuff
} catch (SQLException e) {
throw new BusinessException("something went wrong with database", e);
}
try {
// do some IO stuff
} catch (IOException e) {
throw new BusinessException("something went wrong with IO");
}
}
现在不应该捕获这两个异常以外的任何东西,因为这不是这个特定函数的责任,函数应该只抱怨与其所做的相关的错误。
作为来电者,我可能会这样做:
SomethingDoer s = new SomethingDoer();
s.doSomething();
现在如果我担心可能会意外抛出异常,作为调用者我有责任处理它,因此 API 将未捕获的异常委托给调用者处理,如下所示:
SomethingDoer s = new SomethingDoer();
try {
s.doSomething();
} catch ( BusinessException e) {
LOGGER.error(e.message) // prod logging
LOGGER.debug(e) // debug logging with stacktrace
// hypothetical error listener
errorListener.onError(e);
//handle or log, but not rethrow.
} catch (Exception e) { // cringe..
LOGGER.error("something went wrong, unexpectedly"); // prod logs
LOGGER.debug("something went wrong, unexpectedly", e); // debug logs with stacktrace
/* logged AND rethrown since up till this point all expected
exceptions should be wrapped and rethrown or logged,
so if we get here its a fatal error, and you need to interrupt the application*/
throw e;
The latter - cringe looking - catch( Exception e)
block is also not recommended and the exception should be propagated up the stack to the main thread, Checked Exceptions are usually handled that way.
因此,语言特定的异常 - 内部 - 甚至在到达 ControllerAdvice
处理程序之前就应该被捕获并包装在 BusinessException 中,而这个处理程序 - 因为它相对接近应用程序的视图层,所以应该只处理业务特定异常而非内部异常。
您的经理和高级工程师可能正在考虑 Effective Java。从第三版开始,第 73 条:抛出适合抽象的异常。
It is disconcerting when a method throws an exception that has no apparent connection to the task that it performs. This often happens when a method propagates an exception thrown by a lower-level abstraction. Not only is it disconcerting, but it pollutes the API of the higher layer with implementation details. If the implementation of the higher layer changes in a later release, the exceptions it throws will change too, potentially breaking existing client programs.
To avoid this problem, higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction. This idiom is known as exception translation.
也许您的经理对这条建议过于热心。有效Java继续谨慎,
While exception translation is superior to mindless propagation of exceptions from lower layers, it should not be overused.
您可能有理由向您的经理指出这种过度使用,但我怀疑说服会很困难。您可以从条款 72 中得到一些安慰:赞成使用标准异常。我个人更喜欢这个建议,并且倾向于避免创建自定义异常,但肯定其他开发人员有不同的看法。
想象一些代码:
public void doSomething(Object object){
try {
if (object == null)
throw new BusinessException("Object was null");
try {
// do logic actions
} catch (Exception e) {
throw new BusinessException("Something went wrong doing logic", e)
}
try {
// do some IO actions
} catch (Exception e) {
throw new BusinessException("Something went wrong doing IO.", e)
}
} catch(Exception e){
throw new BusinessException("Something went wrong in doSomething.", e)
}
}
BusinessException
是 RuntimeException
的扩展。我的经理和另一位高级工程师告诉我,BusinessException
是唯一应该抛出的异常,每个方法都应该像上面的方法一样设计以确保这一点。任何时候出现问题,他们都希望抛出相同的 BusinessException
。
他们的想法是,他们希望从用户那里“抽象”掉逻辑异常,而只向用户提供“业务异常”。我的经理不希望我们只捕获特定的异常,例如IOException
他们希望始终 catch(Exception)
以确保没有遗漏任何东西。
我不明白他们所说的这种“抽象”。我很确定没有任何东西被“抽象”掉,一个异常只是被封装(或屏蔽)在一个新的异常中。
撇开语义不谈,我发现这确实很奇怪,而且我正在努力理解他们认为这种冗长的异常处理所提供的价值。我不难想象这会使调试变得更加困难。如果抛出任何业务异常,它会立即被另一个 catch 块捕获并重新包装到一个新的异常中,从而使堆栈跟踪和潜在的调试工作复杂化。
拥有如此多的异常实例化似乎也是一个性能问题。
此外,这是一个 spring 引导应用程序,我们已经有一个 ResponseEntityExceptionHandler
@ControllerAdvice
public class MyAppResponseEntityExceptionHandler extends ResponseEntityExceptionHandler{
@ExceptionHandler(value = { IllegalArgumentException.class })
protected ResponseEntity<Object> handleConflict(IllegalArgumentException ex, WebRequest request) {
String bodyOfResponse = "Check the arguments";
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request);
}
// Several more for different exception types...
}
这只是“各自为政”的情况还是客观上存在问题?
这就像,最终用户不知道如何处理异常,所以通用异常会更好。
您可以为 diff 类型的操作编写 diff 自定义异常,例如数据库调用、api 调用和 return 调用者只有一种类型的异常。
即您可以像这样定义自定义异常。
public class BusinessException extends RuntimeException {
private final ErrorCodes errorCode;
private final Object body;
public BusinessException(String message, ErrorCodes errorCode) {
super(message);
this.errorCode = errorCode;
this.body = null;
}
public BusinessException(String message, ErrorCodes errorCode, Object body) {
super(message);
this.errorCode = errorCode;
this.body = body;
}
}
其中 ErrorCodes 是将包含诸如 InternalError、EntityNotFound、Unauthorised 等错误代码的枚举。
现在您可以使用此自定义异常,您将捕获应用程序中的任何异常并抛出此异常并附上正确的错误消息和错误代码。
类似这样。
throw new BusinessException("Error while fetching some api data.", INTERNAL_SERVER_ERROR);
或
throw new ServiceException("User is not authorised to perform operation.", UNAUTHORIZED);
首先,永远不建议使用通用的 catch
块来捕获 Throwable
或 Exception
的实例,除非它存在于 catch 块链中,因为示例:
public void doSomething() {
try {
// do some database stuff
} catch (SQLException e) {
throw new BusinessException("something went wrong with database", e);
}
try {
// do some IO stuff
} catch (IOException e) {
throw new BusinessException("something went wrong with IO");
}
}
现在不应该捕获这两个异常以外的任何东西,因为这不是这个特定函数的责任,函数应该只抱怨与其所做的相关的错误。
作为来电者,我可能会这样做:
SomethingDoer s = new SomethingDoer();
s.doSomething();
现在如果我担心可能会意外抛出异常,作为调用者我有责任处理它,因此 API 将未捕获的异常委托给调用者处理,如下所示:
SomethingDoer s = new SomethingDoer();
try {
s.doSomething();
} catch ( BusinessException e) {
LOGGER.error(e.message) // prod logging
LOGGER.debug(e) // debug logging with stacktrace
// hypothetical error listener
errorListener.onError(e);
//handle or log, but not rethrow.
} catch (Exception e) { // cringe..
LOGGER.error("something went wrong, unexpectedly"); // prod logs
LOGGER.debug("something went wrong, unexpectedly", e); // debug logs with stacktrace
/* logged AND rethrown since up till this point all expected
exceptions should be wrapped and rethrown or logged,
so if we get here its a fatal error, and you need to interrupt the application*/
throw e;
The latter - cringe looking -
catch( Exception e)
block is also not recommended and the exception should be propagated up the stack to the main thread, Checked Exceptions are usually handled that way.
因此,语言特定的异常 - 内部 - 甚至在到达 ControllerAdvice
处理程序之前就应该被捕获并包装在 BusinessException 中,而这个处理程序 - 因为它相对接近应用程序的视图层,所以应该只处理业务特定异常而非内部异常。
您的经理和高级工程师可能正在考虑 Effective Java。从第三版开始,第 73 条:抛出适合抽象的异常。
It is disconcerting when a method throws an exception that has no apparent connection to the task that it performs. This often happens when a method propagates an exception thrown by a lower-level abstraction. Not only is it disconcerting, but it pollutes the API of the higher layer with implementation details. If the implementation of the higher layer changes in a later release, the exceptions it throws will change too, potentially breaking existing client programs.
To avoid this problem, higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction. This idiom is known as exception translation.
也许您的经理对这条建议过于热心。有效Java继续谨慎,
While exception translation is superior to mindless propagation of exceptions from lower layers, it should not be overused.
您可能有理由向您的经理指出这种过度使用,但我怀疑说服会很困难。您可以从条款 72 中得到一些安慰:赞成使用标准异常。我个人更喜欢这个建议,并且倾向于避免创建自定义异常,但肯定其他开发人员有不同的看法。