我应该在抛出异常的构造函数上记录错误吗?


15

我花了几个月的时间来构建应用程序,然后才发现出现了一种模式:

logger.error(ERROR_MSG);
throw new Exception(ERROR_MSG);

或者,在捕捉时:

try { 
    // ...block that can throw something
} catch (Exception e) {
    logger.error(ERROR_MSG, e);
    throw new MyException(ERROR_MSG, e);
}

因此,无论何时我抛出或捕获异常,都将其记录下来。实际上,这几乎是我在应用程序上所做的所有日志记录(除了一些用于应用程序初始化的操作)。

因此,作为一名程序员,我避免重复。因此,我决定将logger调用移至异常构造,因此,每当构建异常时,都会记录事件。当然,我也可以创建一个ExceptionHelper来为我抛出异常,但是这会使我的代码更难以解释,更糟糕的是,编译器无法很好地处理它,而没有意识到对该成员的调用会立即扔。

那么,这是一种反模式吗?如果是这样,为什么?


如果序列化和反序列化异常怎么办?会记录错误吗?
启示录

Answers:


18

不确定它是否符合反模式的要求,但是IMO这是一个坏主意:将异常与日志纠缠在一起是不必要的。

您可能并不总是想要记录给定异常的所有实例(也许它发生在输入验证期间,记录的过程可能既繁琐又无趣)。

其次,您可能决定使用不同的日志记录级别记录错误的不同发生,这时您必须在构造异常时指定该错误,这再次表示使用日志记录行为混淆了异常创建。

最后,如果在记录另一个异常期间发生异常怎么办?你会记录吗?太乱了...

您的选择基本上是:

  • 捕获,记录和(重新)抛出,如您在示例中给出的
  • 创建一个ExceptionHelper类可以为您做这两种事情,但是Helpers具有代码味道,我也不推荐这样做。
  • 将全面捕获异常处理上移
  • 考虑使用AOP作为更复杂的解决方案来解决诸如日志记录和异常处理之类的跨领域问题(但要比在catch块中仅包含这两行要复杂得多);)

+1表示“无聊而无趣”。什么是AOP?
图兰斯·科尔多瓦

@ user61852面向方面的编程(我添加了一个链接)。这个问题显示了一个使用w / r / t AOP和Java登录的示例:stackoverflow.com/questions/15746676/logging-with-aop-in-spring
Dan1701 2015年

11

因此,作为一名程序员,我避免重复[...]

无论何时,如果"don't repeat yourself"过分重视概念到闻到气味,都有危险。


2
现在,当一切都很好时,我该如何选择正确的答案并在彼此的基础上发展呢?如果我采取狂热的态度,很好地解释了DRY如何会成为问题。
布鲁诺·布兰特

1
那真是DRY的好机会,我必须承认我是DRYholic。现在,当我考虑将这5行代码为了DRY而移动到其他地方时,我将三思而后行。
SZT

@SZaman我本来很相似。从好的方面来说,我认为对于那些在消除冗余方面过分依赖的人来说,比那些使用复制和粘贴编写500行函数并且甚至不考虑对其进行重构的人有更多的希望。记住IMO的主要内容是,每次删除一些小的重复项时,您就在分散代码并在其他位置重定向依赖项。那可能是好事或坏事。它为您提供了更改行为的中央控制权,但分享行为也可能会开始对您

@SZaman如果要进行更改,例如“仅此功能需要此功能,而使用此中央功能的其他功能则不需要。” 无论如何,这在我看来是一种平衡的举动-很难做到完美!但是有时有些重复可以帮助使您的代码更加独立和分离。而且,如果您测试了一段代码并且它确实运行良好,即使它在这里和那里重复了一些基本逻辑,那么更改它的理由也可能很少。同时,依赖于很多外部事物的事物发现了很多必须改变的外部原因。

6

回声@ Dan1701

这是关于关注点的分离-将日志记录移入异常会在异常和日志记录之间建立紧密的耦合,并且还意味着您已为日志记录的异常添加了额外的责任,这反过来可能会为其创建的异常类创建依赖关系不需要

从维护的角度来看,您可以(我会认为)隐藏了维护者正在记录异常的事实(至少在类似示例的上下文中),您也在更改上下文( (从异常处理程序的位置到异常的构造函数),这可能不是您想要的。

最后,您假设您总是想在创建/引发异常时以完全相同的方式记录异常-可能并非如此。除非您有日志记录和非日志记录异常,否则它们将很快变得非常不愉快。

因此,在这种情况下,我认为“ SRP”胜过“ DRY”。


1
[...]"SRP" trumps "DRY"-我认为这句话几乎可以完美地总结出来。

正如@Ike所说的...这就是我一直在寻找的理由。
布鲁诺·布兰特

+1指出更改日志的上下文将记录日志,就好像异常类是源或日志条目(不是)一样。
图兰斯·科尔多瓦

2

您的错误是记录了无法处理的异常,因此无法知道记录是否是正确处理它的任何部分。
在极少数情况下,您可以或必须处理部分错误,其中可能包括日志记录,但必须将错误告知呼叫者。如果执行最低级别的读取,则示例为不可纠正的读取错误等,但它们的共同点通常是,出于安全性和可用性的考虑,严重过滤了传递给调用方的信息。

在您的情况下,由于某种原因,您唯一可以做的就是将实现抛出的异常转换为调用者期望的异常,将原始链链接到上下文,并把其他任何事情搁置一旁。

综上所述,您的代码对部分处理异常的权利和义务提出了质疑,从而违反了SRP。
DRY没有加入。


1

仅因为您决定使用catch块记录异常(意味着该异常根本没有发生变化)而抛出异常是一个坏主意。

我们使用异常,异常消息及其处理方式的原因之一是,让我们知道出了什么问题,并且聪明地编写异常可以大大加快查找错误的速度。

还要记住,处理异常要比使用拥有更多资源if,因此要花更多的资源,所以您不应该仅仅因为自己喜欢就处理所有异常。它会影响应用程序的性能。

但是,使用异常作为标记出现错误的应用程序层的一种好方法。

考虑以下半伪代码:

interface ICache<T, U>
{
    T GetValueByKey(U key); // may throw an CacheException
}

class FileCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from FileCache::getvalueByKey. The File could not be opened. Key: " + key);
    }
}

class RedisCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: " + key);
    }
}

class CacheableInt
{
    ICache<int, int> cache;
    ILogger logger;

    public CacheableInt(ICache<int, int> cache, ILogger logger)
    {
        this.cache = cache;
        this.logger = logger;
    }

    public int GetNumber(int key) // may throw service exception
    {
        int result;

        try {
            result = this.cache.GetValueByKey(key);
        } catch (Exception e) {
            this.logger.Error(e);
            throw new ServiceException("CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: " + key);
        }

        return result;
    }
}

class CacheableIntService
{
    CacheableInt cacheableInt;
    ILogger logger;

    CacheableInt(CacheableInt cacheableInt, ILogger logger)
    {
        this.cacheableInt = cacheableInt;
        this.logger = logger;
    }

    int GetNumberAndReturnCode(int key)
    {
        int number;

        try {
            number = this.cacheableInt.GetNumber(key);
        } catch (Exception e) {
            this.logger.Error(e);
            return 500; // error code
        }

        return 200; // ok code
    }
}

假设有人调用GetNumberAndReturnCode并收到了500代码,表示出现错误。他将致电支持人员,后者将打开日志文件并查看以下内容:

ERROR: 12:23:27 - Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: 28
ERROR: 12:23:27 - CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: 28

然后,开发人员立即知道导致该过程中止的软件的哪一层,并具有识别问题的简便方法。在这种情况下至关重要,因为Redis超时永远都不会发生。

也许另一个用户可以调用相同的方法,也可以接收500代码,但是日志将显示以下内容:

INFO: 11:11:11- Could not retrieve object from RedisCache::getvalueByKey. Value does not exist for the key 28.
INFO: 11:11:11- CacheableInt::GetNumber failed, because the cache layer could not find any data for the key 28.

在这种情况下,支持人员可以简单地响应用户请求无效,因为他正在请求不存在的ID的值。


摘要

如果要处理异常,请确保以正确的方式处理它们。还要确保您的例外情况首先包括正确的数据/消息,紧随体系结构层之后,因此消息将帮助您识别可能发生的问题。


1

我认为问题在更基本的层面上:您记录错误并将其作为异常扔到同一位置。那是反模式。这意味着如果捕获到同一错误,则会记录多次该错误,也许将其包装到另一个异常中并重新抛出。

与其相反,我建议记录错误不是在创建异常时记录,而是在捕获异常时记录。(为此,当然,您必须确保始终将其捕获在某个地方。)捕获到异常时,如果它没有被重新抛出或包装为另一个异常的原因,我只会记录其堆栈跟踪。无论如何,堆栈跟踪和已包装的异常消息都将堆栈跟踪记录为“由...引起”。并且捕获器还可以决定例如重试,而无需在第一次失败时记录错误,或者仅将其视为警告或其他。


1

我知道这是一个旧线程,但是我遇到了一个类似的问题,并且想出了一个类似的解决方案,因此我将加2美分。

我不购买违反SRP的论点。也不完全是。让我们假设两件事:1.您实际上确实想记录异常发生的时间(在跟踪级别,以便能够重新创建程序流)。这与处理异常无关。2.您不能或不会为此使用AOP-我同意这是最好的方法,但是不幸的是,我坚持使用一种不提供此工具的语言。

从我的角度来看,您基本上被判处大规模SRP违规,因为任何想要引发异常的类都必须了解日志。将日志记录移至异常类实际上可以大大减少SRP违规,因为现在只有异常违反了SRP,而不是代码库中的每个类。


0

这是一种反模式。

我认为,在异常的构造函数中进行日志记录调用将是以下示例:缺陷:构造函数执行Real Work

我从不希望(或希望)构造函数进行一些外部服务调用。正如MiškoHevery指出的那样,这是一个非常不希望的副作用,它迫使子类和模拟继承不需要的行为。

因此,这也违反了最小惊讶原则

如果您正在与其他人一起开发应用程序,那么这种副作用可能对他们来说并不明显。即使您一个人工作,也可能会忘记它,并在旅途中感到惊讶。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.