Achtung bei der Verwendung von Javaagent

Vorwort

In einem aktuellen Projekt muss ich einen Agenten implementieren, um den Bytecode während des laufenden Prozesses zu ersetzen. Als der Autor diese Funktionen implementiert, stellt er fest, dass es noch viele Vorsichtsmaßnahmen gibt. Wenn außerdem die Eigenschaften und Methoden der Klasse während des Bytecode-Ersetzungsprozesses aktualisiert werden, wird beim Laden ein Fehler gemeldet. Der Vorteil dieses Ansatzes besteht darin, dass der Code nicht aufdringlich ist, aber auch die Nachteile liegen auf der Hand und er hängt stark von bestimmten JVM-Versionen und Middleware ab.

Einführung in Javaagent

Javaagent ist eigentlich die von JVMTI verwendete Technologie, und der Kern wird von Instrumentation implementiert. Schauen Sie sich dieses Paket an, offizielle Dokumentation: java.lang.instrument (Java Platform SE 8)

Einer der Sätze ist ganz wesentlich: Stellt Dienste bereit, die es Java-Programmiersprachenagenten ermöglichen, Programme zu instrumentieren, die auf der JVM ausgeführt werden. Der Instrumentierungsmechanismus ist die Änderung der Bytecodes von Methoden. Erkennung. Der Erkennungsmechanismus besteht darin, den Bytecode der Methode zu ändern . Es gibt zwei Implementierungen von Javaagent: Eine ist ein JVM-Parameter und die andere ist eine dynamische Anbindung.

Die Implementierungsmethode ist addTransformer. Solange die Klasse nicht geladen wurde, bevor addTransformer geladen wird, wird sie durch unseren benutzerdefinierten Bytecode ersetzt. Wenn die geladene Klasse ersetzt werden muss, können Sie Klassen manuell neu transformieren. Dies ist natürlich auch möglich redefineClasses, aber nur zur Wiederherstellung wird retransformClasses empfohlen.

Bereiten Sie eine Demo und einen Frageprozess vor

Um die Bytecode-Ersetzung und die Demo vorzubereiten, ersetzen Sie zunächst eine JDK-Klasse, um beispielsweise eine Bytecode-Ersetzung für die Liste der Dateien durchzuführen. Zum Beispiel ASM Javassist usw. Javassist ist relativ einfach, während ASM häufiger verwendet wird, z. B. cglib: https://asm.ow2.io/asm4-guide.pdf

 Versuchen Sie es zunächst mit javassist

  • ClassPool: CtClass-Pool, Sie können classPool.get (vollständiger Name der Klasse) verwenden, um CtClass abzurufen
  • CtClass: Klasseninformationen zur Kompilierungszeit, Kapselung von Klassendateien
  • CtMethod: Methode in der Klasse
  • CtField: Attribute und Variablen in der Klasse

Schreiben Sie einen Controller und lösen Sie Bedingungen aus

    @RequestMapping("/file")
    public String[] fileList() {
        File file = new File("/Users/huahua/go");
        return file.list();
    }

Agent

    public class Agent {
        private static synchronized void initAgent(String args, Instrumentation inst) {
            System.out.println("agent exec ......");
            inst.addTransformer(new ClassFileTransformer() {
                @Override
                public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                    //字节码修改,替换
                    String refName = className.replace("/", ".");
                    if (MethodFilter.filterClass(refName)) {
                        try {
                            return MethodFilter.getHook(refName).hookMethod(loader, className, classfileBuffer);
                        } catch (NotFoundException | CannotCompileException | IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    return classfileBuffer;
                }
            }, true);
//                Class<?> clazz = Class.forName("com.feng.agent.demo.ReTransformDemo");
//                inst.retransformClasses(clazz);
            System.out.println("agent exec end......");
        }

        public static void premain(String args, Instrumentation inst) {
            initAgent(args, inst);
        }

        public static void agentmain(String args, Instrumentation inst) {
            initAgent(args, inst);
        }
    }

Hook-Logik

public interface MethodHook {
    byte[] hookMethod(ClassLoader loader, String className, byte[] classfileBuffer) throws NotFoundException, CannotCompileException, IOException;


}

public class FileHook  implements MethodHook{

    public byte[] hookMethod(ClassLoader loader, String className, byte[] classfileBuffer) throws NotFoundException, CannotCompileException, IOException {
        // TODO: 获取ClassPool
        ClassPool classPool = ClassPool.getDefault();
//        CtClass ctClass = classPool.get(className);
        CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
        // TODO: 获取sayHelloFinal方法
        CtMethod ctMethod = ctClass.getMethod("list", "()[Ljava/lang/String;");
        // TODO: 方法前后进行增强
        ctMethod.insertBefore("{ System.out.println(\"start\");}");
        ctMethod.insertAfter("{ System.out.println(\"end\"); }");
        // TODO: CtClass对应的字节码加载到JVM里
//        Class c = ctClass.toClass();
        return ctClass.toBytecode();
    }

}

public class MethodFilter {

    private static Map<String, MethodHook> classMap = new HashMap<>();

    static {
        classMap.put("java.io.File", new FileHook());
    }

    public static boolean filterClass(String classname){
        return classMap.containsKey(classname);
    }

    public static MethodHook getHook(String classname) {
        return classMap.get(classname);
    }
}

Problem 1: Die Vorprüfung greift nicht

Zu diesem Zeitpunkt wird der erste Hinweis ausgelöst. Die geladene Klasse muss Klassen aktiv neu transformieren, um wirksam zu werden, andernfalls ersetzt addTransformer die Klasse nicht. addTransformer ist eine Vorprüfung, und die Bytecode-Ersetzung kann nur durchgeführt werden, wenn die Klasse geladen ist.

Es ist ersichtlich, dass die Klassenersetzung tatsächlich keine Wirkung zeigt, da die File-Klasse geladen wurde. Debuggen Sie, um den Grund zu ermitteln

Arrays.stream(inst.getAllLoadedClasses()).filter((c)->c!=null&&c.getName().startsWith("java.io.File")).collect(Collectors.toList()) 

Wie in der folgenden Abbildung gezeigt, wurde die ersetzte Datei dieses Mal tatsächlich geladen und ist nicht wirksam. Zu den häufig verwendeten Klassen gehören Eingabe- und Ausgabestreams usw.

 

 Die Lösung ist ebenfalls einfach: Fügen Sie retransformClasses nach addTransformer hinzu, um wirksam zu werden

Class<?>[] classes = inst.getAllLoadedClasses();
    Arrays.stream(classes).filter((c) -> c!=null&& MethodFilter.filterClass(c.getName())).forEach((c)->{
    try {
        inst.retransformClasses(c);
    } catch (UnmodifiableClassException e) {
        throw new RuntimeException(e);
    }
});            

 Nachdem der Test zum Code hinzugefügt wurde, funktioniert es wirklich

 Frage 2: Das Klassenersetzungsproblem von JDK

Das system.out, das mit dem JDK geliefert wird, das der Autor hier verwendet. Wenn ich selbst eine Klasse schreibe, ist die tatsächliche Situation sehr häufig.

public class FileCheck {

    public void checkFilePath(File file){
        if (file.getAbsolutePath().startsWith("/Users")) {
            System.out.println("user dir");
        }
        System.out.println("File start " + file.getPath());
    }
}

 ctMethod.insertBefore("{ FileCheck.checkFilePath(this);}");

wird auslösen

javassist.CannotCompileException: [Quellfehler] keine solche Klasse: FileCheck

Da die JDK-Klasse geändert wird, die JDK-Klasse jedoch vom Bootstrap geladen wird, was ist also mit der Klasse, die wir selbst geschrieben haben?

Der Klassenlader von Bootstrap kann die Klasse von AppClassloader nicht laden

Daher benötigen wir appendToBootstrapClassLoaderSearch, um die von uns geschriebene Klasse in den Suchbereich von JDK zu stellen und die Instrumentierungstechnologie zu diesem Zweck zu ändern, da für die Instrumentierung statische Methoden erforderlich sind. Natürlich können auch nicht statische Methoden und Reflexionsinstrumentierung verwendet werden.

public class FileCheck {

    public static void checkFilePath(File file){
        if (file.getAbsolutePath().startsWith("/Users")) {
            System.out.println("user dir");
        }
        System.out.println("File start " + file.getPath());
    }
}



    public byte[] hookMethod(ClassLoader loader, String className, byte[] classfileBuffer) throws NotFoundException, CannotCompileException, IOException {
        // TODO: 获取ClassPool
        ClassPool classPool = ClassPool.getDefault();
//        CtClass ctClass = classPool.get(className);
        CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
        // TODO: 获取sayHelloFinal方法
        CtMethod ctMethod = ctClass.getMethod("list", "()[Ljava/lang/String;");
        // TODO: 方法前后进行增强
        ctMethod.insertBefore("{com.feng.agent.FileCheck.checkFilePath($0);}");
        ctMethod.insertAfter("{System.out.println(\"end\");}");
        // TODO: CtClass对应的字节码加载到JVM里
//        Class c = ctClass.toClass();
        return ctClass.toBytecode();
    }

Nach der Änderung normal ausführen

 Frage 3: Das Problem des Klassenladers

Nach der obigen Verarbeitung kann die JDK-Klasse zwar ersetzt werden, das Problem wird jedoch gelöst, indem das Agent-JAR zur appendToBootstrapClassLoaderSearch-Suche hinzugefügt wird. Der BootstrapClassLoader-Klassenlader lädt jedoch einige zusätzliche Klassen nicht, was zu Mehrfachverwendung und Mehrfachladung führt Das Phänomen. Das Beispiel ist wie folgt

public class CheckStatus {

    private static Map<String, Boolean> statusMap = new HashMap<>();

    public static void initStatus(){
        statusMap.put("FILE_STATUS", true);
    }

    public static Boolean getStatus(String statusKey){
        if (!statusMap.containsKey(statusKey)) return false;
        return statusMap.get(statusKey);
    }
}

Anschließend über den Agenten initialisieren

 Fügen Sie es dann dort hinzu, wo der Bytecode ersetzt wird

public class FileCheck {

    public static void checkFilePath(File file){
        if (file.getAbsolutePath().startsWith("/Users")) {
            System.out.println("user dir");
            System.out.println("CheckStatus: " + CheckStatus.getStatus("FILE_STATUS"));
        }
        System.out.println("File start " + file.getPath());
    }
}

Nach der Ausführung wird festgestellt, dass der Wert von CheckStatus falsch ist

Der Grund ist ebenfalls sehr einfach, da der Ladeklassenlader vor appendToBootstrapClassLoaderSearch APPclassloader ist, der Bootstrapclassloader jedoch nach appendToBootstrapClassLoaderSearch verwendet wird, sodass die Reihenfolge einfach umgekehrt werden kann

 

Aufgrund der Situation der Code-Wiederverwendung sollte der JDK-Ersatz tatsächlich vom Nicht-JDK-Ersatz unterschieden werden. Manchmal kann er jedoch nicht streng unterschieden werden Benutzerdefinierter Klassenlader: Führen Sie das Kern-JAR des Agenten mit der benutzerdefinierten Klassenlader-Reflektion aus. Klassen im Zusammenhang mit JDK müssen jedoch mit der ursprünglichen Logik von JDK geladen werden

public class AgentClassloader extends URLClassLoader {
    public AgentClassloader(URL[] urls) {
        super(urls, ClassLoader.getSystemClassLoader().getParent());
    }

    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        final Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            return loadedClass;
        }

        // 优先从parent(SystemClassLoader)里加载系统类,避免抛出ClassNotFoundException
        if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {
            return super.loadClass(name, resolve);
        }
        try {
            Class<?> aClass = findClass(name);
            if (resolve) {
                resolveClass(aClass);
            }
            return aClass;
        } catch (Exception e) {
            // ignore
        }
        return super.loadClass(name, resolve);
    }
}

Zusammenfassen

Tatsächlich ist die Technologie des Agenten selbst sehr einfach, aber beim Laden von Klassen ist sie viel komplizierter. Klassen haben Klassenlader, Threads haben Klassenlader, und Thread-Klassenlader und Klassen können unterschiedlich sein. Wenn Unterklassenlader geladen werden, Sie können zum übergeordneten Element gehen, um sie zu finden, aber zum übergeordneten Element Sie können nicht nach unten schauen und es zu diesem Zeitpunkt nur selbst laden.

Darüber hinaus besteht das Prinzip des Agenten darin, vor dem Laden der Klasse eine Ersetzung durchzuführen, sodass einige JDK-Klassen nicht ersetzt werden können und die JDK-Klasse vom Bootstrap-Klassenlader geladen wird, sodass sie oft schlecht behandelt werden muss und Ausnahmen geladen werden müssen Fügen Sie die durch JDK ersetzten zugehörigen Klassen zu Bootstrap Find hinzu, und der Appclassloader oder der benutzerdefinierte geladene Bootstrap werden wiederholt geladen.

Ich denke du magst

Origin blog.csdn.net/fenglllle/article/details/130200432
Empfohlen
Rangfolge