Code statt dynamischer Proxies generieren

Es werde Code!

Heinz Kabutz, Sven Ruppert

© Shutterstock.com/Mclek

Warum nicht einen Proxy so generieren, wie man ihn benötigt? Aber wie fängt man an und was sollte beachtet werden? Es ist leichter, als man vermuten könnte.

Im JDK gibt es das Compiler-API. Dieses ermöglicht dem Entwickler, zur Laufzeit Java-Klassen selbst zu übersetzen. Eines der Haupteinsatzgebiete ist die Verwendung im Zusammenhang mit Codegeneratoren, die Java-Quelltexte erzeugen. Nach dem Übersetzungsvorgang kann man das Ergebnis über einen Class Loader direkt laden und verwenden. Prinzipiell gibt es verschiedene Vorgehensweisen. Wir werden uns hier den standardisierten Java-Compiler aus dem Package javax.tools ansehen. Dieser ist seit Java 6 im JDK enthalten und wurde erstmals im JSR-199 „Java Compiler API“ definiert. Um zur Laufzeit dynamisch Bytecode zu erzeugen, ist für uns die Möglichkeit von Interesse, aus einem String heraus Bytecode zu erzeugen und zu laden. Der String soll zur Laufzeit generiert und nicht aus einer Datei geladen werden.

Prinzipiell werden folgende Teile benötigt: Zum einen eine Klasse, die den Quellcode repräsentiert, in unserem Fall GeneratedJavaSourceFile. Zu beachten ist an dieser Stelle, dass die Repräsentation final in einer CharSequence vorliegen muss. Der Aufbau selbst ist recht simpel. Es wird ein URI angegeben, der hier lediglich aus einem Klassennamen mit der Sourcecode-Extension (Java) besteht, und der Typ Kind.SOURCE. Der Inhalt selbst wird in einem Attribut vom Typ CharSequence mit dem Namen javaSource gespeichert (Listing 1).

public class GeneratedJavaSourceFile 
extends SimpleJavaFileObject {
private CharSequence javaSource;
  public GeneratedJavaSourceFile(String className,
                                 CharSequence javaSource) {
    super(URI.create(className + ".java"), Kind.SOURCE);
    this.javaSource = javaSource;
  }
  public CharSequence getCharContent(
    boolean ignoreEncodeErrors)
  throws IOException {
    return javaSource;
  }
}

Das Gegenstück zum Quelltext ist der Bytecode. Dieser wird durch die Klasse GeneratedClassFile wiedergegeben. Der Aufbau ist ähnlich wie der von der Sourcecode-Repräsentation. Der URI enthält den Klassennamen mit der Extension class, und der übergebene Typ ist nun Kind.CLASS. Die generierte Klasse selbst wir in dem Attribut outputStream vom Typ ByteArrayOutputStream gespeichert (Listing 2).

public class GeneratedClassFile extends SimpleJavaFileObject {
  private final ByteArrayOutputStream outputStream 
    = new ByteArrayOutputStream();
  public GeneratedClassFile() {
    super(URI.create("generated.class"), 
    JavaFileObject.Kind.CLASS);
  }
  public OutputStream openOutputStream() {
    return outputStream;
  }
  public byte[] getClassAsBytes() {
    return outputStream.toByteArray();
  }
}

In beiden Klassen wurde übrigens nicht die in der API-Doc angegebene URI-Schreibweise string:///Classname.java gewählt, da die hier verwendete ebenfalls bestens funktioniert.

Nun sind Quelle und Ziel definiert. Wie sieht die Verbindung dazwischen aus? Der Zugriff auf die Quelle und das Ziel erfolgt über den so genannten JavaFileManager (Listing 3), den man sich wie den Dateimanager für den Compiler vorstellen kann. Normalerweise werden hier bei dem StandardJavaFileManager die Zwischenschritte in temporären Dateien auf der Festplatte zwischengespeichert. Genau das wollen wir in unserem Fall nicht. Aus diesem Grund wird ein FileManager implementiert (GeneratingJavaFileManager), der dieses ausschließlich im Speicher hält. Es wird lediglich das ByteArray weitergereicht.

 
public class GeneratingJavaFileManager
extends ForwardingJavaFileManager<JavaFileManager> {
  private final GeneratedClassFile gcf;
  public GeneratingJavaFileManager(
    StandardJavaFileManager sjfm,
    GeneratedClassFile gcf) {
    super(sjfm);
    this.gcf = gcf;
  }
  public JavaFileObject getJavaFileForOutput(
    Location location, String className,
    JavaFileObject.Kind kind, FileObject sibling)
  throws IOException {
    return gcf;
  }
}

Zum Abschluss fehlt noch der Generator selbst. Wie wird also aus dem String der Bytecode? Hierzu sehen wir uns die Klasse Generator ein wenig genauer an.

Der Generator verwendet die private static native-Methode defineClass0 aus der Klasse Proxy, um die generierte Klasse direkt dem Class Loader hinzuzufügen. Sollte die Klasse schon vorhanden sein, wird eine Exeption geworfen. Die einzelnen Schritte sind schnell erklärt. Zuerst wird die Klasse kompiliert. Sollte das zu Fehlern führen, führt dieses in der Methode processResults dazu, dass die Fehler auf System.out geschrieben werden. Ist alles normal verlaufen, wird die Klasse erzeugt und mit dem Aufruf der Methode defineClass0 direkt dem Class Loader übergeben. Der Weg über die Methode defineClass0 wird genommen, damit nicht für jede neu generierte Klasse ein neuer Class Loader erzeugt werden muss.

Die Verwendung des Generators erfolgt, indem ein String, der den Java-Quelltext enthält, der Methode make(..) übergeben wird. Die zurückgelieferte Klasse kann dann per newInstance() dazu verwendet werden, neue Objekte zu erzeugen (Listing 4).

 public class Generator {
  private static final Method defineClassMethod;
  private static final JavaCompiler jc;

// snip init
  public static Class make(ClassLoader loader,
    String className,
    CharSequence javaSource) {
    GeneratedClassFile gcf = new GeneratedClassFile();
    DiagnosticCollector dc =
      new DiagnosticCollector<JavaFileObject>();
    boolean result = compile(className, javaSource, gcf, dc);
    return processResults(loader, javaSource, gcf, dc, result);
  }

  private static boolean compile(
    String className, CharSequence javaSource,
    GeneratedClassFile gcf,
    DiagnosticCollector<JavaFileObject> dc) {
    GeneratedJavaSourceFile gjsf = new GeneratedJavaSourceFile( className, 
      javaSource);
    GeneratingJavaFileManager fileManager =
      new GeneratingJavaFileManager(
      jc.getStandardFileManager(dc, null, null), gcf);
    JavaCompiler.CompilationTask task = jc.getTask(
      null, fileManager, dc, null, null, Arrays.asList(gjsf));
    return task.call();
  }

  private static Class processResults(
    ClassLoader loader, CharSequence javaSource,
    GeneratedClassFile gcf, DiagnosticCollector<?> dc, boolean result) {
    if (result) {
      return createClass(loader, gcf);
    } else {
      // use your logging system of choice here
  }

  private static Class createClass(
    ClassLoader loader, GeneratedClassFile gcf) {
    try {
      byte[] data = gcf.getClassAsBytes();
      return (Class) defineClassMethod.invoke(
        null, loader, null, data, 0, data.length);
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new IllegalArgumentException("Proxy problem", e);
    }
  }
}

Ein Praxisbeispiel

Nachdem die Basiswerkzeuge nun vorhanden sind, stellt sich die Frage nach einem praktischen Beispiel. Gehen wir im Folgenden davon aus, dass eine Firma ein moralisches Bewusstsein hat. Dieses ist ein Luxusgut, wenn keiner nach dieser Moral fragt, bzw. wenn es nach außen keine Wirkung erzielt. Aus diesem Grund wird eine virtuelle Moral erzeugt, die dann zu realer Moral wird, wenn der erste Fall eintritt, bei dem das sichtbar werden kann. Für uns bedeutet dies, dass erst bei einem bestimmten Methodenaufruf eine Instanz der Moral erzeugt wird. Beginnen wir mit der Klasse Company (Listing 5), die aus den Methoden damageEnvironment(), makeMoney() und becomeFocusOfMediaAttention() besteht. Damit wird das Verhalten simuliert, dass erst bei Aufmerksamkeit die Moral zum Tragen kommt, also Geld kostet.

 public class Company {
  private final String name;
  private final MoralFibre moralFibre;
  private double cash;
  public Company(String name, double cash, 
    MoralFibre moralFibre) {
    this.name = name;
    this.cash = cash;
    this.moralFibre = moralFibre;
    System.out.println("Company constructed: " + this);
  }
  public void damageEnvironment() {
    cash += 4000000;
    System.out.println("Company.damageEnvironment(): " 
      + this);
  }
  public void makeMoney() {
    cash += 1000000;
    System.out.println("Company.makeMoney(): " + this);
  }
  public void becomeFocusOfMediaAttention() {
    cash -= moralFibre.actSociallyResponsibly();
    cash -= moralFibre.cleanupEnvironment();
    cash -= moralFibre.empowerEmployees();
    System.out.println("Look how good we are... " + this);
  }
  public String toString() {
    return String.format("%s has $ %.2f", name, cash);
  }
}

Die Funktionsweise der Methoden ist recht eindeutig. Geld wird mit der Schädigung der Umwelt verdient. Sobald das in den Fokus der Allgemeinheit gerät, wird es teuer (becomeFocusOfMediaAttention). Die Moral wird hier durch das Interface MoralFibre beschrieben (Listing 6).

 public interface MoralFibre {
  double actSociallyResponsibly();
  double empowerEmployees();
  double cleanupEnvironment();
}

Damit ist das Verhalten beschrieben. Aber wie steht es nun wirklich um die Moral? Hierzu implementieren wir eine Klasse MoralFibreImpl (Listing 7), die wir absichtlich zu einem teuren Konstrukt machen. Simuliert wird dieses durch das Erzeugen eines unnötig großen Byte Arrays.

 
public class MoralFibreImpl implements MoralFibre {
  // very expensive to create moral fibre!
  private byte[] costOfMoralFibre = new byte[900 * 1000];
  {
    System.out.println("Moral Fibre Created!");
  }
  // AIDS orphans
  public double actSociallyResponsibly() {
    return costOfMoralFibre.length / 3;
  }
  // shares to employees
  public double empowerEmployees() {
    return costOfMoralFibre.length / 3;
  }
  // oiled sea birds
  public double cleanupEnvironment() {
    return costOfMoralFibre.length / 3;
  }
}

Der entscheidende Punkt ist nun: Wann genau wird diese Klasse instanziiert? Der triviale Fall geht davon aus, dass wir das Objekt erst dann erzeugen, wenn es benötigt wird. Allerdings legen wir keinen Wert darauf, dass dies im System nur einmal passieren wird. Die Implementierung der Klasse VirtualMoralFibre ist dementsprechend sehr einfach. Die jeweilige Art der Instanziierung erfolgt nun anhand von verschiedenen Beispielen in der Methode realSubject() (Listing 8).

 public abstract class VirtualMoralFibre 
  implements MoralFibre {
  protected abstract MoralFibre realSubject();
  public final double actSociallyResponsibly() {
    return realSubject().actSociallyResponsibly();
  }
  public final double empowerEmployees() {
    return realSubject().empowerEmployees();
  }
  public final double cleanupEnvironment() {
    return realSubject().cleanupEnvironment();
  }
}

Die einfachste Implementierung (VirtualMoralFibreNotThreadSafe, Listing 9) besteht darin, ein Objekt einfach beim ersten Aufruf zu erzeugen und in einem privaten Attribut zu speichern. Ob das Objekt erzeugt wird, wird anhand einer Prüfung auf null entschieden.

 public class VirtualMoralFibreNotThreadSafe 
  extends VirtualMoralFibre {
  private MoralFibre realSubject;
  protected MoralFibre realSubject() {
    if (realSubject == null) {
      realSubject = new MoralFibreImpl();
    }
    return realSubject;
  }
}

Die erste Verbesserung ist eine Lösung auf Basis von AtomicReferences. Hier allerdings kann es passieren, dass es dennoch mehr als eine Instanziierung geben wird. Der Kern der Vorgehensweise ist hier ebenfalls eine Prüfung auf null bei einem privaten Attribut. Das Setzen des Attributs findet hier mittels des vorherigen Vergleichs durch die Methode compareAndSet statt. Ziel ist hier, dass, obwohl kein Locking stattfindet, nur einmal eine Instanz erzeugt wird.

Das Gegenstück zur LockFree-Implementierung kann die Realisierung mittels ReentrantLock sein. Hier wird ebenfalls wieder auf null geprüft. Ist dem so, wird der Lock gesetzt, die Variable erzeugt und der Lock wieder freigegeben.

Um jetzt eine der Lösungen zu verwenden, muss eine Instanz der Klasse Company im Konstruktor eine beliebige Lösung mit übergeben bekommen. Im letzten Fall wäre der Aufruf wie folgt:

new Company("Cretesoft",20000.0, 
  new VirtualMoralFibreLockFree())

Anstelle der handgeschriebenen Lösung kann man nun auch einen DynamicProxy verwenden. Allerdings funktioniert das nur, wenn final auf ein Interface hingearbeitet wird. Die einfachste Lösung ist auch hier wieder NotThreadSafe (Listing 10). Nicht vergessen sollte man an dieser Stelle, dass der Aufruf über einen Proxy auch immer einen Overhead darstellt.

 public class VirtualDynamicProxyNotThreadSafe 
  implements InvocationHandler {
  private final Class realSubjectClass;
  private Object realSubject;
  public VirtualDynamicProxyNotThreadSafe(
    Class realSubjectClass) {
    this.realSubjectClass = realSubjectClass;
  }
  private Object realSubject() throws Exception {
    if (realSubject == null) {
      realSubject = realSubjectClass.newInstance();
    }
    return realSubject;
  }
  public Object invoke(
    Object proxy, Method method, Object[] args)
  throws Throwable {
    return method.invoke(realSubject(), args);
  }
}

Die Funktionsweise ist äquivalent zur Lösung VirtualMoralFibreNotThreadSafe. Der Unterschied ist lediglich, dass nun alle Aufrufe über einen Proxy geleitet werden. Diese Vorgehensweise lässt sich mit allen bisher dargestellten Lösungen durchführen.

Proxies als statische Klassen

Anstelle von dynamischen Proxies kann der Proxy auch zur Laufzeit generiert werden. Bei der Generierung soll entschieden werden, ob es sich dabei um einen statischen oder dynamischen Proxy handelt und wie das Nebenläufigkeitsverhalten ist. Dazu führen wir zwei Enumerationen ein, ProxyType und Concurrency:

public enum ProxyType {
  STATIC, DYNAMIC
}
public enum Concurrency {
  NONE, SOME_DUPLICATES, NO_DUPLICATES;
}

Um die Klassennamen der erzeugten Klassen für den Menschen lesbarer zu gestalten, implementieren wir eine Util-Klasse mit der Methode prettyPrint(..). Die Klassennamen sollen so lauten, wie sie von einem Menschen gewählt werden würden, wenn sie als Quelltext von Hand geschrieben worden wären.

Was nun noch fehlt, ist der ProxyGenerator (Listing 11), mit dessen Hilfe wir die CharSequence für das gewollte Proxy Subject generieren. Als Eingabe werden die beiden Enumerationen verwendet, die dann die Generierung steuern. Die Auswahl, welche Implementierung des ProxyGenerators verwendet wird, erfolgt in der Methode create(..) und ist durch eine einfache Switch-case-Anweisung realisiert. Alle generierten Klassen werden intern in einer Map gespeichert, um zu verhindern, dass sie zweimal generiert werden.

 
public class ProxyGenerator {
  private static final WeakHashMap cache = new WeakHashMap();
  public static <T> T make(
    Class<T> subject, Class realClass,
    Concurrency concurrency, ProxyType type) {
    return make(subject.getClassLoader(),
      subject, realClass, concurrency, type);
  }
  public static <T> T make(
    Class<T> subject, Class realClass,
    Concurrency concurrency) {
    return make(subject, realClass, concurrency, ProxyType.STATIC);
  }
  public static <T> T make(ClassLoader loader,
    Class<T> subject,Class realClass,
    Concurrency concurrency, ProxyType type) {
    Object proxy = null;
    if (type == ProxyType.STATIC) {
      proxy = createStaticProxy(loader,subject, realClass, concurrency);
    } else if (type == ProxyType.DYNAMIC) {
      proxy = createDynamicProxy(loader,subject, realClass, concurrency);
    }
    return subject.cast(proxy);
  }
  private static Object createStaticProxy(
    ClassLoader loader, Class subject,
    Class realClass, Concurrency concurrency) {
    Map clcache;
    synchronized (cache) {
      clcache = (Map) cache.get(loader);
      if (clcache == null) {
        cache.put(loader, clcache = new HashMap());
      }
    }
    try {
      Class clazz;
      CacheKey key = new CacheKey(subject, concurrency);
      synchronized (clcache) {
        clazz = (Class) clcache.get(key);
        if (clazz == null) {
          VirtualProxySourceGenerator vpsg 
            = create(subject, realClass, concurrency);
          clazz = Generator.make(loader, 
              vpsg.getProxyName(), vpsg.getCharSequence());
          clcache.put(key, clazz);
        }
      }
      return clazz.newInstance();
    } catch (Exception e) {
      // snip
    }

  }
  private static VirtualProxySourceGenerator create(
    Class subject, Class realClass,
    Concurrency concurrency) {
    switch (concurrency) {
      case NONE:
        return 
        new VirtualProxySourceGeneratorNotThreadsafe(
          subject, realClass
        );
      
      default:
        // snip 
);
    }
  }
  private static Object createDynamicProxy(
    ClassLoader loader, Class subject,
    Class realClass, Concurrency concurrency) {
    if (concurrency != Concurrency.NONE) {
      // snip 
    }
    return Proxy.newProxyInstance(
      loader,
      new Class[]{subject},
      new VirtualDynamicProxyNotThreadSafe(realClass));
  }
  private static class CacheKey {
    private final Class subject;
    private final Concurrency concurrency;
    private CacheKey(Class subject, 
      Concurrency concurrency) {
      this.subject = subject;
      this.concurrency = concurrency;
    }
// snip
  }
}

Soweit ist alles recht übersichtlich. Nun kommen wir zum Kern der Angelegenheit: dem abstract VirtualProxySourceGenerator (VPSG). Dieser erzeugt eine CharSequence basierend auf der Struktur des Proxysubjekts. In unserem Fall handelt es sich um das Interface MoralFibre. Für jeden Teilabschnitt, der zu generieren ist, gibt es eine spezielle Methode. Nehmen wir zum Beispiel die return-Anweisungen. Hier wird in der Methode addReturnKeyword der Rückgabewert der übergebenen Instanz der Klasse Methode der Rückgabewert ausgelesen. Handelt es sich um void, wird nichts generiert, ansonsten ein String mit dem Inhalt return:

private void addReturnKeyword(PrintWriter out, Method m) {
  if (m.getReturnType() != void.class) {
    out.print("return ");
  }
}

In dieser Art ist der gesamte Generator aufgebaut. Damit ist der Grundaufbau des Proxys erzeugt. Es findet also eine Delegation an das Original statt. Nun fehlt noch die Methode, die das jeweilige Subject selbst generiert. Mit „Subject“ ist hier die Art gemeint, wie die Erzeugung der Instanz VirtualMoralFibre stattfindet. Von Hand hatten wir die Klassen VirtualMoralFibreLockFree, VirtualMoralFibreNotThreadSafe und VirtualMoralFibreThreadSafe geschrieben. Dies ist die Vorlage für die korrespondierenden Generatoren. Man kann erkennen, dass der Aufbau der Klassen jeweils fast identisch ist. Die von Hand geschriebene Klasse findet sich jetzt als String im jeweiligen SourceGenerator wieder (Listing 12).

class VirtualProxySourceGeneratorNotThreadsafe
extends VirtualProxySourceGenerator {

  public VirtualProxySourceGeneratorNotThreadsafe(
    Class subject, Class realSubject) {
    super(subject, realSubject, Concurrency.NONE);
  }
  protected void addRealSubjectCreation(PrintWriter out,
    String name,
    String realName) {
    out.printf(" private %s realSubject;%n", name);
    out.println();
    out.printf(" private %s realSubject() {%n", name);
    out.printf(" if (realSubject == null) {%n");
    out.printf(" realSubject = new %s();%n", realName);
    out.println(" }");
    out.println(" return realSubject;");
    out.println(" }");
  }
}

Das Ziel ist erreicht: Die Proxies werden dynamisch beim ersten Aufruf erzeugt, kompiliert und dem Class Loader übergeben. Sie können damit instanziiert werden. Die Verwendung erfolgt wie in Listing 13 dargestellt.

 public class CompanyTest {

  public static void main(String[] args) {
    Company company = new Company("Cretesoft", 10000.0,
      new MoralFibreImpl());
    company.makeMoney();
    company.damageEnvironment();
    company.becomeFocusOfMediaAttention();
    
    Company company2 = new Company("Cretesoft2", 20000.0,
      ProxyGenerator.make(MoralFibre.class,
        MoralFibreImpl.class,
        Concurrency.NONE));
    company2.makeMoney();
    company2.makeMoney();
    company2.makeMoney();
    company2.damageEnvironment();
    company2.becomeFocusOfMediaAttention();
  }
}

Die Ausgabe zeigt deutlich den Unterschied zwischen der Verwendung mit und ohne Proxy. Es ist auf der Konsole deutlich zu sehen, wann im Fall der Company 2 die Instanz erzeugt wird:

 
Moral Fibre Created!
Company constructed: Cretesoft has $ 10000,00
Company.makeMoney(): Cretesoft has $ 1010000,00
Company.damageEnvironment(): Cretesoft has $ 5010000,00
Look how good we are... Cretesoft has $ 4110000,00


Company constructed: Cretesoft2 has $ 20000,00
Company.makeMoney(): Cretesoft2 has $ 1020000,00
Company.makeMoney(): Cretesoft2 has $ 2020000,00
Company.makeMoney(): Cretesoft2 has $ 3020000,00
Company.damageEnvironment(): Cretesoft2 has $ 7020000,00
Moral Fibre Created!
Look how good we are... Cretesoft2 has $ 6120000,00

Der Proxy funktioniert auch mit anderen Klassen. Eigenen Experimenten steht also nichts mehr im Wege.

Fazit

Der ProxyGenerator erstellt dynamisch virtuelle Proxies, hat jedoch den Vorteil, dass er nicht den Overhead eines dynamischen Proxys hat. Ein weiterer Vorteil besteht darin, dass von den generierten Klassen abgeleitet werden kann, was bei dynamischen Proxies nicht funktioniert. Hiermit vereinfachen sich einige Dinge im Bereich der Ablaufsteuerung und Architektur.

Fragen und Anregungen können direkt an Sven Ruppert gerichtet werden.

Aufmacherbild: Programming code abstract screen of software developer. Computer script. (MORE SIMILAR IN MY GALLERY) von Shutterstock.com / Urheberrecht: McIek

Verwandte Themen:

Geschrieben von
Heinz Kabutz
Heinz Kabutz
Dr. Heinz Kabutz schreibt den Java-Specialists-Newsletter, der heutzutage in 134 Ländern der Welt gelesen wird, was selbst McDonalds nicht schafft. Seine besonderen Interessen sind fortgeschrittenes Java, Concurrency, Performanz und Kochen.
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.