简体   繁体   English

如何从未经检查的异常中恢复?

[英]How do I recover from an unchecked exception?

Unchecked exceptions are alright if you want to handle every failure the same way, for example by logging it and skipping to the next request, displaying a message to the user and handling the next event, etc. If this is my use case, all I have to do is catch some general exception type at a high level in my system, and handle everything the same way. 如果您想以相同的方式处理每个故障,例如通过记录它并跳到下一个请求,向用户显示消息并处理下一个事件等,如果这是我的用例,那么所有我都可以使用未经检查的异常。要做的是在我的系统中捕获一些高级别的常规异常类型,并以相同的方式处理所有内容。

But I want to recover from specific problems, and I'm not sure the best way to approach it with unchecked exceptions. 但我希望从特定问题中恢复,而且我不确定使用未经检查的异常来处理它的最佳方法。 Here is a concrete example. 这是一个具体的例子。

Suppose I have a web application, built using Struts2 and Hibernate. 假设我有一个使用Struts2和Hibernate构建的Web应用程序。 If an exception bubbles up to my "action", I log it, and display a pretty apology to the user. 如果异常冒泡到我的“动作”,我会记录它,并向用户显示一个非常道歉。 But one of the functions of my web application is creating new user accounts, that require a unique user name. 但我的Web应用程序的一个功能是创建需要唯一用户名的新用户帐户。 If a user picks a name that already exists, Hibernate throws an org.hibernate.exception.ConstraintViolationException (an unchecked exception) down in the guts of my system. 如果用户选择一个已经存在的名称,Hibernate会在我的系统内部抛出一个org.hibernate.exception.ConstraintViolationException (一个未经检查的异常)。 I'd really like to recover from this particular problem by asking the user to choose another user name, rather than giving them the same "we logged your problem but for now you're hosed" message. 我真的想通过要求用户选择另一个用户名来恢复这个特定问题,而不是给他们相同的“我们记录了你的问题,但现在你已经被软化了”的消息。

Here are a few points to consider: 以下是需要考虑的几点:

  1. There a lot of people creating accounts simultaneously. 有很多人同时创建帐户。 I don't want to lock the whole user table between a "SELECT" to see if the name exists and an "INSERT" if it doesn't. 我不想在“SELECT”之间锁定整个用户表以查看名称是否存在,如果不存在则锁定“INSERT”。 In the case of relational databases, there might be some tricks to work around this, but what I'm really interested in is the general case where pre-checking for an exception won't work because of a fundamental race condition. 在关系数据库的情况下,可能有一些技巧可以解决这个问题,但我真正感兴趣的是一般情况下,由于基本的竞争条件,预先检查异常将不起作用。 Same thing could apply to looking for a file on the file system, etc. 同样的事情可能适用于在文件系统上查找文件等。
  2. Given my CTO's propensity for drive-by management induced by reading technology columns in "Inc.", I need a layer of indirection around the persistence mechanism so that I can throw out Hibernate and use Kodo, or whatever, without changing anything except the lowest layer of persistence code. 考虑到我的首席技术官倾向于通过读取“Inc.”中的技术专栏引起的偷渡管理,我需要在持久性机制周围留下一层间接,这样我就可以抛弃Hibernate并使用Kodo,或者其他任何东西,除了最低限度之外不会改变任何东西持久性代码层。 As a matter of fact, there are several such layers of abstraction in my system. 事实上,我的系统中有几个这样的抽象层。 How can I prevent them from leaking in spite of unchecked exceptions? 尽管有未经检查的异常,我怎样才能防止它们泄漏?
  3. One of the declaimed weaknesses of checked exceptions is having to "handle" them in every call on the stack—either by declaring that a calling method throws them, or by catching them and handling them. 已检查异常的一个声明的弱点是必须在堆栈的每个调用中“处理”它们 - 或者通过声明调用方法抛出它们,或者通过捕获它们并处理它们。 Handling them often means wrapping them in another checked exception of a type appropriate to the level of abstraction. 处理它们通常意味着将它们包装在另一个适合抽象级别的类型的检查异常中。 So, for example, in checked-exception land, a file-system–based implementation of my UserRegistry might catch IOException , while a database implementation would catch SQLException , but both would throw a UserNotFoundException that hides the underlying implementation. 因此,例如,在checked-exception UserNotFoundException ,我的UserRegistry的基于文件系统的实现可能捕获IOException ,而数据库实现将捕获SQLException ,但两者都会抛出隐藏底层实现的UserNotFoundException How do I take advantage of unchecked exceptions, sparing myself of the burden of this wrapping at each layer, without leaking implementation details? 如何利用未经检查的异常,免除每层的包装负担,而不泄漏实现细节?

IMO, wrapping exceptions (checked or otherwise) has several benefits that are worth the cost: IMO,包装异常(检查或其他)有几个值得花费的好处:

1) It encourages you to think about the failure modes for the code you write. 1)它鼓励您考虑您编写的代码的失败模式。 Basically, you have to consider the exceptions that the code you call may throw, and in turn you'll consider the exceptions you'll throw for the code that calls yours. 基本上,您必须考虑您调用的代码可能抛出的异常,反过来您将考虑您为调用您的代码抛出的异常。

2) It gives you the opportunity to add additional debugging information into the exception chain. 2)它使您有机会将额外的调试信息添加到异常链中。 For instance, if you have a method that throws an exception on a duplicate username, you might wrap that exception with one that includes additional information about the circumstances of the failure (for example, the IP of the request that provided the dupe username) that wasn't available to the lower-level code. 例如,如果您有一个方法在重复的用户名上抛出异常,您可以用一个包含有关失败情况的附加信息(例如,提供欺骗用户名的请求的IP)的方法来包装该异常。低级代码无法使用。 The cookie trail of exceptions may help you debug a complex problem (it certainly has for me). 异常的cookie跟踪可以帮助您调试复杂的问题(它肯定对我而言)。

3) It lets you become implementation-independent from the lower level code. 3)它允许您从低级代码变为独立于实现。 If you're wrapping exceptions and need to swap out Hibernate for some other ORM, you only have to change your Hibernate-handling code. 如果你要包装异常并需要将Hibernate替换为其他ORM,你只需要更改你的Hibernate处理代码。 All the other layers of code will still be successfully using the wrapped exceptions and will interpret them in the same way, even though the underlying circumstances have changed. 所有其他代码层仍将成功使用包装的异常,并将以相同的方式解释它们,即使基础环境已发生变化。 Note that this applies even if Hibernate changes in some way (ex: they switch exceptions in a new version); 请注意,即使Hibernate以某种方式发生更改,这也适用(例如:它们在新版本中切换异常); it's not just for wholesale technology replacement. 它不仅仅是批发技术的替代品。

4) It encourages you use different classes of exceptions to represent different situations. 4)它鼓励您使用不同类别的异常来表示不同的情况。 For example, you may have a DuplicateUsernameException when the user tries to reuse a username, and a DatabaseFailureException when you can't check for dupe usernames due to a broken DB connection. 例如,当用户尝试重用用户名时可能会出现DuplicateUsernameException,而当由于数据库连接断开而无法检查重写用户名时,可能会出现DatabaseFailureException。 This, in turn, lets you answer your question ("how do I recover?") in flexible and powerful ways. 反过来,这可以让您以灵活而有力的方式回答您的问题(“我该如何恢复?”)。 If you get a DuplicateUsernameException, you may decide to suggest a different username to the user. 如果您收到DuplicateUsernameException,则可能决定向用户建议不同的用户名。 If you get a DatabaseFailureException, you may let it bubble up to the point where it displays a "down for maintenance" page to the user and send off a notification email to you. 如果您收到DatabaseFailureException,您可能会让它冒泡到用户向用户显示“向下维护”页面的位置,并向您发送通知电子邮件。 Once you have custom exceptions, you have customizeable responses -- and that's a good thing. 一旦你有自定义异常,你就有了可定制的响应 - 这是一件好事。

I like to repackage exceptions between the "tiers" of my application, so for example a DB-specific exception is repackaged inside of another exception which is meaningful in the context of my application (of course, I leave the original exception as a member so I don't clobber the stack trace). 我喜欢重新打包我的应用程序的“层”之间的异常,所以例如一个特定于DB的异常在另一个异常中重新打包,这个异常在我的应用程序的上下文中是有意义的(当然,我把原来的异常留给了成员所以我不破坏堆栈跟踪)。

That said, I think that a non-unique user name is not an "exceptional" enough situation to warrant a throw. 也就是说,我认为一个非唯一的用户名不是一个“特殊”的足够的情况来保证投掷。 I'd use a boolean return argument instead. 我会使用布尔返回参数。 Without knowing much about your architecture, it's hard for me to say anything more specific or applicable. 在不了解您的架构的情况下,我很难说出更具体或更适用的内容。

See Patterns for Generation, Handling and Management of Errors 请参阅错误的生成,处理和管理模式

From the Split Domain and Technical Errors pattern 来自拆分域和技术错误模式

A technical error should never cause a domain error to be generated (never the twain should meet). 技术错误永远不会导致生成域错误(从来没有twain应该满足)。 When a technical error must cause business processing to fail, it should be wrapped as a SystemError. 当技术错误必须导致业务处理失败时,它应该被包装为SystemError。

Domain errors should always start from a domain problem and be handled by domain code. 域错误应始终从域问题开始,并由域代码处理。

Domain errors should pass "seamlessly" through technical boundaries. 域错误应该“无缝地”通过技术边界。 It may be that such errors must be serialized and re-constituted for this to happen. 可能必须将这些错误序列化并重新构建以实现此目的。 Proxies and facades should take responsibility for doing this. 代理人和外墙应该对此负责。

Technical errors should be handled in particular points in the application, such as boundaries (see Log at Distribution Boundary). 应在应用程序的特定点处理技术错误,例如边界(请参阅分布边界的日志)。

The amount of context information passed back with the error will depend on how useful this will be for subsequent diagnosis and handling (figuring out an alternative strategy). 与错误一起传回的上下文信息量将取决于它对后续诊断和处理的有用程度(找出替代策略)。 You need to question whether the stack trace from a remote machine is wholly useful to the processing of a domain error (although the code location of the error and variable values at that time may be useful) 您需要询问来自远程计算机的堆栈跟踪是否对域错误的处理完全有用(尽管此时错误和变量值的代码位置可能很有用)

So, wrap the hibernate exception at the boundary to hibernate with an unchecked domain exception such as a "UniqueUsernameException", and let that bubble up all the way to the handler of it. 因此,使用未经检查的域异常(例如“UniqueUsernameException”)将hibernate异常包装在边界处,然后让它一直冒泡到它的处理程序。 Make sure to javadoc the thrown exception even though it isn't a checked exception! 确保抛出异常的javadoc,即使它不是一个经过检查的异常!

Since you're currently using hibernate the easiest thing to do is just check for that exception and wrap it in either a custom exception or in a custom result object you may have setup in your framework. 由于您当前正在使用hibernate,因此最简单的方法就是检查该异常并将其包装在自定义异常或您可能已在框架中设置的自定义结果对象中。 If you want to ditch hibernate later just make sure you wrap this exception in only 1 place, the first place you catch the exception from hibernate, that's the code you'll probably have to change when you make a switch anyway, so if the catch is in one place then the additional overhead is almost zilch. 如果你想稍后抛弃hibernate只是确保你只在一个地方包装这个异常,首先你从hibernate中捕获异常,那就是你在切换时可能需要改变的代码,所以如果抓住的话在一个地方,然后额外的开销几乎是zilch。

help? 救命?

I agree with Nick. 我同意尼克。 Exception you described is not really "unexpected exception" so you should design you code accordingly taking possible exceptions into account. 您描述的异常并非真正的“意外异常”,因此您应该相应地设计代码,并考虑可能的异常。

Also I would recommend to take a look at documentation of Microsoft Enterprise Library Exception Handling Block it has a nice outline of error handling patterns. 另外,我建议您查看Microsoft Enterprise Library 异常处理块的文档,它有一个很好的错误处理模式大纲。

You can catch unchecked exceptions without needing to wrap them. 您可以捕获未经检查的异常而无需包装它们。 For example, the following is valid Java. 例如,以下是有效的Java。

try {
    throw new IllegalArgumentException();
} catch (Exception e) {
    System.out.println("boom");
}

So in your action/controller you can have a try-catch block around the logic where the Hibernate call is made. 因此,在您的操作/控制器中,您可以在进行Hibernate调用的逻辑周围使用try-catch块。 Depending on the exception you can render specific error messages. 根据异常,您可以呈现特定的错误消息。

But I guess in your today it could be Hibernate, and tomorrow SleepLongerDuringWinter framework. 但我猜你今天可能是Hibernate,明天就是SleepLo​​ngerDuringWinter框架。 In this case you need to pretend to have your own little ORM framework that wraps around the third party framework. 在这种情况下,您需要假装拥有自己的包含第三方框架的小型ORM框架。 This will allow you to wrap any framework specific exceptions into more meaningful and/or checked exceptions that you know how to make better sense of. 这将允许您将任何特定于框架的异常包装到更有意义和/或已检查的异常中,您知道如何更好地理解这些异常。

  1. The question is not really related to checked vs. unchecked debate, the same applies to both exception types. 这个问题与检查与未检查的辩论无关,这同样适用于两种异常类型。

  2. Between the point where the ConstraintViolationException is thrown and the point, where we want to handle the violation by displaying a nice error message is a large number of method calls on the stack that should abort immediately and shouldn't care about the problem. 在抛出ConstraintViolationException的点和我们想要通过显示一个好的错误消息来处理违规的点之间是堆栈上的大量方法调用应该立即中止并且不应该关心问题。 That makes the exception mechanism the right choice as opposed to redesigning the code from exceptions to return values. 这使得异常机制成为正确的选择,而不是重新设计从异常到返回值的代码。

  3. In fact, using an unchecked exception instead of a checked exception is a natural fit, since we really want all intermediate methods on the call stack to ignore the exception and not handle it . 实际上,使用未经检查的异常而不是检查异常是很自然的,因为我们真的希望调用堆栈上的所有中间方法都忽略异常而不处理异常。

  4. If we want to handle the "unique name violation" only by displaying a nice error message (error page) to the user, there's not really a need for a specific DuplicateUsernameException. 如果我们只想通过向用户显示一个很好的错误消息(错误页面)来处理“唯一名称违例”,那么实际上并不需要特定的DuplicateUsernameException。 This will keep the number of exception classes low. 这将使异常类的数量保持较低。 Instead, we can create a MessageException that can be reused in many similar scenarios. 相反,我们可以创建一个可以在许多类似场景中重用的MessageException。

    As soon as possible we catch the ConstraintViolationException and convert it to a MessageException with a nice message. 我们尽快捕获ConstraintViolationException并将其转换为带有好消息的MessageException。 It's important to convert it soon, when we can be sure, it's really the "unique user name constraint" that was violated and not some other constraint. 重要的是很快转换它,当我们可以确定时,它确实是违反的“唯一用户名约束”而不是其他约束。

    Somewhere close to the top level handler, just handle the MessageException in a different way. 在靠近顶级处理程序的地方,只需以不同的方式处理MessageException。 Instead of "we logged your problem but for now you're hosed" simply display the message contained in the MessageException, no stack trace. 而不是“我们记录你的问题,但现在你已经被软化”只是显示MessageException中包含的消息,没有堆栈跟踪。

    The MessageException can take some additional constructor parameters, such as a detailed explanation of the problem, available next action (cancel, go to a different page), icon (error, warning)... MessageException可以使用一些额外的构造函数参数,例如问题的详细说明,可用的下一个操作(取消,转到另一个页面),图标(错误,警告)......

The code may look like this 代码可能如下所示

// insert the user
try {
   hibernateSession.save(user);
} catch (ConstraintViolationException e) {
   throw new MessageException("Username " + user.getName() + " already exists. Please choose a different name.");
}

In a totally different place there's a top exception handler 在一个完全不同的地方有一个顶级异常处理程序

try {
   ... render the page
} catch (MessageException e) {
   ... render a nice page with the message
} catch (Exception e) {
   ... render "we logged your problem but for now you're hosed" message
}

@Jan Checked versus unchecked is a central issue here. @Jan Checked与unchecked是一个核心问题。 I question your supposition (#3) that the exception should be ignored in intervening frames. 我质疑你的假设(#3)在中间帧中应该忽略异常。 If I do that, I will end up with an implementation-specific dependency in my high-level code. 如果我这样做,我将在我的高级代码中得到特定于实现的依赖。 If I replace Hibernate, catch blocks throughout my application will have to be modified. 如果我替换Hibernate,则必须修改我的应用程序中的catch块。 Yet, at the same time, if I catch the exception at a lower level, I'm not receiving much benefit from using an unchecked exception. 然而,与此同时,如果我在较低级别捕获异常,我使用未经检查的异常并没有获得太多好处。

Also, the scenario here is that I want to catch a specific logical error and change the flow of the application by re-prompting the user for a different ID. 此外,这里的场景是我想捕获特定的逻辑错误并通过重新提示用户输入不同的ID来更改应用程序的流程。 Simply changing the displayed message is not good enough, and the ability to map to different messages based on exception type is built into Servlets already. 简单地更改显示的消息是不够的,并且已经基于异常类型映射到不同消息的能力已经内置到Servlet中。

@erikson @erikson

Just to add food to your thoughts: 只是为你的想法添加食物:

Checked versus unchecked is also debated here Checked与unchecked也在这里进行辩论

The usage of unchecked exceptions is compliant with the fact they are used IMO for exception caused by the caller of the function (and the caller can be several layers above that function, hence the necessity for other frames to ignore the exception) 未经检查的异常的使用符合它们被IMO用于由函数的调用者引起的异常的事实(并且调用者可以是该函数之上的几个层,因此其他帧必须忽略该异常)

Regarding your specific issue, you should catch the unchecked exception at high level, and encapsulate it, as said by @Kanook in your own exception, without displaying the callstack (as mentionned by @Jan Soltis ) 关于你的具体问题,你应该在高级别捕获未经检查的异常,并将其封装,如@Kanook在你自己的例外中所说,而不显示callstack(由@Jan Soltis提及)

That being said, if the underlying technology changes, that will indeed have an impact on those catch() already present in your code, and that does not answer your latest scenario. 话虽这么说,如果底层技术发生变化,那确实会对你代码中已经存在的那些catch()产生影响,并且不能回答你的最新情况。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM