I recently had a situation where I had a JAR that had dependencies on another JAR, but wouldn’t always be packaged with the other JAR. I’m not completely happy with what I ended up doing, but since I succeeded in doing what I wasn’t certain was possible, I decided to document it. Basically (as the title suggests), I tweaked my code so that at runtime it will download the JAR and add it to the classpath.
Downloading from a URL
Normally I’d use something like the Apache Commons IO library to help with downloading the JAR, but since that’s one of the pieces in the JAR to be downloaded, I’m in a catch-22 situation. Instead, I used vanilla Java for the implementation. I ran into some minor complications because the server hosting the JAR did not have a signed certificate, so I had to force Java to ignore certificate errors. Fortunately, no authentication was required–otherwise things would have been a bit more complicated. Here is my class:
package com.nathanbak.gomi;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class UrlDownloader {
/**
* Downloads the specified URL to the specified file location. Maximum size
* allowed is <code>Long.MAX_VALUE</code> bytes.
*
* @param url
* location to read
* @param file
* location to write
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
* @throws IOException
*/
public void download(URL url, File file) throws NoSuchAlgorithmException, KeyManagementException, IOException {
TrustManager [] trustManagers = new TrustManager [] { new NvbTrustManager() };
final SSLContext context = SSLContext.getInstance("SSL");
context.init(null, trustManagers, null);
// Set connections to use lenient TrustManager and HostnameVerifier
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(new NvbHostnameVerifier());
InputStream is = url.openStream();
ReadableByteChannel rbc = Channels.newChannel(is);
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
} finally {
if (fos != null) {
fos.close();
}
is.close();
}
}
/**
* Simple <code>TrustManager</code> that allows unsigned certificates.
*/
private static final class NvbTrustManager implements TrustManager, X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { }
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { }
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
/**
* Simple <code>HostnameVerifier</code> that allows any hostname and session.
*/
private static final class NvbHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
}
Adding JAR to classpath
This was my first foray into dynamically changing the classpath at runtime. I found many examples of how to load a specific class (when you know the full class name) from a JAR file, but there wasn’t as much information about stuffing a JAR of whatever into the current, running classpath. After much trial and error, this is what I finally produced:
package com.nathanbak.gomi;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
public class JarAdder {
public void addJarToClasspath(File jar) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, MalformedURLException {
// Get the ClassLoader class
ClassLoader cl = ClassLoader.getSystemClassLoader();
Class<?> clazz = cl.getClass();
// Get the protected addURL method from the parent URLClassLoader class
Method method = clazz.getSuperclass().getDeclaredMethod("addURL", new Class[] {URL.class});
// Run projected addURL method to add JAR to classpath
method.setAccessible(true);
method.invoke(cl, new Object[] {jar.toURI().toURL()});
}
}
The addJarToClasspath() method wasn’t necessary on my Windows system. My main JAR had a classpath to the dependency JAR specified in the MANIFEST.MF and as long as the JAR was downloaded there, it would be found. However, on Linux it didn’t work and so the method is necessary (and it doesn’t seem to hurt anything on Windows).
Other thoughts
- It is important to do the download and classpath changes before calling any code that depends on the stuff in the JAR. Even imports in the same class can cause problems.
- The downloadUrl() method is pretty generic and could be reused in a lot of situations provided the content being downloaded doesn’t get to big.
- Different versions of Java seem to behave differently–I’ve only tested two Java’s so far (one on Windows and one on Linux), but have seen very different behaviour.
- Since the certificate checking is disabled and code is loaded and at runtime, it seems like it would be an easy setup to attack or hack.
- This method could potentially be used for applications to self-update without needing to restart.
Conclusion
I’m not sure this is a permanent solution for my problem, but it does work for the time being. Also, I think the parts I learned while going through the process have potential to be used in future situations.