alternative to my ClassLoader hack

C

cbossens73

Hi,

since a very long time I've been using a hack (I think I found
it by lurking on this group many years ago) to add .jar whose
location would only be known at runtime to the default
ClassLoader.

It was pointed out that it was an hack because it relied on:
- the default class loader being an URLClassLoader (which
apparently just happened to always happen since Java exist
but this may change)
- no security manager being installed

My old hack looked like that:

public static void defaultClassLoaderHack( final File
dirWithJarsToAdd ) {
final Method addURL;
try {
addURL = URLClassLoader.class.getDeclaredMethod( "addURL",
new Class[] {URL.class} );
addURL.setAccessible(true); //you're telling the JVM
to override the default visibility
final File[] files = getExternalJarsFromDir
(dirWithJarsToAdd);
final ClassLoader cl = ClassLoader.getSystemClassLoader();
for (File file : files) {
final URL url = file.toURI().toURL();
addURL.invoke(cl, new Object[]{url});
}
} catch (Exception e) {
e.printStackTrace();
}
}

Now I'm not interested in "fixing this hack".

I'm interested in knowing what is the correct way to
achieve what I want to achieve: make my application
find, at runtime, all the classes located inside .jar
whose location will only be known at runtime.

Would the correct (portable) way to do what I want
to do to force my application to use my own
ClassLoader, instead of ClassLoader.getSystemClassLoader()?

But how can I go about writing my application so that
all the classes are loaded into "my" URLClassLoader,
instead of adding classes to the default ClassLoader.

I'm probably way of in my understanding of class loader,
but I some things are sure:
- I cannot know where the jars will be
- I cannot package the jars with my app
- I will have a way to find, for sure, where the jars will
be at runtime

Based on that, I *think* I should use an URLClassLoader
however I've got no idea how to do this correctly.

Any pointer/help/code example much appreciated,

Charles
 
M

Mark Space

Based on that, I *think* I should use an URLClassLoader
however I've got no idea how to do this correctly.


I think you are correct. There's no way to do anything about the
security manager. If one is installed and denies you permissions, well
that's what it's supposed to do. The JVM itself is designed to be
"safe" and will allow the user to deny any program certain operations.
What you might want to think about is recovering gracefully from a
security exception. The easiest is just to pop up a dialog that says
"you need XXX permissions to run this app" then shut down.


For making your own URLClassLoader, just do it. Load your first class
with it, and invoke a method. Everthing afterthat will automatically
use the classloader it was invoked with, yours, and the URLClassLoader
automatically invokes it's parent to load classes it doesn't know about.

I haven't tried this, but I did look into it a while ago. Here's my
little sample:


package fubar;

import java.net.URL;
import java.net.URLClassLoader;


public class MyClassLoader
{
public static void main( String... args )
throws ClassNotFoundException, InstantiationException,
IllegalAccessException
{
URL[] urls;
urls = null; //... add some
URLClassLoader cl = new URLClassLoader( urls );
@SuppressWarnings("unchecked")
Class<MyClassLoader> main = (Class<MyClassLoader>) cl.
loadClass( "fubar.MyClassLoader" );
MyClassLoader mcl = main.newInstance();
mcl.startApplication();
}

private void startApplication()
{
// everything else here
}
}
 
M

Mark Space

Steven said:
Try comparing 'main' with Class.forName("fubar.MyClassLoader") (i.e.
using the system classloader). 'cl' will always try its parent first
before looking for anything itself, so it should return the same thing.
Therefore, the new instance of 'main' will still be using the system
classloader. You'd have to use 'cl' to load a different class.

The code I posted should end up with two copies of MyClassLoader.class
loaded -- one by the system loader and one by the URLClassLoader. That
the URLClassLoader actually invoked the system class loader should not
matter, the second copy should be marked as having been loaded through
the URLClassLoader.

Your version seems to rely on having actual discreet .class files on the
file system, which probably won't work if everything has been packaged
up into a .jar.

I haven't tested my conclusions in the above two paragraphs so it might
be best to experiment a bit.
 
M

Mark Space

Steven said:
public class MyClassLoader {
public static void main(String... args) throws Exception {
// Using subdir of current directory as example.
File curDir = new File(System.getProperty("user.dir"));
curDir = new File(curDir, "subdir").getCanonicalFile();
URL[] urls = { curDir.toURI().toURL() };

ClassLoader cl = new URLClassLoader(urls);
Class<?> main = Class.forName("TheRealMainClass", true, cl);

Method mth = main.getMethod("main", args.getClass());
mth.invoke(null, (Object[]) args);
}
}


Just for the record, this runs, but gets an exception. I'm not sure
100% why. I'll try to clean it up then make it use your method. The
code below must be executed as a jar file or it won't reproduce the error.

$ java -jar test.jar
String name: /fubar/MyClassLoader.class
URL:
jar:file:/C:/Users/Brenden/Dev/misc/fubar/build/classes/test.jar!/fubar/MyClassLoader.class
path to jar file:/C:/Users/Brenden/Dev/misc/fubar/build/classes/test.jar
making FubarLoader:
Classloader: fubar.FubarLoader@addbf1
finding fubar.Launcher
trying super class...
findClass got it
Exception in thread "main" java.lang.IllegalAccessException: Class
fubar.MyClass
Loader can not access a member of class fubar.Launcher with modifiers ""
at sun.reflect.Reflection.ensureMemberAccess(Unknown Source)
at java.lang.Class.newInstance0(Unknown Source)
at java.lang.Class.newInstance(Unknown Source)
at fubar.MyClassLoader.main(MyClassLoader.java:53)

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package fubar;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;


public class MyClassLoader
{
public static void main( String... args )
throws ClassNotFoundException, InstantiationException,
IllegalAccessException, MalformedURLException
{

URL[] urls = new URL[1];

String classResource = "/" +
MyClassLoader.class.getName().replaceAll( "\\.", "/" )
+ ".class";
System.out.println( "String name: " + classResource );
URL myClass =
MyClassLoader.class.getResource( classResource );

System.out.println( "URL: " + myClass );

String pathToClass = myClass.toString();
int index = pathToClass.indexOf( '!' );
pathToClass = pathToClass.substring( 4, index );
System.out.println( "path to jar " + pathToClass );
URL jarURL = new URL( pathToClass );

urls[0] = jarURL;

System.out.println( "making FubarLoader:" );
URLClassLoader cl = new FubarLoader( urls );
System.out.println( "Classloader: " + cl );
@SuppressWarnings( "unchecked" )
Class<Launcher> main = (Class<Launcher>) cl.loadClass(
"fubar.Launcher" );
Launcher mcl = main.newInstance();
mcl.launch();
}

private void startApplication( Class<?> c )
{
System.out.println( "Class files are equal: " + (c ==
MyClassLoader.class) );
System.out.println( "Classloader: " + getClass().
getClassLoader() );
// everything else here
}
}

class Launcher
{
public void launch()
{
System.out.println( "Classloader: " + getClass().
getClassLoader() );
}
}

/*
* classloader call trace:
*
* I. loadClass( String )
*
* II. loadClass( String, false )
* 3. findLoadedClass prot
* A. FindLoadClass0 -- native -- PRIVATE
* 4. loadClass (String) on parent
* 5. findBootstrapClass0 PRIVATE
* 6. findClass prot
* 7. resolveClass prot
* A. resolveClass -- native -- PRIVATE
*
*/

class FubarLoader extends URLClassLoader {

public FubarLoader( URL[] urls )
{
super( urls );
}

@Override
public Class<?> loadClass( String className )
throws ClassNotFoundException
{
if( className.startsWith( "fubar" ) ) {
System.out.println( "finding " + className );
Class<?> c = null;
try {
c = findClass( className );
}
catch( ClassNotFoundException ex ) {
}
if( c != null ) {
System.out.println( "findClass got it" );
return c;
}
}
System.out.println( "trying super class..." );
return super.loadClass( className );
}
}
 
M

Mark Space

Steven said:
I'd go with something like this:

import java.lang.reflect.Method;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class MyClassLoader {
public static void main(String... args) throws Exception {
// Using subdir of current directory as example.
File curDir = new File(System.getProperty("user.dir"));
curDir = new File(curDir, "subdir").getCanonicalFile();
URL[] urls = { curDir.toURI().toURL() };

ClassLoader cl = new URLClassLoader(urls);
Class<?> main = Class.forName("TheRealMainClass", true, cl);

Method mth = main.getMethod("main", args.getClass());
mth.invoke(null, (Object[]) args);
}
}

Impressive! Obi Wan has taught you well. But you have a minor mistake
in the last line: args is interpetted as the parameter itself for
invoke, it should be wrapped up in an object array for the varargs.

Again, the following code must be run as a jar file to work correctly.



/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package fubar;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

/**
*
* @author Brenden
*/
public class FubarLoader2 extends URLClassLoader
{
public FubarLoader2( URL[] urls )
{
super( urls );
}

public static void main( String... args )
throws Exception
{
URL[] urls = new URL[1];

String classResource =
"/" +
MyClassLoader.class.getName().replaceAll( "\\.", "/" ) +
".class";
System.out.println( "String name: " + classResource );
URL myClass =
MyClassLoader.class.getResource( classResource );

System.out.println( "URL: " + myClass );

String pathToClass = myClass.toString();
int index = pathToClass.indexOf( '!' );
pathToClass = pathToClass.substring( 4, index );
System.out.println( "path to jar " + pathToClass );
URL jarURL = new URL( pathToClass );

urls[0] = jarURL;

System.out.println( "making FubarLoader2:" );
URLClassLoader cl = new FubarLoader( urls );
System.out.println( "class for name..." );
Class<?> main =
Class.forName( "fubar.FubarLoader2", true, cl );

System.out.println( "invoking launch" );
Method mth = main.getMethod( "launch", args.getClass() );
mth.invoke( null, new Object[] {args} );

}

public static void launch( String... args )
{
System.out.println( "LAUNCHED!" );
System.out.println( FubarLoader2.class.getClassLoader() );
}

@Override
public Class<?> loadClass( String className )
throws ClassNotFoundException
{
System.out.println( "finding " + className );
if( className.startsWith( "fubar" ) ) {
Class<?> c = null;
try {
c = findClass( className );
}
catch( ClassNotFoundException ex ) {
}
if( c != null ) {
System.out.println( "findClass got it" );
return c;
}
}
System.out.println( "trying super class..." );
return super.loadClass( className );
}
}



Output:

$ java -jar test.jar
String name: /fubar/MyClassLoader.class
URL:
jar:file:/C:/Users/Brenden/Dev/misc/fubar/build/classes/test.jar!/fubar/MyClassLoader.class
path to jar file:/C:/Users/Brenden/Dev/misc/fubar/build/classes/test.jar
making FubarLoader2:
class for name...
finding fubar.FubarLoader2
trying super class...
findClass got it
trying super class...
trying super class...
trying super class...
trying super class...
finding fubar.FubarLoader
findClass got it
invoking launch
trying super class...
trying super class...
trying super class...
trying super class...
trying super class...
LAUNCHED!
fubar.FubarLoader@addbf1
 
M

Mark Space

Mark said:
Just for the record, this runs, but gets an exception. I'm not sure
100% why. I'll try to clean it up then make it use your method. The


I have my version working now. The secret is to poke the object made
with the new classloader reflectively. I guess the exception I get
above is related to the fact that MyClassLoader is loaded by two
different classloaders, and therefore one class type isn't the same as
the other. Weird, but true.

This version does pass the first MyClassLoader.class to the second, and
then compares the two class types and determines they are not equal, as
expected. I think this program and Stevens programs are equivalent,
showing that both ideas are valid.

Again, must be run as a jar or the string mangling code used in the
beginning of main won't be able to produce a valid URL for the
URLClassLoader (actually a FubarLoader here).

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package fubar;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

/**
*
* @author Brenden
*/
public class MyClassLoader
{
public static void main( String... args )
throws ClassNotFoundException, InstantiationException,
IllegalAccessException, MalformedURLException,
NoSuchMethodException, IllegalArgumentException,
InvocationTargetException
{

URL[] urls = new URL[1];

String classResource =
"/" +
MyClassLoader.class.getName().replaceAll( "\\.", "/" ) +
".class";
System.out.println( "String name: " + classResource );
URL myClass =
MyClassLoader.class.getResource( classResource );

System.out.println( "URL: " + myClass );

String pathToClass = myClass.toString();
int index = pathToClass.indexOf( '!' );
pathToClass = pathToClass.substring( 4, index );
System.out.println( "path to jar " + pathToClass );
URL jarURL = new URL( pathToClass );

urls[0] = jarURL;

System.out.println( "making FubarLoader:" );
URLClassLoader cl = new FubarLoader( urls );
System.out.println( "Classloader: " + cl );
@SuppressWarnings( "unchecked" )
Class<MyClassLoader> main = (Class<MyClassLoader>) cl.
loadClass(
"fubar.MyClassLoader" );
/*Exception in thread "main" java.lang.ClassCastException:
fubar.MyClassLoader can
not be cast to fubar.MyClassLoader
at fubar.MyClassLoader.main(MyClassLoader.java:47)
*/
// MyClassLoader mcl = main.newInstance();
// mcl.startApplication( MyClassLoader.class );
Object mcl = main.newInstance();
Method m = mcl.getClass().getMethod( "startApplication",
Class.class );
m.invoke( mcl, MyClassLoader.class );
}

public void startApplication( Class<?> c )
// public void startApplication( )
{
System.out.println( "Class files are equal: " + (c ==
MyClassLoader.class) );
System.out.println( "Classloader: " + getClass().
getClassLoader() );
// everything else here
}
}

class Launcher
{
public void launch()
{
System.out.println( "Classloader: " + getClass().
getClassLoader() );
}
}

/*
* classloader call trace:
*
* I. loadClass( String )
*
* II. loadClass( String, false )
* 3. findLoadedClass prot
* A. FindLoadClass0 -- native -- PRIVATE
* 4. loadClass (String) on parent
* 5. findBootstrapClass0 PRIVATE
* 6. findClass prot
* 7. resolveClass prot
* A. resolveClass -- native -- PRIVATE
*
*/
class FubarLoader extends URLClassLoader
{
public FubarLoader( URL[] urls )
{
super( urls );
}

@Override
public Class<?> loadClass( String className )
throws ClassNotFoundException
{
System.out.println( "finding " + className );
if( className.startsWith( "fubar" ) ) {
Class<?> c = null;
try {
c = findClass( className );
}
catch( ClassNotFoundException ex ) {
}
if( c != null ) {
System.out.println( "findClass got it" );
return c;
}
}
System.out.println( "trying super class..." );
return super.loadClass( className );
}
}


This produces the following output:

$ java -jar test.jar
String name: /fubar/MyClassLoader.class
URL:
jar:file:/C:/Users/Brenden/Dev/misc/fubar/build/classes/test.jar!/fubar/MyC
lassLoader.class
path to jar file:/C:/Users/Brenden/Dev/misc/fubar/build/classes/test.jar
making FubarLoader:
Classloader: fubar.FubarLoader@19821f
finding fubar.MyClassLoader
finding java.lang.Object
trying super class...
findClass got it
finding java.net.URLClassLoader
trying super class...
finding fubar.FubarLoader
findClass got it
finding java.lang.Class
trying super class...
finding java.lang.String
trying super class...
finding java.lang.ClassNotFoundException
trying super class...
finding java.lang.InstantiationException
trying super class...
finding java.lang.IllegalAccessException
trying super class...
finding java.net.MalformedURLException
trying super class...
finding java.lang.NoSuchMethodException
trying super class...
finding java.lang.IllegalArgumentException
trying super class...
finding java.lang.reflect.InvocationTargetException
trying super class...
finding java.lang.System
trying super class...
finding java.lang.StringBuilder
trying super class...
finding java.io.PrintStream
trying super class...
Class files are equal: false
Classloader: fubar.FubarLoader@19821f
 
C

cbossens73

I have my version working now. The secret is to poke the object made
with the new classloader reflectively. I guess the exception I get
above is related to the fact that MyClassLoader is loaded by two
different classloaders, and therefore one class type isn't the same as
the other. Weird, but true.

Thanks a lot for your answers...

Just before going to sleep (western Europe here) I got to
read your first reply.

Now woke up and read the whole thread and I'm going to try
the working version!

Thanks a lot,

Charles
 
C

cbossens73

Hi again,

I tried to modify your "startApplication" method so
I could pass it both the original "String[] args"
*and* the URLClassLoader. I didn't succeed however.

May I bother you a little bit more and ask you how
I could change this:

public void startApplication( Class<?> c )

to that:

public void startApplication( Class<?> c, String[] args,
URLClassLoader ucl)

The call is using reflection and hence I'm completely lost...

Thanks again for your great (and lenghty) piece of working code!,

Charles
 
M

Mark Space

Steven said:
Ah, this inverts the normal order of super.loadClass being called before
this.findClass, hence you can load an already-loaded class with a
different classloader.


Yes it does, and it's probably a very important point. This is a
deliberately hacked class loader that enables me to load the same class
twice, so I can test my little theory, using just one jar file a couple
of classes. It's for test purposes only, and I can't think of a real
world reason to do this outside of weird test cases like this.

A real class loader should probably only override findClass(), like the
class loader docs say to, rather than change the order of the calls like
I've done here. I hope Mr. C. Bossens doesn't get confused by my hacking.
 
L

Lew

Mark said:
Yes it does, and it's probably a very important point. This is a
deliberately hacked class loader that enables me to load the same class
twice, so I can test my little theory, using just one jar file a couple
of classes. It's for test purposes only, and I can't think of a real
world reason to do this outside of weird test cases like this.

There must be such a reason, since WebSphere (and likely other containers)
allow "parent-last" class loading as a standard option.
 
M

Mark Space

Hi again,

I tried to modify your "startApplication" method so
I could pass it both the original "String[] args"
*and* the URLClassLoader. I didn't succeed however.


I'm trying to do this, but it isn't working. The classes loaded by the
default (system) class loader, and those loaded by the new class (the
second time MyClassLoader is loaded) don't match, because the java.*
classes are getting loaded too. I'm not sure why, and it's bugging me.
I don't have time to look into this more right now, however. I'll try
to devel into it later.
 
M

Mark Space

Lew said:
There must be such a reason, since WebSphere (and likely other
containers) allow "parent-last" class loading as a standard option.


That was basically my inspiration for the classloader I wrote. However
one thing bugs me -- my classloader seems to reload the system classes
too -- java.* and others. I only wanted it to reload classes in fubar.*
I'm not sure why it's loading the system classes twice... I thought
calling the super class's method would allow normal class loading for
all other classes.

I don't have time to investigate this more right now... if anyone has a
hint about how I might improve this class loading, I'd appreciate it.
 
C

cbossens73

Hi again,
I tried to modify your "startApplication" method so
I could pass it both the original "String[] args"
*and* the URLClassLoader. I didn't succeed however.

I'm trying to do this, but it isn't working. The classes loaded by the
default (system) class loader, and those loaded by the new class (the
second time MyClassLoader is loaded) don't match, because the java.*
classes are getting loaded too. I'm not sure why, and it's bugging me.
I don't have time to look into this more right now, however. I'll try
to devel into it later.

Ah :)

Don't hesitate to report your findings if/when you find the time
one of these days, I'm very interested.

Charles
 
C

cbossens73

Hi again,
I tried to modify your "startApplication" method so
I could pass it both the original "String[] args"
*and* the URLClassLoader. I didn't succeed however.
May I bother you a little bit more and ask you how
I could change this:
public void startApplication( Class<?> c )
public void startApplication( Class<?> c, String[] args,
URLClassLoader ucl)

OK, I think the problem was that I wasn't setting the parent
classloader, so there was no way for my classloader to commune with the
system classloader, and it was loading all classes itself.

Hi Mark,

once again thanks for all the help. I'll play with
all this today :)

Charles.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,968
Messages
2,570,153
Members
46,699
Latest member
AnneRosen

Latest Threads

Top