Merged in feature/java_9_and_above (pull request #400)

Feature/java 9 and above
This commit is contained in:
Rene Pfeuffer
2020-02-06 11:51:46 +00:00
29 changed files with 577 additions and 357 deletions

View File

@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 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
- Upgrade [Lombok](https://projectlombok.org/) to version 1.18.10 in order to fix build on Java versions > 8
- Upgrade [Mockito](https://site.mockito.org/) to version 2.28.2 in order to fix tests on Java versions > 8
- Upgrade smp-maven-plugin to version 1.0.0-rc3
## 2.0.0-rc3 - 2020-01-31
### Fixed
- Broken plugin order fixed

4
Jenkinsfile vendored
View File

@@ -37,7 +37,7 @@ node('docker') {
}
stage('Integration Test') {
mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true -DClassLoaderLeakPreventor.threadWaitMs=10'
mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true'
}
stage('SonarQube') {
@@ -108,7 +108,7 @@ String mainBranch
Maven setupMavenBuild() {
// Keep this version number in sync with .mvn/maven-wrapper.properties
Maven mvn = new MavenInDocker(this, '3.5.2-jdk-8')
Maven mvn = new MavenInDocker(this, '3.6.3-jdk-11')
if (isMainBranch()) {
// Release starts javadoc, which takes very long, so do only for certain branches

4
lombok.config Normal file
View File

@@ -0,0 +1,4 @@
# since v1.16.20 the @ConstructorProperties is no longer added to constructors,
# but this annotation seem to be used by Jackson.
# Not sure if this is realy required, but without sonia.scm.api.rest.JSONContextResolverTest breaks
lombok.anyConstructor.addConstructorProperties = true

49
pom.xml
View File

@@ -212,7 +212,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
@@ -285,9 +285,9 @@
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
<version>${hibernate-validator.version}</version>
</dependency>
<!-- JUnit 5 -->
@@ -434,7 +434,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.1</version>
<version>3.1.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -456,7 +456,7 @@
<plugin>
<groupId>sonia.scm.maven</groupId>
<artifactId>smp-maven-plugin</artifactId>
<version>1.0.0-rc2</version>
<version>1.0.0-rc3</version>
</plugin>
<plugin>
@@ -625,6 +625,7 @@
<encoding>${project.build.sourceEncoding}</encoding>
<docencoding>${project.build.sourceEncoding}</docencoding>
<breakiterator>true</breakiterator>
<source>${project.build.javaLevel}</source>
<version>true</version>
<author>true</author>
<keywords>true</keywords>
@@ -664,11 +665,11 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId>
<version>2.3.2</version>
<version>2.5.3</version>
<configuration>
<pushChanges>false</pushChanges>
<localCheckout>true</localCheckout>
<releaseProfiles>release,APIviz,doc,nativepkg</releaseProfiles>
<releaseProfiles>release,doc,nativepkg</releaseProfiles>
<tagNameFormat>@{project.version}</tagNameFormat>
<autoVersionSubmodules>true</autoVersionSubmodules>
</configuration>
@@ -798,32 +799,6 @@
</profile>
<profile>
<id>APIviz</id>
<activation>
<property>
<name>sonia.apiviz</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<failOnError>false</failOnError>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<distributionManagement>
@@ -844,7 +819,7 @@
<properties>
<!-- test libraries -->
<mockito.version>2.23.0</mockito.version>
<mockito.version>2.28.2</mockito.version>
<hamcrest.version>1.3</hamcrest.version>
<junit.version>5.2.0</junit.version>
@@ -863,7 +838,7 @@
<hibernate-validator.version>6.1.0.Final</hibernate-validator.version>
<!-- event bus -->
<legman.version>1.6.1</legman.version>
<legman.version>1.6.2</legman.version>
<!-- webserver -->
<jetty.version>9.4.22.v20191022</jetty.version>
@@ -885,8 +860,8 @@
<yarn.version>1.16.0</yarn.version>
<!-- build properties -->
<project.build.javaLevel>1.8</project.build.javaLevel>
<project.test.javaLevel>1.8</project.test.javaLevel>
<project.build.javaLevel>8</project.build.javaLevel>
<project.test.javaLevel>8</project.test.javaLevel>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<netbeans.hint.license>SCM-BSD</netbeans.hint.license>
<jdk.classifier />

View File

@@ -40,7 +40,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* The I18n annotation is used by the {@link I18nMessages} class to define the
* The I18n annotation is used by the I18nMessages class to define the
* resource bundle key.
*
* @author Sebastian Sdorra
@@ -53,7 +53,7 @@ public @interface I18n
{
/**
* Returns the key for the resource bundle.
*
*
* @return resource bundle key
*/
String value();

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -15,7 +15,7 @@
<name>scm-core</name>
<dependencies>
<!-- servlet api -->
<dependency>
@@ -55,13 +55,13 @@
</dependency>
<!-- security -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- injection -->
<dependency>
@@ -176,7 +176,7 @@
<!-- validation -->
<dependency>
<groupId>org.hibernate</groupId>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
@@ -230,12 +230,12 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<useStandardDocletOptions>true</useStandardDocletOptions>
<charset>${project.build.sourceEncoding}</charset>
<encoding>${project.build.sourceEncoding}</encoding>
<docencoding>${project.build.sourceEncoding}</docencoding>
<source>${project.build.javaLevel}</source>
<breakiterator>true</breakiterator>
<version>true</version>
<author>true</author>
@@ -253,20 +253,6 @@
<link>http://www.slf4j.org/api/</link>
<link>http://shiro.apache.org/static/${shiro.version}/apidocs/</link>
</links>
<doclet>org.jboss.apiviz.APIviz</doclet>
<docletArtifact>
<groupId>org.jboss.apiviz</groupId>
<artifactId>apiviz</artifactId>
<version>1.3.2.GA</version>
</docletArtifact>
<additionalOptions>
<additionalOption>
-sourceclasspath ${project.build.outputDirectory}
</additionalOption>
<additionalOption>
-nopackagediagram
</additionalOption>
</additionalOptions>
</configuration>
</plugin>

View File

@@ -36,6 +36,7 @@ package sonia.scm;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import sonia.scm.util.Util;
//~--- JDK imports ------------------------------------------------------------
@@ -57,7 +58,13 @@ public class BasicContextProvider implements SCMContextProvider
{
/** Default version {@link String} */
public static final String DEFAULT_VERSION = "unknown";
public static final String VERSION_DEFAULT = "unknown";
/**
* System property to override scm-manager version.
* <b>Note</b>: This should only be used for testing and never in production.
**/
public static final String VERSION_PROPERTY = "sonia.scm.version.override";
/** Default name of the SCM-Manager base directory */
public static final String DIRECTORY_DEFAULT = ".scm";
@@ -95,7 +102,7 @@ public class BasicContextProvider implements SCMContextProvider
try
{
baseDirectory = findBaseDirectory();
version = loadVersion();
version = determineVersion();
stage = loadProjectStage();
}
catch (Exception ex)
@@ -157,7 +164,7 @@ public class BasicContextProvider implements SCMContextProvider
/**
* Returns the version of the SCM-Manager. If the version is not set, the
* {@link #DEFAULT_VERSION} is returned.
* {@link #VERSION_DEFAULT} is returned.
*
*
* @return the version of the SCM-Manager
@@ -257,6 +264,14 @@ public class BasicContextProvider implements SCMContextProvider
return s;
}
private String determineVersion() {
String version = System.getProperty(VERSION_PROPERTY);
if (Strings.isNullOrEmpty(version)) {
version = loadVersion();
}
return version;
}
/**
* Loads the version of the SCM-Manager from maven properties file.
*
@@ -295,7 +310,7 @@ public class BasicContextProvider implements SCMContextProvider
}
}
return properties.getProperty(MAVEN_PROPERTY_VERSION, DEFAULT_VERSION);
return properties.getProperty(MAVEN_PROPERTY_VERSION, VERSION_DEFAULT);
}
//~--- get methods ----------------------------------------------------------

View File

@@ -2,7 +2,7 @@ package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotEmpty;
@Getter @Setter
public class MergeCommandDto {

View File

@@ -91,6 +91,7 @@ public final class RepositoryService implements Closeable {
private final PreProcessorUtil preProcessorUtil;
private final RepositoryServiceProvider provider;
private final Repository repository;
@SuppressWarnings("rawtypes")
private final Set<ScmProtocolProvider> protocolProviders;
private final WorkdirProvider workdirProvider;
@@ -104,7 +105,10 @@ public final class RepositoryService implements Closeable {
*/
RepositoryService(CacheManager cacheManager,
RepositoryServiceProvider provider, Repository repository,
PreProcessorUtil preProcessorUtil, Set<ScmProtocolProvider> protocolProviders, WorkdirProvider workdirProvider) {
PreProcessorUtil preProcessorUtil,
@SuppressWarnings("rawtypes") Set<ScmProtocolProvider> protocolProviders,
WorkdirProvider workdirProvider
) {
this.cacheManager = cacheManager;
this.provider = provider;
this.repository = repository;
@@ -445,19 +449,23 @@ public final class RepositoryService implements Closeable {
return provider.getSupportedFeatures().contains(feature);
}
public <T extends ScmProtocol> Stream<T> getSupportedProtocols() {
public Stream<ScmProtocol> getSupportedProtocols() {
return protocolProviders.stream()
.filter(protocolProvider -> protocolProvider.getType().equals(getRepository().getType()))
.map(this::<T>createProviderInstanceForRepository);
.map(this::createProviderInstanceForRepository);
}
private <T extends ScmProtocol> T createProviderInstanceForRepository(ScmProtocolProvider<T> protocolProvider) {
@SuppressWarnings("rawtypes")
private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) {
return protocolProvider.get(repository);
}
@SuppressWarnings("unchecked")
public <T extends ScmProtocol> T getProtocol(Class<T> clazz) {
return this.<T>getSupportedProtocols()
return this.getSupportedProtocols()
.filter(scmProtocol -> clazz.isAssignableFrom(scmProtocol.getClass()))
// no idea how to fix this, without cast
.map(p -> (T) p)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(),getRepository().getType())));
}

View File

@@ -1,6 +1,7 @@
package sonia.scm;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junitpioneer.jupiter.TempDirectory;
@@ -13,32 +14,59 @@ import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(TempDirectory.class)
class BasicContextProviderTest {
private Path baseDirectory;
@Nested
class VersionTests {
private BasicContextProvider context;
@Test
void shouldReturnVersionFromSystemProperty() {
System.setProperty(BasicContextProvider.VERSION_PROPERTY, "3.0.0");
try {
SCMContextProvider context = new BasicContextProvider();
assertThat(context.getVersion()).isEqualTo("3.0.0");
} finally {
System.clearProperty(BasicContextProvider.VERSION_PROPERTY);
}
}
@Test
void shouldReturnDefaultVersion() {
SCMContextProvider context = new BasicContextProvider();
assertThat(context.getVersion()).isEqualTo(BasicContextProvider.VERSION_DEFAULT);
}
@BeforeEach
void setUpContext(@TempDirectory.TempDir Path baseDirectory) {
this.baseDirectory = baseDirectory;
context = new BasicContextProvider(baseDirectory.toFile(), "x.y.z", Stage.PRODUCTION);
}
@Test
void shouldReturnAbsolutePathAsIs(@TempDirectory.TempDir Path path) {
Path absolutePath = path.toAbsolutePath();
Path resolved = context.resolve(absolutePath);
@Nested
class PathTests {
assertThat(resolved).isSameAs(absolutePath);
}
private Path baseDirectory;
@Test
void shouldResolveRelatePath() {
Path path = Paths.get("repos", "42");
Path resolved = context.resolve(path);
private BasicContextProvider context;
@BeforeEach
void setUpContext(@TempDirectory.TempDir Path baseDirectory) {
this.baseDirectory = baseDirectory;
context = new BasicContextProvider(baseDirectory.toFile(), "x.y.z", Stage.PRODUCTION);
}
@Test
void shouldReturnAbsolutePathAsIs(@TempDirectory.TempDir Path path) {
Path absolutePath = path.toAbsolutePath();
Path resolved = context.resolve(absolutePath);
assertThat(resolved).isSameAs(absolutePath);
}
@Test
void shouldResolveRelatePath() {
Path path = Paths.get("repos", "42");
Path resolved = context.resolve(path);
assertThat(resolved).isAbsolute();
assertThat(resolved).startsWithRaw(baseDirectory);
assertThat(resolved).endsWithRaw(path);
}
assertThat(resolved).isAbsolute();
assertThat(resolved).startsWithRaw(baseDirectory);
assertThat(resolved).endsWithRaw(path);
}
}

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

@@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
@@ -27,7 +27,7 @@ public class ObjectMapperProvider implements Provider<ObjectMapper> {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true);
mapper.setDateFormat(new ISO8601DateFormat());
mapper.setDateFormat(new StdDateFormat());
return mapper;
}

View File

@@ -2,7 +2,7 @@ package sonia.scm.api.v2.resources;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotEmpty;
import sonia.scm.ReducedModelObject;
import sonia.scm.group.GroupDisplayManager;
import sonia.scm.user.UserDisplayManager;

View File

@@ -7,8 +7,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
@Getter @Setter @NoArgsConstructor

View File

@@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

View File

@@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotEmpty;
@Getter
@Setter

View File

@@ -3,7 +3,7 @@ package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotEmpty;
@Getter
@Setter

View File

@@ -7,8 +7,8 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Pattern;

View File

@@ -6,7 +6,7 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotEmpty;
import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Pattern;

View File

@@ -6,7 +6,7 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotEmpty;
import java.time.Instant;
import java.util.Collection;

View File

@@ -7,8 +7,8 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Pattern;

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 without 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

@@ -0,0 +1,24 @@
package sonia.scm.api.rest;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.junit.jupiter.api.Test;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import static org.assertj.core.api.Assertions.assertThat;
class ObjectMapperProviderTest {
private final ObjectMapperProvider provider = new ObjectMapperProvider();
@Test
void shouldFormatInstantAsISO8601() throws JsonProcessingException {
ZoneId zone = ZoneId.of("Europe/Berlin");
ZonedDateTime date = ZonedDateTime.of(2020, 2, 4, 15, 21, 42, 0, zone);
String value = provider.get().writeValueAsString(date.toInstant());
assertThat(value).isEqualTo("\"2020-02-04T14:21:42Z\"");
}
}

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;
}
}
}