diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java index d8a6467..5c5374d 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java @@ -17,11 +17,13 @@ package org.apache.logging.log4j.core.pattern; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; import java.util.TimeZone; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.util.SoftThreadLocal; /** * Converts and formats the event's date in a StringBuilder. @@ -31,7 +33,18 @@ public final class DatePatternConverter extends LogEventPatternConverter implements ArrayPatternConverter { private abstract static class Formatter { - abstract String format(long time); + private String cachedDateString; + private long lastTimestamp = -1; + + public String format(long timestamp) { + if (timestamp != this.lastTimestamp) { + this.lastTimestamp = timestamp; + this.cachedDateString = format0(timestamp); + } + return cachedDateString; + } + + abstract String format0(long time); public String toPattern() { return null; @@ -46,7 +59,7 @@ } @Override - String format(final long time) { + String format0(final long time) { return simpleDateFormat.format(Long.valueOf(time)); } @@ -59,19 +72,17 @@ private static class UnixFormatter extends Formatter { @Override - String format(final long time) { + String format0(final long time) { return Long.toString(time / 1000); } - } private static class UnixMillisFormatter extends Formatter { @Override - String format(final long time) { + String format0(final long time) { return Long.toString(time); } - } /** @@ -158,14 +169,14 @@ return new DatePatternConverter(options); } - /** - * Date format. - */ - private String cachedDateString; - - private final Formatter formatter; - - private long lastTimestamp; + private final SoftThreadLocal threadLocalFormatter = new SoftThreadLocal() { + @Override + public Formatter initialValue() { + return createFormatter(); + } + }; + + private final String[] options; /** * Private constructor. @@ -175,53 +186,52 @@ */ private DatePatternConverter(final String[] options) { super("Date", "date"); - + this.options = options == null ? null : Arrays.copyOf(options, options.length); + } + + private Formatter createFormatter() { // null patternOption is OK. final String patternOption = options != null && options.length > 0 ? options[0] : null; + + if (UNIX_FORMAT.equalsIgnoreCase(patternOption)) { + return new UnixFormatter(); + } else if (UNIX_MILLIS_FORMAT.equalsIgnoreCase(patternOption)) { + return new UnixMillisFormatter(); + } String pattern = null; - Formatter tempFormatter = null; - - if (patternOption == null || patternOption.equalsIgnoreCase(DEFAULT_FORMAT)) { + if (patternOption == null || DEFAULT_FORMAT.equalsIgnoreCase(patternOption)) { pattern = DEFAULT_PATTERN; - } else if (patternOption.equalsIgnoreCase(ISO8601_FORMAT)) { + } else if (ISO8601_FORMAT.equalsIgnoreCase(patternOption)) { pattern = ISO8601_PATTERN; - } else if (patternOption.equalsIgnoreCase(ISO8601_BASIC_FORMAT)) { + } else if (ISO8601_BASIC_FORMAT.equalsIgnoreCase(patternOption)) { pattern = ISO8601_BASIC_PATTERN; - } else if (patternOption.equalsIgnoreCase(ABSOLUTE_FORMAT)) { + } else if (ABSOLUTE_FORMAT.equalsIgnoreCase(patternOption)) { pattern = ABSOLUTE_TIME_PATTERN; - } else if (patternOption.equalsIgnoreCase(DATE_AND_TIME_FORMAT)) { + } else if (DATE_AND_TIME_FORMAT.equalsIgnoreCase(patternOption)) { pattern = DATE_AND_TIME_PATTERN; - } else if (patternOption.equalsIgnoreCase(COMPACT_FORMAT)) { + } else if (COMPACT_FORMAT.equalsIgnoreCase(patternOption)) { pattern = COMPACT_PATTERN; - } else if (patternOption.equalsIgnoreCase(UNIX_FORMAT)) { - tempFormatter = new UnixFormatter(); - } else if (patternOption.equalsIgnoreCase(UNIX_MILLIS_FORMAT)) { - tempFormatter = new UnixMillisFormatter(); } else { pattern = patternOption; } - if (pattern != null) { - SimpleDateFormat tempFormat; + SimpleDateFormat tempFormat; + try { + tempFormat = new SimpleDateFormat(pattern); + } catch (final IllegalArgumentException e) { + LOGGER.warn("Could not instantiate SimpleDateFormat with pattern " + patternOption, e); - try { - tempFormat = new SimpleDateFormat(pattern); - } catch (final IllegalArgumentException e) { - LOGGER.warn("Could not instantiate SimpleDateFormat with pattern " + patternOption, e); - - // default to the DEFAULT format - tempFormat = new SimpleDateFormat(DEFAULT_PATTERN); - } - - // if the option list contains a TZ option, then set it. - if (options != null && options.length > 1) { - final TimeZone tz = TimeZone.getTimeZone(options[1]); - tempFormat.setTimeZone(tz); - } - tempFormatter = new PatternFormatter(tempFormat); + // default to the DEFAULT format + tempFormat = new SimpleDateFormat(DEFAULT_PATTERN); } - formatter = tempFormatter; + + // if the option list contains a TZ option, then set it. + if (options != null && options.length > 1) { + final TimeZone tz = TimeZone.getTimeZone(options[1]); + tempFormat.setTimeZone(tz); + } + return new PatternFormatter(tempFormat); } /** @@ -233,9 +243,7 @@ * buffer to which formatted date is appended. */ public void format(final Date date, final StringBuilder toAppendTo) { - synchronized (this) { - toAppendTo.append(formatter.format(date.getTime())); - } + toAppendTo.append(doFormat(date.getTime())); } /** @@ -243,15 +251,11 @@ */ @Override public void format(final LogEvent event, final StringBuilder output) { - final long timestamp = event.getTimeMillis(); + output.append(doFormat(event.getTimeMillis())); + } - synchronized (this) { - if (timestamp != lastTimestamp) { - lastTimestamp = timestamp; - cachedDateString = formatter.format(timestamp); - } - } - output.append(cachedDateString); + private String doFormat(final long timestamp) { + return threadLocalFormatter.get().format(timestamp); } /** @@ -281,7 +285,7 @@ * @return the pattern string describing this date format. */ public String getPattern() { - return formatter.toPattern(); + return threadLocalFormatter.get().toPattern(); } } diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/SoftThreadLocal.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/SoftThreadLocal.java new file mode 100644 index 0000000..750fbc9 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/SoftThreadLocal.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.logging.log4j.core.util; + +import java.lang.ref.SoftReference; + +/** + * This class provides thread-local variables wrapped in a {@link SoftReference} to prevent memory leaks in web + * application containers where web applications can be stopped and unloaded. Without using a soft reference, threads in + * the web application thread pool will keep references to the ThreadLocal values, which may prevent these values (and + * their class and all classes it refers to) from being unloaded when the web application is stopped. + */ +public class SoftThreadLocal { + private final ThreadLocal> local = new ThreadLocal>(); + + /** + * Returns the value in the current thread's copy of this thread-local variable. If the variable has no value for + * the current thread, it is first initialized to the value returned by an invocation of the {@link #initialValue} + * method. + * + * @return the current thread's value of this thread-local + */ + public T get() { + final SoftReference ref = local.get(); + if (ref != null) { + final T result = ref.get(); + if (result != null) { + return result; + } + } + // either the SoftReference or its value is null. Reinitialize. + final T result = initialValue(); + set(result); + return result; + } + + /** + * Sets the current thread's copy of this thread-local variable to the specified value, wrapped in a + * {@link SoftReference}. Most subclasses will have no need to override this method, relying solely on the + * {@link #initialValue} method to set the values of thread-locals. + * + * @param value the value to be stored in the current thread's copy of this thread-local. + */ + public void set(final T value) { + if (value == null) { + remove(); + } else { + local.set(new SoftReference(value)); + } + } + + /** + * Removes the current thread's value for this thread-local variable. If this thread-local variable is subsequently + * {@linkplain #get read} by the current thread, its value will be reinitialized by invoking its + * {@link #initialValue} method, unless its value is {@linkplain #set set} by the current thread in the interim. + * This may result in multiple invocations of the initialValue method in the current thread. + */ + public void remove() { + SoftReference old = local.get(); + if (old != null) { + old.clear(); + } + local.remove(); + } + + /** + * Returns the current thread's "initial value" for this thread-local variable. This method will be invoked the + * first time a thread accesses the variable with the {@link #get} method, unless the thread previously invoked the + * {@link #set} method, in which case the initialValue method will not be invoked for the thread. Normally, + * this method is invoked at most once per thread, but it may be invoked again in case of subsequent invocations of + * {@link #remove} followed by {@link #get}. + * + *

+ * This implementation simply returns null; if the programmer desires thread-local variables to have an + * initial value other than null, SoftThreadLocal must be subclassed, and this method overridden. + * Typically, an anonymous inner class will be used. + * + * @return the initial value for this thread-local + */ + protected T initialValue() { + return null; + } +} \ No newline at end of file diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/SoftThreadLocalTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/SoftThreadLocalTest.java new file mode 100644 index 0000000..1eced7d --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/SoftThreadLocalTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.logging.log4j.core.util; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SoftThreadLocalTest { + SoftThreadLocal softLocal = new SoftThreadLocal() { + public Integer initialValue() { + return new Integer(123); + } + }; + + @Test + public void testInitialValue() { + softLocal.remove(); + assertEquals(123, softLocal.get().intValue()); + } + + @Test + public void testGetReturnsSetValue() { + softLocal.set(new Integer(567)); + assertEquals(567, softLocal.get().intValue()); + } + + @Test + public void testRemoveErasesSetValue() { + softLocal.set(new Integer(567)); + assertEquals(567, softLocal.get().intValue()); + softLocal.remove(); + assertEquals(123, softLocal.get().intValue()); + } + + @Test + public void testInitialValueOtherThread() throws Exception { + softLocal.set(new Integer(567)); + final boolean[] success = {true}; + Thread other = new Thread() { + public void run() { + if (softLocal.get().intValue() != 123) { + success[0] = false; + } + } + }; + other.start(); + other.join(); + assertTrue("Other thread value independent", success[0]); + assertEquals("current thread value unchanged", 567, softLocal.get().intValue()); + } + + @Test + public void testGetReturnsSetValueOtherThread() throws Exception { + softLocal.set(new Integer(567)); + assertEquals(567, softLocal.get().intValue()); + final boolean[] success = {true}; + Thread other = new Thread() { + public void run() { + softLocal.set(new Integer(876)); + if (softLocal.get().intValue() != 876) { + success[0] = false; + } + } + }; + other.start(); + other.join(); + assertTrue("Other thread value independent", success[0]); + assertEquals("current thread value unchanged", 567, softLocal.get().intValue()); + } + + @Test + public void testRemoveErasesSetValueOtherThread() throws Exception { + softLocal.set(new Integer(567)); + assertEquals(567, softLocal.get().intValue()); + softLocal.remove(); + assertEquals(123, softLocal.get().intValue()); + final boolean[] success = {true}; + Thread other = new Thread() { + public void run() { + softLocal.set(new Integer(876)); + if (softLocal.get().intValue() != 876) { + success[0] = false; + } + } + }; + other.start(); + other.join(); + assertTrue("Other thread value independent", success[0]); + assertEquals("current thread value unchanged", 123, softLocal.get().intValue()); + } + +}