Javaklasse zur Laufzeit ändern (Methode, Felder hinzufügen)

Alex_

Gesperrt
Hallo,

ich hab als Vorgabe für ein Projekt eine library gegeben. Ist es möglich eine der enthaltenen Klassen zur Laufzeit zu ändern und die geänderte Klasse in der JVM zu benutzen? Konkret will ich einige Felder hinzufügen, sowie eine bestehende Methode ändern.

Subclassing geht leider nicht, da in der library selber an einigen Stellen Instanzen dieser Klasse erstellt werden auf die ich aber auch Zugriff brauche. Hab mir schon cglib angeguckt, allerdings müssten Instanzen der Veränderten Klasse über die Enhancer Klasse erstellt werden.

Java:
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Someclass.class);
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                     // [...]
            }
        });
Someclass instance = (Someclass) enhancer.create();  // Die Instanzen in der library selbst können so ja nicht erstellt werden

Ist es also möglich eine Klasse global für die JVM zu ändern, sodas die bestehenden Aufrufe unverändert bleiben können?

Besten Dank :)

Alex
 
Danke erstmal für die Antwort :)

Wenn ich den Artikel richtig verstanden habe, wird zum laden der einzelnen Instanzen ein eigenes ClassLoader Objekt benötigt

Java:
        URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{dir.toURI().toURL()});
        Class clazz1= classLoader.loadClass(className);
        Someclass instance = (Someclass) clazz1.newInstance();

Dies klappt aber nur solang ich selber Einfluss auf die Erstellung der Instanzen habe. In der library selber klappt das leider nicht :(

Java:
      KlasseAusLibrary kL = ...
      kL.erstelleSomeclass();
      kL.machWasMitSomeclass();

      ...

      Someclass ref = kL.getSomeclassInstanz(); // Hier z.B. sollte die Instanz bereits die Methoden und Felder haben


Freundliche Grüße

Alex
 
Hi

man könnte einen eigenen Classloader schreiben (bzw. von vorhandenen erben lassen),
der alles seinen Parent machen lässt, außer diese eine Klasse (den Klassennamen) zu laden
Wenn der Name geladen werden soll dann eben die eigene Klasse
(als schon vorkompiliertes Bytearray) nehmen.
 
Dann müsste die Library aber auch diesen Classloader verwenden, da er ja die zusätzliche Funktionalität ggf. auch in der Library haben will. Und da der Code in der Library der Code ist, der nun mal ist, wird er den Standard-Classloader verwenden.

Mein Eindruck ist, dass das nicht funktionieren wird, da man kompilierten Bytecode zur Laufzeit nicht ändern kann, jedenfalls solange man keinen Agent bzw. Instrumentation (http://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html) verwendet. Und der müsste AFAIK in einer nativen Library gebaut sein. Profiler bspw. machen sowas.
 
Dann müsste die Library aber auch diesen Classloader verwenden, da er ja die zusätzliche Funktionalität ggf. auch in der Library haben will. Und da der Code in der Library der Code ist, der nun mal ist, wird er den Standard-Classloader verwenden.
Hab da etwas in Erinnerung, dass sich das "vererben" lasst;
also wenn die verwendene Klasse einen eigenen verwenden...
kann mich aber auch komplett irren und finde grad das Lesezeichen nicht
 
Hallo,

eine Möglichkeit wäre via (BCI) Byte-code instrumentation und load-time weaving.
Dafür kannst du dann einen AspectJ Aspekt schreiben der per Method / Field Introductions die entsprechenden Methoden / Fields an die gewünschte Klasse dran schiebt.

Du kannst das aber auch von Hand machen, in dem du einen Java Agent schreibst der über das Instrumentation API via ClassFileTransformer den Bytecode der entsprechenden Klasse wie gewünscht umschreibt.

Je nachdem wie die JVM Capabilities aussehen kann es sein, dass du nur Bytecode von bestehenden Methoden ändern, jedoch keine neuen Methoden oder Felder hinzufügen kannst. Aber hier kann man sich oft auch damit behelfen, dass man einfach den passenden Methoden body so patched, dass man einfach noch Methoden einer anderen Klasse aufruft, die dann die gewünschte Zusatzfunktionalität bietet.

Kannst du deine Fragestellung mal als einfaches Beispiel formulieren, dann können wir mal schauen.

Ach jam ganz vergessen - wenn du die Änderungen "nur" on-the-fly haben möchtest könntest du auch JRebel von Zeroturnaround anschauen :)

Gruß Tom
 
Hallo,

hier mal ein kleines Beispiel dazu wie man mit ASM, dem Java Instrumentation API und einem Java Agent selbst Bytecode von Class-Files manipulieren kann.

Die Klasse:
Java:
package de.tutorials.training.java.bci.example;

public class DomainClass {
}

Möchten wir um eine neue Methode "hello" erweitern die folgende Implementierung haben soll.
Java:
  public void hello() {
        System.out.printf("Hello: %s%n", String.valueOf(this));
    }

Dazu lassen wir uns mittels eines Bytecode-Genrierungstools (z.Bsp. das ASM Plugin in intellij) folgenden Bytecode Generierungssequenz erzeugen:
Java:
{
            mv = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(5, l0);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hello: %s%n");
            mv.visitInsn(ICONST_1);
            mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
            mv.visitInsn(DUP);
            mv.visitInsn(ICONST_0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;");
            mv.visitInsn(AASTORE);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "printf", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;");
            mv.visitInsn(POP);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(6, l1);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("this", "Lde/tutorials/training/java/bci/example/DomainClass;", null, l0, l2, 0);
            mv.visitMaxs(6, 1);
            mv.visitEnd();
        }

Nun haben wir ein Projekt "de.tutorials.training.java.bci.example" in dem die obige DomainClass sowie eine Klasse mit einer main-Methode enthalten sind.

Java:
package de.tutorials.training.java.bci.example;

public class ExtendJavaBootstrapClassExample {

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

		DomainClass.class.getMethod("hello").invoke(new DomainClass());
	}
}

Wenn wir diese Klasse nun ausführen erhalten wir eine Fehlermeldung:
Code:
C:\development\workspaces\sts340\de.tutorials.training.java.bci.example>java -cp target/classes de.tutorials.training.java.bci.example.ExtendJavaBootstrapClassExample
Exception in thread "main" java.lang.NoSuchMethodException: de.tutorials.training.java.bci.example.DomainClass.hello()
        at java.lang.Class.getMethod(Class.java:1665)
        at de.tutorials.training.java.bci.example.ExtendJavaBootstrapClassExample.main(ExtendJavaBootstrapClassExample.java:7)
... die besagt, dass es die Methode "hello" auf die wir per Reflection zugreifen wollten an dieser Klasse nicht gibt.

Diese Methode werden wir nun über einen Java Agent mit dem Instrumentation API nachträglich in den Bytecode der Klasse "DomainClass" reingenerieren.

Dazu legen wir ein neues Projekt "de.tutorials.training.java.bci.agent" an, in dem wir unsere Java Agent Definition ablegen:
(Wir verwenden die ASM Bibliothek zur Erzeugung der Bytecode Instruktionen - diese muss sich nachher natürlich auch entsprechend im classpath befinden):
Java:
package de.tutorials.training.java.bci.agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class SimpleAgent implements Opcodes {

	public static void premain(String agentArgs, Instrumentation inst) {
		
		inst.addTransformer(createDomainClassTransformer(), true);
	}

	private static ClassFileTransformer createDomainClassTransformer() {
		return new ClassFileTransformer() {

			public byte[] transform(ClassLoader loader, String className,
					Class<?> classBeingRedefined,
					ProtectionDomain protectionDomain, byte[] classfileBuffer)
					throws IllegalClassFormatException {

				// System.out.println("Transform: " + className);

				if (className
						.equals("de/tutorials/training/java/bci/example/DomainClass")) {

					System.out.println("Transform: " + className);

					try {
						
						ClassReader reader = new ClassReader(classfileBuffer);

						ClassWriter cw = new ClassWriter(0);

						{
							/**
							 * <pre>
							 * public void hello(){
							 * 	 System.out.printf("Hello: %s%n", String.valueOf(this));
							 * }
							 * 
							 * <pre>
							 */

							MethodVisitor mv = cw.visitMethod(ACC_PUBLIC,
									"hello", "()V", null, null);
							mv.visitCode();
							Label l0 = new Label();
							mv.visitLabel(l0);
							mv.visitLineNumber(5, l0);
							mv.visitFieldInsn(GETSTATIC, "java/lang/System",
									"out", "Ljava/io/PrintStream;");
							mv.visitLdcInsn("Hello: %s%n");
							mv.visitInsn(ICONST_1);
							mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
							mv.visitInsn(DUP);
							mv.visitInsn(ICONST_0);
							mv.visitVarInsn(ALOAD, 0);
							mv.visitMethodInsn(INVOKESTATIC,
									"java/lang/String", "valueOf",
									"(Ljava/lang/Object;)Ljava/lang/String;");
							mv.visitInsn(AASTORE);
							mv.visitMethodInsn(INVOKEVIRTUAL,
									"java/io/PrintStream", "printf",
									"(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;");
							mv.visitInsn(POP);
							Label l1 = new Label();
							mv.visitLabel(l1);
							mv.visitLineNumber(6, l1);
							mv.visitInsn(RETURN);
							Label l2 = new Label();
							mv.visitLabel(l2);
							mv.visitLocalVariable(
									"this",
									"Lde/tutorials/training/java/bci/example/DomainClass;",
									null, l0, l2, 0);
							mv.visitMaxs(6, 1);
							mv.visitEnd();
						}

						reader.accept(cw, ClassReader.EXPAND_FRAMES);

						return cw.toByteArray();

					} catch (Exception e) {
						throw new RuntimeException(e);
					}
				}

				return null;
			}
		};
	}
}

Damit der Agent auch so arbeitet wie er soll müssen wir noch eine entsprechende Manifest Datei in META-INF/MANIFEST.MF erzeugen:
Code:
Manifest-Version: 1.0
Premain-Class: de.tutorials.training.java.bci.agent.SimpleAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true

Wenn nun beide Projekte bauen (siehe Anhang), und die Klasse mit der main-Methode von oben wie folgt starten:
Code:
C:\development\workspaces\sts340\de.tutorials.training.java.bci.example>java -Xbootclasspath/a:"C:\Users\tom\.m2\repository\asm\asm\3.3.1\asm-3.3.1.jar" -javaagent:..\de.tutorials.training.java.bci.agent\target\de.tutorials.training.java.bci.agent-0.0.1-SNAPSHOT.jar -cp target/classes de.tutorials.training.java.bci.example.ExtendJavaBootstrapClassExample

So erhalten wir folgenden Ausgabe:
Code:
Transform: de/tutorials/training/java/bci/example/DomainClass
Hello: de.tutorials.training.java.bci.example.DomainClass@4d88e490
... dies zeigt, dass die JVM unsere dynamisch generierte Methode "hello" an der Klasse "DomainClass" finden und ausführen kann.
Dies ist nur ein kleines Beispiel dafür, was man alles mit Bytecode Instrumentation / Java Agents machen kann.

Für den gegebenen Anwendungsfall eigenen sich andere Technologien wie AspectJ IMHO jedoch etwas besser, da diese über ihre Introduction Fähigkeiten einfacher zu verwenden sind.

Gruß Tom
 

Anhänge

  • de.tutorials.training.java.bci.zip
    33,7 KB · Aufrufe: 18
Zuletzt bearbeitet von einem Moderator:
Hallo,
hier mal ein kleines Beispiel dazu wie man mit ASM, dem Java Instrumentation API und einem Java Agent selbst Bytecode von Class-Files manipulieren kann.

Hallo,

Ich danke dir. Dein Beispiel war genau das, was ich gesucht habe :)

Für den gegebenen Anwendungsfall eigenen sich andere Technologien wie AspectJ IMHO jedoch etwas besser, da diese über ihre Introduction Fähigkeiten einfacher zu verwenden sind.

Hab mich zuvor noch nie mit AOP beschäftigt. Nach dem durschauen der Beipiele zu AspectJ sieht das aber mehr als interessant aus. Werd mich wohl in nächster Zeit intensiver damit beschäftigen. Auch hierfür vielen Dank :)

Freundliche Grüße

Alex
 
Zurück