MSDN Magazin > Home > Ausgaben > 2007 > May >  Migration: Umwandeln einer Java-Webanwendung in...
Migration
Umwandeln einer Java-Webanwendung in eine ASP.NET-Anwendung mit JLCA
Brian Jimerson

Themen in diesem Artikel:
  • Ressourcensuche
  • E/A-Streams
  • Protokollierung und Sammlungen
  • Umgestaltung
In diesem Artikel werden folgende Technologien verwendet:
ASP.NET, Konvertierungs-Assistent für die Programmiersprache Java und C#
Laden Sie den Code für diesen Artikel herunter: JLCA2007_05.exe (157 KB)
Code online durchsuchen
Der typische Softwareentwicklungszyklus folgt einem einfachen Modell: Zusammenstellen der Anforderungen, Entwerfen der Anwendung, Schreiben des Codes, Testen der Software und Bereitstellung. Gelegentlich jedoch ist die Grundlage für ein neues Entwicklungsprojekt ganz einfach die Plattform, auf der der Kunde die Anwendung bereitstellen möchte. In einem solchen Fall kann die Codebasis einer vorhandenen Anwendung auf die gewünschte Plattform konvertiert (oder portiert) werden.
Thema dieses Artikels ist die Konvertierung einer Java-Webanwendung in eine in C# implementierte ASP.NET-Anwendung. Der Artikel basiert auf einem Projekt, an dem ich beteiligt war. Das Ziel dieses Projekts bestand darin, für einen Kunden eine ASP.NET-Version einer vorhandenen Java-basierten Anwendung zu erstellen. Zu Beginn des Artikels wird der Microsoft® Konvertierungs-Assistent für die Programmiersprache Java vorgestellt. Es folgt eine Erläuterung allgemeiner Entwicklungsparadigmen, die auf den beiden Plattformen keine direkten Entsprechungen haben, z. B.:
  • Eingabe/Ausgabe
  • Ressourcenauflösung
  • Quellstrukturlayout und Benennungskonventionen
  • Nutzen der Ausführungsumgebung
Diese Anwendung wird als SOAP-kompatibler Webdienst mit einem herkömmlichen permanenten Speicher einer relationalen Datenbank implementiert. Die eigentliche Präsentationsebene des Webdiensts und die SOAP-Schnittstellen sollen an dieser Stelle nicht besprochen werden. Stattdessen wird auf die Anwendung dahinter eingegangen. Beispielcode für diesen Artikel steht als Download zur Verfügung.

Der Konvertierungs-Assistent für die Programmiersprache Java
Mit dem Konvertierungs-Assistenten für die Programmiersprache Java kann eine Java-Anwendung in eine C#-Anwendung konvertiert werden. Das Tool ist seit Visual Studio .NET 2003 in Visual Studio® enthalten. In Visual Studio 2005 ist derzeit Version 3.0 des Konvertierungs-Assistenten für die Programmiersprache Java integriert. Außerdem steht er auf der Homepage des Konvertierungs-Assistenten für die Programmiersprache Java als kostenloser Download zur Verfügung.
Version 3.0 enthält Verbesserungen zur Konvertierung von Java-Artefakten wie Servlets und Java Server Pages (JSPs) sowie Rich Client-Anwendungen, die Swing oder das Abstract Windowing Toolkit (AWT) verwenden. In der Praxis ist der Konvertierungs-Assistent für die Programmiersprache Java ein sehr guter Ausgangspunkt für eine Konvertierung, kann jedoch nicht den gesamten Prozess erfolgreich abschließen. Ihnen wird also nicht die ganze Arbeit abgenommen. Nach der Verwendung des Tools müssen Sie einige konvertierte Elemente manuell auflösen.
Klicken Sie auf Datei | Öffnen | Konvertieren, um in Visual Studio 2005 den Konvertierungs-Assistenten für die Programmiersprache Java zu verwenden. Die Fenster des Assistenten erklären sich praktisch von selbst. Am wichtigsten ist die Eingabe des Stammverzeichnisses Ihres vorhandenen Java-Projekts (siehe Abbildung 1).
Abbildung 1 Eingeben des Java-Stammverzeichnisses in den Konvertierungs-Assistenten für die Programmiersprache Java (Klicken Sie zum Vergrößern auf das Bild)
Der Konvertierungs-Assistent für die Programmiersprache Java beginnt dann mit der Konvertierung des Java-Quellcodes in C#. Der Vorgang nimmt nicht allzu viel Zeit in Anspruch. Zur Illustration: Die Konvertierung unserer Codebasis mit ungefähr 100 Klassendateien war in weniger als 10 Minuten abgeschlossen – gerade genug Zeit für eine Kaffeepause. Dieser Zeitraum variiert natürlich je nach Projekt und System.
Nach Abschluss des Konvertierungsvorgangs erstellt der Konvertierungs-Assistent für die Programmiersprache Java einen HTML-Bericht über Fehler und Warnungen. Diese Elemente werden außerdem von den problematischen Members als Kommentare in den generierten C#-Code eingetragen. Zur Unterstützung enthält jedes Element außerdem einen Hyperlink, der auf weitere Informationen zur Lösung des Problems verweist.
Viele der Warnungen können ohne weiteres ignoriert werden. Darin wird lediglich auf Verhaltensunterschiede zwischen Java und C# hingewiesen, wie in der folgenden Warnung: „Typumwandlungen zwischen primitiven Typen können ein unterschiedliches Verhalten aufweisen.“ Sie sollten sich jedoch alle Warnungen ansehen, um sicherzustellen, dass diese Verhaltensunterschiede keine Auswirkungen auf Ihre Anwendung haben.
Beim ersten Blick in den Konvertierungsbericht bekommen Sie möglicherweise den Eindruck, dass die Zahl der Probleme überwältigend ist. In diesem Fall wurden 816 Fehler und 16 Warnungen vermerkt. Die meisten Fehler konnten jedoch in eine von drei Kategorien eingeordnet und recht einfach behoben werden. Diese drei Kategorien sind folgende:
  • Members ohne Entsprechungen in C# oder Microsoft .NET Framework.
  • Beliebte Drittanbieterbibliotheken für Java, die in .NET Framework keine direkten Entsprechungen haben (z. B. Hibernate und log4j).
  • Elemente, die mit Klassenladung oder Ressourcenauflösung zu tun haben.
Erwähnenswert ist außerdem, dass der Konvertierungs-Assistent für die Programmiersprache Java nicht zu versuchen scheint, importierte Pakete aufzulösen (oder Namespaceanweisungen zu verwenden), die er nicht finden kann. Stattdessen übergibt er sie einfach an den generierten C#-Code. Bei dem Versuch, Ihre neue C#-Anwendung zu kompilieren, würden wahrscheinlich noch einige Compilerfehler mehr auftreten als im Konvertierungsbericht angegeben.
Das ist jedoch kein Grund zur Besorgnis. Wie bereits festgestellt, weist der überwiegende Teil der Fehler dieselben sich wiederholenden Muster auf. Eine individuelle Problembehandlung erübrigt sich damit. Die folgenden Abschnitte enthalten Informationen zur Behebung dieser üblichen Fehler. Darüber hinaus wird erläutert, welche Aufgaben Sie außerdem ausführen müssen, um Ihre konvertierte Anwendung in eine echte C#-Anwendung zu verwandeln.

Suchen von Ressourcen
Der Vorgang der Ressourcensuche (insbesondere das Suchen nach und Laden von Dateiressourcen) in Java unterscheidet sich erheblich von der Ressourcensuche in C#. Java verwendet ein Klassenladeprogramm zum Laden und Analysieren einer Klassendefinition zur Laufzeit. Die Aufgabe eines Klassenladeprogramms besteht zum Teil darin, den Kontext einer Klasse zu verwalten und Umgebungseinrichtungen für Klassen bereitzustellen, die von dem Programm geladen werden. In Java wird eine besondere Art Umgebungsvariable, classpath genannt, von einem Klassenladeprogramm zur Ressourcensuche verwendet. Die Variable classpath ist einer Umgebungsvariable path insofern ähnlich, als sie definiert, wo ein Klassenladeprogramm nach weiteren Klassen und Ressourcen suchen soll. Eine Anwendung, die eine weitere Klasse oder Ressource laden möchte, kann dies tun, indem sie dem Klassenladeprogramm den Speicherort der Datei in Relation zu classpath mitteilt.
Eine häufig angewandte Methode zur Ressourcenauflösung in Java besteht darin, einen besonderen Dateityp namens Eigenschaftendatei für konfigurierbare Informationen zu verwenden, z. B. Verbindungs- oder Hostinformationen, Pfade zu anderen Ressourcen, Lokalisierungszeichenfolgen und Anmeldeinformationen, die für die Authentifizierung verwendet werden sollen. Eigenschaftendateien enthalten Name=Wert-Paare, die durch einen Zeilenumbruch voneinander getrennt sind. Dieses Format ähnelt dem von INI-Dateien, es fehlen jedoch die Abschnitte.
Darüber hinaus wird die Suche nach Ressourcen immer mithilfe von Dateisystemnotationen durchgeführt. Damit soll festgestellt werden, ob sie sich tatsächlich im Dateisystem befinden. Daher muss der Entwickler nicht wissen, wie die Anwendung bereitgestellt wird. Andere Arten von Ressourcen, z. B. Bilder und Binärdateien, werden auf dieselbe Weise gesucht und geladen. Es folgt ein Beispiel für das Suchen und Verwenden einer Eigenschaftendatei in Java:
InputStream is = this.getClass().getResourceAsStream(
    “/application.properties”);
Properties properties = new Properties();
properties.load(is);
String myValue = properties.getProperty(“myKey”);
Im Gegensatz dazu können Ressourcen in .NET Framework auf zwei verschiedene Arten bereitgestellt und geladen werden: als eingebettete, binäre Ressource mit einer Assembly oder als Datei im lokalen Dateisystem.
Welches Verfahren sich für den Zugriff auf eine Ressource eignet, hängt vom Speicherort ab. Im Falle einer Dateisystemressource bietet sich etwa Folgendes an:
string fileName = Path.Combine(Path.GetFullPath(
    @”..\config\”), “properties.xml”);
Stream fileStream = File.Open(fileName, FileMode.Open);
//Do something with the stream
Wenn die Ressource jedoch in einer Assembly eingebettet ist, wäre eine Methode wie die folgende geeignet:
Assembly assembly = Assembly.GetExecutingAssembly();
Stream fileStream = assembly.GetManifestResourceStream(
    GetType(), “properties.xml”);
//Do something with the stream
Die Anwendung meines Teams wurde in der Annahme geschrieben, dass eine Auflösung möglich sein würde, ohne zu wissen, wie die Ressource bereitgestellt wurde. Da in der gesamten Anwendung viele Ressourcen geladen werden, hätte es viel Zeit und Mühe gekostet, jede einzelne geladene Ressource zu analysieren; zu bestimmen, wie die Ressource bereitgestellt wurde; und dann den Code zu ändern, um sie korrekt zu laden. Stattdessen erstellte das Team daher eine Dienstprogrammklasse namens ResourceLocator.
ResourceLocator wurde entworfen, um annähernd die Fähigkeit eines Java-Klassenladeprogramms zur Auflösung einer Ressource auf der Basis von classpath zu erreichen. Da alle Aufrufe zum Laden von Ressourcen auf diese Weise geschrieben wurden, erschien sie als die am wenigsten intrusive Konvertierungsmethode. Nach dem Schreiben von ResourceLocator bliebe als einzige Aufgabe übrig, Aufrufe von Class.getResourceAsStream in Resource­Locator.LocateResource zu ändern. Dies könnte mit einem einfachen Such- und Ersetzungsvorgang in Visual Studio ausgeführt werden.
Im Grunde übernimmt ResourceLocator den Namen und den relativen Pfad der zu suchenden Ressource und sucht dann danach, indem er verfügbare Assemblys und das lokale Dateisystem durchgeht. Es gibt daneben auch überladene Methoden, die eine präzisere Steuerung ermöglichen, z. B. die Angabe der Reihenfolge, in der Bereiche durchsucht werden sollen, und das Beschränken der Suche auf Assemblys oder das Dateisystem. Die Quelle für ResourceLocator ist in den Codebeispielen auf der MSDN® Magazine-Website enthalten.
Es drängt sich der Gedanke auf, dass es sehr kostspielig ist, an allen verfügbaren Speicherorten nach einer Ressource zu suchen. Dies ist nicht von der Hand zu weisen. Alle in der Anwendung befindlichen und geladenen Ressourcen wurden jedoch zwischengespeichert. Dies bedeutet, dass jede Ressource nur einmal während der Ausführung der Anwendung geladen wird. Folglich wird der Aufwand verringert (obgleich der Speicherverbrauch zunimmt, was ein Problem darstellen könnte, wenn eine große Anzahl von Ressourcen geladen werden muss). Wir haben daher entschieden, dass der Kompromiss annehmbar ist, da er uns zahlreiche Codeänderungen erspart hat. Diese Arten von Änderungen können auch im Laufe der Zeit vorgenommen werden. So können Sie mit der einfachen Lösung beginnen, die Portierung schnell einzuleiten, und dann nach und nach die Implementierung ändern, um sie den Design- und Implementierungsrichtlinien für .NET-basierte Anwendungen besser anzupassen.

Umgang mit Eingabe/Ausgabe-APIs
Es gibt mehrere Unterschiede zwischen den E/A-APIs in Java und jenen in .NET, die nach der Konvertierung berücksichtigt werden müssen. Ein wichtiger Unterschied besteht darin, dass E/A-Streams in .NET bidirektional sind; E/A-Streams in Java dagegen sind unidirektional. Dies bedeutet, dass es bei der .NET-Programmierung theoretisch möglich ist, in denselben Stream zu schreiben, aus dem gelesen wird. In Java können Sie jedoch entweder aus einem Stream lesen oder in einen Stream schreiben. Es ist nicht möglich, beide Vorgänge gleichzeitig für denselben Stream auszuführen. Dieser Unterschied bereitet während der Konvertierung keine großen Schwierigkeiten, da es sich um einen erweiterten Unterschied handelt und die .NET-Streams mindestens dieselbe Funktionsvielfalt bieten wie ihre Java-Pendants.
Wenn die Sicherheit unidirektionaler Datenströme beibehalten werden muss, können die E/A-Reader und -Writer in .NET genutzt werden. Diese dienen als Wrapper für einen zugrunde liegenden Stream, um Lese- oder Schreibfunktionen bereitzustellen. Das Ergebnis sind E/A-Vorgänge, die E/A-Vorgängen in Java programmtechnisch ähnlich sind.
Für unsere Anwendung ist der direkte Zugriff auf Streams ausreichend. Der Konvertierungs-Assistent für die Programmiersprache Java konvertiert E/A-Vorgänge ordnungsgemäß, daher sind für die Kompilierung keine Änderungen erforderlich. Wir haben jedoch den konvertierten Code sorgfältig überprüft, da es durchaus möglich ist, dass bei diesen Vorgängen auf niedriger Ebene Logikfehler auftreten.

Protokollierung
Protokollierung bezieht sich auf die Fähigkeit, an bestimmten Punkten der Ausführung Meldungen an einen Zielort im Code zu schreiben, z. B. erfasste Ausnahmen, Bereiche in der Logik, in denen Debuginformationen nützlich sein könnten, und Konfigurationsinformationen, die geladen werden. Sowohl Java als auch .NET Framework bieten eine leistungsfähige Umgebung für die Protokollierung von Informationen, unterscheiden sich jedoch erheblich in Bezug auf Entwurf und Implementierung.
In Java geschieht die Anwendungsprotokollierung für gewöhnlich über das log4j-Framework der Apache Software Foundation (ASF) oder die Java Logging APIs, die in den neuen Versionen des Java Development Kit (JDK) von Sun Microsystems enthalten sind. Die Java Logging API wird auf sehr ähnliche Weise ausgeführt wie log4j. Daher sind die beiden Umgebungen zum Zwecke dieser Erörterung austauschbar. Für .NET-basierte Anwendungen bietet die Microsoft Enterprise Library einen robusten Anwendungsblock für die Protokollierung, Logging Application Block genannt (siehe die Homepage des Microsoft Logging Application Block).
Die Standardprotokollierungsumgebungen sowohl in Java als auch in .NET bieten leistungsstarke Funktionen, darunter Support für Entwurfszeitkonfiguration, eine Vielzahl von Zielen für Protokolle (z. B. Datenbanken, Dateien und E-Mail-Empfänger) und so weiter. Beachten Sie jedoch, dass die API-Designs sich unterscheiden und daher während der Konvertierung manuelle Schritte Ihrerseits erforderlich sind.
Zum besseren Verständnis der Unterschiede zwischen den APIs sehen Sie sich die beiden Codeausschnitte in Abbildung 2 an. Sie veranschaulichen ein übliches Protokollierungsszenario in Java und in C#. Vom Ausschnitt mit log4j wird aus der Factory-Methode getLog eine statische Instanz eines Log-Objekts erstellt und der Kategorie MyClass zugewiesen. Vom Code werden dann auf Debugebene einige Informationen an das Protokoll ausgegeben. Vom C#-Ausschnitt mit dem Logging Application Block wird eine neue Instanz einer LogEntry-Klasse erstellt. Dies entspricht einem Eintrag im Zielprotokoll. Der Code weist dann dem Protokolleintrag eine Priorität von 2 zu, legt als Kategorie Debug fest und stellt eine Meldung bereit. Anschließend wird er in die Klasse Logger geschrieben.

Java mit log4j
private static final Log log = Logger.getLog(MyClass.class);
...
//Somewhere else in the class
log.debug(“Printing some debug information.”);
C# mit dem Logging Application Block
Logger.Write(”Printing some debug information.”, “Debug”);
Wichtig ist, dass die Protokollierung auf beiden Plattformen von der externen Konfiguration gesteuert wird. Die Konfiguration umfasst Informationen wie Protokollierungsziele, selektive Filterung von Protokollmeldungen sowie die Formatierung von Protokolleinträgen. In diesem Beispiel wurde die Konfiguration absichtlich nicht näher erläutert, da sie für die Erörterung nicht relevant ist.
Die Betrachtung dieser beiden Beispiele macht deutlich, dass sehr ähnliche Funktionen durchgeführt werden, jedoch auf unterschiedliche Weise. Im Beispiel mit log4j wird die Kategorie verwendet, um zu bestimmen, ob eine Meldung protokolliert wurde (basierend auf deren Ebene) und wo. Der Logging Application Block hingegen verwendet eine Kombination aus Filtern für Prioritäten und Kategorien, um zu bestimmen, was protokolliert wird.
Die Klasse Logger stellt mehrere überladene Write-Methoden bereit, alle mit unterschiedlichen Flexibilitätsgraden (einschließlich einer Überladung, die es Ihnen ermöglicht, eine LogEntry-Instanz bereitzustellen, die über zahlreiche Parameter und Steuerelemente zum Optimieren der Protokollierung der Informationen verfügt). In Anbetracht der einfachen Überladung in Abbildung 2 jedoch konnten wir den größten Teil des Konvertierungsprozesses mit einem Such- und Ersetzungsvorgang in Kombination mit regulären Ausdrücken ausführen.
Die Protokollierung ist ein sehr ressourcenintensiver Prozess. Sie müssen daher darauf achten, dass die Leistung Ihrer Anwendung nicht beeinträchtigt wird. Zu guter Letzt hat das Suchen und Ersetzen der Protokollaufrufe in der Anwendung nur wenige Stunden in Anspruch genommen.

Sammlungen
Sowohl .NET Framework als auch Java bieten eine leistungsstarke Sammlungs-API. Beide sind erweiterbar und lassen sich auf die meisten Szenarios anwenden, die bei der Arbeit mit Sammlungen auftreten könnten. Sehen wir uns zwei häufig verwendete Arten von Sammlungen etwas näher an: Listen und Wörterbücher.
Listen sind Sammlungen, auf die über einen Index zugegriffen werden kann. Es handelt sich um geordnete Sammlungen, die Sie sich als eine Art eindimensionales Array vorstellen können. Wörterbücher hingegen sind Sammlungen von Name/Wert-Paaren. Der Name ist der Schlüssel, der für den Zugriff auf Werte in der Sammlung verwendet wird. Beachten Sie, dass es in Wörterbüchern nicht unbedingt eine garantierte Reihenfolge gibt.
In Abbildung 3 werden die entsprechenden C#- und Java-Implementierungen für Listen und Wörterbücher aufgeführt. Der Konvertierungs-Assistent für die Programmiersprache Java erfüllt die Aufgabe des Konvertierens dieser Java-Sammlungsklassen in ihre .NET-Entsprechungen sehr gut, so dass nach der Konvertierung nicht mehr viel zu tun bleibt. Zu diesem Zeitpunkt werden Warnungen generiert, die darauf hinweisen, dass es zwischen diesen Sammlungen auf den beiden Plattformen Verhaltensunterschiede gibt und die Konvertierung daher überprüft werden sollte. Im Großen und Ganzen sollte der Konvertierungsvorgang jedoch erfolgreich abgeschlossen worden sein.

  .NET Java
Listenschnittstelle IList, IList<T> Liste
Allgemeine Listenklassen ArrayList, List<T> ArrayList, Vektor
Wörterbuchschnittstelle IDictionary, IDictionary<TKey,TValue> Zuordnung
Allgemeine Wörterbuchklassen Hashtable, Dictionary<TKey,TValue> HashMap, HashTable
Allerdings sind wir beim Konvertieren von Sammlungen auf ein Problem gestoßen. Dieses Problem trat auf, wenn wir in der ursprünglichen Java-Anwendung spezialisierte Sammlungen verwendet hatten. Ein Beispiel dafür ist die Verwendung der Java-Klasse LinkedHashSet. Laut Java API-Dokumentation stellt die Klasse LinkedHashSet die konsistente Reihenfolge der Einträge im HashSet (einer Art Wörterbuch) sicher, ohne den Aufwand, der bei anderen geordneten Wörterbüchern entsteht. Die Definition eines LinkedHashSet ist zwar einfach, der Verwendungszweck in der Anwendung hingegen war weniger deutlich. (Der Code wurde von einer Person geschrieben, die das Team inzwischen verlassen hat, und der Verwendungszweck wurde nicht dokumentiert.) Auch der Verwendungskontext half uns nicht weiter, und so war es unklar, ob diese Klasse zur Behebung eines Problems verwendet wurde oder einem anderen Zweck diente.
Bei der näheren Untersuchung des Codes fanden wir keine Begründung für seine Verwendung. Folglich blieben uns drei Möglichkeiten: Wir konnten unterstellen, dass er in der ursprünglichen Anwendung eigentlich überflüssig war, eine eigene Implementierung für unsere .NET-Anwendung schreiben oder in .NET Framework die Sammlung auswählen, die sich am besten eignete. Wir gingen davon aus, dass die spezialisierte Implementierung von LinkedHashSet nicht erforderlich war, da es keine Hinweise darauf gab, dass die geordneten Name/Wert-Paare an anderer Stelle verwendet wurden. Daher ersetzten wir die .NET-Basisklasse Hashtable und überprüften das korrekte Verhalten mit unseren vorhandenen Komponenten- und Integrationstests. Wenn jedoch Leistungs- oder Funktionsprobleme aufgetreten wären, hätten wir die Klasse SortedDictionary<TKey, TValue> in .NET Framework 2.0 ersetzen können, die eine Sammlung von Schlüssel/Wert-Paaren repräsentiert, die nach dem Schlüssel sortiert sind; intern wird von der Implementierung ein Satz verwendet, der auf einer Rot-Schwarz-Baumstruktur basiert.
In unserem Projekt gab es nur vier Verwendungsmöglichkeiten für spezialisierte Java-Sammlungsklassen, und alle stellten ähnliche Umstände dar. Die zusätzlich bereitgestellte Funktion wurde nicht genutzt, und die aktuelle Aufgabe konnte unter Verwendung der generischeren .NET-Entsprechungen ausgeführt werden.
Unsere Java-Anwendung wurde in Java Version 1.4 geschrieben. Generische Sammlungen, die eine starke Typisierung von Sammlungs-Members ermöglichen, wurden erst in Java Version 1.5 eingeführt. Daher mussten wir uns nicht mit dem Konvertieren von Generika befassen. Wahrscheinlich wird der Konvertierungsvorgang durch das Konvertieren von Auflistungen in Java Version 1.5 noch komplexer, da nicht nur die Sammlungen konvertiert werden müssen, sondern auch ihre typisierten Einträge.

Filter und HTTP-Handler
Ein Filter ist ein allgemeines Modell, das in J2EE-Webanwendungen zum selektiven Abfangen von Anforderungen und Antworten zur Vorabverarbeitung und nachträglichen Verarbeitung verwendet wird. Gebräuchliche Verwendungsbereiche für Filter sind Protokollierung, Nutzungsüberwachung und Sicherheit.
Java-Filter implementieren die Filterschnittstelle, die bestimmte Lebenszyklusereignisse definiert. Filter werden vom Anwendungsserver mithilfe einer URL-Zuordnung zum Zuordnen von Filterklassen zu vollständigen oder partiellen URLs aufgerufen. Im Falle einer Übereinstimmung implementiert der Anwendungsserver die Lebenszyklusereignisse des zugeordneten Filters und übergibt ein Handle an die Anforderung und die Antwort.
ASP.NET stellt eine ähnliche Funktion in Form einer Schnittstelle namens IHttpHandler bereit. Sowohl Filter als auch HTTP-Handler verfügen über sehr einfache Schnittstellen, die jedoch äußerst leistungsfähig sind. In unserer Anwendung kamen zwei verschiedene Arten von Filtern zum Einsatz: Ein Filter verwendete die GZIP-Komprimierung zur Komprimierung von Antworten und ein anderer fing Anforderungen ab, um zu ermitteln, ob sie von einem bekannten Benutzer-Agent stammten.
Der Komprimierungsfilter wurde in der Java-Anwendung implementiert, um die Leistung zu verbessern. Der Filter überprüfte alle Antwortstreams, um festzustellen, ob der Client die GZIP-Komprimierung unterstützt. Wenn dies der Fall war, wurde die Antwort komprimiert. Normalerweise ist dies nicht erforderlich, da die meisten modernen HTTP-Server diese Funktion ohne benutzerdefinierten Code bereitstellen. Unsere Anwendung wurde jedoch als Servlet-Anwendung eingebunden, wofür kein HTTP-Server erforderlich ist. Die Komprimierungsfunktion war daher ein wertschöpfendes Modul.
Der zweite Filter, der Anforderungen auf der Basis des Headerwerts eines Benutzer-Agents zurückweist, ist von größerem Interesse. Viele unserer Kunden hatten den Wunsch, eine Authentifizierungsebene für den Vergleich des HTTP-Headers des Benutzer-Agents mit einer Liste zulässiger Agents zu implementieren. Wenn es sich bei dem Benutzer-Agent der eingehenden Anforderung nicht um einen zulässigen Benutzer-Agent handelte, sollte die Anforderung zurückgewiesen werden (normalerweise durch Zurückgeben eines nicht autorisierten HTTP-Rückgabewerts).
Es gibt viele Möglichkeiten, diese Art von Anforderungs-/Antwortfilter einzurichten. Die meisten Lösungen erfordern allerdings das Einfügen von Code oder Attributen in großem Umfang. Glücklicherweise bieten sowohl J2EE als auch .NET sehr einfache Mechanismen für das Abfangen, Ändern und Anpassen von Anforderungen und Antworten auf Anwendungsebene. Außerdem durchdringen HTTP-Interceptors wie diese nicht den gesamten Code, das heißt, nicht in jeder Klasse sind Codezeilen vorhanden. Stattdessen handelt es sich um separate Klassen, die über den Anwendungsserver verwaltet und injiziert werden. Daher ist es wesentlich einfacher, die Funktionen zu ändern, anstatt einen globalen Such- und Ersetzungsvorgang durchzuführen.
Der Konvertierungs-Assistent für die Programmiersprache Java 3.0 stellt Hilfsklassen zur Unterstützung der Migration von Filtern von Java in ASP.NET-Anwendungen bereit. In Abbildung 4 wird einen Beispielfilter in Java dargestellt, der die Serververarbeitung misst. Außerdem wird deutlich, wie der Konvertierungs-Assistent für die Programmiersprache Java versucht, diesen Filter für ASP.NET zu konvertieren. Idealerweise wäre es für das Portieren in eine .NET-Implementierung vermutlich erforderlich, den Filter von Grund auf als HTTP-Handler umzuschreiben. Der Konvertierungs-Assistent für die Programmiersprache Java stellt jedoch die Unterstützungsklasse SupportClass.ServetFilter bereit, die Ihnen über weite Strecken bei der Neuimplementierung hilft. ServletFilter emuliert den Lebenszyklus, den Java bereitstellt. Dadurch werden zwar nicht alle Probleme gelöst, die Portimplementierung wird jedoch erleichtert.

Java
import javax.servlet.*;
import java.io.*;

public final class TimerFilter implements Filter 
{

    public void doFilter(ServletRequest request, 
                         ServletResponse response,
                         FilterChain chain)
        throws IOException, ServletException 
    {

        long startTime = System.currentTimeMillis();
        chain.doFilter(request, response);
        long stopTime = System.currentTimeMillis();
        System.out.println(“Time to execute request: “ + 
            (stopTime - startTime) + “ milliseconds”);
    }

    public void destroy() {}

    public void init(FilterConfig fc) {}
}
C#
using System;
using System.Web;

// UPGRADE_TODO: Verify list of registered servlet filters. 
public sealed class TimerFilter : SupportClass.ServletFilter
{
    public override void DoFilter(HttpRequest request, 
        HttpResponse response, SupportClass.ServletFilterChain chain)
    {
            
        long startTime = 
            (System.DateTime.Now.Ticks - 621355968000000000) / 10000;
        chain.doFilter(request, response);
        long stopTime = 
            (System.DateTime.Now.Ticks - 621355968000000000) / 10000;
        Console.Out.WriteLine(“Time to execute request: “ + 
            (stopTime - startTime) + “ milliseconds”);
      }

      public void  destroy() {}

      // UPGRADE_ISSUE: Interface ‘javax.servlet.FilterConfig’ 
      // was not converted.
      public void  init() {}
}
Unser Team entschied sich allerdings für die manuelle Konvertierung der Java-Filterfunktion in einen HTTP-Handler. Um die erforderlichen Schritte zu erläutern, müssen die beiden Schnittstellen miteinander verglichen werden. Einem J2EE-Filter stehen drei Methoden zur Verfügung: init, destroy und doFilter. Die Methoden init und destroy sind Lebenszyklusmethoden von einiger Bedeutung, sollen jedoch an dieser Stelle nicht erörtert werden. Die Methode doFilter übernimmt den Großteil der Aufgaben in einem Filter.
In unserem Szenario ruft die Methode doFilter die Headervariable des Benutzer-Agents aus der eingehenden Anforderung ab und überprüft sie anhand einer Liste konfigurierbarer, bekannter Benutzer-Agents. Das Schwierigste bei der Konvertierung ist der Unterschied zwischen Objekten, die als Argumente an die relevante Methode übergeben werden.
In der Java-Methode Filter.doFilter werden drei Argumente übergeben: Ein Anforderungsobjekt, ein Antwortobjekt und ein Filterkettenobjekt. Die Methode ProcessRequest der Klasse IHttpHandler verfügt dagegen über ein einziges Argument: eine HttpContext-Variable. Die Variable HttpContext verweist auf ein HttpRequest- und ein HttpResponse-Objekt. Der Großteil dieser Funktionen kann über den Zugriff auf die Members von HttpContext bereitgestellt werden.
Das Java-Objekt FilterChain ist interessant. Es repräsentiert eine Kette von Filtern in einer HTTP-Anforderung. Ein Filter kann die Anforderung an den nächsten Filter in der Kette weitergeben. So ist es möglich, die Verantwortungen sequenziell zu delegieren. Das Objekt FilterChain kommt nicht oft zum Einsatz. Wenn es jedoch verwendet wird, kann ein ähnliches Verhalten mit IHttpModule-Implementierungen erreicht werden.
Sieht man vom Objekt FilterChain ab, ist die Konvertierung von Java-Filtern in .NET IHttpHandlers eine unkomplizierte Aufgabe Zuerst muss eine Implementierung von IHttp­Handler erstellt werden. Im Anschluss ist es erforderlich, die Logik innerhalb der Methode doFilter des Filters in die Methode ProcessRequest der Implementierung zu rekonstruieren. Hierbei werden die Members von HttpContext anstelle der übergebenen Anforderungs- und Antwortobjekte verwendet. Schließlich muss die Methode IsReusable in der Implementierung definiert werden. Der Einfachheit halber kann sie einfach den Wert false zurückgeben (ein weiterer Fall, in dem Sie später mehr Code schreiben können, um zu bestimmen, ob dieselbe Handlerinstanz von späteren Anforderungen verwendet werden kann, um die Leistung zu steigern).

Quellstruktur und Benennungskonventionen
Die Sprachen Java und C# weisen zwar Ähnlichkeiten auf, es gibt jedoch Unterschiede im Hinblick auf Lexikografie, Quellstrukturlayout und Benennungskonventionen. Auf einige dieser Unterschiede soll hier genauer eingegangen werden.
Java-Pakete (das Äquivalent von Namespaces in C#) folgen dieser Konvention: umgekehrter Domänenname, gefolgt vom Anwendungs- oder Modulnamen, gefolgt von der Funktion. Diese Pakete sind für gewöhnlich tief verschachtelt (normalerweise vier oder fünf Ebenen tief). C#-Namespaces werden dagegen in der Regel nach funktionell beschreibenden Namen gruppiert und sind normalerweise flach verschachtelt (für gewöhnlich ein bis vier Ebenen). Außerdem wird in Java-Paketen in der Regel die Kleinschreibung oder Höckerschreibweise verwendet, während in C#-Namespaces normalerweise die Pascal-Schreibweise zur Anwendung kommt. Auch für Java-Methoden gilt für gewöhnlich die Höckerschreibweise, während in C#-Methoden und -Eigenschaften normalerweise die Pascal-Schreibweise verwendet wird.
C#-Schnittstellen beginnen in der Regel mit einem großen I, das auf eine Schnittstelle hinweist (dies ist lediglich eine Konvention und keinesfalls für die korrekte Funktion erforderlich). In der älteren Java-Konvention wurde festgelegt, dass Schnittstellennamen auf „able“ enden sollten, was auf die Möglichkeit zum Ausführen einer Aktion hinwies. Diese Konvention wird heute kaum noch beachtet. Daher gibt es für gewöhnlich keinen Unterschied mehr zwischen dem Namen einer Schnittstelle und dem Namen einer Klasse.
In C# werden Eigenschaften zur Implementierung des Zugriffs auf private Member und die Mutation von privaten Members verwendet. Eigenschaften sind Metadatenwrapper für Get- und Set-Accessormethoden. Java verfügt jedoch nicht über Eigenschaften. Accessoren und Mutatoren für einen privaten Member werden normalerweise als Methoden namens Getter oder Setter implementiert. Mit anderen Worten: Ein Accessor für ein privates Feld namens „name“ wäre getName.
Schließlich müssen sich Java-Klassen in einem Verzeichnis befinden, das dem deklarierten Paket entspricht, in Relation zum Stamm der Quelldateien (oder classpath). In C# gibt es keine Beschränkung dieser Art.

Der richtige Zeitpunkt für die Umgestaltung gemäß den Konventionen
Obwohl eine konvertierte C#-Anwendung sich kompilieren und ausführen lässt, ohne dass diese Unterschiede beseitigt werden, ist es immer empfehlenswert, sich an die Konventionen zu halten. Die Wahl des richtigen Zeitpunkts für die Umgestaltung des konvertierten Codes gemäß den Konventionen ist keine leichte Aufgabe. Zwei Faktoren sprechen jedoch dafür, dass dies lieber früher statt später geschehen sollte.
Da das Umgestalten des Codes gemäß den Konventionen für das Funktionieren der Anwendung nicht unbedingt erforderlich ist (ganz davon abgesehen, dass es eine langwierige Aufgabe sein kann), kann es zu einem späteren Zeitpunkt im Projekt als unnötig erachtet werden. Eine Vielzahl von Faktoren kann dazu beitragen, dass es als Aufgabe mit niedriger Priorität angesehen wird. Daher besteht das Risiko, dass es vollkommen unter den Tisch fällt. Wenn jedoch ein Team von Entwicklern an dem Projekt arbeitet, kann die Umgestaltung des Codes dazu beitragen, die Effektivität und Produktivität des Teams zu erhöhen. Sie sollten die Bedeutung dieser Aufgaben nicht unterschätzen. Eine Umgestaltung dieser Art ist genauso wichtig wie die anderen Aufgaben im Konvertierungsprozess, um den Erfolg zu sichern.

Verzeichnislayout und Namespaces
Benennungs- und Verzeichniskonventionen sind subjektiv, doch dafür gelten allgemeine Richtlinien. Für unser Projekt nehmen wir an, dass es eine Klasse für die benutzerdefinierte XML-Analyse gibt, die sich im Namespace com.mycompany.myapplication.xml.util befindet (und nach der Konvertierung in dem Verzeichnis). C#-Konventionen sehen vor, dass diese Klasse sich im Namespace Xml.Util befinden sollte. Vor der Umgestaltung würde die Verzeichnisstruktur in etwa so aussehen wie in Abbildung 5 dargestellt. Durch Ziehen und Ablegen von Dateien in Visual Studio können Sie die Datei physisch verschieben, so dass die Verzeichnisstruktur nun aussieht wie in Abbildung 6.
Abbildung 5 Java-Quellstruktur 
Abbildung 6 C#-Quellstruktur 
Von C# wird jedoch nicht vorgeschrieben, dass der Verzeichnispfad einer Datei mit dem deklarierten Namespace übereinstimmen muss. Daher wird der Namespace der Klasse nicht zwecks Übereinstimmung mit dem Dateisystemspeicherort aktualisiert. Es gibt keine automatische Möglichkeit, in Visual Studio mehrere Klassen in verschiedene Namespaces zu verschieben. Die beste Methode besteht wohl darin, einen Such- und Ersetzungsvorgang für die gesamte Lösung durchzuführen, wie in Abbildung 7 gezeigt. Dies setzt selbstverständlich voraus, dass Sie alle Klassen in einen Namespace an demselben Zielort verschieben.
Abbildung 7 Suchen und Ersetzen von Namespacedeklarationen (Klicken Sie zum Vergrößern auf das Bild)

„Eigenschaften“
Die Konstruktion der Eigenschaften in C# unterscheidet sich erheblich von der in Java. Eigenschaften können Sie sich als öffentliche Felder vorstellen, in denen der Zustand einer Klasse oder (in UML-Begriffen) Attribute der Klasse gespeichert sind. In Java existiert jedoch kein Eigenschaftenkonstrukt, Eigenschaften werden stattdessen in Methoden namens Getter und Setter repräsentiert.
Stellen Sie sich eine Klasse mit einer Instanzvariablen namens „name“ vor. Diese Variable dürfen Sie nicht veröffentlichen, da Sie in diesem Fall die Kontrolle über die Änderung der Variable verlieren würden. In Java erfolgt der Zugriff auf diese Variable standardmäßig über Getter und Setter, die deshalb so genannt werden, weil der Konvention nach zur Bildung des Methodennamens dem Variablennamen ein „get“ oder „set“ vorangestellt wird. Wenn die Variable name eine Zeichenfolge ist, könnten die Java-Getter und -Setter in etwa so aussehen:
public String getName() {
    return this.name;
}

protected void setName(String name) {
    this.name = name;
}
Mit den in C# bereitgestellten Eigenschaftenkonstrukten wird dasselbe erreicht. Obgleich sie für die Anwendungslogik verwendet werden können, besteht ihr Zweck eigentlich darin, geschützten Zugriff auf private Implementierungsinformationen zu ermöglichen, wie oben beschrieben. Daher würde eine C#-Implementierung desselben Zugriffs wie folgt aussehen:
public String Name
{
    get {return this.name;}
    protected set {this.name = value;}
}
C#-Eigenschaften, mit denen dasselbe Ziel erreicht wird, sind außerdem eindeutiger: In Java können Methoden, die den Zustand einer Klasse nicht ändern, mit Get oder Set beginnen, was zu Unklarheiten führt. Daher wird empfohlen, Java-Getter und -Setter in C#-Eigenschaften umzugestalten.
Allerdings gibt es keine einfache Möglichkeit, Java-Getter und -Setter in C#-Eigenschaften zu konvertieren. Das Suchen und Ersetzen regulärer Ausdrücke wäre eine ausgesprochen komplexe Aufgabe, und der Konvertierungs-Assistent für die Programmiersprache Java migriert die Java-Methoden Getter und Setter einfach unverändert, da nicht festgestellt werden kann, ob die Methode den Zustand einer Klasse ändert oder eine andere Funktion durchführt. Wir haben zur Lösung dieses Problems eine praktischere Methode gewählt. Visual Studio stellt einen Assistenten für die Einkapselung privater Members mit Eigenschaften bereit. Damit können die gewünschten Eigenschaften generiert werden, ohne die konvertierten Java-Getter und -Setter zu löschen.
Da unser Team noch aus anderen Gründen Code bearbeitet hat, generierten die Teammitglieder die C#-Eigenschaften in Visual Studio und löschten die entsprechenden Methoden Getter und Setter. Im Anschluss daran verwendeten die Entwickler die Funktion „Verweise suchen“ in Visual Studio 2005, mit der eine Liste aller Aufrufsites für eine bestimmte Methode abgerufen werden kann. So erhielten sie die Verweise zu den alten Methoden Getter und Setter und waren ohne weiteres in der Lage, die Verweise in die neuen Eigenschaften zu ändern. Diese Lösung war zwar nicht elegant, funktionierte jedoch erstaunlich gut.
Es muss betont werden, dass dies als ausgesprochen wichtiger Schritt im Portierungsprozess betrachtet wurde, da Eigenschaften ein tief verankerter Kernaspekt von C# und .NET sind und unser Ziel darin bestand, eine C#-Anwendung zu erstellen. (Für die Unterstützung dieser Anwendung würde schließlich ein anderes Team verantwortlich sein, das eine C#-Anwendung erwartete).

Methodennamen in Pascal-Schreibweise
Wie bereits erwähnt, gilt für Namen von Java-Methoden für gewöhnlich die Höckerschreibweise. Mit anderen Worten: Die Namen beginnen mit einem Kleinbuchstaben, und an jeder folgenden Wortgrenze wird ein Großbuchstabe verwendet. Die C#-Konventionen schreiben vor, dass für Methoden und andere Members die Pascal-Schreibweise verwendet wird. Die Pascal-Schreibweise ähnelt der Höckerschreibweise. Der Unterschied besteht darin, dass der erste Buchstabe im Namen ebenfalls ein Großbuchstabe ist.
Was bedeutet dies nun für Sie? Sie sollten wahrscheinlich all Ihre Methoden so umbenennen, dass sie mit einem Großbuchstaben beginnen und nicht mit einem Kleinbuchstaben. So sollte z. B. die Java-Methode
public void getResponseCode()
als C#-Methode umgestaltet werden:
public String GetResponseCode()
Wie beim Konvertieren von Getter und Setter in Eigenschaften gilt auch hier, dass es keine einfache Möglichkeit gibt, den gesamten konvertierten Code durchzugehen und Members so zu aktualisieren, dass sie der C#-Benennungskonvention entsprechen. Wir haben dies jedoch als sehr wichtige Aufgabe betrachtet, da die konvertierten Members keine Ähnlichkeit mit C#-Members hatten und sowohl unser Team als auch unser Kunde eine korrekte C#-Anwendung wünschten.
Theoretisch wäre es möglich, einen Codekonverter für die Ausführung dieser Aufgabe zu schreiben, und wir haben erwogen, dies selbst in Angriff zu nehmen. Dann entschieden wir uns jedoch dafür, denselben Ansatz anzuwenden wie bei den Eigenschaften und die Member-Namen schrittweise zu aktualisieren. Für die Anwendung der manuellen Methode gab es mehrere Gründe.
Zunächst einmal waren unsere Entwickler bereits dabei, den gesamten Code zu analysieren, um andere Elemente wie Getter und Setter zu aktualisieren. Dieser Prozess konnte stufenweise durchgeführt werden, da die Änderung der Member-Namen keine Auswirkungen auf die Fähigkeit zur Kompilierung oder die Funktionsweise hatte. Und wenn ein oder zwei Members aus einem beliebigen Grund ausgelassen würden, hätte dies keine negativen Folgen für die Anwendung. Der Vorgang nahm weniger Zeit in Anspruch, als man vermuten könnte. Das Hauptproblem bestand darin, dass es sich um eine ermüdende Aufgabe handelte.
Visual Studio 2005 bietet integrierte Umgestaltungsunterstützung. Ein Teil der Umgestaltungsunterstützung besteht in der sicheren Umbenennung von Members. Es ist möglich, einen Member umzubenennen und alle Verweise auf diesen Member zu aktualisieren. Die Aktualisierung aller gewünschten Member nimmt ein wenig Zeit in Anspruch, ist jedoch eine effektive Lösung.

Schlussbemerkung
In den vorangegangenen Abschnitten wurde ein reales Beispiel der Konvertierung einer Java-Webanwendung in ASP.NET beschrieben. Ein Großteil der Arbeit wurde vom Konvertierungs-Assistenten für die Programmiersprache Java übernommen, es gab jedoch mehrere Elemente, die ein manuelles Eingreifen erforderten. Der Konvertierungs-Assistent für die Programmiersprache Java ist dennoch ein leistungsfähiges Tool, das die schnelle Portierung unserer Anwendung in eine .NET-Anwendung von Produktionsqualität ermöglichte.
Anliegen dieser Ausführungen war es, die Durchführbarkeit der Konvertierung einer Java-Anwendung in .NET zu demonstrieren und einige der Probleme aufzuzeigen, die vom Konvertierungs-Assistenten für die Programmiersprache Java nicht gelöst werden können und daher anderweitig bearbeitet werden müssen. Jede Portierung einer Anwendung ist mit einzigartigen Problemen verbunden, von denen in diesem Artikel nur einige besprochen werden konnten. Es wurden jedoch einige Verfahren und Methoden vorgestellt, die Ihnen bei der Lösung eventuell auftretender Probleme sicherlich behilflich sein können.

Brian Jimerson ist als dienstältester technischer Architekt für Avantia, Inc. (www.avantia-inc.com) tätig, einen Anbieter benutzerdefinierter Lösungen mit Sitz in Cleveland, Ohio. Vor kurzem war Brian Jimerson damit beschäftigt, Kunden bei der Definition und Implementierung von Infrastruktur- und Architekturlösungen sowohl für Java als auch für .NET Framework zu unterstützen.

Page view tracker