implemented simple ClassLoaderLifeCycle to fix integration tests on Java > 8

This commit is contained in:
Sebastian Sdorra
2020-02-04 15:59:11 +01:00
parent a36551597d
commit 71c5f68878
9 changed files with 424 additions and 253 deletions

View File

@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Support for Java versions > 8
- Simple ClassLoaderLifeCycle to fix integration tests on Java > 8
### Changed
- Upgrade [Legman](https://github.com/sdorra/legman) to v1.6.2 in order to fix execution on Java versions > 8

View File

@@ -200,6 +200,10 @@
<name>java.awt.headless</name>
<value>true</value>
</systemProperty>
<systemProperty>
<name>sonia.scm.classloading.lifecycle</name>
<value>simple</value>
</systemProperty>
</systemProperties>
<webApp>
<contextPath>/scm</contextPath>

View File

@@ -720,6 +720,10 @@
<name>scm.stage</name>
<value>${scm.stage}</value>
</systemProperty>
<systemProperty>
<name>sonia.scm.classloading.lifecycle</name>
<value>simple</value>
</systemProperty>
</systemProperties>
<jettyXml>${project.basedir}/src/main/conf/jetty.xml</jettyXml>
<scanIntervalSeconds>0</scanIntervalSeconds>
@@ -805,6 +809,10 @@
<name>scm.home</name>
<value>target/scm-it</value>
</systemProperty>
<systemProperty>
<name>sonia.scm.classloading.lifecycle</name>
<value>simple</value>
</systemProperty>
</systemProperties>
<jettyXml>${project.basedir}/src/main/conf/jetty.xml</jettyXml>
<scanIntervalSeconds>0</scanIntervalSeconds>

View File

@@ -3,198 +3,75 @@ package sonia.scm.lifecycle.classloading;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory;
import se.jiderhamn.classloader.leak.prevention.cleanup.IIOServiceProviderCleanUp;
import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp;
import se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp;
import se.jiderhamn.classloader.leak.prevention.cleanup.StopThreadsCleanUp;
import se.jiderhamn.classloader.leak.prevention.preinit.AwtToolkitInitiator;
import se.jiderhamn.classloader.leak.prevention.preinit.Java2dDisposerInitiator;
import se.jiderhamn.classloader.leak.prevention.preinit.Java2dRenderQueueInitiator;
import se.jiderhamn.classloader.leak.prevention.preinit.SunAwtAppContextInitiator;
import sonia.scm.lifecycle.LifeCycle;
import sonia.scm.plugin.ChildFirstPluginClassLoader;
import sonia.scm.plugin.DefaultPluginClassLoader;
import java.io.Closeable;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.Deque;
import static com.google.common.base.Preconditions.checkState;
import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp.SHUTDOWN_HOOK_WAIT_MS_DEFAULT;
/**
* Creates and shutdown SCM-Manager ClassLoaders.
* Base class for ClassLoader LifeCycle implementation in SCM-Manager.
*/
public final class ClassLoaderLifeCycle implements LifeCycle {
public abstract class ClassLoaderLifeCycle implements LifeCycle {
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class);
private Deque<ClassLoaderAndPreventor> classLoaders = new ArrayDeque<>();
@VisibleForTesting
static final String PROPERTY = "sonia.scm.classloading.lifecycle";
public static ClassLoaderLifeCycle create() {
ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader();
String implementation = System.getProperty(PROPERTY);
if (SimpleClassLoaderLifeCycle.NAME.equalsIgnoreCase(implementation)) {
LOG.info("create new simple ClassLoaderLifeCycle");
return new SimpleClassLoaderLifeCycle(webappClassLoader);
}
LOG.info("create new ClassLoaderLifeCycle with leak prevention");
return new ClassLoaderLifeCycleWithLeakPrevention(webappClassLoader);
}
private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory;
private final ClassLoader webappClassLoader;
private BootstrapClassLoader bootstrapClassLoader;
private ClassLoaderAppendListener classLoaderAppendListener = new ClassLoaderAppendListener() {
@Override
public <C extends ClassLoader> C apply(C classLoader) {
return classLoader;
}
};
@VisibleForTesting
public static ClassLoaderLifeCycle create() {
ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = createClassLoaderLeakPreventorFactory(webappClassLoader);
return new ClassLoaderLifeCycle(webappClassLoader, classLoaderLeakPreventorFactory);
}
ClassLoaderLifeCycle(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) {
this.classLoaderLeakPreventorFactory = classLoaderLeakPreventorFactory;
this.webappClassLoader = initAndAppend(webappClassLoader);
}
private static ClassLoaderLeakPreventorFactory createClassLoaderLeakPreventorFactory(ClassLoader webappClassLoader) {
// Should threads tied to the web app classloader be forced to stop at application shutdown?
boolean stopThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopThreads");
// Should Timer threads tied to the web app classloader be forced to stop at application shutdown?
boolean stopTimerThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopTimerThreads");
// Should shutdown hooks registered from the application be executed at application shutdown?
boolean executeShutdownHooks = Boolean.getBoolean("ClassLoaderLeakPreventor.executeShutdownHooks");
// No of milliseconds to wait for threads to finish execution, before stopping them.
int threadWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.threadWaitMs", ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT);
/*
* No of milliseconds to wait for shutdown hooks to finish execution, before stopping them.
* If set to -1 there will be no waiting at all, but Thread is allowed to run until finished.
*/
int shutdownHookWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.shutdownHookWaitMs", SHUTDOWN_HOOK_WAIT_MS_DEFAULT);
LOG.info("Settings for {} (CL: 0x{}):", ClassLoaderLifeCycle.class.getName(), Integer.toHexString(System.identityHashCode(webappClassLoader)) );
LOG.info(" stopThreads = {}", stopThreads);
LOG.info(" stopTimerThreads = {}", stopTimerThreads);
LOG.info(" executeShutdownHooks = {}", executeShutdownHooks);
LOG.info(" threadWaitMs = {} ms", threadWaitMs);
LOG.info(" shutdownHookWaitMs = {} ms", shutdownHookWaitMs);
// use webapp classloader as safe base? or system?
ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(webappClassLoader);
classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter());
final ShutdownHookCleanUp shutdownHookCleanUp = classLoaderLeakPreventorFactory.getCleanUp(ShutdownHookCleanUp.class);
shutdownHookCleanUp.setExecuteShutdownHooks(executeShutdownHooks);
shutdownHookCleanUp.setShutdownHookWaitMs(shutdownHookWaitMs);
final StopThreadsCleanUp stopThreadsCleanUp = classLoaderLeakPreventorFactory.getCleanUp(StopThreadsCleanUp.class);
stopThreadsCleanUp.setStopThreads(stopThreads);
stopThreadsCleanUp.setStopTimerThreads(stopTimerThreads);
stopThreadsCleanUp.setThreadWaitMs(threadWaitMs);
// remove awt and imageio cleanup
classLoaderLeakPreventorFactory.removePreInitiator(AwtToolkitInitiator.class);
classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class);
classLoaderLeakPreventorFactory.removeCleanUp(IIOServiceProviderCleanUp.class);
classLoaderLeakPreventorFactory.removePreInitiator(Java2dRenderQueueInitiator.class);
classLoaderLeakPreventorFactory.removePreInitiator(Java2dDisposerInitiator.class);
// the MBeanCleanUp causes a Exception and we use no mbeans
classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class);
return classLoaderLeakPreventorFactory;
ClassLoaderLifeCycle(ClassLoader webappClassLoader) {
this.webappClassLoader = webappClassLoader;
}
@Override
public void initialize() {
bootstrapClassLoader = initAndAppend(new BootstrapClassLoader(webappClassLoader));
}
@VisibleForTesting
void setClassLoaderAppendListener(ClassLoaderAppendListener classLoaderAppendListener) {
this.classLoaderAppendListener = classLoaderAppendListener;
}
protected abstract <T extends ClassLoader> T initAndAppend(T classLoader);
public ClassLoader getBootstrapClassLoader() {
checkState(bootstrapClassLoader != null, "%s was not initialized", ClassLoaderLifeCycle.class.getName());
return bootstrapClassLoader;
}
public ClassLoader createPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) {
LOG.debug("create new PluginClassLoader for {}", plugin);
DefaultPluginClassLoader pluginClassLoader = new DefaultPluginClassLoader(urls, parent, plugin);
return initAndAppend(pluginClassLoader);
}
public ClassLoader createChildFirstPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) {
LOG.debug("create new ChildFirstPluginClassLoader for {}", plugin);
ChildFirstPluginClassLoader pluginClassLoader = new ChildFirstPluginClassLoader(urls, parent, plugin);
return initAndAppend(pluginClassLoader);
}
public ClassLoader createPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) {
LOG.debug("create new PluginClassLoader for {}", plugin);
DefaultPluginClassLoader pluginClassLoader = new DefaultPluginClassLoader(urls, parent, plugin);
return initAndAppend(pluginClassLoader);
}
@Override
public void shutdown() {
LOG.info("shutdown classloader infrastructure");
ClassLoaderAndPreventor clap = classLoaders.poll();
while (clap != null) {
clap.shutdown();
clap = classLoaders.poll();
}
// be sure it is realy empty
classLoaders.clear();
classLoaders = new ArrayDeque<>();
shutdownClassLoaders();
bootstrapClassLoader.markAsShutdown();
bootstrapClassLoader = null;
}
private <T extends ClassLoader> T initAndAppend(T originalClassLoader) {
LOG.debug("init classloader {}", originalClassLoader);
T classLoader = classLoaderAppendListener.apply(originalClassLoader);
ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader);
preventor.runPreClassLoaderInitiators();
classLoaders.push(new ClassLoaderAndPreventor(classLoader, preventor));
return classLoader;
}
interface ClassLoaderAppendListener {
<C extends ClassLoader> C apply(C classLoader);
}
private class ClassLoaderAndPreventor {
private final ClassLoader classLoader;
private final ClassLoaderLeakPreventor preventor;
private ClassLoaderAndPreventor(ClassLoader classLoader, ClassLoaderLeakPreventor preventor) {
this.classLoader = classLoader;
this.preventor = preventor;
}
void shutdown() {
LOG.debug("shutdown classloader {}", classLoader);
preventor.runCleanUps();
if (classLoader != webappClassLoader) {
close();
}
}
private void close() {
if (classLoader instanceof Closeable) {
LOG.trace("close classloader {}", classLoader);
try {
((Closeable) classLoader).close();
} catch (IOException e) {
LOG.warn("failed to close classloader", e);
}
}
}
}
protected abstract void shutdownClassLoaders();
}

View File

@@ -0,0 +1,163 @@
package sonia.scm.lifecycle.classloading;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory;
import se.jiderhamn.classloader.leak.prevention.cleanup.IIOServiceProviderCleanUp;
import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp;
import se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp;
import se.jiderhamn.classloader.leak.prevention.cleanup.StopThreadsCleanUp;
import se.jiderhamn.classloader.leak.prevention.preinit.AwtToolkitInitiator;
import se.jiderhamn.classloader.leak.prevention.preinit.Java2dDisposerInitiator;
import se.jiderhamn.classloader.leak.prevention.preinit.Java2dRenderQueueInitiator;
import se.jiderhamn.classloader.leak.prevention.preinit.SunAwtAppContextInitiator;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp.SHUTDOWN_HOOK_WAIT_MS_DEFAULT;
/**
* Creates and shutdown SCM-Manager ClassLoaders with ClassLoader leak detection.
*/
final class ClassLoaderLifeCycleWithLeakPrevention extends ClassLoaderLifeCycle {
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycleWithLeakPrevention.class);
private Deque<ClassLoaderAndPreventor> classLoaders = new ArrayDeque<>();
private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory;
private ClassLoaderAppendListener classLoaderAppendListener = new ClassLoaderAppendListener() {
@Override
public <C extends ClassLoader> C apply(C classLoader) {
return classLoader;
}
};
ClassLoaderLifeCycleWithLeakPrevention(ClassLoader webappClassLoader) {
this(webappClassLoader, createClassLoaderLeakPreventorFactory(webappClassLoader));
}
ClassLoaderLifeCycleWithLeakPrevention(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) {
super(webappClassLoader);
this.classLoaderLeakPreventorFactory = classLoaderLeakPreventorFactory;
}
private static ClassLoaderLeakPreventorFactory createClassLoaderLeakPreventorFactory(ClassLoader webappClassLoader) {
// Should threads tied to the web app classloader be forced to stop at application shutdown?
boolean stopThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopThreads");
// Should Timer threads tied to the web app classloader be forced to stop at application shutdown?
boolean stopTimerThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopTimerThreads");
// Should shutdown hooks registered from the application be executed at application shutdown?
boolean executeShutdownHooks = Boolean.getBoolean("ClassLoaderLeakPreventor.executeShutdownHooks");
// No of milliseconds to wait for threads to finish execution, before stopping them.
int threadWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.threadWaitMs", ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT);
/*
* No of milliseconds to wait for shutdown hooks to finish execution, before stopping them.
* If set to -1 there will be no waiting at all, but Thread is allowed to run until finished.
*/
int shutdownHookWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.shutdownHookWaitMs", SHUTDOWN_HOOK_WAIT_MS_DEFAULT);
LOG.info("Settings for {} (CL: 0x{}):", ClassLoaderLifeCycleWithLeakPrevention.class.getName(), Integer.toHexString(System.identityHashCode(webappClassLoader)) );
LOG.info(" stopThreads = {}", stopThreads);
LOG.info(" stopTimerThreads = {}", stopTimerThreads);
LOG.info(" executeShutdownHooks = {}", executeShutdownHooks);
LOG.info(" threadWaitMs = {} ms", threadWaitMs);
LOG.info(" shutdownHookWaitMs = {} ms", shutdownHookWaitMs);
// use webapp classloader as safe base? or system?
ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(webappClassLoader);
classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter());
final ShutdownHookCleanUp shutdownHookCleanUp = classLoaderLeakPreventorFactory.getCleanUp(ShutdownHookCleanUp.class);
shutdownHookCleanUp.setExecuteShutdownHooks(executeShutdownHooks);
shutdownHookCleanUp.setShutdownHookWaitMs(shutdownHookWaitMs);
final StopThreadsCleanUp stopThreadsCleanUp = classLoaderLeakPreventorFactory.getCleanUp(StopThreadsCleanUp.class);
stopThreadsCleanUp.setStopThreads(stopThreads);
stopThreadsCleanUp.setStopTimerThreads(stopTimerThreads);
stopThreadsCleanUp.setThreadWaitMs(threadWaitMs);
// remove awt and imageio cleanup
classLoaderLeakPreventorFactory.removePreInitiator(AwtToolkitInitiator.class);
classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class);
classLoaderLeakPreventorFactory.removeCleanUp(IIOServiceProviderCleanUp.class);
classLoaderLeakPreventorFactory.removePreInitiator(Java2dRenderQueueInitiator.class);
classLoaderLeakPreventorFactory.removePreInitiator(Java2dDisposerInitiator.class);
// the MBeanCleanUp causes a Exception and we use no mbeans
classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class);
return classLoaderLeakPreventorFactory;
}
@VisibleForTesting
void setClassLoaderAppendListener(ClassLoaderAppendListener classLoaderAppendListener) {
this.classLoaderAppendListener = classLoaderAppendListener;
}
@Override
protected void shutdownClassLoaders() {
ClassLoaderAndPreventor clap = classLoaders.poll();
while (clap != null) {
clap.shutdown();
clap = classLoaders.poll();
}
// be sure it is realy empty
classLoaders.clear();
classLoaders = new ArrayDeque<>();
}
@Override
protected <T extends ClassLoader> T initAndAppend(T originalClassLoader) {
LOG.debug("init classloader {}", originalClassLoader);
T classLoader = classLoaderAppendListener.apply(originalClassLoader);
ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader);
preventor.runPreClassLoaderInitiators();
classLoaders.push(new ClassLoaderAndPreventor(classLoader, preventor));
return classLoader;
}
interface ClassLoaderAppendListener {
<C extends ClassLoader> C apply(C classLoader);
}
private static class ClassLoaderAndPreventor {
private final ClassLoader classLoader;
private final ClassLoaderLeakPreventor preventor;
private ClassLoaderAndPreventor(ClassLoader classLoader, ClassLoaderLeakPreventor preventor) {
this.classLoader = classLoader;
this.preventor = preventor;
}
void shutdown() {
LOG.debug("shutdown classloader {}", classLoader);
preventor.runCleanUps();
close();
}
private void close() {
if (classLoader instanceof Closeable) {
LOG.trace("close classloader {}", classLoader);
try {
((Closeable) classLoader).close();
} catch (IOException e) {
LOG.warn("failed to close classloader", e);
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
package sonia.scm.lifecycle.classloading;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* Creates and shutdown SCM-Manager ClassLoaders with ClassLoader leak detection.
*/
class SimpleClassLoaderLifeCycle extends ClassLoaderLifeCycle {
static final String NAME = "simple";
private static final Logger LOG = LoggerFactory.getLogger(SimpleClassLoaderLifeCycle.class);
private Deque<ClassLoader> classLoaders = new ArrayDeque<>();
SimpleClassLoaderLifeCycle(ClassLoader webappClassLoader) {
super(webappClassLoader);
}
@Override
protected <T extends ClassLoader> T initAndAppend(T classLoader) {
LOG.debug("init classloader {}", classLoader);
classLoaders.push(classLoader);
return classLoader;
}
@Override
protected void shutdownClassLoaders() {
ClassLoader classLoader = classLoaders.poll();
while (classLoader != null) {
shutdown(classLoader);
classLoader = classLoaders.poll();
}
// be sure it is realy empty
classLoaders.clear();
classLoaders = new ArrayDeque<>();
}
private void shutdown(ClassLoader classLoader) {
LOG.debug("shutdown classloader {}", classLoader);
if (classLoader instanceof Closeable) {
LOG.trace("close classloader {}", classLoader);
try {
((Closeable) classLoader).close();
} catch (IOException e) {
LOG.warn("failed to close classloader", e);
}
}
}
}

View File

@@ -1,116 +1,25 @@
package sonia.scm.lifecycle.classloading;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory;
import java.io.Closeable;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ClassLoaderLifeCycleTest {
@Mock
private ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory;
@Mock
private ClassLoaderLeakPreventor classLoaderLeakPreventor;
@Test
void shouldThrowIllegalStateExceptionWithoutInit() {
ClassLoaderLifeCycle lifeCycle = ClassLoaderLifeCycle.create();
assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader);
void shouldCreateSimpleClassLoader() {
System.setProperty(ClassLoaderLifeCycle.PROPERTY, SimpleClassLoaderLifeCycle.NAME);
try {
ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create();
assertThat(classLoaderLifeCycle).isInstanceOf(SimpleClassLoaderLifeCycle.class);
} finally {
System.clearProperty(ClassLoaderLifeCycle.PROPERTY);
}
}
@Test
void shouldThrowIllegalStateExceptionAfterShutdown() {
ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle();
lifeCycle.initialize();
lifeCycle.shutdown();
assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader);
void shouldCreateDefaultClassLoader() {
ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create();
assertThat(classLoaderLifeCycle).isInstanceOf(ClassLoaderLifeCycleWithLeakPrevention.class);
}
@Test
void shouldCreateBootstrapClassLoaderOnInit() {
ClassLoaderLifeCycle lifeCycle = ClassLoaderLifeCycle.create();
lifeCycle.initialize();
assertThat(lifeCycle.getBootstrapClassLoader()).isNotNull();
}
@Test
void shouldCallTheLeakPreventor() {
ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle();
lifeCycle.initialize();
verify(classLoaderLeakPreventor, times(2)).runPreClassLoaderInitiators();
lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a");
lifeCycle.createPluginClassLoader(new URL[0], null, "b");
verify(classLoaderLeakPreventor, times(4)).runPreClassLoaderInitiators();
lifeCycle.shutdown();
verify(classLoaderLeakPreventor, times(4)).runCleanUps();
}
@Test
void shouldCloseCloseableClassLoaders() throws IOException {
// we use URLClassLoader, because we must be sure that the classloader is closable
URLClassLoader webappClassLoader = spy(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()));
ClassLoaderLifeCycle lifeCycle = createMockedLifeCycle(webappClassLoader);
lifeCycle.setClassLoaderAppendListener(new ClassLoaderLifeCycle.ClassLoaderAppendListener() {
@Override
public <C extends ClassLoader> C apply(C classLoader) {
return spy(classLoader);
}
});
lifeCycle.initialize();
ClassLoader pluginA = lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a");
ClassLoader pluginB = lifeCycle.createPluginClassLoader(new URL[0], null, "b");
lifeCycle.shutdown();
closed(pluginB);
closed(pluginA);
neverClosed(webappClassLoader);
}
private void neverClosed(Object object) throws IOException {
Closeable closeable = closeable(object);
verify(closeable, never()).close();
}
private void closed(Object object) throws IOException {
Closeable closeable = closeable(object);
verify(closeable).close();
}
private Closeable closeable(Object object) {
assertThat(object).isInstanceOf(Closeable.class);
return (Closeable) object;
}
private ClassLoaderLifeCycle createMockedLifeCycle() {
return createMockedLifeCycle(Thread.currentThread().getContextClassLoader());
}
private ClassLoaderLifeCycle createMockedLifeCycle(ClassLoader classLoader) {
when(classLoaderLeakPreventorFactory.newLeakPreventor(any(ClassLoader.class))).thenReturn(classLoaderLeakPreventor);
return new ClassLoaderLifeCycle(classLoader, classLoaderLeakPreventorFactory);
}
}

View File

@@ -0,0 +1,116 @@
package sonia.scm.lifecycle.classloading;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory;
import java.io.Closeable;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class ClassLoaderLifeCycleWithLeakPreventionTest {
@Mock
private ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory;
@Mock
private ClassLoaderLeakPreventor classLoaderLeakPreventor;
@Test
void shouldThrowIllegalStateExceptionWithoutInit() {
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = new ClassLoaderLifeCycleWithLeakPrevention(Thread.currentThread().getContextClassLoader());
assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader);
}
@Test
void shouldThrowIllegalStateExceptionAfterShutdown() {
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle();
lifeCycle.initialize();
lifeCycle.shutdown();
assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader);
}
@Test
void shouldCreateBootstrapClassLoaderOnInit() {
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = new ClassLoaderLifeCycleWithLeakPrevention(Thread.currentThread().getContextClassLoader());
lifeCycle.initialize();
assertThat(lifeCycle.getBootstrapClassLoader()).isNotNull();
}
@Test
void shouldCallTheLeakPreventor() {
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle();
lifeCycle.initialize();
verify(classLoaderLeakPreventor, times(1)).runPreClassLoaderInitiators();
lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a");
lifeCycle.createPluginClassLoader(new URL[0], null, "b");
verify(classLoaderLeakPreventor, times(3)).runPreClassLoaderInitiators();
lifeCycle.shutdown();
verify(classLoaderLeakPreventor, times(3)).runCleanUps();
}
@Test
void shouldCloseCloseableClassLoaders() throws IOException {
// we use URLClassLoader, because we must be sure that the classloader is closable
URLClassLoader webappClassLoader = spy(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()));
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle(webappClassLoader);
lifeCycle.setClassLoaderAppendListener(new ClassLoaderLifeCycleWithLeakPrevention.ClassLoaderAppendListener() {
@Override
public <C extends ClassLoader> C apply(C classLoader) {
return spy(classLoader);
}
});
lifeCycle.initialize();
ClassLoader pluginA = lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a");
ClassLoader pluginB = lifeCycle.createPluginClassLoader(new URL[0], null, "b");
lifeCycle.shutdown();
closed(pluginB);
closed(pluginA);
neverClosed(webappClassLoader);
}
private void neverClosed(Object object) throws IOException {
Closeable closeable = closeable(object);
verify(closeable, never()).close();
}
private void closed(Object object) throws IOException {
Closeable closeable = closeable(object);
verify(closeable).close();
}
private Closeable closeable(Object object) {
assertThat(object).isInstanceOf(Closeable.class);
return (Closeable) object;
}
private ClassLoaderLifeCycleWithLeakPrevention createMockedLifeCycle() {
return createMockedLifeCycle(Thread.currentThread().getContextClassLoader());
}
private ClassLoaderLifeCycleWithLeakPrevention createMockedLifeCycle(ClassLoader classLoader) {
when(classLoaderLeakPreventorFactory.newLeakPreventor(any(ClassLoader.class))).thenReturn(classLoaderLeakPreventor);
return new ClassLoaderLifeCycleWithLeakPrevention(classLoader, classLoaderLeakPreventorFactory);
}
}

View File

@@ -0,0 +1,37 @@
package sonia.scm.lifecycle.classloading;
import org.junit.jupiter.api.Test;
import java.io.Closeable;
import static org.assertj.core.api.Assertions.assertThat;
class SimpleClassLoaderLifeCycleTest {
@Test
void shouldCloseClosableClassLoaderOnShutdown() {
SimpleClassLoaderLifeCycle lifeCycle = new SimpleClassLoaderLifeCycle(Thread.currentThread().getContextClassLoader());
lifeCycle.initialize();
ClosableClassLoader classLoader = new ClosableClassLoader();
lifeCycle.initAndAppend(classLoader);
lifeCycle.shutdown();
assertThat(classLoader.closed).isTrue();
}
private static class ClosableClassLoader extends ClassLoader implements Closeable {
private boolean closed = false;
public ClosableClassLoader() {
super();
}
@Override
public void close() {
closed = true;
}
}
}