Simplify application delivery with one-jar-Promote programming with a custom Class Loader

Source: Internet
Author: User
. Simon tuffs(Simon@simontuffs.com), Independent Consultant, simontuffs.com

November 23, 2004

If you have tried to deliver a Java application as a single Java archive file (JAR file), you may have encountered the following requirement: Before building the final archive file, to expand the support JAR file (supporting JAR file ). This is not only a difficult issue in development, but may cause you to violate the license agreement. In this article, tuffs introduces you to the one-jar tool, which uses a custom class loader to dynamically load classes from the jar files inside the executable jar files.

Someone once said that history is always repeating itself, first of all tragedy, and then farce. Recently, I have personally experienced this for the first time. I have to deliver a Java application that can run to the customer, but I have already delivered it many times, and it is always full of complexity. When collecting all the jar files of the application, writing startup scripts for DOS and Unix (and cygwin), and ensuring that the client environment variables point to the correct location, there are always many errors. If everything can be done well, the application can run as expected. However, when there is a problem (and this is a common case), the result is that a large amount of time is spent on client support.

Recently a large numberClassNotFoundAfter talking to clients with an exception, I decided I couldn't bear it anymore. Therefore, I am looking for a method to package my application into a single JAR file and provide a simple mechanism (for examplejava -jar) To run the program.

The result is one-jar, a very simple software packaging solution. It uses Java's custom class loader to dynamically load all classes of the application from a single file, at the same time, the structure that supports jar files is retained. In this article, I will introduce how to develop one-jar, and then tell you how to use it to deliver your own running applications in a self-contained file.

One-jar Overview

Before introducing the details of One-jar, let me first discuss the purpose of building it. I'm sure a one-JAR file should be:

  • Availablejava -jarMechanism execution.

  • Can contain the Needs of the applicationAllFile-that is, including the class and resources in the original form (not expanded.
  • Has a simple internal structure and only usesjarTools can be assembled.
  • Invisible to the original application-that is, you can package it in the one-JAR file without modifying the original application.



Back to Top

Problems and Solutions

The biggest problem I solve during the development of One-jar is how to load the jar files contained in another jar file. Java Class Loadersun.misc.Launcher$AppClassLoader(Injava -jarAt the beginning) only knows how to do two things:

  • Classes and resources mounted to the root of the JAR file.

  • LoadClass-path in META-INF/manifest. MFClass and resource in the code base to which the property points.

In addition, it also deliberately ignoresCLASSPATHAnd ignore the command line parameters you provide.-cp. Therefore, it does not know how to load classes or resources from a jar file that is contained in other jar files.

Obviously, I need to overcome this problem to achieve the One-jar goal.

Solution 1: Expand jar files

The first attempt I made to create a single executable JAR file is obviously to expand the support JAR file in the deliverable JAR file. We call the deliverable file main. jar. Suppose there is an application class calledcom.main.MainAnd it depends on two classes --com.a.A(In a. Jar) andcom.b.B(In B. Jar), the one-JAR file should look like this:

    main.jar    |  com/main/Main.class    |  com/a/A.class    |  com/b/B.class 

In this way,A.classLost,B.classThis is also true. Although this seems to be a small problem, it will actually cause problems and I will explain why soon.

One-jar and fjep

A recently released tool named fjep (fatjar eclipse plugin) allows you to directly build a flat JAR file in eclipse. One-Jar has been integrated with fatjar to support embedding the JAR file without expanding the JAR file. For more information, see references.

It may be time-consuming to expand the JAR file to the file system to create a flat structure. You also need to use a build tool like ant to expand and rearchive the support class.

In addition to this little trouble, I soon encountered two serious problems related to expanding the support for jar files:

  • If a. jar and B. Jar contain the same resource path (for examplelog4j.properties), Which one do you want to select?

  • If B. Jar's permission explicitly requires that you cannot modify it when publishing it again, what should you do? You cannot expand it like this without prejudice to the license terms.

I think these restrictions provide clues for another method.

Solution 2: manifest class-Path

I decided to studyjava -jarAnother mechanism in the loader: The loaded class is specified in a special file named META-INF/manifest. MF in the file. By specifyingClass-PathTo add other file files to the class loader at startup. The following is how the one-JAR file looks:

    main.jar    |  META-INF/MANIFEST.MF    |  +  Class-Path: lib/a.jar lib/b.jar    |  com/main/Main.class    |  lib/a.jar    |  lib/b.jar     

Description and clues

URLClassloaderYessun.misc.Launcher$AppClassLoaderIt supports a mysterious URL syntax, allowing you to reference resources in the jar file. This syntax is used like this:jar:file:/ fullpath/main.jar!/a.resource .

Theoretically, you need to obtain a JAR FileInternalYou must usejar:file:/ fullpath/main.jar!/lib/a.jar!/a.resource However, unfortunately, this method is useless. When the JAR file protocol processor finds the JAR file, it only recognizes the last "! /"Separator.

However, this syntax does provide clues for my final one-jar solution ......

Can this work? When I move main. jar to another place and try to run it, it seems that I can. To assemble main. jar, I created a subdirectory named lib and put a. jar and B. jar in it. Unfortunately, the application's class loader only extracts jar files from the file system, rather than the embedded jar files.

To overcome this problem, I tried to use the mysteriousjar:!/Several variants of the syntax are used.Class-Path(See "instructions and clues"), but there is no success. MeYesOnly a. jar and B. jar are delivered separately and put them in the file system together with main. Jar. But this is exactly what I want to avoid.



Back to Top

Enter jarclassloader

At this time, I felt frustrated. How can I get an application from its own JAR FilelibDirectory to load its own class? I decided to create a custom class loader to undertake this task. Writing a custom class loader is not an easy task. But in fact, this work is not so complex. The class loader has a profound impact on the applications it controls. Therefore, it is difficult to diagnose and interpret faults in the event of a fault. Although the complete handling of class loading is beyond the scope of this article (see references), I still want to introduce some basic concepts so that you can get the most out of the discussion below.

Load class

When the JVM encounters an unknown class of an object, it will call the class loader. The job of the Class Loader is to find the class bytecode (based on the class name), then pass these bytes to the JVM, And the JVM then links these bytecode to the rest of the system, so that the running code can use the newly installed class. The key classes in JDK are:java.lang.ClassloaderAndloadClassThe method is summarized as follows:

    public abstract class ClassLoader {        ...        protected synchronized Class loadClass(String name, boolean resolve)            throws ClassNotFoundException {...}    } 

ClassLoaderThe main entry point of the class isloadClass()Method. You will notice that,ClassLoaderIs an abstract class, but it does not declare any abstract method.loadClass()The method is not a method that requires attention, and there is no clue at all. Actually, itNoThe main method to focus on: Go back to the past and check out the class loader of JDK 1.1.loadClass()It is the only place where you can effectively extend the Class Loader. But from JDK 1.2, it is best to let the Class Loader do its work independently, that is, the following work:

  • Check whether the class has been loaded.
  • Check whether the upper-level class loader can load the class.
  • CallfindClass(String name)Method to load the derived class loader into the class.

ClassLoader.findClass()Is to throw a newClassNotFoundExceptionException, and it is the first method we should consider when implementing the custom class loader.

When is a jar file not a jar file?

To load data in a jar fileInternalClass in the jar file (this is a key issue, you can recall), I must first be able to open and read the top-level JAR file (the above main. jar file ). Now, because I am usingjava -jarMechanism, so,java.class.pathThe first (and only) element in the system attribute is the complete path name of the One-JAR file! Use the following code to get it:

    jarName = System.getProperty("java.class.path");

The next step is to traverse all JAR file items of the application and load them into the memory, as shown in Listing 1:

Listing 1. traverse and find the embedded JAR File

    JarFile jarFile = new JarFile(jarName);    Enumeration enum = jarFile.entries();    while (enum.hasMoreElements()) {        JarEntry entry = (JarEntry)enum.nextElement();        if (entry.isDirectory()) continue;        String jar = entry.getName();        if (jar.startsWith(LIB_PREFIX) || jar.startsWith(MAIN_PREFIX)) {            // Load it!             InputStream is = jarFile.getInputStream(entry);            if (is == null)                 throw new IOException("Unable to load resource /" + jar + " using " + this);            loadByteCode(is, jar);            ...                

Note,LIB_PREFIXGenerate stringLIB/,MAIN_PREFIXGenerate stringMain/. I wantLIB/OrMain/The byte code of the starting item is loaded into the memory for the class loader, and any other JAR file items are ignored in the loop.

Main directory

I have already talked about the role of LIB/subdirectory. What is the main/directory? In brief, the Agent Mode of the Class Loader requires mecom.main.MainPut it in its own JAR file so that it can find the Library Class (the Library Class it depends on ). The new JAR file looks like this:

one-jar.jar|  META-INF/MANIFEST.MF|  main/main.jar|  lib/a.jar|  lib/b.jar 

In listing 1 above,loadByteCode()The method accepts the stream and an item name from the jar file item, loads the byte of the item into the memory, and indicatesClassOrResourcesTo allocate a maximum of two names to it. The best way to demonstrate this technology is through an example. Suppose a. jar contains a classA.classAnd a resourceA.resource. The one-jar Class Loader constructs the following:MapStructure, namedJarClassLoader.byteCodeIt only has a combination of keywords and values for the class, and two keywords for the resource.

Figure 1. Structure of One-jar in memory

If you look at Figure 1 for a while, you can see that the class items are set by class name, while the resource keyword is set by a pair of names: Global name and local name. The mechanism used to resolve resource name conflicts is: if the jar files of the two libraries define a resource with the same global name, the local name is used based on the stack frame of the calling program. For more details, see references.

Locate class

Recall that when I gave an overview of class loading, I finally introducedfindClass()Method. MethodfindClass()Take the class nameStringParameter, and the bytecode represented by this name must be found and defined. BecauseloadByteCodeA good construction of the Class Name and bytecodeMapSo the implementation of this method is now very simple: you only need to find the bytecode according to the class name, and then calldefineClass(), As shown in Listing 2:

Listing 2. findclass () Summary

    protected Class findClass(String name) throws ClassNotFoundException {        ByteCode bytecode = (ByteCode)JarClassLoader.byteCode.get(name);        if (bytecode != null) {            ...            byte bytes[] = bytecode.bytes;            return defineClass(name, bytes, pd);        }        throw new ClassNotFoundException(name);    }  


Back to Top

Load Resources

During one-jar development,findClassIt is the first thing I put my ideas into practice. However, when I started to deploy more complex applications, I found that in addition to loading classes, I had to handle resource loading issues. This time, things are a bit tricky. To search for resources, you mustClassLoaderFind a suitable method to overwrite, and I chose one that I am most familiar with, as shown in listing 3:

Listing 3. getresourceasstream () method

    public InputStream getResourceAsStream(String name) {        URL url = getResource(name);        try {            return url != null ? url.openStream() : null;        } catch (IOException e) {            return null;        }    }

At this time, the alarm should be triggered: I just cannot understand why the URL is used to locate the resource. So I don't need this implementation, but insert my own implementation, as shown in Listing 4:

Listing 4. getresourceasstream () implementation in one-jar

    public InputStream getResourceAsStream(String resource) {        byte bytes[] = null;        ByteCode bytecode = (ByteCode)byteCode.get(resource);        if (bytecode != null) {            bytes = bytecode.bytes;         }        ...        if (bytes != null) {            return new ByteArrayInputStream(bytes);        }        ...        return null;    }  

Last obstacle

My pairgetResourceAsStream()The new implementation of the method seems to solve the problem, but until I try to use one-jar to processURL url = object.getClass().getClassLoader().getResource()The actual situation is different from what you think when you load the resource application in mode. Why? BecauseClassLoaderBy default, the returned URL is null, which destroys the code of the caller.

At this time, things have become hard to tell. I have to find out what URL should be used to reference resources inside the JAR file in the LIB/directory. Should it be likejar:file:main.jar!lib/a.jar!com.a.A.resourceThis is good?

I tried all the combinations I could think of, but none worked.jar:The syntax does not support nested jar files, which makes my whole one-jar method face a dead end. Although most applications do not seem to useClassLoader.getResourceMethod, but some of them use this method, so I really don't want to exclude the situation, let me say, "If your application usesClassLoader.getResource()You cannot use one-jar ."

The final solution ......

When I try to figure outjar:During the syntax, I accidentally learned how the Java Runtime Environment Maps the URL prefix to the processor. This becomes my fixfindResourceAll I need to do is to create a protocol prefix calledonejar:. In this way, I can map the new prefix to the Protocol processor, and the processor will return the resource byte stream, as shown in listing 5. Note: Listing 5 indicates the code in two files. These two files are jarclassloader andCOM/simontuffs/onejar/handler. Java.

Listing 5. findresource and onejar: Protocol

        com/simontuffs/onejar/JarClassLoader.java    protected URL findResource(String $resource) {        try {            // resolve($resource) returns the name of a resource in the            // byteCode Map if it is known to this classloader.            String resource = resolve($resource);            if (resource != null) {                // We know how to handle it.                return new URL(Handler.PROTOCOL + ":" + resource);             }            return null;        } catch (MalformedURLException mux) {            WARNING("unable to locate " + $resource + " due to " + mux);        }        return null;    }        com/simontuffs/onejar/Handler.java    package com.simontuffs.onejar;    ...    public class Handler extends URLStreamHandler {        /**         * This protocol name must match the name of the package in which this class         * lives.         */        public static String PROTOCOL = "onejar";        protected int len = PROTOCOL.length()+1;                protected URLConnection openConnection(URL u) throws IOException {            final String resource = u.toString().substring(len);            return new URLConnection(u) {                public void connect() {                }                public InputStream getInputStream() {                    // Use the Boot classloader to get the resource.  There                    // is only one per one-jar.                    JarClassLoader cl = Boot.getClassLoader();                    return cl.getByteStream(resource);                }            };        }    }          


Back to Top

Start jarclassloader

Now, you have only one question:JarClassLoaderInsert the startup sequence so that it starts to load the class from the one-JAR file first? The details are beyond the scope of this article. However, basically, I didn't use the main classcom.main.MainAsMETA-INF/MANIFEST.MF/Main-ClassInstead, a new main startup class is created.com.simontuffs.onejar.Boot, Which is specifiedMain-ClassAttribute. The new category should do the following:

  • Create a newJarClassLoader.

  • Use a new loader to load data from main/Main. jar.com.main.Main(Based onMETA-INF/MANIFEST.MF Main-Class).
  • Load the class and call it Using Reflectionmain()To callcom.main.Main.main(String[])(Or, for examplemain.jar/MANIFEST.MFFileMain-Class). Parameters passed on the One-jar command line are passed to the main method of the application without modification.



Back to Top

Conclusion

Don't worry if the above is a headache: using one-jar is much easier than understanding it. With the release of the fatjar Eclipse plug-in (see fjep in reference), eclipse users can create one-jar applications by selecting a check box in the Wizard. The dependent library is put into the LIB/directory, the main program and class are put into main/Main. jar, and the META-INF/manifest. MF file is automatically written. If you use jarplug (or refer to references), you can view the internal structure of the JAR file you have built and start it from IDE.

In short, one-jar is a simple and powerful solution that solves the problem of application packaging and delivery. However, it does not solve all application scenarios. For example, if your application uses the old JDK 1.1 class loader and does not delegate the loader to the previous layer, the Class Loader cannot find the class in the nested JAR file. You can build and deploy a "wrap" class loader to modify the stubborn class loader to overcome this problem, however, this may need to be used with tools such as ipvsist or byte code Engineering Library (bcel.

You may also encounter problems with specific types of class loaders used by embedded applications and web servers. Especially for class loaders that do not delegate the loading work to the upper level first, and the loaders that find the code base in the file system, you may encounter problems. However, one-jar contains a mechanism to expand the JAR file items in the file system, which should be helpful. This mechanism consistsOne-JAR-ExpandProperty Control. In addition, you can try to use bytecode Manipulation Technology to dynamically modify the Class Loader so that the integrity of jar files is not damaged. If you use this method, a custom package loader may be required in each case.

Refer to the documentation to download the fatjar Eclipse plug-in and jarplug, and learn more about one-jar.

References

  • For more information, see the original article on the developerworks global site.

  • On sourceforge.net, you will find more documentation, downloads, and examples for One-jar.
  • One-Jar has recently been integrated with fat jar eclipse plugin (fjep). This tool helps you build a flat JAR file for deployment. It can be used from the 0.0.12 release.
  • The Java archive eclipse plugin tool allows you to view and start jar files in eclipse.
  • Study the following bytecode manipulation tools (my personal preferred is Javassist ):
    • Javassist
    • Byte Code Engineering Library (bcel)

  • "Java programming dynamics, Part 1: classes and class loading" (developerworks, April 2003) introduces the topic of the class loader at a high level, there are also many resources you can use. The authors Dennis sosnoski discussed Javassist and bcel later in the same series. Links to these articles are included in this first part.
  • "J2EE class loading demystified" (developerworks, August 2002) is an entry-level class loading introduction.
  • David Gallardo's "getting started with the eclipse platform" (developerworks, April 2003) introduced the features and architecture behind eclipse.
  • InDeveloperworksIn the Java technology area, you can find hundreds of technical articles on various aspects of Java.
  • Visit developer bookstore to obtain a complete list of technical books, including hundreds of books on Java-related topics.

About the author

Dr. P. Simon tuffs, an independent consultant, is currently studying the scalability of Java Web Services. In his spare time, he created and released some open source projects, such as one-jar. To learn about Dr. tuffs and his work, visit www.simontuffs.com.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.