Description
Looking at the current Logger API, there are a few methods (for each log level) that take a vararg parameter:
// org.apache.logging.log4j.Logger debug(String, Object...) // arguably this method is used the most debug(String, Supplier<?>...) debug(Marker, String, Object...) debug(Marker, String, Supplier<?>...)
This ticket is to explore options for providing similar functionality without creating temporary objects (see LOG4J2-1270 for motivation).
Here are some of the options I can think of:
1. Thread-local StringBuilderMessage
One option is that we provide a new MessageFactory that always returns the same Message object (cached in a ThreadLocal). This Message exposes its StringBuilder. Users build the log message text by appending to the StringBuilder. When the Logger.log method is called, and the Message is enqueued in the RingBufferLogEvent, the StringBuilder text is copied to the RingBufferLogEvent.
Pros: feasible, low effort, no Logger API changes
Cons: not the nicest user-facing API
2. Logger with unrolled varargs
Another way to avoid the varargs is adding extra methods to the Logger interface that take a varying number of parameters. For example:
// add methods to org.apache.logging.log4j.Logger trace(String, Object) trace(String, Object, Object) trace(String, Object, Object, Object) trace(String, Object, Object, Object, Object) trace(String, Object, Object, Object, Object, Object) trace(String, Object, Object, Object, Object, Object, Object) trace(String, Object...) // fallback to vararg if 7 parameters or more ... // same for DEBUG, INFO, WARN, ERROR, FATAL, and LOG(Level)
Pros: transparent to users
Cons: grows Logger interface from 200+ methods(!) to an even larger number
3. New interface (LevelLogger) with unrolled varargs
Another option that avoids the method explosion is providing a new interface (e.g. LevelLogger). This interface has extra methods for a varying number of parameters. For example:
// new interface LevelLogger log(String, Object) log(String, Object, Object) log(String, Object, Object, Object) log(String, Object, Object, Object, Object) log(String, Object, Object, Object, Object, Object) log(String, Object, Object, Object, Object, Object, Object) log(String, Object...) // fallback to vararg if 7 parameters or more
For each log level, the same interface can be used, so the number of methods can stay small. There are several ways in which client code could obtain a LevelLogger. One option is from the Logger, but other things are possible too.
Logger logger = LogManager.getLogger(MyClass.class); logger.info().log("This a {} message with {} params", "vararg-free", 2);
Avoiding Autoboxing
To avoid auto-boxing of primitive parameters (like the integer in the above example), one option is to provide a utility method. Client code would look like this:
// static import Unboxer.*; logger.info().log("This a {} message with {} params", "GC-free", box(2));
Unboxer.box(int) returns a StringBuilder containing the primitive value converted to text. Unboxer internally can be implemented with a cached ring buffer of StringBuilders, with as many slots as there are unrolled varargs.
4. LevelLogger with method chaining
A different way to avoid auto-boxing is method chaining.
Client code would look like this:
String type = "GC-free"; int count = 2; logger.info().log("This a ").add(type).add(" message with ").add(count).add(" params").commit();
Pros: feasible, medium effort
Cons: API very similar to StringBuilder, but more fragile since users must remember to call commit()