【问题标题】:String.format with lazy evaluation带有惰性求值的 String.format
【发布时间】:2010-10-30 21:53:00
【问题描述】:

我需要类似于String.format(...) 方法的东西,但需要延迟评估。

此lazyFormat 方法应返回某个对象,其toString() 方法将评估格式模式。

我怀疑有人已经这样做了。这在任何库中都可用吗?

我想替换这个(记录器是 log4j 实例):

if(logger.isDebugEnabled() ) {
   logger.debug(String.format("some texts %s with patterns %s", object1, object2));
}

用这个:

logger.debug(lazyFormat("some texts %s with patterns %s", object1, object2));

仅当启用调试日志记录时,我才需要lazyFormat 来格式化字符串。

【问题讨论】:

    标签: java logging lazy-evaluation


    【解决方案1】:

    如果您比 {0} 语法更喜欢 String.format 语法并且可以使用 Java 8 / JDK 8,您可以使用 lambdas / Suppliers:

    logger.log(Level.FINER, () -> String.format("SomeOperation %s took %04dms to complete", name, duration));

    ()->... 在这里充当供应商,将被懒惰地评估。

    【讨论】:

      【解决方案2】:

      如果您正在寻找“简单”的解决方案:

       public class LazyFormat {
      
          public static void main(String[] args) {
              Object o = lazyFormat("some texts %s with patterns %s", "looong string", "another loooong string");
              System.out.println(o);
          }
      
          private static Object lazyFormat(final String s, final Object... o) {
              return new Object() {
                  @Override
                  public String toString() {
                      return String.format(s,o);
                  }
              };
          }
      }
      

      输出:

      一些文本 looong 字符串 模式化另一个 loooong 字符串

      如果您愿意,当然可以在lazyFormat 中添加任何isDebugEnabled() 语句。

      【讨论】:

      • 较新版本的 Log4J 允许参数替换参见 stackoverflow.com/a/14078904/620113
      • 这真的有效吗 - 它会格式化字符串,但它是否在做“懒惰”的事情?我做了一个小测试,其中lazyFormat的参数是对打印到System.err的函数的调用,它似乎对象中的参数... o即使String.format(s,o)没有得到评估。
      • OneSolitaryNoob - 它不是在做懒惰的事情。答案提供者表明它可以通过使用 toString() 方法中的“isDebugEnabled()”方法来完成。我在回答中提供了更通用的惰性日志记录模式:stackoverflow.com/a/18317629/501113
      • 在 Scala 中怎么样?
      【解决方案3】:

      Log4j 1.2.16 中引入了两个可以为您执行此操作的类。

      org.apache.log4j.LogMF 使用 java.text.MessageFormat 格式化您的消息,org.apache.log4j.LogSF 使用“SLF4J 模式语法”,据说速度更快。

      以下是示例:

      LogSF.debug(log, "Processing request {}", req);
      

       LogMF.debug(logger, "The {0} jumped over the moon {1} times", "cow", 5); 
      

      【讨论】:

        【解决方案4】:

        重要提示:强烈建议将所有日志记录代码移至使用SLF4J(尤其是 log4j 1.x)。它可以保护您免受特定日志记录实现的任何类型的特殊问题(即错误)的困扰。它不仅对众所周知的后端实现问题进行了“修复”,而且还适用于多年来出现的更新更快的实现。


        直接回答您的问题,在这里使用SLF4J 会是什么样子:

        LOGGER.debug("some texts {} with patterns {}", object1, object2);
        

        您提供的最重要的一点是您传递了两个 Object 实例。 object1.toString()object2.toString() 方法不会立即评估。更重要的是,toString() 方法仅在它们返回的数据将被实际使用时才被评估;即惰性求值的真正含义。

        我试图想出一个我可以使用的更通用的模式,它不需要我在大量的类中覆盖toString()(并且有些类我无权进行覆盖)。我想出了一个简单的就地解决方案。同样,使用SLF4J,仅当/当启用级别的日志记录时,我才编写字符串。这是我的代码:

            class SimpleSfl4jLazyStringEvaluation {
              private static final Logger LOGGER = LoggerFactory.getLogger(SimpleSfl4jLazyStringEvaluation.class);
        
              ...
        
              public void someCodeSomewhereInTheClass() {
        //all the code between here
                LOGGER.debug(
                    "{}"
                  , new Object() {
                      @Override
                      public String toString() {
                        return "someExpensiveInternalState=" + getSomeExpensiveInternalState();
                      }
                    }
        //and here can be turned into a one liner
                );
              }
        
              private String getSomeExpensiveInternalState() {
                //do expensive string generation/concatenation here
              }
            }
        

        为了简化为 单行,您可以将 someCodeSomewhereInTheClass() 中的 LOGGER 行缩短为:

        LOGGER.debug("{}", new Object(){@Override public String toString(){return "someExpensiveInternalState=" + getSomeExpensiveInternalState();}});
        

        我现在已经重构了我的所有日​​志记录代码以遵循这个简单的模型。它已经大大整理了一些东西。现在,当我看到任何不使用它的日志记录代码时,我会重构日志记录代码以使用这个新模式,即使它还需要它。这样,如果/当稍后进行更改以需要添加一些“昂贵”的操作时,基础架构样板已经存在,将任务简化为仅添加操作。

        【讨论】:

          【解决方案5】:

          可以在最新的log4j 2.X版本http://logging.apache.org/log4j/2.x/log4j-users-guide.pdf中使用参数替换来完成:

          4.1.1.2 参数替换

          记录的目的通常是提供有关系统中正在发生的事情的信息,这 需要包括有关被操作对象的信息。在 Log4j 1.x 这可以通过以下方式完成:

          if (logger.isDebugEnabled()) {     
            logger.debug("Logging in user " + user.getName() + " with id " + user.getId()); 
          } 
          

          反复这样做的效果是 代码感觉更像是记录而不是手头的实际任务。 此外,它会导致日志级别被检查两次;一次 在对 isDebugEnabled 的调用和一次调试方法上。一个更好的 替代方案是:

          logger.debug("Logging in user {} with id {}", user.getName(), user.getId()); 
          

          使用日志级别以上的代码 只会被检查一次并且字符串构造只会发生 启用调试日志记录时。

          【讨论】:

          • 您的示例有点误导,因为您的未格式化示例和格式化示例之间几乎没有区别;即,格式化程序中对于 user.getName() 或 user.getId() 操作没有真正的价值,因为它们被立即调用并且它们的值被传递到 logger.debug 方法中。最初的海报传递了两个 Object 实例,因为除非需要它们,否则不会调用这些实例上的 toString() 方法。我发布了一个答案,更准确地捕捉了这种“只有在使用数据时才调用数据”状态。
          • logger.debug("Logging in user {} with id {}", user.getName(), user.getId()); 中,get 方法将在logger.debug() 运行之前立即调用,要完成log4j 的惰性模式,您需要:logger.debug("Logging in user {} with id {}", () -> user.getName(), () -> user.getId());
          【解决方案6】:

          或者你可以写成

          debug(logger, "some texts %s with patterns %s", object1, object2);
          

          public static void debug(Logger logger, String format, Object... args) {
              if(logger.isDebugEnabled()) 
                 logger.debug(String.format("some texts %s with patterns %s", args));
          }
          

          【讨论】:

            【解决方案7】:

            您可以将 Log4J 记录器实例包装在您自己的 Java5 兼容/String.format 兼容类中。比如:

            public class Log4jWrapper {
            
                private final Logger inner;
            
                private Log4jWrapper(Class<?> clazz) {
                    inner = Logger.getLogger(clazz);
                }
            
                public static Log4jWrapper getLogger(Class<?> clazz) {
                    return new Log4jWrapper(clazz);
                }
            
                public void trace(String format, Object... args) {
                    if(inner.isTraceEnabled()) {
                        inner.trace(String.format(format, args));    
                    }
                }
            
                public void debug(String format, Object... args) {
                    if(inner.isDebugEnabled()) {
                        inner.debug(String.format(format, args));    
                    }
                }
            
                public void warn(String format, Object... args) {
                    inner.warn(String.format(format, args));    
                }
            
                public void error(String format, Object... args) {
                    inner.error(String.format(format, args));    
                }
            
                public void fatal(String format, Object... args) {
                    inner.fatal(String.format(format, args));    
                }    
            }
            

            要使用包装器,请将您的记录器字段声明更改为:

            private final static Log4jWrapper logger = Log4jWrapper.getLogger(ClassUsingLogging.class);
            

            包装类需要一些额外的方法,例如它当前不处理日志记录异常(即 logger.debug(message, exception)),但这应该不难添加。

            使用该类几乎与 log4j 相同,除了字符串被格式化:

            logger.debug("User {0} is not authorized to access function {1}", user, accessFunction)
            

            【讨论】:

              【解决方案8】:

              Andreas' answer 的基础上,我可以想到几种方法来解决仅在Logger.isDebugEnabled 返回true 时才执行格式化的问题:

              选项 1:传入“格式化”标志

              一个选项是有一个方法参数,告诉是否实际执行格式化。一个用例可能是:

              System.out.println(lazyFormat(true, "Hello, %s.", "Bob"));
              System.out.println(lazyFormat(false, "Hello, %s.", "Dave"));
              

              输出的位置:

              Hello, Bob.
              null
              

              lazyFormat 的代码是:

              private String lazyFormat(boolean format, final String s, final Object... o) {
                if (format) {
                  return String.format(s, o);
                }
                else {
                  return null;
                }
              }
              

              在这种情况下,String.format 仅在format 标志设置为true 时执行,如果设置为false,它将返回null。这将停止记录消息的格式化,并且只会发送一些“虚拟”信息。

              所以记录器的用例可能是:

              logger.debug(lazyFormat(logger.isDebugEnabled(), "Message: %s", someValue));
              

              此方法不完全符合问题中要求的格式。

              选项 2:检查记录器

              另一种方法是直接询问记录器是否isDebugEnabled

              private static String lazyFormat(final String s, final Object... o) {
                if (logger.isDebugEnabled()) {
                  return String.format(s, o);
                }
                else {
                  return null;
                }
              }
              

              在这种方法中,预计logger 将在lazyFormat 方法中可见。而且这种方法的好处是调用者在调用lazyFormat时不需要检查isDebugEnabled方法,所以典型的用法可以是:

              logger.debug(lazyFormat("Debug message is %s", someMessage));
              

              【讨论】:

              • 最后一个例子不应该是 logger.debug(lazyFormat("Debug message is %s", someMessage)); ?
              • @Juha S.:是的,你是对的。发布答案后,我注意到了一点错误,因此已修复。
              【解决方案9】:

              您可以定义一个包装器,以便仅在需要时调用String.format()

              有关详细代码示例,请参阅this question

              同样的问题也有variadic function example,正如 Andreas 的回答中所建议的那样。

              【讨论】:

                【解决方案10】:

                如果您为了高效的日志记录而寻找惰性连接,请查看Slf4J 这允许你写:

                LOGGER.debug("this is my long string {}", fatObject);
                

                只有在设置了调试级别时才会进行字符串连接。

                【讨论】:

                • 延迟评估的问题正是引入 slf4j 的 {} 语法的原因
                • 否则很好,但我坚持使用 log4j。我有一种复杂的日志记录设置,所以我不能只放入 slf4j。
                • SLF4J 具有 log4j 的绑定。所以无论你的“复杂日志设置”是什么,SLF4J 都能很好地处理它。只有在需要惰性字符串评估时,您才能继续直接使用 log4j 和 SLF4J。
                猜你喜欢
                • 2014-01-04
                • 1970-01-01
                • 1970-01-01
                • 2012-08-31
                • 2014-02-28
                • 2010-09-20
                • 2011-02-23
                • 2013-12-30
                • 2020-05-19
                相关资源
                最近更新 更多