The YUI Compressor uses a slightly modified version of the parser used in the Rhino JavaScript engine. The reason for modifying the parser came from the need to support JScript conditional comments, unescaped forward slashes in regular expressions — which all browsers support and many people use — and a few micro optimizations. The problem is that many users had the original Rhino Jar file somewhere on their system, either in their classpath, or in their JRE extension directory (<JRE_HOME>/lib/ext) This caused many headaches because the wrong classes were being loaded, leading to many weird bugs.
Today, I finally decided to do something about it. This meant writing a custom class loader to load the modified classes directly from the YUI Compressor Jar file. You can download the source and binary package here:
Download version 2.4 of the YUI Compressor
The skeleton of the custom class loader is pretty straightforward:
package com.yahoo.platform.yui.compressor;
public class JarClassLoader extends ClassLoader
{
public Class loadClass(String name) throws ClassNotFoundException
{
// First check if the class is already loaded
Class c = findLoadedClass(name);
if (c == null) {
// Try to load the class ourselves
c = findClass(name);
}
if (c == null) {
// Fall back to the system class loader
c = ClassLoader.getSystemClassLoader().loadClass(name);
}
return c;
}
protected Class findClass(String name)
{
// Most of the heavy lifting takes place here
}
}
The role of the findClass
method is to first locate the YUI Compressor Jar file. To do that, we look in the classpath for a Jar file that contains the com.yahoo.platform.yui.compressor.JarClassLoader
class:
private static String jarPath;
private static String getJarPath()
{
if (jarPath != null) {
return jarPath;
}
String classname = JarClassLoader.class.getName().replace('.', '/') + ".class";
String classpath = System.getProperty("java.class.path");
String classpaths[] = classpath.split(System.getProperty("path.separator"));
for (int i = 0; i < classpaths.length; i++) {
String path = classpaths[i];
JarFile jarFile = new JarFile(path);
JarEntry jarEntry = findJarEntry(jarFile, classname);
if (jarEntry != null) {
jarPath = path;
break;
}
}
return jarPath;
}
private static JarEntry findJarEntry(JarFile jarFile, String entryName)
{
Enumeration entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = (JarEntry) entries.nextElement();
if (entry.getName().equals(entryName)) {
return entry;
}
}
return null;
}
Once we know where the YUI Compressor Jar file is, we can load the appropriate class from that file. Note the need to define the package the class belongs to before calling defineClass
!
protected Class findClass(String name)
{
Class c = null;
String jarPath = getJarPath();
if (jarPath != null) {
JarFile jarFile = new JarFile(jarPath);
c = loadClassData(jarFile, name);
}
return c;
}
private Class loadClassData(JarFile jarFile, String className)
{
String entryName = className.replace('.', '/') + ".class";
JarEntry jarEntry = findJarEntry(jarFile, entryName);
if (jarEntry == null) {
return null;
}
// Create the necessary package if needed...
int index = className.lastIndexOf('.');
if (index >= 0) {
String packageName = className.substring(0, index);
if (getPackage(packageName) == null) {
definePackage(packageName, "", "", "", "", "", "", null);
}
}
// Read the Jar File entry and define the class...
InputStream is = jarFile.getInputStream(jarEntry);
ByteArrayOutputStream os = new ByteArrayOutputStream();
copy(is, os);
byte[] bytes = os.toByteArray();
return defineClass(className, bytes, 0, bytes.length);
}
private void copy(InputStream in, OutputStream out)
{
byte[] buf = new byte[1024];
while (true) {
int len = in.read(buf);
if (len < 0) break;
out.write(buf, 0, len);
}
}
The last thing we need to do is bootstrap the application. In order to do that, we simply load the main class (YUICompressor
) using our new custom class loader. All the classes that will be needed at runtime will use the same class loader:
package com.yahoo.platform.yui.compressor;
public class Bootstrap
{
public static void main(String args[]) throws Exception
{
ClassLoader loader = new JarClassLoader();
Thread.currentThread().setContextClassLoader(loader);
Class c = loader.loadClass(YUICompressor.class.getName());
Method main = c.getMethod("main", new Class[]{String[].class});
main.invoke(null, new Object[]{args});
}
}
As you can see, it's not terribly complicated to write a custom class loader. Note: I left out all the exception handling code and the import statements for clarity. The final code can be found in the downloadable archive. Cheers!