Posts mit dem Label open source werden angezeigt. Alle Posts anzeigen
Posts mit dem Label open source werden angezeigt. Alle Posts anzeigen

Sonntag, 2. Dezember 2012

Runtime Generic Type Resolution for Java

Bekanntlich stehen in Java die meisten generische Typinformationen zur Laufzeit nicht zur Verfügung. Insbesondere können sie nicht mittels Reflection ausgelesen werden. Doch stimmt diese Aussage unter allen Umständen?

Das Verfahren, bei dem die Typinformationen gelöscht werden, heisst Type Erasure. Damit soll das sogenannte "Generic Legacy Problem" gelöst werden.

Aus Gründen der Rückwärtskompatibilität mit altem Code, hat man sich damals entschieden, beim Kompiliervorgang die Generics weitgehend zu löschen oder in andere Konstrukte zu transformieren. Im Detail löscht der Kompiler die Typparameter, ersetzt die Typrariablen durch ihren Grenztyp, fügt Typecasts hinzu, und erweitert Klassen um sogenannte Brückenmethoden, damit das Overriding auch weiterhin funktioniert.

Folglich kann zur Laufzeit nicht herausgefunden werden, welchen Typ die unten stehende Liste "myList" hat:

public class Scribble { 

    public static void main(String[] args) {
        List<String> myList = new ArrayList<String>();

        Class elementType = getElementTypeOf(myList);
        System.out.println(elementType);
    }

    /**
    * Returns the generic element type of the given list.
    */
    public static Class getElementTypeOf(List list) {
        // This is not possible in Java !?!
    }
}

Für das obige Beispiel ist das tatsächlich unmöglich. Allerdings ist das nicht die ganze Wahrheit. Wenn wir den Code etwas abwandeln, dann lässt sich die Typinformation auslesen:

public class Scribble { 

    static List<String> myList = new ArrayList<String>();

    public static void main(String[] args) {
        Class elementType = getElementTypeOf("myList");
        System.out.println(elementType);
    }

    /**
    * Returns the generic element type of the field with the given name
    */
    public static Class getElementTypeOf(String fieldname) {
        return new GenericType(Scribble.class)
                   .getFieldType( fieldname)
                   .getTypeParameter(List.class.getTypeParameters()[0])
                   .asClass();
    }
}

Das liegt daran, dass die Variable "myList" nun als Member der Klasse deklariert ist, d.h. mit vollständigen Metainformationen im Classfile abgelegt wird.

Die Klasse "GenericType" kapselt die Suche nach dem Wert des Typparameters, welche mit Hilfe der etwas umständlichen Reflection API realisiert ist. Sie ist Teil des gleichnamigen Github-Projekt GenericType.

Dienstag, 1. November 2011

Ein Codegenerator für POJO-Builders


Seit Ende September beschäftige ich mich auf Github mit einem Open-Source-Projekt mit dem Namen PojoBuilder.

Ich entwickle dort einen Java-Codegenerator, der für ein beliebiges POJO einen passenden Pojo-Builder erzeugen kann. Dieser bietet die folgende Funktionalitäten:

  • ein "fluent interface" zum Festlegen der Eigenschaftswerte des POJOs
  • und eine build-Methode für die Erstellung einer neuen POJO Instanz mit diesen Werten
Unter anderem eignet sich so ein Builder besonders für die Erstellung von Testdaten im Rahmen von automatisierten Tests, wie das Nat Pryce anschaulich beschrieben hat.

Ein kleines Beispiel

Der Codegenerator lässt sich naheliegenderweise am einfachsten anhand eines Code-Beispiels erläutern. Nehmen wir an, wir haben die folgende Javaklasse erstellt und benötigen dafür einen Builder.
public class Contact { 
    private final String name;
    private String email;

    public Contact(String aName) {
        this.name = aName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }
}

Der dazugehörige Builder sollte beispielsweise auf die folgende Weise bedient werden können:

Contact james = new ContactBuilder()
    .withName("James Bond")
    .withEmail("007@secretservice.org")
    .build();

Mit dem Einsatz von PojoBuilder lässt sich dieses Ziel durch die Markierung der Klasse mit zwei Annotations erreichen.

@GeneratePojoBuilder(intoPackage = "samples.builder")
public class Contact { 
    private final String name;
    private String email;

    @ConstructorProperties({ "name"})
    public Contact(String aName) {
        this.name = aName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getName() {
        return name;
    }
}

Mit @GeneratePojoBuilder wird der Codegenerator angewiesen, für die annotierte Klasse einen Builder zu generieren. Das Attribut intoPackage ist optional und bestimmt das Java-Package, in das der Builder generiert werden soll. Mit @ConstructorProperties legt man fest, welcher Konstruktor verwendet werden soll und wie die Parameter auf Properties abgebildet werden.

Der generierte Builder sieht dann so aus (Kommentare habe ich entfernt):

public class ContactBuilder implements Cloneable {

   protected String value$email$java$lang$String; 
   protected boolean isSet$email$java$lang$String = false; 

   protected String value$name$java$lang$String; 
   protected boolean isSet$name$java$lang$String = false; 

   public ContactBuilder() {
   }

   public ContactBuilder withEmail( String value) {
      this.value$email$java$lang$String = value;
      this.isSet$email$java$lang$String = true;
      return this;
   }

   public ContactBuilder withName( String value) {
      this.value$name$java$lang$String = value;
      this.isSet$name$java$lang$String = true;
      return this;
   }

   public Object clone() {
      try {
            ContactBuilder result = (ContactBuilder)super.clone();
            return result;
      } catch (CloneNotSupportedException e) {
            throw new InternalError(e.getMessage());
      }        
   }
   
   public ContactBuilder but() {
      return (ContactBuilder)clone();
   }

   public Contact build() {
      Contact result = new Contact( this.value$name$java$lang$String );
      if ( this.isSet$email$java$lang$String) {    
           result.setEmail( this.value$email$java$lang$String);  
      }
      return result;
   }
}

Wie ist das ganze nun implementiert?

Für die eigentliche Codegenerierung nutze ich, wie schon in früheren Projekten, die Stringtemplate Engine. Offen war ursprünglich lediglich die Frage, wie der Generator angestoßen werden sollte. Ich wollte erreichen, dass er sowohl in einem Build-Lauf als auch in einer IDE genutzt werden kann. Da sie sich für beides sehr gut eignet, habe ich mich für die Annotation Processor API entschieden, die seit Java 5 verfügbar ist. Somit war auch festgelegt, dass der Generator über Annotations gesteuert wird (und nicht etwa über XML oder eine andere Beschreibungssprache).

Automatisierte Tests

Eine Schwierigkeit im Projekt ist allerdings die Frage, wie man einen Annotation-Processor ordentlich testet, und zwar im Rahmen von Acceptance-Tests sowie Unit-Tests (ja, ich gebe zu, die Tests nicht vorher geschrieben zu haben). Bislang meine ich immerhin einen guten Weg für die Acceptance-Tests gefunden zu haben, und zwar mittels einer JavaProject-Fassade für die Java Compiler API, mit der ich tatsächlich on-the-fly ein kleines Java-Projekt komplett mit Sourcen erstellen und kompilieren kann. Anschließend kann ich in meinem TestCase die generierten Klassen per Classloader laden und mittels Reflection prüfen, oder alternativ dazu, könnte ich auch vorbereitete Unit-Tests laufen lassen, welche die generierten Builder testen.

Was allerdings das Unit-Testen des AnnotationProcessor angeht, tappe ich noch im Dunkeln. Grundsätzlich bin ich ein ein Fan von Interaction-Based-Testing, allerdings erscheint mir in diesem Fall mit Mock-Objekten zu arbeiten sehr aufwändig. Alternativ dazu bleibt wohl nur ein State-Based Ansatz, wie etwa in diesem Post auf Stackoverflow beschrieben.

Donnerstag, 7. Mai 2009

Ohloh.net - das Open-Source-Netzwerk

Ohloh.net ist eine Community rund um Open-Source-Software. Angemeldete User können sich als "Benutzer" von Software eintragen, sowie Bewertungen und Kommentare abgeben. Wer ein Open-Source-Projekt leitet oder sich daran beteilligt, kann auch dies angeben.


Der Screenshot zeigt den Eintrag für den Firefox-Browser.

Aktuell gibt es 29.632 Einträge. Offenbar wurden die meisten automatisch durch einen Crawler eingetragen. Sogar ein Eintrag für jrexx ist dabei. Das ist eine Bibliothek für reguläre Ausdrücke von Ralf Meyer, für die ich in der Steinzeit einen Visualisierer und Editor mit dem Namen jrexx-Lab geschrieben habe.

Blog einbinden

In den FAQs von Google-Code befindet sich eine kleine Notiz darüber, wie man einen Blog in die Projektseite einbinden kann.


Das habe ich gleich mal ausprobiert. Unter "Administer / Project Summary" habe ich in das Feld "Description" folgendes Wiki-Markup eingefügt:

== News ==
<wiki:gadget
url="http://google-code-feed-gadget.googlecode.com/svn/trunk/gadget.xml"
up_feeds="http://objectprops.blogspot.com/atom.xml"
width="780" height="140" border="0" up_showaddbutton="0">
</wiki:gadget>

Das Ergebnis ist auf der Projektseite von ObjectProps zu sehen.

ObjectProps auf Google-Code

Auf code.google.com stellt Google seit ein paar Jahren eine eigene Plattform für das Hosting von Open-Source-Projekten ins Netz. Um mir die Features genauer ansehen zu können, habe ich dort ein kleines Projekt mit dem Namen ObjectProps angelegt.

Vollkommen sinnlos sollte das Projekt ja nicht sein. Deswegen habe mein Code-Archiv nach Klassen durchsucht, die sich für ein kleines Open-Source-Projekt eignen und deren Funktionalität noch nirgends im Web zu finden ist. Entschieden habe ich mich für eine Komponente, die "den Zustand von Java-Objekten in ein java.util.Properties-Objekt speichern und wieder auslesen kann".

Nach der Registrierung stehen folgende Features zur Verfügung:
  • Verwaltung des Projekts über den Browser
  • Einfache Verwaltung von Projektmitgliedern
  • Versionskontrolle über Subversion
  • Code-Browsing im Web
  • Aufgabenverwaltung (Issue tracking)
  • Wiki-Seiten
  • Download-Bereich
  • Mailing Listen
Der GettingStarted-Guide ist leider etwas dünn. Speziell der Import von bestehendem Quellcode in das SVN erklärt er nicht zufriedenstellend. Aber glücklicherweise bietet er einen Kommentarblock an, wo ein paar freundliche Menschen eine genauere Beschreibung liefern.

In den FAQs und in den anderen Wiki-Artikeln befinden sich weitere Infos für den Einsteiger.