diff --git a/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/ClassLoaderUtil.java b/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/ClassLoaderUtil.java new file mode 100644 index 0000000..ac276ce --- /dev/null +++ b/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/ClassLoaderUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2022 Piotr P. Karwasz + * + * 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 eu.copernik.log4j.tomcat; + +class ClassLoaderUtil { + + static boolean isLog4jApiResource(final String name, final boolean isClassName) { + if (isClassName && name.startsWith("org.apache.logging.log4j.")) { + if (name.indexOf('.', 25) == -1 + || name.startsWith("internal.", 25) + || name.startsWith("message.", 25) + || name.startsWith("simple.", 25) + || name.startsWith("spi.", 25) + || name.startsWith("status.", 25) + || name.startsWith("util.", 25)) { + return true; + } + } else if (!isClassName && name.startsWith("org/apache/logging/log4j/")) { + if (name.indexOf('/', 25) == -1 + || name.startsWith("internal/", 25) + || name.startsWith("message/", 25) + || name.startsWith("simple/", 25) + || name.startsWith("spi/", 25) + || name.startsWith("status/", 25) + || name.startsWith("util/", 25)) { + return true; + } + } + return false; + } +} diff --git a/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/Log4jParallelWebappClassLoader.java b/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/Log4jParallelWebappClassLoader.java new file mode 100644 index 0000000..9e5954e --- /dev/null +++ b/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/Log4jParallelWebappClassLoader.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2022 Piotr P. Karwasz + * + * 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 eu.copernik.log4j.tomcat; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.loader.ParallelWebappClassLoader; + +/** + * A classloader that delegates lookups for Log4j2 API classes to the parent classloader. + * + *

This allows multiple applications bundled with Log4j2 API to use a single copy of Log4j2 in + * the parent classloader, without modifying the servlet delegation model for the application. + * + *

In order to replace the default application classloader add: + * + *

+ * <Context>
+ *     ...
+ *     <Loader loaderClass="eu.copernik.log4j.tomcat.Log4jParallelWebappClassLoader"/>
+ * </Context>
+ * 
+ * + *

to the application's context definition (cf. defining a + * context). + */ +public class Log4jParallelWebappClassLoader extends ParallelWebappClassLoader { + + public Log4jParallelWebappClassLoader() { + super(); + } + + public Log4jParallelWebappClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected boolean filter(String name, boolean isClassName) { + if (name == null || name.length() < 25) { + return super.filter(name, isClassName); + } + if (ClassLoaderUtil.isLog4jApiResource(name, isClassName)) { + return true; + } + return super.filter(name, isClassName); + } + + @Override + public Log4jParallelWebappClassLoader copyWithoutTransformers() { + + Log4jParallelWebappClassLoader result = new Log4jParallelWebappClassLoader(getParent()); + + super.copyStateWithoutTransformers(result); + + try { + result.start(); + } catch (LifecycleException e) { + throw new IllegalStateException(e); + } + + return result; + } +} diff --git a/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/Log4jWebappClassLoader.java b/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/Log4jWebappClassLoader.java index 6c67676..fad0ea0 100644 --- a/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/Log4jWebappClassLoader.java +++ b/log4j-tomcat/src/main/java/eu/copernik/log4j/tomcat/Log4jWebappClassLoader.java @@ -16,7 +16,7 @@ package eu.copernik.log4j.tomcat; import org.apache.catalina.LifecycleException; -import org.apache.catalina.loader.WebappClassLoaderBase; +import org.apache.catalina.loader.WebappClassLoader; /** * A classloader that delegates lookups for Log4j2 API classes to the parent classloader. @@ -29,7 +29,7 @@ *

  * <Context>
  *     ...
- *     <Loader loaderClass="eu.copernik.log4j.tomcat.Log4jWebappClassLoader"/>
+ *     <Loader loaderClass="eu.copernik.log4j.tomcat.Log4jParallelWebappClassLoader"/>
  * </Context>
  * 
* @@ -37,7 +37,7 @@ * "https://tomcat.apache.org/tomcat-10.0-doc/config/context.html#Defining_a_context">defining a * context). */ -public class Log4jWebappClassLoader extends WebappClassLoaderBase { +public class Log4jWebappClassLoader extends WebappClassLoader { public Log4jWebappClassLoader() { super(); @@ -52,26 +52,8 @@ protected boolean filter(String name, boolean isClassName) { if (name == null || name.length() < 25) { return super.filter(name, isClassName); } - if (isClassName && name.startsWith("org.apache.logging.log4j.")) { - if (name.indexOf('.', 25) == -1 - || name.startsWith("internal.", 25) - || name.startsWith("message.", 25) - || name.startsWith("simple.", 25) - || name.startsWith("spi.", 25) - || name.startsWith("status.", 25) - || name.startsWith("util.", 25)) { - return true; - } - } else if (!isClassName && name.startsWith("org/apache/logging/log4j/")) { - if (name.indexOf('/', 25) == -1 - || name.startsWith("internal/", 25) - || name.startsWith("message/", 25) - || name.startsWith("simple/", 25) - || name.startsWith("spi/", 25) - || name.startsWith("status/", 25) - || name.startsWith("util/", 25)) { - return true; - } + if (ClassLoaderUtil.isLog4jApiResource(name, isClassName)) { + return true; } return super.filter(name, isClassName); } diff --git a/log4j-tomcat/src/test/java/eu/copernik/log4j/tomcat/Log4jWebappClassLoaderTest.java b/log4j-tomcat/src/test/java/eu/copernik/log4j/tomcat/Log4jWebappClassLoaderTest.java index f72e111..4b55444 100644 --- a/log4j-tomcat/src/test/java/eu/copernik/log4j/tomcat/Log4jWebappClassLoaderTest.java +++ b/log4j-tomcat/src/test/java/eu/copernik/log4j/tomcat/Log4jWebappClassLoaderTest.java @@ -140,4 +140,24 @@ public void testLog4jClassloader() throws IOException { }); } } + + @RepeatedTest(100) + public void testParallelLog4jClassloader() throws IOException { + try (final URLClassLoader cl = createClassLoader(Log4jParallelWebappClassLoader.class); ) { + classes() + .forEach( + arg -> { + final Class clazz = (Class) arg.get()[0]; + final boolean isEqual = (boolean) arg.get()[1]; + final Class otherClazz = + assertDoesNotThrow(() -> Class.forName(clazz.getName(), true, cl)); + final ClassAssert assertion = assertThat(otherClazz); + if (isEqual) { + assertion.isEqualTo(clazz); + } else { + assertion.isNotEqualTo(clazz); + } + }); + } + } }