For stand-alone java application I can use the following codes to load jar lib dynamically during the runtime based on the library path. It seems not working if I deploy same codes in Java Web container and run as a servlet. I actually want to be able to load the different jar libs based on the jar lib path in servlet request.
It means that single servlet will have to be able to load different jar libs dynamically during runtime and run onward biz logic
user1 might request to load jar files under /tmp/lib/v1.0/.jar
user2 might request to load jar files under /tmp/lib/v1.1/.jar
(The jar files in v1.0 and v1.1 got the exactly same class names)
Thanks!!!
=== Main =============
LibraryLoader loader = new LibraryLoader();
loader.addClassPath(<jar lib root path>);
// below will run biz logic
=== LibraryLoader.java ==========
public class LibraryLoader {
URLClassLoader urlClassLoader;
public LibraryLoader() {
urlClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
}
public void addClassPath(String jarLibPath) throws Exception {
Class urlClass = URLClassLoader.class;
File jarPath = new File(jarLibPath);
FileFilter jarFilter = new FileFilter() {
public boolean accept(File f) {
if (f.isDirectory())
return true;
String name = f.getName().toLowerCase();
return name.endsWith("jar");
}
};
File[] files = jarPath.listFiles(jarFilter);
for (File file : files) {
if (file.isFile()) {
URI uriPath = file.toURI();
URL urlPath = uriPath.toURL();
Method method = urlClass.getDeclaredMethod("addURL", new Class[] { URL.class });
method.setAccessible(true);
method.invoke(urlClassLoader, new Object[] { urlPath });
System.out.println(file.getCanonicalPath());
} else {
addClassPath(file.getCanonicalPath());
}
}
}
}
that's not what you want to do. even in your stand-alone program, this won't really work (as you could be pushing multiple versions of the same jar into the main classloader). what you want to do is create a new classloader containing the new jars, then invoke some class in the new classloader.
e.g.:
// get relevant jar urls
URL[] urls = ...;
ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
try {
URLClassLoader loader = new URLClassLoader(urls);
Thread.currentThread().setContextClassLoader(loader);
Class<?> entryClass = loader.loadClass("entry.class.name");
// do something here w/ entryClass (e.g. instantiate it) ...
} finally {
Thread.currentThread().setContextClassLoader(oldLoader);
}
of course, you probably want to cache these classloaders and re-use them on subsequent requests.
of course, i'm not sure why you don't just deploy multiple versions of the servlet with the different versioned jars (since you indicate that the version is part of the servlet path).
You should create different classloader for different users and set to the Current Thread.
ClassLoader ctxLoader = Thread.currentThread().getContextClassLoader();
ClassLoader currentLoader;
if(user1){
//initialize classloader with jar files under /tmp/lib/v1.0/.jar
currentLoader = user1ClassLoader;
} else if(user2){
//initialize classloader with jar files under /tmp/lib/v1.1/.jar
currentLoader = user2ClassLoader;
}
Thread.currentThread().setContextClassLoader(currentLoader );
//invoke business logic
//reset the actual loader
Thread.currentThread().setContextClassLoader(ctxLoader );
Related
I've come across many posts about these two topics: Auto-Updating and URLClassloaders. I'll start with the auto updating goal. I found this post here that talks about a 2 jar system. One jar that launches the main app jar: From Stephen C:
The launcher could be a Java application that creates a classloader for the new JAR, loads an entrypoint class and calls some method on it. If you do it this way, you have to watch for classloader storage leaks, but that's not difficult. (You just need to make sure that no objects with classes loaded from the JAR are reachable after you relaunch.)
This is the approach I'm taking, but I'm open to other ideas if they prove easier and/or more reliable. The Coordinator has posted some pretty cool launcher code to which I plan on incorporating some of this reload type code in my launcher, but first I need to get it to work.
My issue is that my main app jar has many other dependencies, and I cannot get some of those classes to load despite the fact that all the jars have been added to the URL's array. This brings up the second topic URLClassloader.
Side Note for future readers: When passing a URL to the URLClassloader that is a directory, a helpful note that would have saved me (an embarrassingly large) amount of time is that the contents of the directory must be .class files! I was originally pointing to my dependent jar directory, no good.
Context for the code below, my launcher jar resides in the same directory as my app jar, which is why I'm using user.dir. I will probably change this, but for now the code works and gets far enough into my app's code to request a connection to a sqlite database before failing.
Launcher:
public class Launcher {
public static void main(String[] args) {
try {
String userdir = System.getProperty("user.dir");
File parentDir = new File(userdir);
ArrayList<URL> urls = getJarURLs(parentDir);
URL[] jarURLs = new URL[urls.size()];
int index = 0;
for (URL u : urls) {
System.out.println(u.toString());
jarURLs[index] = u;
index ++;
}
URLClassLoader urlCL = new URLClassLoader(jarURLs);
Class<?> c = urlCL.loadClass("main.AppStart");
Object [] args2 = new Object[] {new String[] {}};
c.getMethod("main", String[].class).invoke(null, args2);
urlCL.close();
} catch (Exception e1) {
e1.printStackTrace();
}
}
public static ArrayList<URL> getJarURLs(File parentDir) throws MalformedURLException {
ArrayList<URL> list = new ArrayList<>();
for (File f : parentDir.listFiles()) {
if (f.isDirectory()) {
list.addAll(getJarURLs(f));
} else {
String name = f.getName();
if (name.endsWith(".jar")) {
list.add(f.toURI().toURL());
}
}
}
return list;
}
}
Here's an example of the URL output added to the array:
file:/C:/my/path/to/dependent/jars/sqlite-jdbc-3.32.3.2.jar
file:/C:/my/path/to/main/app.jar
file: ... [10 more]
The URLClassloader seems to work well enough to load my main method in app.jar. The main executes a some startup type stuff, before attempting to load a login screen. When the request is made to get the user info database, my message screen loads and displays (<-this is important for later)
the stacktrace containing:
java.sql.SQLException: No suitable driver found for jdbc:sqlite:C:\...\users.db
I understand that this is because that jar is not on the class path, but it's loaded via the class loader, so why can't it find the classes from the jar? From this post JamesB suggested adding Class.forName("org.sqlite.JDBC"); before the connection request. I rebuilt the app jar with this line of code and it worked!
The weird thing that happened next, is that my message screen class can no longer be found even though earlier it loaded and displayed correctly. The message screen is a class inside my main app.jar and not in a dependent jar, which is why I'm baffled. Am I going to have to add Class.forName before every instance of any of my classes? That seems rude..
So what could I be doing wrong with the class loader? Why does it load some classes and not others despite that fact that all the jars have been added to the URL array?
Some other relative info: My app works perfectly as intended when launched from windows command line when the classpath is specified: java -cp "main-app.jar;my/dependent/jar/directory/*" main.AppStart. It's only when I try launching the app via this classloader that I have these issues.
By the way, is this java command universal? Will it work on all operating systems with java installed? If so, could I not just scrap this launcher, and use a process builder to execute the above command? Bonus points for someone who can tell me how to execute the command from a jre packaged with my app, as that's what I plan on doing so the user does not have to download Java.
EDIT
I figured out one of the answers to one of the questions below. Turns out, I didn't need to do any of the code below. My main method loads a login screen but after it's loaded it returns back to the AppLauncher code, thus closing the URLClassLoader! Of course, at that point any requested class will not be found as the loader has been closed! What an oof! Hopefully I will save someone a headache in the future...
Original
Well, after more time, effort, research, and effective use of Eclipse's debugging tool, I was able to figure out what I needed to do to resolve my issues.
So the first issue was my JDBC driver was never registered when passing the jars to the URLClassloader. This is the part I sorta don't understand, so advisement would be welcomed, but there is a static block in the JDBC class that registers the driver so it can be used by DriverManager see code below. Loading the class is what executes that static block, hence why calling Class.forName works.
static {
try {
DriverManager.registerDriver(new JDBC());
} catch (SQLException e) {
e.printStackTrace();
}
}
What I don't understand, is how class loading works if jars are specified via the class path. The URLClassLoader doesn't load any of those classes until they are called, and I never directly work with the JDBC class, thus no suitable driver exception, but are all the classes specified via the classpath loaded initially? Seems that way for static blocks to execute.
Anyhow, to resolve my other issue with some of my app's classes not being found I had to implement my own classloader. I get what I did and how it works well, but still don't understand why I had to do it. All of my jars were loaded to the original URLClassloader so if I could find them and the files within, why couldn't it do it?
Basically, I had to override the findClass and findResource methods to return jarEntry information that I had to store. I hope this code helps someone!
public class SBURLClassLoader extends URLClassLoader {
private HashMap<String, Storage> map;
public SBURLClassLoader(URL[] urls) {
super(urls);
map = new HashMap<>();
try {
storeClasses(urls);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
private void storeClasses(URL[] urls) throws ClassNotFoundException {
for (URL u : urls) {
try {
JarFile jarFile = new JarFile(new File(u.getFile()));
Enumeration<JarEntry> e = jarFile.entries();
while (e.hasMoreElements()) {
JarEntry jar = e.nextElement();
String entryName = jar.getName();
if (jar.isDirectory()) continue;
if (!entryName.endsWith(".class")) {
//still need to store these non-class files as resources
//let code continue to store entry un-altered
} else {
entryName = entryName.replace(".class", "");
entryName = entryName.replace("/", ".");
}
map.put(entryName, new Storage(jarFile, jar));
System.out.println(entryName);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
#Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> c = null;
try {
c = super.findClass(name);
} catch (ClassNotFoundException e) {
Storage s = map.get(name);
try {
InputStream in = s.jf.getInputStream(s.je);
int len = in.available();
c = defineClass(name, in.readAllBytes(), 0, len);
resolveClass(c);
} catch (IOException e1) {
e1.printStackTrace();
}
if (c == null) throw e;
}
return c;
}
#Override
public URL findResource(String name) {
URL url = super.findResource(name);
if (url == null) {
Storage s = map.get(name);
if (s != null) {
try {
url = new URL("jar:"+s.base.toString() + "!/" + name);
} catch (Exception e) {
e.printStackTrace();
}
}
}
return url;
}
private class Storage {
public JarFile jf;
public JarEntry je;
public URL base;
public Storage(JarFile jf, JarEntry je) {
this.jf = jf;
this.je = je;
try {
base = Path.of(jf.getName()).toUri().toURL();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
}
I created folder src/test/resources/ in root project directory, and inside this I added a file in folder jsons as jsons/server_request.json.
Now I am trying to read this file by calling a the static function in CommonTestUtilityclass given as:
public class CommonTestUtility {
public static String getFileAsString(String fileName) throws IOException {
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
File file = new File(classLoader.getResource(fileName).getFile());
String content = new String(Files.readAllBytes(file.toPath()));
return content;
}
}
Now while calling this function as
class ServerTest {
#Test
void test_loadResource() {
String content = CommonTestUtility.getFileAsString("jsons/server_request.json");
}
}
, It's giving me the error as:
CommonTestUtility - Cannot invoke "java.net.URL.getFile()" because the return value of "java.lang.ClassLoader.getResource(String)" is null.
I tried to include the src/test/resources/ in the run configuration
of Junit ServerTest.java, but still it's not able to find out the
resource
How to resolve this issue?
https://mkyong.com/java/java-read-a-file-from-resources-folder/
This above link might be helpful.
The getResource() method return an URI you need to change
.getFile() function to. toURI().
Simple code
private File getFileFromResource(String fileName) throws URISyntaxException{
ClassLoader classLoader = getClass().getClassLoader();
URL resource = classLoader.getResource(fileName);
if (resource == null) {
throw new IllegalArgumentException("file not found! " + fileName);
} else {
// failed if files have whitespaces or special characters
//return new File(resource.getFile());
return new File(resource.toURI());
}
}
I recreated the same scenario you describe and your code works for me.
Could you double-check that your project looks like mine below? If so, I suspect it might be something with your environment.
i've loaded jar dynamically from other jar file, then at some point, i want to delete this jar and replace it by newer version, on linux it works fine, while on windows when i try to move the file to backup directory i get file being used by another process exception.
public void loadJarAndClass() {
URL[] jarUrl = new URL[1];
jarUrl[0] = jarFile.toURI().toURL();
classLoader = new URLClassLoader (jarUrl, this.getClass().getClassLoader());
classToLoad = Class.forName ("Burner.MainWindow", true, classLoader);
}
public void unloadJarAndClass() {
/* all object must be collected in order to reload jar */
jarFile = null;
dukeClassLoader = null;
classToLoad = null;
System.gc();
}
my main:
jarPath = currentPath.getAbsolutePath() + File.separator + JAR_NAME;
jarFile = new File(jarPath);
loadJarAndClass();
unloadJarAndClass();
Files.delete(FileSystems.getDefault().getPath(jarPath));
my problem is with the delete which throws exception " the process cannot access the file because it is being used by another process"
how can i bypass this exception and close any handler opened ?
Try using Classloader.close() method,
Here is the extract from the Oracle link,
In Java SE 7, the URLClassLoader close() method effectively invalidates the loader, so that no new classes can be loaded from it. It also closes any JAR files that were opened by the loader. This allows the application to delete or replace these files and, if necessary, create new loaders using new implementations.
Following is the simplified version of the code that might serve the purpose,
public class Example {
public static void main(String[] args) throws IOException, ClassNotFoundException {
String jarPath = "C:\\example.jar";
File jarFile = new File(jarPath);
URL[] jarUrl = new URL[1];
jarUrl[0] = jarFile.toURI().toURL();
URLClassLoader classLoader = new URLClassLoader (jarUrl, Example.class.getClassLoader());
Class classToLoad = Class.forName ("DataGenerator", true, classLoader);
classLoader.close();
Files.delete(FileSystems.getDefault().getPath(jarPath));
}
}
please try closing the classloader before you 'unload' it.
public void unloadJarAndClass() throws IOException {
dukeClassLoader.close();
/* all object must be collected in order to reload jar */
jarFile = null;
dukeClassLoader = null;
classToLoad = null;
// System.gc(); don't do this unless you're really
// sure you have to !
}
I would recommend not calling System.gc() explicitly !
(see Why is it bad practice to call System.gc()? for example)
In order to list file contents of a specific directory on classpath I'm using the new FileSystem and Path features of Java 7. In one deployment the directory is stored on file system, directly. In another deployment it is stored into a JAR file.
My approach works fine with JAR files: I create a FileSystem object which refers to the JAR file and access the content via Path object.
...
URI dir = ...
String[] array = dir.toString().split("!");
try (final FileSystem fs = FileSystems.newFileSystem(URI.create(array[0]), new HashMap<String, Object>()))
{
final Path directory = fs.getPath(array[1]);
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory))
{
...
Due to the dir object has following value, it works:
jar:file:/C:/Users/pax/.../Detector-1.0.jar!/org/.../destinationdir
But in the other environment the destination directory is stored on file system, directly. dir object contains the value:
file:/C:/Users/pax/.../destinationdir
FileSystems.newFileSystem(...) always throws following exception for / and file:/C:/Users/pax/.../destinationdir as URI:
java.lang.IllegalArgumentException: Path component should be '/'
at sun.nio.fs.WindowsFileSystemProvider.checkUri(WindowsFileSystemProvider.java:68)
How do you use FileSystem.newFileSystem for destinations on file system?
Is there a better approach in order to list the directories content independently from its specific kind of storage (file system or JAR file)?
Following question's resolution tackles the issue ("destination on file system" versus "destination in JAR file") by try-catch approach: NIO2: how to generically map a URI to a Path?
This utility method tries to obtain a correct Path instance. But there may occur a further problem: If this destination resource is contained by a JAR file (instead of file system) then you can only access the resource via its associated FileSystem instance which must not be closed! So, your helper method needs to return the Path object as well as the FileSystem instance (only required if it's not on file system directly). The invoker has to close the FileSystem object, manually:
public static PathReference getPath(final URI resPath) throws IOException
{
try
{
// first try getting a path via existing file systems
return new PathReference(Paths.get(resPath), null);
}
catch (final FileSystemNotFoundException e)
{
/*
* not directly on file system, so then it's somewhere else (e.g.:
* JAR)
*/
final Map<String, ?> env = Collections.emptyMap();
final FileSystem fs = FileSystems.newFileSystem(resPath, env);
return new PathReference(fs.provider().getPath(resPath), fs);
}
}
The wrapper class PathReference should implement AutoClosable so that it can be used in try block:
public class PathReference implements AutoCloseable
{
...
#Override
public void close() throws Exception
{
if (this.fileSystem != null)
this.fileSystem.close();
}
public Path getPath()
{
return this.path;
}
public FileSystem getFileSystem()
{
return this.fileSystem;
}
}
This makes the release of the FileSystem instance a bit more transparent:
...
try (final PathReference fileObj = SignatureUtils.getPath(file))
{
...
try (InputStream fileStream = Files.newInputStream(fileObj.getPath()))
{
...
I have selected a jar file using file selector, then loaded all the classes in the jar file using java reflection. Some classes has dependency on another jar file.
But when I try to get method of class then following exception is thrown because this class has a import statement import com.thoughtworks.xstream.XStream; and XStream class is defined in another jar file.
Exception in thread "main" java.lang.NoClassDefFoundError: com/thoughtworks/xstream/io/HierarchicalStreamDriver
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2365)
at java.lang.Class.privateGetPublicMethods(Class.java:2488)
at java.lang.Class.getMethods(Class.java:1406)
at com.axway.xtp.testgenerator.templatewizard.MethodSelectionWizardUI.updateListofMethods(MethodSelectionWizardUI.java:744)
at com.axway.xtp.testgenerator.templatewizard.MethodSelectionWizardUI$7.widgetSelected(MethodSelectionWizardUI.java:474)
at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:90)
at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:66)
at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:928)
I wanted to know that is there any way to prevent the dependency class or jar file to be loaded using java reflection. Following are the piece of code I am using to load classes.
URLClassLoader ucl = new URLClassLoader(new URL[] { new URL("file:" + codeRepository) });
JarFile jarFile = new JarFile(new File(codeRepository));
Enumeration enm = jarFile.entries();
while (enm.hasMoreElements()) {
JarEntry entry = ((JarEntry) enm.nextElement());
if (entry.getName().endsWith(".class")) {
String fullClassNameWithPath = entry.getName();
String fullyClassifiedClassName = fullClassNameWithPath
.replace('/', '.');
try {
Class c = ucl.loadClass(fullyClassifiedClassName.substring(
0, fullyClassifiedClassName.indexOf(".class")));
String className = c.getPackage().getName() + "."
+ c.getSimpleName();
listClasses.add(className);
} catch (Exception e) {
continue;
} catch (Throwable t) {
continue;
}
}
}
Well, if your application depends on that class, you most definitely need to have the jar containing it (or provide an alternative path containing the package+class) on the classpath.
As part of the classloading process, any classes which the class you want to load depends upon will also be loaded.
I don't think there's anything you can do about that.