关于带有懒惰求值的java:String.format

String.format with lazy evaluation

我需要类似于String.format(...)方法的内容,但需要进行惰性计算。

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

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

我要替换它(记录器是log4j实例):

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

有了这个:

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 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);
            }
        };
    }
}

输出:

some texts looong string with
patterns another loooong string

您当然可以在lazyFormat内添加任何isDebugEnabled()语句。


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

4.1.1.2 Parameter Substitution

Frequently the purpose of logging is to provide information about what is happening in the system, which
requires including information about the objects being manipulated. In
Log4j 1.x this could be accomplished by doing:

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

Doing this repeatedly has the effect of making the
code feel like it is more about logging than the actual task at hand.
In addition, it results in the logging level being checked twice; once
on the call to isDebugEnabled and once on the debug method. A better
alternative would be:

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

With the code above the logging level
will only be checked once and the String construction will only occur
when debug logging is enabled.


如果您为了高效的日志记录而正在寻找延迟连接,请查看Slf4J
这使您可以编写:

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

只有设置了调试级别,字符串连接才会发生。


重要说明:强烈建议所有记录代码都移至使用SLF4J(尤其是log4j 1.x)。它可以防止您因特定的日志记录实现而陷入任何形式的特有问题(即错误)。它不仅具有针对众所周知的后端实现问题的"修补程序",而且还可以与多年来出现的更新更快的实现一起使用。

直接回答您的问题,以下是使用SLF4J的样子:

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

您提供的最重要的一点是您正在传递两个Object实例。 object1.toString()object2.toString()方法不会立即进行评估。更重要的是,只有在实际使用返回的数据时,才对toString()方法进行评估。即懒惰评估的真正含义。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    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行缩短为:

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

现在,我已经重构了所有日志记录代码以遵循此简单模型。它已经整理得相当多了。现在,当我看到任何不使用此日志记录的代码时,我将重构该日志记录代码以使用此新模式,即使仍需要它。这样,如果/以后进行更改以需要添加一些"昂贵"的操作,则基础架构样板已经存在,从而简化了任务,仅添加了操作即可。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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));    
    }    
}

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

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

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

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

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

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

选项1:传递"执行格式设置"标志

一种选择是拥有一个方法参数,该参数指示是否实际执行格式化。一个用例可以是:

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

输出将是:

1
2
Hello, Bob.
null

lazyFormat的代码是:

1
2
3
4
5
6
7
8
private String lazyFormat(boolean format, final String s, final Object... o) {
  if (format) {
    return String.format(s, o);
  }
  else {
    return null;
  }
}

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

因此,记录器的用例可能是:

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

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

选项2:检查记录仪

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

1
2
3
4
5
6
7
8
private static String lazyFormat(final String s, final Object... o) {
  if (logger.isDebugEnabled()) {
    return String.format(s, o);
  }
  else {
    return null;
  }
}

在这种方法中,预计loggerlazyFormat方法中将可见。这种方法的好处是,调用lazyFormat时,调用方将不需要检查isDebugEnabled方法,因此典型用法可以是:

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


Log4j 1.2.16中引入了两个类,它们将为您完成此操作。

org.apache.log4j.LogMF使用java.text.MessageFormat设置消息格式,而org.apache.log4j.LogSF使用" SLF4J模式语法",据说速度更快。

以下是示例:

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

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

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

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

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


或者你可以写成

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

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

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

有关详细的代码示例,请参见此问题。

正如安德烈亚斯(Andreas)的答案所建议的,相同的问题还有一个可变参数的例子。