Sign in to follow this  
Mattox

[java] JAR Class loading

Recommended Posts

Hey guys, I'm working on a little Java RPG. At the moment I'm working on the quest system, and I decided to make the quests load from external class files. It's fine now, loads and runs just as I like it, except that there are a lot of anonymous classes used in the quest code and I end up with a heck of a lot of QuestName.class$1, $2 etc etc. What I'd like to do is jar up all the quest class files and make the game load the quests from inside the jar file (for no other reason than better compression and class file organization). Is this practical for my needs? Is it worth doing just to save a few kb on the HDD and make the folder look neater? I don't know much about class loading, so any educated opinions would be appreciated. I know it's possible, but I can't find a simple, to-the-point tutorial or resource on the net that explains how I can load my class files easily. This is my current class loader code:
	/**
	 * @return if the quests were loaded successfully or not
	 */
	public final boolean loadQuests()
	{
		try
		{
			long start = System.currentTimeMillis();
			quests.clear();
			questIds.clear();
			
			File questDir = new File(Config.CONF_DIR + System.getProperty("file.separator") + "quests");
			
			if(questDir == null || !questDir.exists())
				throw new Exception("Quests directory " + questDir.getAbsolutePath() + " does not exist!");
			
			// List valid .class files
			File[] questFiles = questDir.listFiles(new FilenameFilter()
			{
				@Override
				public boolean accept(File dir, String name)
				{
					return name.endsWith(".class") && name.indexOf("$") <= -1;
				}
			});
			
			int questCount = 0;
			for(File quest : questFiles)
			{
				try
				{
					String name = quest.getName().substring(0, quest.getName().length() - 6); // Remove .class ext
					Class c = Class.forName(name);
					Quest script = (Quest)c.getConstructor().newInstance();
					name = name.trim().toLowerCase();
					script.init();
					questIds.add(script.getUniqueID());
					quests.add(script);
					questCount++;
				} catch(Exception e2)
				{
					e2.printStackTrace();
					System.out.println("Quest file \"" + quest.getName() + "\" skipped due to load failure.");
				}
			}
			
			System.out.println("Loaded " + questCount + " quests successfully (" + (System.currentTimeMillis() - start) + "ms)");
		} catch(Exception e)
		{
			e.printStackTrace();
			return false;
		}
		
		return true;
	}
If anyone can help me or explain to me how to convert that code to load from a .jar file in as simple a manner as possible, I'd greatly appreciate it! Regards, Matt.

Share this post


Link to post
Share on other sites
If I understand correctly, you want to load classes from an external jar file, during runtime, that is not in the classpath? If that is the case then you would have to load it manually (binary) and add it to you're current classloader (at least that is what I do, as Java does not access classes that are in unreferenced files). So this is my way:

public class JarClassLoader<T> extends ClassLoader {

/** */
private String root = null;

/** */
private JarFile jar = null;

/** Some system properties */
private static String userdir = System.getProperty("user.dir");
private static char fileseparator = File.separatorChar;
private static char pathseparator = File.pathSeparatorChar;


/**
*
* @param parent
* @param root
* @throws FileNotFoundException
*/
public JarClassLoader(ClassLoader parent, String root) throws FileNotFoundException {
super(parent);

//
try {
//
jar = new JarFile(root);
this.root = root;

} catch (IOException ioe) {
ioe.printStackTrace();
}
}

/**
*
* @param className
* @return
*/
protected byte[] getClassBytes(String className) {
try {
//jar:file:path\to\jar\file.jar!/path/to/class.class
URL url = new URL("jar:file:" +
root + "!/" +
className.replace('.', '/') + ".class");

//
JarURLConnection conn = (JarURLConnection)url.openConnection();

//
InputStream input = conn.getInputStream();
byte[] bytes = new byte[input.available()];
input.read(bytes);

return bytes;

} catch(IOException ioe) {
ioe.printStackTrace();
}

return null;
}

/**
*
*/
public Class<T> findClass(String className) throws ClassNotFoundException {
byte[] bytes = getClassBytes(className);

if(bytes == null){
try {
return (Class<T>)loadClass(className);

} catch(ClassNotFoundException cnfe) {
throw new ClassNotFoundException(className);
}

} else {
if(findLoadedClass(className) != null) {
return (Class<T>)findLoadedClass(className);
}
return (Class<T>)defineClass(className, bytes, 0, bytes.length);
}
}


Probably not the nicest way but it works, as I had the same problems to dynamically load jar files during runtime. Just initiliaze with your current loader, give the jar path, and then use findClass as loader.

I hope it helps.

[Edited by - INsanityDesign on September 15, 2008 10:36:56 AM]

Share this post


Link to post
Share on other sites
Thanks InsanityDesign, I don't currently have a custom ClassLoader at all but I'll give your code a play and get back to you :)

Share this post


Link to post
Share on other sites
Here is one way of doing it which seems like it would work really well.

import my.External;

public class ExternalLoader {

public ExternalLoader() {

}

/**
* @param args
*/

public static void main(String[] args) {
ExternalLoader el = new ExternalLoader();

System.out.println("Loading classes by name...");
el.load("data&#47;quests&#47;quests.jar", "External1").print();
el.load("data&#47;quests&#47;quests.jar", "External2").print();
el.load("data&#47;quests&#47;quests.jar", "more.External3").print();

System.out.println("\nLoading classes by enumeration...");
LinkedList<External> extlist = el.getExternalsInJar("data&#47;quests&#47;quests.jar");
for(External e : extlist) {
e.print();
}
}


private Object[] listFilesInJar(String name) {
JarFile jFile = null;
LinkedList<String> list = new LinkedList<String>();
try {
jFile = new JarFile(new File(name));
Enumeration<JarEntry> en = jFile.entries();
while(en.hasMoreElements()) {
list.add(en.nextElement().getName());
}
} catch (IOException e) {
e.printStackTrace();
}

return list.toArray();
}

private LinkedList<External> getExternalsInJar(String jarpath) {
Object list[] = listFilesInJar(jarpath);
LinkedList<External> extlist = new LinkedList<External>();
for(Object o : list) {
String filepath = (String)o;
if(filepath.matches(".*\\.class")) {
String name = filepath.split("\\.")[0];
name = name.replace('/', '.');
extlist.add(load(jarpath, name));
}
}

return extlist;
}

public External load(String jarpath, String classname) {
File f = new File(jarpath);
try {
URL urls[] = new URL[]{f.toURI().toURL()};

URLClassLoader loader = new URLClassLoader(urls);

Class ext = loader.loadClass(classname);

return (External) ext.newInstance();

} catch (MalformedURLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (ClassCastException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

return null;
}

}






The external interface class is here:
package my;

public interface External {

public void print();

}







You can modify this code to load anonymous classes as well. Let me know if you find this useful. There might be some issues if you want to get this working from within an applet though.

NOTE: replace the "& # 4 7 ;" with a /'s, I don't know why it did that.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this