【问题标题】:Built-in string formatting vs string concatenation as logging parameter内置字符串格式与字符串连接作为日志记录参数
【发布时间】:2017-02-22 10:02:01
【问题描述】:

我正在使用SonarLint,它在以下行中显示了一个问题。

LOGGER.debug("Comparing objects: " + object1 + " and " + object2);

旁注:包含此行的方法可能会经常被调用。

这个问题的描述是

“前提条件”和日志记录参数不应该需要评估 (鱿鱼:S2629)

将需要进一步评估的消息参数传递给 Guava com.google.common.base.Preconditions 检查可能会导致性能 惩罚。那是因为无论他们是否需要,每个论点 必须在实际调用方法之前解决。

同样,将连接的字符串传递给日志记录方法也可以 会导致不必要的性能损失,因为串联将是 每次调用该方法时执行,无论是否记录日志 级别足够低,可以显示消息。

相反,您应该构建代码以传递静态或预先计算的 值到 Preconditions 条件检查和记录调用。

具体来说,应该使用内置的字符串格式,而不是 字符串连接,如果消息是方法的结果 调用,则应完全跳过先决条件,并且 应该有条件地抛出相关异常。

不合规代码示例

logger.log(Level.DEBUG, "Something went wrong: " + message);  // Noncompliant; string concatenation performed even when log level too high to show DEBUG messages

LOG.error("Unable to open file " + csvPath, e);  // Noncompliant

Preconditions.checkState(a > 0, "Arg must be positive, but got " + a); // Noncompliant. String concatenation performed even when a > 0

Preconditions.checkState(condition, formatMessage());  //Noncompliant. formatMessage() invoked regardless of condition

Preconditions.checkState(condition, "message: %s", formatMessage()); // Noncompliant

合规解决方案

logger.log(Level.SEVERE, "Something went wrong: %s", message);  // String formatting only applied if needed

logger.log(Level.SEVERE, () -> "Something went wrong: " + message); //since Java 8, we can use Supplier , which will be evaluated lazily

LOG.error("Unable to open file {}", csvPath, e);

if (LOG.isDebugEnabled() {   LOG.debug("Unable to open file " + csvPath, e);  // this is compliant, because it will not evaluate if log level is above debug. }

Preconditions.checkState(arg > 0, "Arg must be positive, but got %d", a);  // String formatting only applied if needed

if (!condition) {   throw new IllegalStateException(formatMessage()); // formatMessage() only invoked conditionally }

if (!condition) {   throw new IllegalStateException("message: " + formatMessage()); }

我不能 100% 确定我是否理解正确。那么为什么这真的是一个问题。特别是关于使用字符串连接时性能影响的部分。因为我经常读到字符串连接比格式化要快。

编辑:也许有人可以解释一下

LOGGER.debug("Comparing objects: " + object1 + " and " + object2);

LOGGER.debug("Comparing objects: {} and {}",object1, object2);

在后台。因为我认为 String 在传递给方法之前会被创建。正确的?所以对我来说没有区别。但显然我错了,因为 SonarLint 在抱怨它

【问题讨论】:

    标签: java string logging concatenation sonarlint


    【解决方案1】:

    我相信你在那里有你的答案。

    在条件检查之前计算连接。因此,如果您有条件地调用您的日志框架 10K 次并且所有这些都评估为 false,那么您将无缘无故地连接 10K 次。

    同时检查this topic。并检查 Icaro 的答案的 cmets。

    也可以关注StringBuilder

    【讨论】:

    • Icaro 的回答是说明字符串连接要快得多。所以我想知道为什么这是一个问题。关于 StringBuilder --> 如果您在 Icaro 的回答中看到 AmirRaminar 的评论,说明“+”被编译器转换为 StringBuilder 调用
    • 下一条评论对我来说更有趣 ;)
    • SonarLint 示例建议在 Preconditions.checkState 错误字符串中使用 %d。但根据 Guava 文档,仅支持 %s。见guava.dev/releases/19.0/api/docs/com/google/common/base/…。将整数参数转换为字符串将破坏仅在前提条件失败时评估/连接参数的原始目标。
    【解决方案2】:

    字符串连接的意思 LOGGER.info("程序开始于" + new Date());

    内置记录器格式
    LOGGER.info("程序开始于 {}", new Date());

    很好的文章来了解其中的区别 http://dba-presents.com/index.php/jvm/java/120-use-the-built-in-formatting-to-construct-this-argument

    【讨论】:

    • 感谢您的链接。但是请您用您自己的话来概括一下。因为万一有一天链接消失了,至少有一个摘要存在。
    • 对了,麻烦的Date类已经换成Instant了。见Oracle Tutorial
    【解决方案3】:

    考虑下面的日志语句:

    LOGGER.debug("Comparing objects: " + object1 + " and " + object2);
    

    这是什么“调试”?

    这是日志语句的级别,而不是 LOGGER 的级别。 看,有 2 个级别:

    a) 记录语句之一(此处为调试):

    "Comparing objects: " + object1 + " and " + object2
    

    b) 一个是 LOGGER 的级别。那么,LOGGER 对象的级别是多少: 这也必须在代码或某些 xml 中定义,否则它会从它的祖先中获取级别。

    现在我为什么要说这一切?

    当且仅当:

    Level of logging statement >= Level of LOGGER defined/obtained from somewhere in the code
    

    一个级别的可能值可以是

    DEBUG < INFO <  WARN < ERROR
    

    (可能会更多,具体取决于日志框架)

    现在让我们回到问题:

    "Comparing objects: " + object1 + " and " + object2
    

    即使我们发现上面解释的“级别规则”失败,也总是会导致创建字符串。

    然而,

    LOGGER.debug("Comparing objects: {} and {}",object1, object2);
    

    仅在满足“上述级别规则”的情况下才会形成字符串。

    那么哪个更聪明?

    请咨询url

    【讨论】:

    • 感谢链接。您能否提供链接内容的简短摘要? (以防万一链接在未来某个时间刹车)
    【解决方案4】:

    首先让我们了解问题,然后谈谈解决方案。

    我们可以简单点,假设下面的例子

    LOGGER.debug("User name is " + userName + " and his email is " + email );
    

    上述日志消息字符串由 4 部分组成
    并且 将需要 3 个字符串连接 来构建。

    现在,让我们来看看这个日志记录语句的问题。

    假设我们的日志级别是OFF,这意味着我们现在对日志不感兴趣。

    我们可以想象字符串连接(慢操作)将始终应用并且不会考虑日志记录级别。

    哇,了解了性能问题后,我们来谈谈最佳实践。

    解决方案 1(不是最佳)
    除了使用字符串连接,我们可以使用String Builder

    StringBuilder loggingMsgStringBuilder = new StringBuilder();
    loggingMsgStringBuilder.append("User name is ");
    loggingMsgStringBuilder.append(userName);
    loggingMsgStringBuilder.append(" and his email is ");
    loggingMsgStringBuilder.append(email );
    LOGGER.debug(loggingMsgStringBuilder.toString());
    

    解决方案 2(最佳)
    在检查调试级别之前,我们不需要构造日志消息。
    所以我们可以将日志消息格式所有部分作为参数传递给LOGGING引擎,然后将字符串连接操作委托给它,根据日志记录级别,引擎将决定是否连接。

    所以,推荐使用参数化日志作为下面的例子

    LOGGER.debug("User name is {} and his email is {}", userName, email);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-08-10
      • 2020-05-01
      • 1970-01-01
      • 1970-01-01
      • 2019-01-17
      • 1970-01-01
      • 2012-02-02
      • 1970-01-01
      相关资源
      最近更新 更多