Classpath.java
/*
* Copyright (C) 2016 essobedo.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.github.essobedo.appma.core.util;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>Class allowing to release the jar files defined in the {@code JarFileFactory} which is specific to the Oracle's
* JDK.
*
* <p>This is a hack needed to properly release jar files from which we get resources thanks to
* {@link ClassLoader#getResource(String)}, {@link ClassLoader#getResourceAsStream(String)} or
* {@link ClassLoader#getResources(String)}. Indeed by default, the {@link JarFile} instances are automatically stored
* into the cache of {@code JarFileFactory} in case we call directly or indirectly one of the previous methods and
* those instances are not released even if we call {@link java.net.URLClassLoader#close()}, so the purpose of this
* hack is to remove them from the cache to fully release them.
*
* @author Nicolas Filotto (nicolas.filotto@gmail.com)
* @version $Id$
* @since 1.0
*/
public final class Classpath {
/**
* The logger of the class.
*/
private static final Logger LOG = Logger.getLogger(Classpath.class.getName());
/**
* Instance of {@code JarFileFactory} that contains the {@code JarFile} instances into its cache to release.
*/
private static final Object JAR_FILE_FACTORY = Classpath.getJarFileFactory();
/**
* Method allowing to get the instance of {@link JarFile} corresponding to a given {@link URL} that could
* be found into the cache.
*/
private static final Method GET = Classpath.getMethodGetByURL();
/**
* Method allowing to close a given instance of {@link JarFile} and remove it from the cache to make it available
* to the GC.
*/
private static final Method CLOSE = Classpath.getMethodCloseJarFile();
/**
* The urls of the resources corresponding to the classpath.
*/
private final URL[] urls;
/**
* Constructs a {@code Classpath} with the specified urls.
* @param urls the urls of the resources corresponding to the classpath.
*/
public Classpath(final URL... urls) {
this.urls = urls.clone();
}
/**
* Releases all the urls to make it available for the GC.
*/
public void release() {
for (final URL url : urls) {
if (url.getPath().endsWith("jar") || url.getPath().endsWith("zip")) {
try {
CLOSE.invoke(JAR_FILE_FACTORY, GET.invoke(JAR_FILE_FACTORY, url));
} catch (InvocationTargetException | IllegalAccessException e) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(
Level.WARNING,
String.format(
"Could not close the jar file '%s' used by the classloader",
url
),
e
);
}
}
}
}
}
/**
* Gives the {@link Method} allowing to release a given {@link JarFile} instance.
* @return The {@link Method} allowing to release a given {@link JarFile} instance.
*/
private static Method getMethodCloseJarFile() {
if (JAR_FILE_FACTORY != null) {
try {
final Method method = JAR_FILE_FACTORY.getClass().getMethod("close", JarFile.class);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Could not find the method close of the class JarFileFactory", e);
}
}
}
return null;
}
/**
* Gives the {@link Method} allowing to access to {@link JarFile} instances of the cache.
* @return The {@link Method} allowing to access to {@link JarFile} instances of the cache.
*/
private static Method getMethodGetByURL() {
if (JAR_FILE_FACTORY != null) {
try {
final Method method = JAR_FILE_FACTORY.getClass().getMethod("get", URL.class);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Could not find the method get of the class JarFileFactory", e);
}
}
}
return null;
}
/**
* Gives the instance of the singleton {@code JarFileFactory}.
* @return The instance of the singleton {@code JarFileFactory}.
*/
private static Object getJarFileFactory() {
try {
final Method getInstance = Class.forName(
"sun.net.www.protocol.jar.JarFileFactory",
true,
Classpath.class.getClassLoader()
).getMethod("getInstance");
getInstance.setAccessible(true);
return getInstance.invoke(null);
} catch (NoSuchMethodException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Could not find the method getInstance of the class JarFileFactory", e);
}
} catch (ClassNotFoundException e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "Could not find the class JarFileFactory", e);
}
} catch (IllegalAccessException | InvocationTargetException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Could not invoke the method getInstance of the class JarFileFactory", e);
}
}
return null;
}
}