Dynamic Proxy Builder

Der DynamicProxy ist seit Java 1.3 im Core JDK enthalten. Mithilfe dieser Klasse kann man zur Laufzeit Dynamic Proxies erzeugen. Dabei sind einige Dinge zu beachten. Aber sehen wir uns erst einmal ein Beispiel an.

public class Main {
  public static void main(String[] args) {
    Service subject = ProxyGenerator.makeProxy(Service.class, new Subject_A());
    String hello = subject.work("Hello");
    System.out.println("hello = " + hello);
  }
}

public interface Service {
  String work(String txt);
}

public class Subject_A implements Service {
  public String work(String str) {
    System.out.println("str = " + str);
    return str + "_DONE";
  }
}

public class ProxyGenerator {
  public static <P> P makeProxy(Class<P> subject, P realSubject) {
    Object proxyInstance = Proxy.newProxyInstance(
        subject.getClassLoader(),
        new Class<?>[]{subject},
        (proxy, method, args)
            -> method.invoke(realSubject, args)
    );
    return subject.cast(proxyInstance);
  }
}

Hier wird zu dem Interface Service und der Implementierung Subject_A ein Proxy generiert. Der Proxy selbst hat keine weitere Funktion an dieser Stelle. Es wird lediglich der Methodenaufruf durchgereicht. Wichtig ist die Stelle:

        (proxy, method, args)
            -> method.invoke(realSubject, args)

Hierbei handelt es sich um die Implementierung des FunctionalInterface InvocationHandler. Dieser InvocationHandler wird immer dann aufgerufen, wenn eine Methode auf dem Proxy aufgerufen wird. An dieser Stelle kann man nun das Verhalten des Originals ändern oder sogar auch gänzlich unterbinden bzw. ersetzen. Der Nachteil hier ist, das es immer ein Interface geben muss, auf das die neu erzeugte Instanz des Proxies überführt werden kann. Mit dem DynamicProxy wird allerdings auch die Vererbungskette unterbrochen. Es gibt somit einiges, was man im Detail beachten muss. ALs Beispiel möchte ich die Methoden hashCode() und equals() nennen. Aber sehen wir uns an, was man damit noch alles im Bereich der Pattern realisieren kann.

In diesem Beispiel möchte ich auf den DynamicProxyBuilder zu sprechen kommen. Das Pattern Proxy gibt es in einigen Grundausprägungen. Virtual-Proxy, Security-Proxy und Remote-Proxy. Alle diese kann man mit dem Dynamic-Proxy realisieren. Das Vorgehen ist immer gleich, man muss den InvocationHandler implementieren. Im Falle des Security-Proxy wird bei dem jeweiligen Aufruf einer Methode überprüft, ob der Aufruf durchgeführt werden darf. Vereinfacht kann man sich das in trivialer Art und Weise so vorstellen.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  final boolean checkRule = rule.checkRule();
  if (checkRule) {
    return method.invoke(original, args);
  } else {
    return null;
  }
}

Nun stellt sich die Frage, wieviele Regeln gibt es die durchlaufen werden müssen und wer stellt die zur Verfügung. Aus einer Regel machen wir eine Liste von Regeln und definieren für eine Regel das Interface SecurityRule (hier handelt es sich um eine Beispiel-Lösung)

public interface SecurityRule {
  boolean checkRule();
}

In dem DynamicProxyBuilder definieren wir nun zum einen eine List und die Methode zum Hinzufügen von SecurityRules

  private List<SecurityRule> securityRules = new ArrayList<>();

  public ProxyBuilder<I, T> addSecurityRule(SecurityRule rule) {
    securityRules.add(rule);
    return this;
  }

Nun gibt es zwei verschiedene Lösungen wie man den finalen DynamicProxy dazu aufbauen kann. Die erste Version besteht darin, das man in dem InvocationHandler über die Liste der SecurityRule - Instanzen iteriert. Sobald eine Regel fehlschlägt wird der Methodenaufruf auf dem Original unterbunden. Ausserdem muss sichergestellt sein, dass die Regeln in der Reihenfolge angewendet werden, in der sie in die Liste ein geführt worden sind. Es kann ja eine implizite Abhängigkeit bestehen.

Aber es gibt auch noch die Möglichkeit für jede Instanz von SecurityRule einen Proxy zu erzeugen. Die Proxies werden dann in der Reihenfolge ineinander geschachtelt. Ich möchte diesen Weg gehen, um zu zeigen wie DynamicProxies ineinenader geschachtelt werden können.

  private ProxyBuilder<I, T> build_addSecurityRule(SecurityRule rule) {
    final ClassLoader classLoader = original.getClass().getClassLoader();
    final Class<?>[] interfaces = {clazz};
    final Object nextProxy = Proxy.newProxyInstance(
        classLoader,
        interfaces,
        new InvocationHandler() {
          private T original = ProxyBuilder.this.original;
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            final boolean checkRule = rule.checkRule();
            if (checkRule) {
              return method.invoke(original, args);
            } else {
              return null;
            }
          }
        });
    original = (T) clazz.cast(nextProxy);
    return this;
  }

Der Weg ist recht einfach. Es wird in jeder Instanz des InvocationHandlers das Original (kann ein Proxy sein, muss es aber nicht) gehalten und sich selbst als Proxy darum herum gelegt. Das bedeutet, dass man in umgekehrter Reihenfolge über die Liste der Regeln iterieren muss. An dieser Stelle steht noch nicht fest, wann das genau passieren wird. Dazu komme ich später.

Aber so wie man es mit den Regeln gemacht hat, kann man auch einen Virtual-Proxy dazwischen schalten. Das macht meines Erachtens nur als erster Proxy um das Original Sinn. Demnach ist das ein Kandidat für die Methode in der man die Instanz des Builders erzeugt. Hier nehme ich die Methoden createBuilder(..). Einmal indem man das Original mit übergibt, und zum anderen das man mit angibt welche Form von Virtual-Proxy man erzeugen möchte. Auf die verschiedenen Versionen eines Virtual-Proxy gehe ich hier nicht ein. Das beschreibe ich ausführlich in dem Kapitel "Virtual Proxies".

  public static <I, T extends I> 
      ProxyBuilder<I, T> createBuilder(Class<I> clazz, T original) {
    final ProxyBuilder<I, T> proxyBuilder = new ProxyBuilder<>();
    proxyBuilder.original = original;
    proxyBuilder.clazz = clazz;
    return proxyBuilder;
  }

  public static <I, T extends I> 
      ProxyBuilder<I, T> createBuilder(
          Class<I> clazz, Class<T> original, Concurrency concurrency) {

    final ProxyBuilder<I, T> proxyBuilder = new ProxyBuilder<>();
    final I proxy = ProxyGenerator.make(clazz, original, concurrency);
    proxyBuilder.original = (T)proxy;
    proxyBuilder.clazz = clazz;
    return proxyBuilder;
  }

Aber auch andere Dinge wie zum Beispiel das Erfassen von Metriken kann man einfach realisieren. In dem nachfolgenden Beispiel verwende ich die Metrics Lib (Dropwizard).

  public ProxyBuilder<I, T> addMetrics() {
    final MetricRegistry metrics = MetricsRegistry.getInstance().getMetrics();
    final ClassLoader classLoader = original.getClass().getClassLoader();
    final Class<?>[] interfaces = {clazz};
    final Object nextProxy = Proxy.newProxyInstance(
        classLoader,
        interfaces,
        new InvocationHandler() {
          private final Histogram methodCalls = metrics.histogram(clazz.getSimpleName());
          private final T original = ProxyBuilder.this.original;

          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//            System.out.println("addMetrics = is running");
            final long start = System.nanoTime();
            final Object invoke = method.invoke(original, args);
            final long stop = System.nanoTime();
            methodCalls.update((stop - start));
            return invoke;
          }
        });
    original = (T) clazz.cast(nextProxy);
    return this;
  }

Das kann man nun mit beliebig vielen fachlichen Queschnittsthemen machen. Die Verwendung zeige ich anhand eines jUnit Tests. Hierfür dfiniere ich ein Interface InnerDemoInterface (wird wegen des DynamicProxy benötigt) und eine Beispiel Implementierung InnerDemoClass.

Der zur Erzeugung des finalen Proxies notwendige Quelltext ist der folgende.

    final InnerDemoClass original = new InnerDemoClass();

    final InnerDemoInterface demoLogic = ProxyBuilder
        .createBuilder(InnerDemoInterface.class, original)
        .addSecurityRule(() -> true)
        .addSecurityRule(() -> true)
        .addMetrics()
        .build();

Es werden zwei triviale Sicherheitsregeln hinzugefügt, kein VirtualProxy, aber dafür werden die Metriken gemessen.

public interface InnerDemoInterface {
  String doWork();
}
public class InnerDemoClass implements InnerDemoInterface {

  public InnerDemoClass() {
    System.out.println("InnerDemoClass = init");
  }

  @Override
  public String doWork() {
    return "InnerDemoClass.doWork()";
  }
}

  @Test
  public void testAddMetrics() throws Exception {

    final InnerDemoClass original = new InnerDemoClass();

    final InnerDemoInterface demoLogic = ProxyBuilder
        .createBuilder(InnerDemoInterface.class, original)
        .addSecurityRule(() -> true)
        .addSecurityRule(() -> true)
        .addMetrics()
        .build();

    Assert.assertNotNull(demoLogic);
    Assert.assertEquals(demoLogic.doWork(), original.doWork());

    final MetricRegistry metrics = MetricsRegistry.getInstance().getMetrics();
    final ConsoleReporter reporter = ConsoleReporter.forRegistry(metrics)
        .convertRatesTo(TimeUnit.NANOSECONDS)
        .convertDurationsTo(TimeUnit.MILLISECONDS)
        .build();
    reporter.start(1, TimeUnit.SECONDS);

    IntStream.range(0, 10_000_000).forEach(i -> {
      final String s = demoLogic.doWork();
      workingHole(s.toUpperCase());
    });
    System.out.println("s1 = " + s1);

    reporter.close();
  }

  String s1;
  private void workingHole(String s) {
    s1 = s;
  }

aber da fehlt doch noch was

Was noch fehlt ist die Methode build() des Builders. Hier werden alle notwendigen Regeln der Erzeugung abgelegt. In unserem Fall z.B. das die Security Regeln in der Reihenfolge angewendet werden wir sie von dem Entwickler angegeben worden sind.

  public I build() {
    Collections.reverse(securityRules);
    securityRules.forEach(this::build_addSecurityRule);
    return this.original;
  }

Ich habe diesen ProxyBuilder als eigenständiges OpenSource Projekt auf github abgelegt und werde nach und nach eine generische Version der jeweiligen Builder dort zur Verfügung stellen.

https://github.com/RapidPM/proxybuilder

results matching ""

    No results matching ""