Java 9之後如何動態改變CLASSPATH

Problem

JDK9 has changed the class loading hierarchy and the AppClassloader is no more a subclass of URLClassLoader. This caused some chaos in our test tool because it depends on this feature to boost up the classpath at runtime by URLClassloader.addUrl() method. This article will introduce an alternative to URLClassloader.addUrl().

Solution

Java Instrument API 

Though we can fix our code by various means, there still is a direct alternative to URLClassloader.addUrl(): the instrument API provided by Java.

This API was introduced in Java 5 and was improved in Java 6, adding the dynamic instrument capability and the classpath expansion methods. Specifically, they are

  • java.lang.instrument.Instrumentation.appendToBootstrapClassLoaderSearch(JarFile jarfile)
  • java.lang.instrument.Instrumentation.appendToSystemClassLoaderSearch(JarFile jarfile)

With these methods we can get exactly what URLClassloader.addUrl gave us and even more.

The original purpose of Java instrument API is to enable AOP in JVM level at runtime. Unlike dynamic proxy, it does not depend on interfaces, and can instrument any JVM at runtime with the help of byte code manipulation tools.

How to use it

Suppose there is a VM to which we would like to enable it to load new classes, we call it target VM or target process. The steps are:

  1. Write an agent class, with a method agentmain whose signature is 

    public static void agentmain(String agentArgs, Instrumentation inst)

  2. Package this class in a jar file, and its MANIFEST must contain the following property. We will call this jar file the agent jar.

    Agent-Class: <our agent class name>

  3. Write some other code to install this agent to the target VM. And we will call this code install code

Once the install code runs, it will find the target VM by PID, then it attaches the target VM with a VirtualMachine object, after that, we can call VirtualMachine.loadAgent() to load our agent jar, at this moment, the agentmain method will be invoked in the target VM, and here we can call any method on Instrumentation object such as appendToSystemClassLoaderSearch, or we can save the Instrumentation object for later use.

Note that the agent jar will be automatically loaded into the target VM and hence we don't have to call appendToSystemClassLoaderSearch or appendToBootstrapClassLoaderSearch for itself.

In addition to "Agent-Class", there is another property called "Boot-Class-Path", which provides us a one-off way to add some extra classpath to the target VM.

Sample application

This is a sample application. 

TargetProcess is a class with main method, it is the target VM to be instrumented. 

package test.instrument;

import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * This class demonstrates the capability of adding extra classpath entries dynamically to a living JVM.
 * It mocks a server process, when it starts, it prints the PID of it self, and then tries to load two classes periodically:
 *   test.instrument.InstruAgent is the instrument agent, which will be injected by InstrumentInjector
 *   mypackage.MyClass is another class that in a jar file mylib.jar
 * 
 * Once it finds the class InstruAgent, it will call InstruAgent.addClassPath to add the mylib.jar to 
 * itself's classpath, and hence it can find the other class as well.
 * 
 * To run this demo (in Eclipse):
 * 1. Rename InstruAgent.java.txt to InstruAgent.java
 * 2. Package InstruAgent.class to a jar file with the MANIFEST stated in the class's comments
 * 3. Delete InstruAgent.class from your workspace and rename InstruAgent.java back to InstruAgent.java.txt
 * 4. Run TargetProcess it should output its PID and say both the classes are *Not* found
 * 5. Update InstrumentInjector.getWLSPID method with the PID in last step
 * 6. At some point, run InstrumentInjector
 * 7. The TargetProcess process now should be able to find both the classes
 *  
 * @author io
 *
 */
public class TargetProcess {
  
  /**
   * On linux+windows it returns a value like 12345@hostname (12345 being the process id).
   * Beware though that according to the docs, there are no guarantees about this value.
   * In JDK9, use ProcessHandle.current().pid() instead.
   * @return
   */
  public static String getSelfPID() {
    String s = ManagementFactory.getRuntimeMXBean().getName();
    return s.substring(0, s.indexOf('@'));
  }

  public static void main(String[] args) {
    System.out.println("[TargetProcess] PID: " + getSelfPID());
    int cnt = 0;
    while(cnt < 100) {
      // Try to load the 1st class i.e. the agent itself
      // Once the agent is loaded, it will be in target VM's classpath automatically
      String clazz = "test.instrument.InstruAgent"; 
      try {
        Class c = Class.forName(clazz);
        try {
          Method m = c.getDeclaredMethod("addClassPath", String.class);
          // note that mylib contains the class mypackage.MyClass
          m.invoke(c, "/mypath/mylib.jar");
        } catch(NoSuchMethodException nsme) {
          nsme.printStackTrace();
        } catch( InvocationTargetException ite) {
          ite.printStackTrace();
        } catch ( IllegalAccessException iae) {
          iae.printStackTrace();
        }
        System.out.println("[TargetProcess] " + clazz + " Found!");
      } catch(ClassNotFoundException cnfe) {
        System.out.println("[TargetProcess] " + clazz +" *Not* found.");
      }
      
      // Try to load the 2nd class which is in some jar file dynamically added
      clazz = "mypackage.MyClass";
      try {
        Class.forName(clazz);
        System.out.println("[TargetProcess] " + clazz + " Found!");
      } catch(ClassNotFoundException cnfe) {
        System.out.println("[TargetProcess] " + clazz +" *Not* found.");
      }
      
      try {
        Thread.sleep(5000);
      } catch(InterruptedException ie) {
        
      } finally {
        cnt++;
      }
    }
  }

}

InstruAgent is the agent class, it has the agentmain method, and it will be packaged into instruagent.jar which is our agent jar.

package test.instrument;

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

/**
 * Package this class to a jar with a MANIFEST of following content:
 * 
 * Manifest-Version: 1.0 
 * Agent-Class: test.instrument.InstruAgent
 * 
 * @author io
 *
 */
public class InstruAgent {
  
  static Instrumentation instru = null;
  
  // Note that the new classpath entry added in this way does not change the system property "java.class.path"
  public static void addClassPath(String path) throws Exception {
    JarFile jf = new JarFile(path, false);
    addClassPath(jf);
  }
  
  public static void addClassPath(JarFile path) {
    instru.appendToSystemClassLoaderSearch(path);
  }
  
  public static void agentmain(String agentArgs, Instrumentation inst) {
    System.out.println("[InstruAgent]: agentmain called!");
    instru = inst;
  }

}

InstrumentInjector is the install code, which is responsible for wiring the former two up.

package test.instrument;

import java.util.List;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;


/**
 * 
 * This class will inject the agent to the wls process.
 * 
 * If com.sun.tools.attach cannot be solved:
 * 
 * Solution 1:
 * Go to Eclipse preferences (on Windows: Window-->Preferences)
 * Open the preference Java-->Installed JREs
 * Select your JRE and press edit
 * Use "Add external jars" to include the tools.jar (found in JDK_HOME/lib)
 *
 * Solution 2:
 * Edit your project build path and add an external library: tools.jar found in JDK_HOME/lib
 * @author io
 *
 */
public class InstrumentInjector {
  
  public static String getWLSPID() {
    return "10416"; // Change it to the PID of the target process
  }
  
  public static void inject() throws Exception {
    System.out.println("[InstrumentInjector]: Start Injection!");
    List<VirtualMachineDescriptor> vmds = VirtualMachine.list();
    
    //System.out.println(vmds.size());
    
    for(VirtualMachineDescriptor vmd : vmds) {
//      System.out.println(vmd.id());
//      System.out.println(vmd.displayName());
//      System.out.println(vmd.provider());
//      System.out.println(vmd.toString());
//      System.out.println("  ---  ");
      if(vmd.id().equals(getWLSPID())) {
        //System.out.println(vmd.id());
        VirtualMachine wls = VirtualMachine.attach(vmd);
        wls.loadAgent("d:/instruagent.jar");
        wls.detach();
        break;
      }
    }
    
    System.out.println("[InstrumentInjector]: Finished Injection!");
    
  }

  public static void main(String[] args) throws Exception {
    inject();
  }

}

Issues

  1. This API disallows self-attaching in JDK9, we may consider using process API such as Runtime.exec().
  2. There is no corresponding "removeFromBootstrapClassLoaderSearch" or "removeFromSystemClassLoaderSearch" methods, so, though the classpath can be added dynamically, it cannot be removed so.

References

https://docs.oracle.com/javase/9/docs/api/java/lang/instrument/package-summary.html
https://www.ibm.com/developerworks/cn/java/j-lo-jse61/
http://jiangbo.me/blog/2012/02/21/java-lang-instrument/
https://www.programcreek.com/java-api-examples/?code=lambdalab-mirror/jdk8u-jdk/jdk8u-jdk-master/test/java/lang/instrument/appendToClassLoaderSearch/BasicTest.java
https://github.com/netroby/jdk9-dev/blob/master/jdk/src/java.base/share/classes/jdk/internal/loader/ClassLoaders.java
https://www.javamex.com/tutorials/memory/instrumentation.shtml
http://www.importnew.com/22466.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章