generated typesafe DynamicObjectAdapter - Builder

Das Pattern DynamicObjectAdapter stellt ein sehr flexibles und mächtiges Basispattern dar. Allerdings hat es auch ein paar Nachteile. In der Grundversion ist der Adapter nicht in der Vererbnungslinie zu dem verwendeten Service. Das hat zur Folge, dass eine Änderung einer Methode im Interface nicht zu fehlschlagenden Übersetzungsprozessen bei den Adaptern führt. Es existieren also DynamicObjectAdapter, die Apdater haben die nicht mehr aufgerufen werden. Der DynamicObjectAdapter entscheidet sich immer für die Verwendung des Originals. Das ist erst zur Laufzeit erkennbar, falls dieser explizite Fall nicht durch jUnit Tests abgedeckt worden ist.

In dem Abschnitt DynamicObjectAdapter - Builder habe ich gezeigt, wie man manuell dieses ein wenig mehr in die statische Semantik heben kann. Aber auch hier ist nicht automatisch gewährleistet, das Modifikationen direkt bei einem Übersetzungsprozess erkannt werden können. Allerdings sind alles Informationen vorhanden, um diesem Problem zu begegnen und dabei auch noch viel manuelle Arbeit zu sparen. Die Antwort liegt in der Verwendung von dem AnnotationProcessor-Plugin. Dieses existiert immerhin seit der Java Version 5. Die grundlegende Verwendung habe ich im Abschnitt AnnotationProcessor - Basics beschrieben.

Aber sehen wir uns nochmal kurz an, wie sich FunctionalInterfaces in Verbindung mit DynamicProxies verwendet werden können.

DynamicProxies and Lambdas

Bei der Verwendung von DynamicProxies müssen wir immer auf ein Interface reduzieren können. Demnach stellt sich die Frage, kann an eigentlich den Adapter mittels Lambdas realisieren? Beginnen wir mit einem einfachen Beispiel.


  public static <P> P makeProxy(Class<P> subintf, final P realSubject) {
    return subintf.cast(Proxy.newProxyInstance(
        subintf.getClassLoader(),
        new Class<?>[]{subintf},
        (proxy, method, args) -> method.invoke(realSubject, args)
    ));
  }


  public static interface Service {
    public String work(String txt);
  }
  public static class ServiceImpl implements Service {
    @Override
    public String work(String txt) {
      return txt + "-Impl";
    }
  }

Wir haben hier die Methode makeProxy mit der wir einen DynamicProxy erzeugen. Das Interface heißt in diesem Fall Service und besitzt genau eine Imlementierung. (ServiceImpl) Wir können dieses nun wie gewohnt verwenden.

Assert.assertEquals("Hello-Impl", makeProxy(Service.class, new ServiceImpl()).work("Hello"));

Da es sich bei dem Interface Service um ein FunctionalInterface handelt, kann man die Implementierung auch mit einem Lambda realisieren.

Assert.assertEquals("HelloLambda-Impl", 
                    makeProxy(Service.class, (txt)-> txt + "-Impl").work("HelloLambda"));

Soweit ist alles wie gewohnt, wie aber gehen wir mit Interfaces um, die mehr als eine Methode haben? Erweitern wir also das Interface und die Implementierung um eine weitere Methode.

  public static interface Service {
    public String work1(String txt);
    public String work2(int i);
  }

  public static class ServiceImpl implements Service {
    @Override public String work1(String txt) { return txt + "-Impl 1"; }
    @Override public String work2(int i) { return i + "-Impl 2"; }
  }

Hier kann man leider keinen Lambda-Ausdruck verwenden, da es sich nicht mehr um ein FunctionalInterface handelt. Eine Implementierung on-the-fly ist allerdings ebenfalls möglich.

Assert.assertEquals("HelloLambda-Impl 1", makeProxy(Service.class, new Service() {
      @Override public String work1(String txt) { return txt + "-Impl 1"; }
      @Override public String work2(int i)      { return null; }
    }).work1("HelloLambda"));

Hier gibt es nun verschiedene Dinge die nicht sonderlich handlich sind. Zum einen muss man nicht benötigte Implementierungen zumindest einmal als leere Methode vorsehen. Zum anderen kann man die Implementierungen nur noch auf dem klassischen Weg realisieren. Abhilfe schafft hier die Verwendung von Mixins.www.javaspecialists.eu

Aber die Verwendung von Mixins hilft uns in Verbindung mit dem DynamicObjectAdapter nicht weiter.

FunctionalInterfaces - der manuelle Weg

In dem Abschnitt DynamicObjectAdapter - Builder wurde manuelle aus dem Interface Service jede einzelne Methode in ein FunctionalInterface überführt. Diese Arbeit ist zeitaufwendig und langweilig.

public interface Service {
  String doWork_A(String txt);
  String doWork_B(String txt);
}

@FunctionalInterface
public interface ServiceAdapter_A {
  String doWork_A(String txt);
}

@FunctionalInterface
public interface ServiceAdapter_B {
  String doWork_B(String txt);
}

Ebenfalls muss ein InvocationHandler implementiert werden, der auf diese Signaturen passt und vollständig ist.

public static class ServiceInvocationHandler extends ExtendedInvocationHandler<Service> {
    public void withDoWork_A(ServiceAdapter_A adapter) {
      addAdapter(adapter);
    }

    public void withDoWork_B(ServiceAdapter_B adapter) {
      addAdapter(adapter);
    }
}

Und als letztes delegiert der Builder auf die jeweiligen Methoden des InvocationHandler´s.

public static class ServiceAdapterBuilder extends AdapterBuilder<Service> {

    public static ServiceAdapterBuilder newBuilder() {
      return new ServiceAdapterBuilder();
    }

    private final ServiceInvocationHandler invocationHandler = new ServiceInvocationHandler();

    //evtl auch statisch initialisiert, wenn gewuenscht
//    {
//      invocationHandler.setOriginal(new ServiceImpl());
//    }

//delegate , modify
    public ServiceAdapterBuilder withDoWork_A(ServiceAdapter_A adapter) {
      invocationHandler.withDoWork_A(adapter);
      return this;
    }

    public ServiceAdapterBuilder withDoWork_B(ServiceAdapter_B adapter) {
      invocationHandler.withDoWork_B(adapter);
      return this;
    }

    public ServiceAdapterBuilder setOriginal(Service original) {
      invocationHandler.setOriginal(original);
      return this;
    }

    @Override
    protected InvocationHandler getInvocationHandler() {
      return invocationHandler;
    }
}

FunctionalInterfaces - der generative Weg

Bei der Betrachtung von diesem Quelltext fällt einem recht schnell auf, dass es sich um Muster handelt die sich bestens eigenen um mittels Generator erzeugt zu werden. Beginnen wir mit den Interfaces. Als erstes erzeugen wir die Annotation *DynamicObjectAdapterBuilder.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface DynamicObjectAdapterBuilder {
}

Damit wir auf diese Annotation reagieren könne wird ein korrespondierender AnnotationProcessor imlementiert.

@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {

  private Types typeUtils;
  private Elements elementUtils;
  private Filer filer;
  private Messager messager;

  @Override
  public Set<String> getSupportedAnnotationTypes() {
    Set<String> annotataions = new LinkedHashSet<String>();
    annotataions.add(DynamicObjectAdapterBuilder.class.getCanonicalName());
    return annotataions;
  }

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    typeUtils = processingEnv.getTypeUtils();
    elementUtils = processingEnv.getElementUtils();
    filer = processingEnv.getFiler();
    messager = processingEnv.getMessager();
  }

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(DynamicObjectAdapterBuilder.class)) {

    // magic will start here....
    }
    return true;
  }


  @Override
  public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
  }

  public void error(Element e, String msg, Object... args) {
    messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
  }

}

Bis auf die Implementierung der Methode process() und die Annotation DynamicObjectAdapterBuilder ist alles wie in dem Abschnitt AnnotationProcessor - Basics.

Als erstes wollen wir prüfen ob es sich um ein Interface handelt, dass mit der Annotation DynamicObjectAdapterBuilder annotiert worden ist.

      if (annotatedElement.getKind() != ElementKind.INTERFACE) {
        error(annotatedElement, "Only classes can be annotated with @%s",
                    DynamicObjectAdapterBuilder.class.getSimpleName());
        return true; // Exit processing
      }

Nun können wir davon ausgehen, das wir überhaupt einen DynamicProxy erzeugen können. Der nun folgende Quelltext macht in etwa folgendes. Wenn der nächste KindKnoten eine Methode ist, prüfe ob diese public ist. Wenn dem so ist, dann hole folgende Informationen

  • Name der Methode
  • Name und Typ der Parameter
  • Rückgabetyp

Mit diesen Informationen erzeuge pro Methode ein Interface mit dem Namen Klassenname_Methodenname und füge genau eine Methode hinzu. (natürlich die Methode die auch den Klassennamen mit ergeben hat) Hiermit ist sichergestellt, das eine Namenskollision recht selten ist. Um den String zu erzeugen der den generierten Quelltext ergibt, verwende ich das OpenSource Projekt GitHub - JavaPoet.

      // We can cast it, because we know that it of ElementKind.INTERFACE
      TypeElement typeElement = (TypeElement) annotatedElement;
      // Get the full QualifiedTypeName
      for (Element enclosed : annotatedElement.getEnclosedElements()) {
        if (enclosed.getKind() == ElementKind.METHOD) {
          ExecutableElement methodElement = (ExecutableElement) enclosed;
          if (methodElement.getModifiers().contains(Modifier.PUBLIC)) {
            final TypeMirror returnType = methodElement.getReturnType();

            final List<? extends VariableElement> parameters = methodElement.getParameters();
            List<ParameterSpec> parameterSpecs = new ArrayList<>();
            String params = "";
            for (VariableElement parameter : parameters) {
              final Name simpleName = parameter.getSimpleName();
              final TypeMirror typeMirror = parameter.asType();
              params = params + " " + typeMirror + " " + simpleName;
              TypeName typeName = TypeName.get(typeMirror);

              final ParameterSpec parameterSpec = ParameterSpec
                  .builder(typeName, simpleName.toString(), Modifier.FINAL).build();
              parameterSpecs.add(parameterSpec);
            }

            final MethodSpec.Builder methodSpecBuilder 
                  = createMethodSpecBuilder(methodElement, returnType, parameterSpecs);

            writeFunctionalInterface(typeElement, methodElement, methodSpecBuilder);

            //InvocationHandler
            //SNIPP...
            //Builder
            //SNIPP...
          }
        }
      }
  private MethodSpec.Builder createMethodSpecBuilder(ExecutableElement methodElement, 
                  TypeMirror returnType, List<ParameterSpec> parameterSpecs) {
    final MethodSpec.Builder methodSpecBuilder = MethodSpec
        .methodBuilder(methodElement.getSimpleName().toString())
        .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
        .returns(TypeName.get(returnType));

    for (ParameterSpec parameterSpec : parameterSpecs) {
      methodSpecBuilder.addParameter(parameterSpec);
    }
    return methodSpecBuilder;
  }
  private void writeFunctionalInterface(TypeElement typeElement, 
              ExecutableElement methodElement, MethodSpec.Builder methodSpecBuilder) {
    final TypeSpec functionalInterface = TypeSpec
        .interfaceBuilder(typeElement.getSimpleName().toString() + "_" 
                        + methodElement.getSimpleName().toString())
        .addAnnotation(FunctionalInterface.class)
        .addMethod(methodSpecBuilder.build())
        .addModifiers(Modifier.PUBLIC)
        .build();

    final Element enclosingElement = typeElement.getEnclosingElement();
    final JavaFile javaFile = JavaFile
        .builder(enclosingElement.toString(), functionalInterface)
        .build();
    final String className = typeElement.getQualifiedName().toString() + "_" 
                        + methodElement.getSimpleName().toString();
    try {
      JavaFileObject jfo = filer.createSourceFile(className);
      Writer writer = jfo.openWriter();
      javaFile.writeTo(writer);
      writer.flush();

    } catch (IOException e) { e.printStackTrace(); }
  }

Nun existieren die FunctionalInterfaces die als Eingabeparameter für die Builder-Methoden verwendet werden können. Dem Einsatz von Lambdas steht nun an dieser Stelle nichts mehr im Wege.

Nun fehlen noch die Imlpementierung des InvocationHandlers und der Builder selbst. Hier wird wie gerade auch beschrieben vorgegangen. Das Listing ist ein wenig umfrangreich. Aus diesem Grunde habe ich ein extra Repository mit diesem Beispiel angelegt. GitHub - GeneratedTypesaveDynamicObjectAdapter-Builder

Die Verwendung

Die Verwendung selbst ist dann recht einfach. Annotiere Dein Interface und starte den Übersetzungsprozess. Danach sind die typesave - Builder vorhanden und es kann mit der Verwendung begonnen werden. Sollte sich mal etwas an dem Interface ändern, dann schlägt der Übersetzungsprozess fehl.

  public static void main(String[] args) {

    Service service = ServiceAdapterBuilder.newBuilder()
        .setOriginal(new ServiceImpl())
        .withDoWork_A((txt) -> txt + "_part")
        .buildForTarget(Service.class);

    System.out.println(service.doWork_A("Hallo Adapter"));

    final boolean proxyClass = Proxy.isProxyClass(service.getClass());
    System.out.println("proxyClass = " + proxyClass);

    final InvocationHandler invocationHandler = Proxy.getInvocationHandler(service);
    final ServiceInvocationHandler serviceInvocationHandler
                    = (ServiceInvocationHandler) invocationHandler;

    serviceInvocationHandler.doWork_A((txt) -> txt + "_part_modified");
    System.out.println(service.doWork_A("Hallo Adapter"));

  }

results matching ""

    No results matching ""