3

I have two jar files (for the example lets call them Updater.jar and Code.jar). Updater.jar is launched with its main method, and then it launches itself again with a premain method:

package Update;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

public class InstructionLauncher {

    private List<UpdateInstruction> instructions = new ArrayList<UpdateInstruction>();
    private static InstructionLauncher instance;
    private Process process;

    public static InstructionLauncher initialise(){
        if(instance !=null) return instance;
        else return new InstructionLauncher();
    }

    public void registerPremain(UpdateInstruction inst){
        instructions.add(inst);
    }

    public void launchNext(){
        UpdateInstruction inst = instructions.get(0);
        String cls = inst.getClassName() + "." + inst.getMethodName();
        String[] args = new String[]{"java", "-javaagent", "JOSUpdater.jar", "-jar", inst.getClassName() + "." + inst.getMethodName()};
        ProcessBuilder builder = new ProcessBuilder(args);
        try {
            exportResource(cls, cls);
        } catch (Exception e) {
            UpdateManager.revert();
        }
        try {
            Process p = builder.start();
            process = p;
        } catch (IOException e) {
            e.printStackTrace();
        }
        while(!process.isAlive())launchNext();
    }

    private InstructionLauncher(){
        instance = this;
    }

    //From http://stackoverflow.com/questions/10308221/how-to-copy-file-inside-jar-to-outside-the-jar
    private String exportResource(String resourceName, String clazz) throws Exception {
        InputStream stream = null;
        OutputStream resStreamOut = null;
        String jarFolder;
        try {
            stream = Class.forName(clazz).getResourceAsStream(resourceName);//note that each / is a directory down in the "jar tree" been the jar the root of the tree
            if(stream == null) {
                throw new Exception("Cannot get resource \"" + resourceName + "\" from Jar file.");
            }

            int readBytes;
            byte[] buffer = new byte[4096];
            jarFolder = new File(Class.forName(clazz).getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getParentFile().getPath().replace('\\', '/');
            resStreamOut = new FileOutputStream(jarFolder + resourceName);
            while ((readBytes = stream.read(buffer)) > 0) {
                resStreamOut.write(buffer, 0, readBytes);
            }
        } catch (Exception ex) {
            throw ex;
        } finally {
            stream.close();
            resStreamOut.close();
        }

        return jarFolder + resourceName;
    }

}

The premain method looks like this at the moment:

package Update;

import java.lang.instrument.Instrumentation;

public class PremainLauncher {

    public static void premain(String args, Instrumentation inst){
        inst.addTransformer(new Transformer(), true);
        System.out.println("Registered instruction for package: " + args);
    }

}

What I am wondering, is how do I add the whole external JAR (Code.jar in this example) into the path for the instrumentation?

I know about the Instrumentation.retransformClasses method, but to use that I would need to get a List> of all the classes in the jar, which I have been unable to complete.

Lets say that Code.jar has three class files: Main.class, writer.class and display.class. Is there a way to get a list of each of their class object, not their name?

JD9999
  • 394
  • 1
  • 7
  • 18
  • Just add your required jar on the class path. – ManoDestra Jul 05 '16 at 22:14
  • Is that just by modifying the java.library.path variable? – JD9999 Jul 05 '16 at 22:16
  • I meant by simply adding your required jar to the classpath e.g. java -cp path/to/your.jar;path/to/other.jar com.example.app.MainApp. Not sure if this is what you meant? Beyond that, you can add JARs dynamically at runtime via the URLClassLoader class, if you're looking for greater flexibility. – ManoDestra Jul 05 '16 at 22:22
  • Can't I do `System.setProperty("java.library.path", System.getProperty("java.library.path" + File.separator + "JOSUser.jar");`? Or does that do something different? I would prefer the answer programatically rather then with the command line. – JD9999 Jul 06 '16 at 02:59
  • URLClassLoader will do what you require in that case. – ManoDestra Jul 06 '16 at 11:22
  • 2
    `"java.library.path"` doesn’t have anything to do with `.jar` files. That’s the path for loading *native* libraries, see [`ClassLoader.findLibrary(String)`](https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html#findLibrary-java.lang.String-) – Holger Jul 06 '16 at 13:17

1 Answers1

2

A Java agent can add jar files simply via the Instrumentation interface it received in the startup method, e.g.

import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.jar.JarFile;

public class PremainLauncher {
    public static void premain(String args, Instrumentation inst) throws IOException{
        inst.appendToSystemClassLoaderSearch(new JarFile("Code.jar"));
        inst.addTransformer(new Transformer(), true);
        System.out.println("Registered instruction for package: " + args);
    }
}

See Instrumentation.appendToSystemClassLoaderSearch(…). If you intent to instrument JRE classes as well in a way that the instrumented classes need access to the classes of Code.jar, you’ll have to change the bootstrap path instead.

Note that this has to happen as early as possible:

The Java™ Virtual Machine Specification specifies that a subsequent attempt to resolve a symbolic reference that the Java virtual machine has previously unsuccessfully attempted to resolve always fails with the same error that was thrown as a result of the initial resolution attempt. Consequently, if the JAR file contains an entry that corresponds to a class for which the Java virtual machine has unsuccessfully attempted to resolve a reference, then subsequent attempts to resolve that reference will fail with the same error as the initial attempt.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • So if a different class with the same name fails to load, so will the class here? – JD9999 Jul 07 '16 at 21:13
  • 1
    Well, if you append a jar to a class path, you assume that there is no other class with the same name in that scope, as otherwise, your jar will never be searched. But if an attempt to load that class with this class loader was made before you appended your jar, the failure will be remembered. Since the `premain` runs before any application code, this should not be an issue, as long as you append the jar before doing any action that could lead to a load attempt. – Holger Jul 08 '16 at 07:58
  • There is a main application that launches. This application launches the first jar (let's call it start.jar). After start.jar is done, the application launches the updater. The updater then launches the premain method in itself. So it does run before application code, would this make a difference? – JD9999 Jul 08 '16 at 08:47