diff --git a/src/java/org/apache/commons/logging/impl/WeakHashtable.java b/src/java/org/apache/commons/logging/impl/WeakHashtable.java new file mode 100644 index 0000000..232b365 --- /dev/null +++ b/src/java/org/apache/commons/logging/impl/WeakHashtable.java @@ -0,0 +1,488 @@ +/* + * Copyright 2004 The Apache Software Foundation. + * + * Licensed 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.commons.logging.impl; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.*; + +/** + *
Implementation of Hashtable that uses WeakReference's
+ * to hold its keys thus allowing them to be reclaimed by the garbage collector.
+ * The associated values are retained using strong references.
This class follows the symantics of Hashtable as closely as
+ * possible. It therefore does not accept null values or keys.
Note:
+ * This is not intended to be a general purpose hash table replacement.
+ * This implementation is also tuned towards a particular purpose: for use as a replacement
+ * for Hashtable in LogFactory. This application requires
+ * good liveliness for get and put. Various tradeoffs
+ * have been made with this in mind.
+ *
+ * Usage: typical use case is as a drop-in replacement
+ * for the Hashtable used in LogFactory for J2EE enviroments
+ * running 1.3+ JVMs. Use of this class in most cases (see below) will
+ * allow classloaders to be collected by the garbage collector without the need
+ * to call {@link org.apache.commons.logging.LogFactory#release(ClassLoader) LogFactory.release(ClassLoader)}.
+ *
org.apache.commons.logging.LogFactory looks to see whether this
+ * class is present in the classpath, and if so then uses it to store
+ * references to the LogFactory implementationd it loads
+ * (rather than using a standard Hashtable instance).
+ * Having this class used instead of Hashtable solves
+ * certain issues related to dynamic reloading of applications in J2EE-style
+ * environments. However this class requires java 1.3 or later (due to its use
+ * of java.lang.ref.WeakReference and associates) and therefore cannot be
+ * included in the main logging distribution which supports JVMs prior to 1.3.
+ * And by the way, this extends Hashtable rather than HashMap
+ * for backwards compatibility reasons. See the documentation
+ * for method LogFactory.createFactoryStore for more details.
The reason all this is necessary is due to a issue which
+ * arises during hot deploy in a J2EE-like containers.
+ * Each component running in the container owns one or more classloaders; when
+ * the component loads a LogFactory instance via the component classloader
+ * a reference to it gets stored in the static LogFactory.factories member,
+ * keyed by the component's classloader so different components don't
+ * stomp on each other. When the component is later unloaded, the container
+ * sets the component's classloader to null with the intent that all the
+ * component's classes get garbage-collected. However there's still a
+ * reference to the component's classloader from the "global" LogFactory's
+ * factories member! If LogFactory.release() is called whenever component
+ * is unloaded (as happens in some famous containers), the classloaders will be correctly
+ * garbage collected.
+ * However, holding the classloader references weakly ensures that the classloader
+ * will be garbage collected without programmatic intervention.
+ * Unfortunately, weak references are
+ * only available in java 1.3+, so this code only uses WeakHashtable if the
+ * class has explicitly been made available on the classpath.
+ * Because the presence of this class in the classpath ensures proper + * unload of components without the need to call method + * {@link org.apache.commons.logging.LogFactory#release(ClassLoader) LogFactory.release ClassLoader)}, + * it is recommended that this class be deployed along with the standard + * commons-logging.jar file when using commons-logging in J2EE + * environments (which will presumably be running on Java 1.3 or later). + * There are no know ill effects from using this class.
+ * + *+ * Limitations: + * There is still one (unusual) scenario in which a component will not + * be correctly unloaded without an explicit release. Though weak references + * are used for its keys, it is necessary to use + * strong references for its values.
+ * + * If the abstract class LogFactory is
+ * loaded by the container classloader but a subclass of
+ * LogFactory [LogFactory1] is loaded by the component's
+ * classloader and an instance stored in the static map associated with the
+ * base LogFactory class, then there is a strong reference from the LogFactory
+ * class to the LogFactory1 instance (as normal) and a strong reference from
+ * the LogFactory1 instance to the component classloader via
+ * getClass().getClassLoader(). This chain of references will prevent
+ * collection of the child classloader.
+ * Such a situation occurs when the commons-logging.jar is
+ * loaded by a parent classloader (e.g. a server level classloader in a
+ * servlet container) and a custom LogFactory implementation is
+ * loaded by a child classloader (e.g. a web app classloader).
To avoid this scenario, ensure
+ * that any custom LogFactory subclass is loaded by the same classloader as
+ * the base LogFactory. Creating custom LogFactory subclasses is,
+ * however, rare. The standard LogFactoryImpl class should be sufficient
+ * for most or all users.
null
+ */
+ private Referenced(Object referant) {
+ reference = new WeakReference(referant);
+ // Calc a permanent hashCode so calls to Hashtable.remove()
+ // work if the WeakReference has been cleared
+ hashCode = referant.hashCode();
+ }
+
+ /**
+ *
+ * @throws NullPointerException if key is null
+ */
+ private Referenced(Object key, ReferenceQueue queue) {
+ reference = new WeakKey(key, queue, this);
+ // Calc a permanent hashCode so calls to Hashtable.remove()
+ // work if the WeakReference has been cleared
+ hashCode = key.hashCode();
+
+ }
+
+ public int hashCode() {
+ return hashCode;
+ }
+
+ private Object getValue() {
+ return reference.get();
+ }
+
+ public boolean equals(Object o) {
+ boolean result = false;
+ if (o instanceof Referenced) {
+ Referenced otherKey = (Referenced) o;
+ Object thisKeyValue = getValue();
+ Object otherKeyValue = otherKey.getValue();
+ if (thisKeyValue == null) {
+ result = (otherKeyValue == null);
+
+ // Since our hashcode was calculated from the original
+ // non-null referant, the above check breaks the
+ // hashcode/equals contract, as two cleared Referenced
+ // objects could test equal but have different hashcodes.
+ // We can reduce (not eliminate) the chance of this
+ // happening by comparing hashcodes.
+ if (result == true) {
+ result = (this.hashCode() == otherKey.hashCode());
+ }
+ // In any case, as our c'tor does not allow null referants
+ // and Hashtable does not do equality checks between
+ // existing keys, normal hashtable operations should never
+ // result in an equals comparison between null referants
+ }
+ else
+ {
+ result = thisKeyValue.equals(otherKeyValue);
+ }
+ }
+ return result;
+ }
+ }
+
+ /**
+ * WeakReference subclass that holds a hard reference to an
+ * associated value and also makes accessible
+ * the Referenced object holding it.
+ */
+ private final static class WeakKey extends WeakReference {
+
+ private final Referenced referenced;
+
+ private WeakKey(Object key,
+ ReferenceQueue queue,
+ Referenced referenced) {
+ super(key, queue);
+ this.referenced = referenced;
+ }
+
+ private Referenced getReferenced() {
+ return referenced;
+ }
+ }
+}
diff --git a/src/test/org/apache/commons/logging/impl/WeakHashtableTest.java b/src/test/org/apache/commons/logging/impl/WeakHashtableTest.java
new file mode 100644
index 0000000..7798f74
--- /dev/null
+++ b/src/test/org/apache/commons/logging/impl/WeakHashtableTest.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2004 The Apache Software Foundation.
+ *
+ * Licensed 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.commons.logging.impl;
+
+import java.lang.ref.*;
+import junit.framework.*;
+import java.util.*;
+
+public class WeakHashtableTest extends TestCase {
+
+
+ /** Maximum number of iterations before our test fails */
+ private static final int MAX_GC_ITERATIONS = 50;
+
+ private WeakHashtable weakHashtable;
+ private Long keyOne;
+ private Long keyTwo;
+ private Long keyThree;
+ private Long valueOne;
+ private Long valueTwo;
+ private Long valueThree;
+
+ public WeakHashtableTest(String testName) {
+ super(testName);
+ }
+
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ weakHashtable = new WeakHashtable();
+
+ keyOne = new Long(1);
+ keyTwo = new Long(2);
+ keyThree = new Long(3);
+ valueOne = new Long(100);
+ valueTwo = new Long(200);
+ valueThree = new Long(300);
+
+ weakHashtable.put(keyOne, valueOne);
+ weakHashtable.put(keyTwo, valueTwo);
+ weakHashtable.put(keyThree, valueThree);
+ }
+
+ /** Tests public boolean contains(ObjectÊvalue) */
+ public void testContains() throws Exception {
+ assertFalse(weakHashtable.contains(new Long(1)));
+ assertFalse(weakHashtable.contains(new Long(2)));
+ assertFalse(weakHashtable.contains(new Long(3)));
+ assertTrue(weakHashtable.contains(new Long(100)));
+ assertTrue(weakHashtable.contains(new Long(200)));
+ assertTrue(weakHashtable.contains(new Long(300)));
+ assertFalse(weakHashtable.contains(new Long(400)));
+ }
+
+ /** Tests public boolean containsKey(ObjectÊkey) */
+ public void testContainsKey() throws Exception {
+ assertTrue(weakHashtable.containsKey(new Long(1)));
+ assertTrue(weakHashtable.containsKey(new Long(2)));
+ assertTrue(weakHashtable.containsKey(new Long(3)));
+ assertFalse(weakHashtable.containsKey(new Long(100)));
+ assertFalse(weakHashtable.containsKey(new Long(200)));
+ assertFalse(weakHashtable.containsKey(new Long(300)));
+ assertFalse(weakHashtable.containsKey(new Long(400)));
+ }
+
+ /** Tests public boolean containsValue(ObjectÊvalue) */
+ public void testContainsValue() throws Exception {
+ assertFalse(weakHashtable.containsValue(new Long(1)));
+ assertFalse(weakHashtable.containsValue(new Long(2)));
+ assertFalse(weakHashtable.containsValue(new Long(3)));
+ assertTrue(weakHashtable.containsValue(new Long(100)));
+ assertTrue(weakHashtable.containsValue(new Long(200)));
+ assertTrue(weakHashtable.containsValue(new Long(300)));
+ assertFalse(weakHashtable.containsValue(new Long(400)));
+ }
+
+ /** Tests public Enumeration elements() */
+ public void testElements() throws Exception {
+ ArrayList elements = new ArrayList();
+ for (Enumeration e = weakHashtable.elements(); e.hasMoreElements();) {
+ elements.add(e.nextElement());
+ }
+ assertEquals(3, elements.size());
+ assertTrue(elements.contains(valueOne));
+ assertTrue(elements.contains(valueTwo));
+ assertTrue(elements.contains(valueThree));
+ }
+
+ /** Tests public Set entrySet() */
+ public void testEntrySet() throws Exception {
+ Set entrySet = weakHashtable.entrySet();
+ for (Iterator it = entrySet.iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ Object key = entry.getKey();
+ if (keyOne.equals(key)) {
+ assertEquals(valueOne, entry.getValue());
+ } else if (keyTwo.equals(key)) {
+ assertEquals(valueTwo, entry.getValue());
+ } else if (keyThree.equals(key)) {
+ assertEquals(valueThree, entry.getValue());
+ } else {
+ fail("Unexpected key");
+ }
+ }
+ }
+
+ /** Tests public Object get(ObjectÊkey) */
+ public void testGet() throws Exception {
+ assertEquals(valueOne, weakHashtable.get(keyOne));
+ assertEquals(valueTwo, weakHashtable.get(keyTwo));
+ assertEquals(valueThree, weakHashtable.get(keyThree));
+ assertNull(weakHashtable.get(new Long(50)));
+ }
+
+ /** Tests public Enumeration keys() */
+ public void testKeys() throws Exception {
+ ArrayList keys = new ArrayList();
+ for (Enumeration e = weakHashtable.keys(); e.hasMoreElements();) {
+ keys.add(e.nextElement());
+ }
+ assertEquals(3, keys.size());
+ assertTrue(keys.contains(keyOne));
+ assertTrue(keys.contains(keyTwo));
+ assertTrue(keys.contains(keyThree));
+ }
+
+ /** Tests public Set keySet() */
+ public void testKeySet() throws Exception {
+ Set keySet = weakHashtable.keySet();
+ assertEquals(3, keySet.size());
+ assertTrue(keySet.contains(keyOne));
+ assertTrue(keySet.contains(keyTwo));
+ assertTrue(keySet.contains(keyThree));
+ }
+
+ /** Tests public Object put(ObjectÊkey, ObjectÊvalue) */
+ public void testPut() throws Exception {
+ Long anotherKey = new Long(2004);
+ weakHashtable.put(anotherKey, new Long(1066));
+
+ assertEquals(new Long(1066), weakHashtable.get(anotherKey));
+
+ // Test compliance with the hashtable API re nulls
+ Exception caught = null;
+ try {
+ weakHashtable.put(null, new Object());
+ }
+ catch (Exception e) {
+ caught = e;
+ }
+ assertNotNull("did not throw an exception adding a null key", caught);
+ caught = null;
+ try {
+ weakHashtable.put(new Object(), null);
+ }
+ catch (Exception e) {
+ caught = e;
+ }
+ assertNotNull("did not throw an exception adding a null value", caught);
+ }
+
+ /** Tests public void putAll(MapÊt) */
+ public void testPutAll() throws Exception {
+ Map newValues = new HashMap();
+ Long newKey = new Long(1066);
+ Long newValue = new Long(1415);
+ newValues.put(newKey, newValue);
+ Long anotherNewKey = new Long(1645);
+ Long anotherNewValue = new Long(1815);
+ newValues.put(anotherNewKey, anotherNewValue);
+ weakHashtable.putAll(newValues);
+
+ assertEquals(5, weakHashtable.size());
+ assertEquals(newValue, weakHashtable.get(newKey));
+ assertEquals(anotherNewValue, weakHashtable.get(anotherNewKey));
+ }
+
+ /** Tests public Object remove(ObjectÊkey) */
+ public void testRemove() throws Exception {
+ weakHashtable.remove(keyOne);
+ assertEquals(2, weakHashtable.size());
+ assertNull(weakHashtable.get(keyOne));
+ }
+
+ /** Tests public Collection values() */
+ public void testValues() throws Exception {
+ Collection values = weakHashtable.values();
+ assertEquals(3, values.size());
+ assertTrue(values.contains(valueOne));
+ assertTrue(values.contains(valueTwo));
+ assertTrue(values.contains(valueThree));
+ }
+
+ public void testRelease() throws Exception {
+ assertNotNull(weakHashtable.get(new Long(1)));
+ ReferenceQueue testQueue = new ReferenceQueue();
+ WeakReference weakKeyOne = new WeakReference(keyOne, testQueue);
+
+ // lose our references
+ keyOne = null;
+ keyTwo = null;
+ keyThree = null;
+ valueOne = null;
+ valueTwo = null;
+ valueThree = null;
+
+ int iterations = 0;
+ int bytz = 2;
+ while(true) {
+ System.gc();
+ if(iterations++ > MAX_GC_ITERATIONS){
+ fail("Max iterations reached before resource released.");
+ }
+
+ if(weakHashtable.get(new Long(1)) == null) {
+ break;
+
+ } else {
+ // create garbage:
+ byte[] b = new byte[bytz];
+ bytz = bytz * 2;
+ }
+ }
+
+ // some JVMs seem to take a little time to put references on
+ // the reference queue once the reference has been collected
+ // need to think about whether this is enough to justify
+ // stepping through the collection each time...
+ while(testQueue.poll() == null) {}
+
+ // Test that the released objects are not taking space in the table
+ assertEquals("underlying table not emptied", 0, weakHashtable.size());
+ }
+}