Praktikum aus Softwareentwicklung 2 Version: 2011-09-15_11:53 Markus Loeberbauer 2010, 2011 Praktikum aus Softwareentwicklung 2 Inhalt Inhalt Inhalt ............................................................................................................................................................ 1 Java ............................................................................................................................................................... 2 Subversion ................................................................................................................................................. 8 Graphische Oberflächen .................................................................................................................... 11 Dateien ...................................................................................................................................................... 26 Reflection ................................................................................................................................................. 27 Threads...................................................................................................................................................... 31 Streams ..................................................................................................................................................... 41 Serialisierung .......................................................................................................................................... 46 Netzwerkprogrammierung................................................................................................................ 48 Datenbanken .......................................................................................................................................... 49 Remoting .................................................................................................................................................. 59 XML............................................................................................................................................................. 67 Java Servlet.............................................................................................................................................. 80 Dynamischer Kontext in JavaServer Pages ................................................................................ 89 JSP Custom Tags................................................................................................................................ 104 Applets ................................................................................................................................................... 112 Java Service .......................................................................................................................................... 115 Sicherheit............................................................................................................................................... 118 Java Native Interface (JNI) ............................................................................................................. 124 Eclipse-Tastenkürzel ......................................................................................................................... 130 Markus Loeberbauer 2010, 2011 1 Praktikum aus Softwareentwicklung 2 Java Java Sun hat 1995 die Programmiersprache Java vorgestellt. Java ist eine statisch stark typisierte, objektorientierte Sprache mit dem Ziel betriebssystemunabhängige Programme zu ermöglichen. Java ist von der Syntax her an C++ angelehnt, aber von Umfang her reduziert und dadurch vereinfacht. Wesentliche Eigenschaften von Java sind: die automatische Speicherbereinigung, Verzicht auf Zeigerarithmetik und die mächtige Klassenbibliothek. Mit Java 5.0 (auch Java 1.5) wurde die Sprache um einige interessante Eigenschaften erweitert, zB: Autoboxing, Enumerationen, eine erweiterte forSchleife und variable Argumentlisten. Autoboxing In Java gilt grundsätzlich "everything is an object", außer es handelt sich um einen primitiven Datentyp. Primitive Datentypen müssen daher vor der Zuweisung an eine Variable vom Typ Object in Wrapper-Objekte gekapselt werden (Boxing). Das ist nötig wenn man primitive Daten in eine Sammlung wie zB ArrayList einfügen will. Seit Version 5.0 macht der Java-Compiler dieses Boxing automatisch (Autoboxing). Abbildung 1 zeigt wie man primitive Datentypen boxen und unboxen kann. Achtung: Autoboxing benutzt die erste gezeigte Möglichkeit (valueOf). Dabei werden Objekte wiederverwendet, d.h., geboxte Objekte können referenzgleich sein, per default sind das die Objekte im Bereich zwischen -128 und 127. Der Bereich kann sich aber ändern, die Objektwiederverwendung ist nur eine Optimierung, auf die Referenzgleichheit darf man sich also auf keinen Fall verlassen. Markus Loeberbauer 2010, 2011 2 Praktikum aus Softwareentwicklung 2 Java int i = 42; int x; Object obj; // Manual (Un)Boxing Possibility 1 obj = Integer.valueOf(i); x = ((Integer) obj).intValue(); // Manual (Un)Boxing Possibility 2 obj = new Integer(i); x = ((Integer) obj).intValue(); // Auto(un)boxing, equivalent with Possiblity 1 obj = i; x = (Integer) obj; Abbildung 1) Vergleich manuelles Boxing und Autoboxing Enumerationen Enumerationen sind Typen mit festgelegten Werten, zB eine Enumeration für Sortierreihenfolgen könnte SortOrder heißen und die Werte ASCENDING und DESCENDING haben. Vor Version 5.0 hat man in Java dafür gerne int-Konstanten verwendet. Das hat aber den Nachteil, dass Enumerationswerte nicht typsicher sind. Wollte man typsichere Enumeration hat man mit final-Klassen und Konstanten dieser Klassen gearbeitet. Seit Java 5.0 gibt es expliziten Support für typsichere Konstanten. Abbildung 2 zeigt die unterschiedlichen Möglichkeiten wie man Enumerationen anlegen kann. Markus Loeberbauer 2010, 2011 3 Praktikum aus Softwareentwicklung 2 Java // Before Java 5.0 NOT type safe public class SortOrder { public static final int ASCENDING = 1; public static final int DESCENDING = 2; } // Before Java 5.0 type safe public final class SortOrder { public static final SortOrder ASCENDING = new SortOrder(); public static final SortOrder DESCENDING = new SortOrder(); private SortOrder() { /* avoid public object creation */ } } // In Java 5.0 (type safe) public enum SortOrder { ASCENDING, DESCENDING } Abbildung 2) Enumerationen in Java Java Enumeration sind Klassen und ihre Werte sind Objekte dieser Klassen. Damit kann der Programmierer auch Methoden und Konstruktoren zu Enumerationen hinzufügen, siehe Abbildung 3. Damit bei Enumerationen nur die vorgesehen Werte existieren können und jeder dieser Werte nur einmal existiert sind die Konstruktoren immer privat und die Enumerations-Klasse ist final (d.h., von einer Enumerations-Klasse kann man nicht erben). Markus Loeberbauer 2010, 2011 4 Praktikum aus Softwareentwicklung 2 Java public enum SortOrder { ASCENDING(1), DESCENDING(2); private final int legacyValue; SortOrder(int legacyValue) { this.legacyValue = legacyValue; } public int getLegacyValue() { return legacyValue; } } Abbildung 3) Funktionen in Java-Enumerationen Erweiterte for-Schleife Mit der erweiterten for-Schleife (oder for-each Schleife) kann man über alle Werte eines Arrays (siehe Abbildung 4) oder einer Sammlung (siehe Abbildung 5) laufen. int[] numbers = { 5, 7, 23, 42 }; // classic for loop for (int index = 0; index < numbers.length; ++index) { int number = numbers[index]; System.out.println(number); } // extended for loop for (int number : numbers) { System.out.println(number); } Abbildung 4) Erweiterte for-Schleife, angewandt auf ein Array Markus Loeberbauer 2010, 2011 5 Praktikum aus Softwareentwicklung 2 Java List names = new ArrayList(); names.add("Susi"); names.add("Karl"); // classic collection iteration Iterator iter = names.iterator(); while (iter.hasNext()) { String name = (String) iter.next(); System.out.println(name); } // extended for loop for (int name : names) { System.out.println(name); } Abbildung 5) Erweiterte for-Schleife, angewandt auf eine Sammlung Variable Argumentlisten Es gibt Methoden, die funktionieren mit einem oder mit mehreren Parametern. Vor Java 5.0 musste man dazu mit einem Array oder einer Klasse arbeiten. Obwohl das innerhalb der Methode bequem ist, ist es für den Rufer der Methode aufwändig. Aus diesem Grund kann man seit Java 5.0 eine Methode mit einer variablen Anzahl von Parametern (vararg-Parameter) definieren. Die Parameter sind innerhalb der Methode als Array verfügbar. Der Rufer kann die Methode aufrufen als hätte sie die gewünschte Parameteranzahl. Hat die Methode neben dem vararg-Parameter auch andere Parameter, dann müssen diese davor definiert sein (Anmerkung: Es darf nur einen vararg-Parameter geben). Abbildung 6 zeigt die Verwendung einer variablen Argumentliste. Markus Loeberbauer 2010, 2011 6 Praktikum aus Softwareentwicklung 2 Java // classic method with variable parameters public int sum(int[] values) { int res = 0; for (int index = 0; index < values.length; ++index) { res += values[index]; } return res; } // classic method call int x = sum(new int[] {1, 2, 3, 4, 5}); // method with variable argument list public int sum(int... values) { // same method body as above } // method call to method with variable argument list int x = sum(1, 2, 3, 4, 5); Abbildung 6) Methoden mit variabler Argumentliste Markus Loeberbauer 2010, 2011 7 Praktikum aus Softwareentwicklung 2 Subversion Subversion Zur Übungsabgabe benutzen wir die Versionsverwaltung Subversion. Die Zugangsdaten haben Sie auf Ihre Kusss-Email-Adresse bekommen. Ihr SubversionRepository liegt auf dem Server ssw.jku.at, der Pfad setzt sich aus dem Semester, dem Lehrveranstaltungs-Kürzel und der Matrikel-Nummer zusammen. Für die Lehrveranstaltung Praktikum aus Softwareentwicklung 2 ist das Kürzel PSW2. Das Semester besteht aus der Jahreszahl und einem „S“ für Sommer- oder einem „W“ für Winter-Semester. Der Matrikel-Nummer muss ein „k“ vorangestellt werden. Beispiel: Für das Praktikum aus Softwareentwicklung 2, im Sommersemester 2011 und dem Studenten mit der Matrikel-Nummer 1234567 lautet die Subversion-Url: svn://www.ssw.uni-linz.ac.at/2011S/PSW2/k1234567/ Voraussetzungen Die Voraussetzung für die Übungsabgabe mit Subversion ist ein Subversion-Client. Die folgenden Clients haben wir schon am Institut verwendet: a. Subversion-Client für die Kommandozeile: http://subversion.apache.org/ b. Subversion-Eclipse-Plugin: http://subclipse.tigris.org/ c. NetBeans unterstützt Subversion d. IntelliJ IDEA unterstützt Subversion e. Subversion-Windows-Explorer-Plugin: http://tortoisesvn.tigris.org/ f. Subversion-Applikationen für Mac OS X: http://versionsapp.com/ http://www.zennaware.com/ Was ist Subversion? Ein Subversion-Repository kann man sich als Dateisystem vorstellen. Ein Dateisystem mit Vergangenheit, Änderungen in Dateien können jederzeit nachvollzogen und alte Versionen von Dateien wiederhergestellt werden. Markus Loeberbauer 2010, 2011 8 Praktikum aus Softwareentwicklung 2 Subversion Subversion ist ein zentrales Versionsverwaltungssystem, d.h. die versionierten Daten liegen auf einem Server. Wenn man mit den Daten arbeiten will holt man sich eine Arbeitskopie auf den Rechner (checkout). Ist man fertig schreibt man die Änderungen auf den Server zurück (commit). Als alternative zu zentralen Versionsverwaltungen gibt es dezentrale Systeme wie zB Mercurial (hg) oder git. Bei dezentralen Systemen kopiert man das ganze Repository auf seinen Rechner um damit zu arbeiten. Vorteile von dezentralen Versionsverwaltungen: Unabhängigkeit von der Netzverbindung, jede Kopie ist ein Backup, häufig besserer Branch-/MergeSupport. Eignet sich gut für die Verteilte Softwareentwicklung, wie zB bei OpenSource-Projekten. Vorteile von zentralen Versionsverwaltungen: Einfacher in der Handhabung. Eignet sich gut für Softwareentwicklung mit wenigen Entwicklern; und Übungsabgaben☺ . Wichtige Befehle von Subversion help Befehlsübersicht und Hilfe zu Befehlen, zB: svn help checkout import Importieren eines Pfads in ein Repository, zB: svn import svn://server.tld/<repoPfad> oder svn import <lokalerPfad> svn://server.tld/<repoPfad> add Datei oder Verzeichnis unter Versionskontrolle stellen, zB: svn add <lokalerPfad> move, Verschieben oder umbenennen von Dateien und Verzeichnissen, zB: mv svn mv Main.java Test.java checkout, Auschecken einer Arbeitskopie, zB: svn co svn://server.tld/<repoPfad> co oder svn co svn://server.tld/<repoPfad> <lokalerPfad> commit, Einchecken der lokalen Änderungen in das Repository, zB: svn ci Markus Loeberbauer 2010, 2011 9 Praktikum aus Softwareentwicklung 2 Subversion ci update, Aktualisieren der Arbeitskopie, zB: svn up up status, st Anzeigen der Änderungen in der Arbeitskopie, zB: svn st delete, Löschen einer Datei aus der Versionskontrolle, Gegenstück zu add, zB: del, rm svn del Main.java diff Anzeigen der Änderungen in Dateien list, ls Auflisten der Dateien unter Versionskontrolle, zB: svn ls svn://server.tld/<repoPfad> log Änderungsübersicht revert Lokale Änderungen rückgängig machen, zB: svn revert Main.java Markus Loeberbauer 2010, 2011 10 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen Graphische Oberflächen Graphische Oberflächen (GUI) sind teil jeder Desktopanwendung. In Java kann man eine GUI mit AWT oder Swing erstellen. AWT ist die Grundlage der graphischen Oberflächen in Java, es bildet die Verbindung zwischen dem Betriebssystem und der Java VM. Wir werden uns in diesem Kapitel die Komponente zur Baumdarstellung in der Java Klassenbibliothek ansehen. Aufbau von Oberflächen in Java In Java werden die GUI-Komponenten hierarchisch angeordnet, außen ein Fenster, im Fenster ein Menü und eine Komponente. Diese Komponente kann ein Container sein und damit weite Komponenten enthalten. Ein frei wählbarer Layout-Manager (Component.setLayout(LayoutManager)) legt fest wie die Komponenten in einem Container angeordnet werden. Komponenten werden immer relativ zu ihrem Container angeordnet. Die X-Koordinate ist die Entfernung vom linken Rand des Containers zum linken Rand der Komponente. Die Y-Koordinate ist die Entfernung vom oberen Rand des Containers zum oberen Rand der Komponente. Abstract Window Toolkit Die Klassen des Abstract Window Toolkit (AWT) liegen im Paket java.awt. Das AWT ist die Grundlage der Graphischen Oberflächen in Java, es abstrahiert die GUI-Komponenten des Betriebssystems. Für jede unterstützte Betriebssystemkomponente in AWT gibt es eine Peer-Klasse, die die Verbindung zu Java herstellt. Java-Programme mit AWT Oberflächen sehen wie nativ entwickelte Programme aus. Die Nachteile von AWT sind: es werden Betriebssystem-Ressourcen verwendet (zB: Window Handles), und es werden nur Komponenten unterstützt die auf allen Plattformen verfügbar sind. Denn eine Java Anwendung muss unverändert auf allen Plattformen laufen für die eine Java- Markus Loeberbauer 2010, 2011 11 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen Laufzeit-Umgebung existiert. Die Basisklasse aller AWT-Komponenten ist java.awt.Component. Swing Die Klassen von Swing liegen im Paket javax.swing. Klassennamen von Graphischen Komponenten in Swing beginnen mit einem „J“, wie zum Beispiel: JTree, JList, JButton. Swing Komponenten zeichnen in Java, nur Basiskomponenten wie Fenster nutzen AWT und damit Betriebssystem-Ressourcen. Komponenten wie Buttons oder Listen werden ausschließlich in Java gezeichnet, daher bezeichnen wir Swing als leichtgewichtig. Die Vorteile dieses Vorgehens sind: es können beliebige Komponenten gestaltet werden, das Aussehen der Komponenten kann beliebig gestaltet werden (Pluggable Look-and-Feel). Der Nachteil, Java Programme sind optisch von nativen Programmen unterscheidbar, auch wenn versucht wird das native Aussehen so gut wie möglich nachzubilden. Swing-AWT-Integration Die Basisklasse aller Swing-Komponenten ist javax.swing.JComponent. Diese leitet von java.awt.Container ab, damit können Swing-Komponenten in AWTOberflächen eingebettet werden. Das soll aber nur bei vorhandenen AWTAnwendungen genutzt werden, bei Neuentwicklungen sollte man die Oberfläche ganz in Swing gestalten. Wichtige Swing-Komponenten Die folgende Aufzählung gibt eine thematische Übersicht über die graphischen Komponenten in Swing: Buttons: o JButton o JRadioButton o JCheckBox Drop-Down Markus Loeberbauer 2010, 2011 12 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen o JComboBox Fenster o JFrame: mit Rahmen o JWindow: ohne Rahmen o JDialog: Dialogfenster, modal und nicht modal o JFileChooser: Standarddialog, Datei öffnen, Datei speichern Layout o JPanel: Container für weitere UI-Komponenten o JScrollPane, JScrollBar o Siehe Interface: LayoutManager Listen o JComboBox: Dropdown-Liste o JList: Liste o JSpinner: selektieren eines Elements, „flache Dropdown-Liste“ o JTable: Tabelle o JTree: Baum Menü: o JMenuBar, JMenu, JMenuItem o JToolBar o JPopupMenu: Kontextmenü o JSeparator: Trennstrich zwischen Menüeinträgen Statusanzeige o JProgressBar Text o JLabel: kurze Beschreibungstexte o JTextField: einzeilig, Text-Eingabe/-Ausgabe o JTextArea: mehrzeilig, Text-Eingabe/-Ausgabe Markus Loeberbauer 2010, 2011 13 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen Fenster Programmatisch Schließen Unter dem Begriff Fenster schließen kann man verschiede Sachen verstehen: das Fenster soll unsichtbar werden, das Fenster soll zerstört werden oder das Fenster soll so reagieren als hätte der Benutzer das Fenster geschlossen. Um das Fenster zu verstecken muss man auf dem JFrame die Methode void setVisible(boolean) mit dem Parameter false aufrufen. Ein solches unsichtbares Fenster kann man mit setVisible(true) wieder anzeigen. Um ein Fenster zu zerstören muss man die Methode void dispose() aufrufen, diese gibt alle Betriebssystemressourcen des Fensters frei. Ein solches Fenster kann nicht mehr angezeigt werden. Will man aber ein Fenster programmatisch schließen und das Verhalten haben als hätte ein Benutzer das Fenster geschlossen muss man ein WindowEvent auslösen: final EventQueue eventQueue = Toolkit.getDefaultToolkit() .getSystemEventQueue(); final WindowEvent event = new WindowEvent(frame, WindowEvent.WINDOW_CLOSING); eventQueue.postEvent(event); Bei diesem Verfahren werden die installierten WindowListener benachrichtigt und die DefaultCloseOperation (siehe JFrame: void setDefaultCloseOperation(int)) ausgeführt. Listendarstellung in Java (JList<E>) Mit JList kann man Listen von gleichartigen Elementen anzeigen. Die Elemente können als Array, Vector oder ListModel angegeben werden. Will man die Daten zur Laufzeit ändern sollte man ein ListModel verwenden. JList zeigt standardmäßig die Ausgabe von toString der Elemente an. Will man die Ausgabe beeinflussen muss man einen ListCellRenderer implementieren. Einfache Cellrenderer kann man von DefaultListCellRenderer ableiten. Wenn man viele Einträge in einer JList anzeigt, dann sollte man alle Zellen gleich groß rendern, um die Darstellung zu beschleunigen. Die definierte Größe kann man über setFixedCellWidth und setFixedCellHeight setzen. Markus Loeberbauer 2010, 2011 14 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen Baumdarstellung in Java (JTree) In vielen Programmen werden Modelle als Baum dargestellt. Die Klassenbibliothek enthält dafür die Klasse JTree. JTree ist eine graphische Komponente nach dem Model-View-Controller-Muster und benötigt ein Modell. Solche Modelle müssen die Schnittstelle TreeModel implementieren. Da Business-Modelle unabhängig von der GUI-Technologie sein sollen, wird hier oft das Adapter-Muster eingesetzt, um zwischen Buisiness-Model und TreeModel zu adaptieren. JTree stellt für jeden Knoten ein Icon und die Ausgabe von toString dar. Das Icon zeigt an ob ein Knoten Kinder hat oder ein Blatt-Knoten ist. Durch setzen eines TreeCellRenderers über setCellRenderer kann man die Darstellung ändern. Will man nur wenig an der Darstellung ändern, dann kann man die Klasse DefaultTreeCellRenderer beerben. Scrollen in Java (JScrollPane) Komponenten die grösser sind als der zur Verfügung stehende Platz werden rechts unten abgeschnitten. Mit scrollen hat man eine Möglichkeit solche Komponenten ganz darzustellen. In Java übernimmt JScrollPane diese Aufgabe. Damit eine Komponente scrollbar wird muss sie in eine JScrollPane gekapselt werden, zB: new JScrollPane(new JVeryBigComponent()). Und die Komponente muss ihre Größe für die JScrollPane berechnen können. Dazu kann man die Methode getPreferredSize überschreiben. Oder wenn man mehr Einfluss auf die JScrollPane haben will das Interface Scrollable implementieren. Wenn man am sichtbaren Ausschnitt der JScrollPane interessiert ist, kann man den JViewport mit getViewport abfragen. Änderungen am Viewport kann man mit einem ChangeListener überwachen. Zeichnen eigener Komponenten in Swing Viele Anwendungen haben komplexe Daten die man mit Standard-Komponenten nur schlecht darstellen kann. In diesem Fall kann man eine eigene Swing Markus Loeberbauer 2010, 2011 15 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen Komponente implementieren, indem man von JComponent ableitet. Swing zeichnet Komponenten über die Methode paintComponent. Überschreibt man paintComponent kann man seine eigene Visualisierung machen. Achtung, man sollte immer super.paintComponent aufrufen, damit zum Beispiel der Hintergrund richtig dargestellt wird. Oft wird in Beispielen von JPanel abgeleitet, anstelle von JComponent. JPanel ist eine fertige Klasse, es ist gedacht sie zu verwenden; JComponent ist abstrakt und es ist gedacht sie zu erweitern. Unterschiede zwischen den Klassen sind zum Beispiel: JPanel zeichnet die Hintergrundfarbe, JComponent nicht (kann man in der Ableitung machen); JPanel hat als Layout FlowLayout installiert, JComponent keines; und für JPanels wird als „Look And Feel“ (L&F) PanelUI benutzt. Das kann unvorhergesehene Auswirkungen haben, weil das L&F des Panels die abgeleitete Komponente beeinflusst. Dabei ist zu beachten, dass L&Fs getauscht werden können! In Java zeichnet man mit der Klasse Graphics, ein Objekt dieser Klasse bekommt die Methode paintComponent übergeben. Mit diesem Objekt kann man die Zeichenfarbe setzen und Graphikprimitive wie beispielsweise Rechtecke, Ellipsen und Text zeichnen. Das Koordinatensystem startet in der linken oberen Ecke der Komponente mit (0, 0). Die X-Koordinate wächst nach rechts, die Y-Koordinate nach unten. Das Graphics-Objekt ist so konfiguriert, dass man nur innerhalb der vorgegebenen Komponente zeichnen kann (Clipping). Das Graphics-Objekt darf nur genutzt werden solange die Zeichenmethode aktiv ist. Speichert man das Objekt in einem Feld und benutzt es später ist das Verhalten undefiniert. Ereignisbehandlung Java setzt das Observer-Muster in seinem Listener-Konzept um. Listener werden in Interfaces definiert und sollen vom Interface EventListener ableiten. EventListener Markus Loeberbauer 2010, 2011 16 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen ist ein Marker-Interface, d.h. es enthält keine Methoden. Eine Event-Methode hat üblicherweise einen Parameter der von EventObject erbt. EventObject speichert den Ereignis-Sender, alles Weitere speichert man in der abgeleiteten Event-Klasse. Anmelden an ein Ereignis Klassen die Ereignisse auslösen, haben Methoden mit denen man sich als Listener anmelden und abmelden kann. Zum Beispiel löst JComponent MouseEvents aus dafür können sich Listener mit den Methoden addMouseListener anmelden und removeMouseListener abmelden. Auslösen von Ereignissen Löst ein Objekt Ereignisse aus, muss es eine Methode implementieren mit der sich ein Listener anmelden und eine Methode mit der er sich wieder abmelden kann. Die Methode zum Anmelden muss add<ListenerName> und die Methode zum Abmelden remove<ListenerName> heißen. Unterstützt eine Klasse zum Beispiel MouseListener dann müssen die Methoden addMouseListener und removeMouseListener heißen. Außerdem muss es die Methode fire<EventName> geben mit der die Events verschickt werden können. Meistes ist die fire-Methode protected, damit ableitende Klassen das Ereignis auslösen können. Listener-Interface Für jedes mögliche Ereignis muss ein Listener-Interface existieren. ListenerInterfaces können beliebige Methoden enthalten. Zum Beispiel hat MouseListener die Methoden mouseClicked, mousePressed und mouseReleased. Die Methoden müssen als Parameter ein Ereignis-Objekt haben, zB: im MouseListener ein MouseEvent. Listener-Interfaces müssen von dem Marker-Interface java.util.EventListener erben. Ereignis-Objekte Für jedes Ereignis muss ein Ereignis-Objekt erstellt und an alle Listener verschickt werden. Jeder Listener erhält dasselbe Ereignis-Objekt, daher müssen EreignisObjekte unveränderbar (immutable) sein. Ereignis-Objekte in Java erben von der Markus Loeberbauer 2010, 2011 17 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen Klasse EventObject. Diese Klasse implementiert Serializable, daher müssen nichtserialisierbare Felder als transient markiert werden. Push oder Pull? Ereignisse sind häufig mit Änderungen in einem Datenmodell verbunden. Soll man die Änderung gleich im Event übertragen (Push)? Oder soll der Interessierte das Datenmodell selbst inspizieren (Pull)? Eine klare Antwort darauf gibt es nicht. Man muss von Fall zu Fall unterscheiden was die bessere Alternative ist. Push ist für den Event-Empfänger häufig bequemer, während Pull bei Änderungen des Datenmodells stabil ist. Hinweis: Bei der Entscheidung Push oder Pull sollte man auch an die Serialisierbarkeit der gepushten Objekte denken. Java2D Seit Java 1.2 übernimmt Java2D die Zeichenaufgaben in Java, es ist aber kompatibel zu dem Render-Verhalten von Java 1.1. Java2D ermöglicht zB: Abstraktion von physikalischen Pixeln, Transformationen, Effekte und Antialiasing. Der Einstiegspunkt in Java2D ist die Klasse Graphics2D. Seit Java 1.2 ist jedes Graphics-Objekt ein Graphics2D-Objekt und kann daher gecastet werden. Graphics wird nur aus Kompatibilitäts-Gründen in der Schnittstelle übergeben. Mit Graphics2D kann man die Strichart ändern (setStroke); Transformationen wie zB Skalieren, Rotieren anbringen (getTransform); das Füllmuster festlegen (setPaint); den Clipping-Bereich setzen; und die Komposition bestimmen (setComposite). Unter Komposition versteht man: wie vorhandene Farben auf der Zeichenfläche mit der aktuellen Zeichenoperation verbunden werden sollen. Beispielsweise kann man das neue Element darüber oder darunter legen oder verschiedene Schnitte machen, siehe java.awt.AlphaComposite. Markus Loeberbauer 2010, 2011 18 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen Benötigt man ein spezielles Clipping oder spezielle Transformationen sollte man mit einer Kopie des übergebenen Graphics-Objekts arbeiten. Eine Kopie kann mit der Methode Graphics.create anlegen werden. Schrift Text wird mit der Methode Graphics.drawString ausgegeben. Die Schriftart (java.awt.Font) kann man mit Graphics.setFont setzen. Zeichnet man eine komplexe Komponente die Text enthält, dann braucht man häufig die Länge des auszugebenden Texts. Damit man umbrechen oder beispielsweise abkürzen kann. Solche Maßzahlen über Text kann man über die Klasse FontMetrics erhalten. Objekte der Klasse FontMetrics kann man über Graphics.getFontMetrics abfragen. Bilder Bilder werden mit der Methode Graphics.drawImage gezeichnet. Bilder kann man in Java als BufferedImage erzeugen, und über ein Graphics2D-Objekt verändern. Ein Graphics2D-Objekt erhält man über die Methoden getGraphics und createGraphics. Die beiden Methoden liefern das gleiche Objekt, allerdings liefert getGraphics das Objekt aus Kompatibilitätsgründen als Graphics-Objekt. Häufig muss man Bilder aus Dateien laden oder in Dateien speichern. Laden kann man Bilder mit der Methode ImageIO.read, speichern mit ImageIO.write. Copy & Paste Mit Copy & Paste kann man Daten von einem Programm in ein anders übertragen. In Java sind die dafür notwendigen Klassen im Paket java.awt.datatransfer. Das System-Clipboard bekommt man über Toolkit.getDefaultToolkit().getSystemClipboard(). Den gespeicherten Wert kann man aus dem Clipboard mit Transferable getContents(Object) abfragen und mit void setContents(Transferable, ClipboardOwner) setzen. Das Objekt bei getContents gibt an wer die Daten haben möchte, da dieses Objekt aber nicht benutzt wird kann man null übergeben. Bei setContents ist der erste Parameter Markus Loeberbauer 2010, 2011 19 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen das Objekt, das man in das Clipbord legen will und der zweite Parameter ein Callback um darüber informiert zu werden wenn das Objekt im Clipboard überschrieben wird. Braucht man diese Information nicht, dann kann man als ClipboardOwner null übergeben. Das Clipboard kann verschiedene Formate von Daten halten, zB String oder Bilder, das Clipboard dieselben Daten auch gleichzeitig in verschiedenen Formaten halten. Ob die Daten im Clipboard gerade in einem speziellen Format vorliegen kann man mit boolean Transferable.isDataFlavorSupported(DataFlavor) prüfen. DataFloavor definiert Konstanten für die üblichen Formate wie zB String, Liste mit Dateien Bild. Hier ein Beispiel wie man Strings vom Clipboard lesen und ins Clipboard schreiben kann: void writeHelloWorldToClipboard() { final Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard(); clip.setContents(new StringSelection("Hello World!"), null); } void printContentFromClipboard() { final Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard(); final Transferable content = clip.getContents(null); if (content != null && content.isDataFlavorSupported(DataFlavor.stringFlavor)) { try { final String text = (String) content.getTransferData(DataFlavor.stringFlavor); System.out.println(text); } catch (final UnsupportedFlavorException e) { System.out.println("Data no longer available in requested flavor."); } catch (final IOException e) { System.out.println("Requested flavor not supported."); } } } Swing und Threads Das Swing-Framework ist darauf ausgerichtet, dass man einfach graphische Komponenten entwickeln kann. Diese Ausrichtung hat zur Folge, dass SwingMarkus Loeberbauer 2010, 2011 20 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen Komponenten nicht Threadsicher sind, d.h. der Aufbau, alle Änderungen und alle Abfragen müssen aus einem Thread erfolgen. Java hat dafür einen dedizierten Thread, den Event Dispatcher Thread, dieser Thread wird oft auch EDT, AWTThread, GUI-Thread oder Swing-Thread genannt. Einfache Programme (zB Übungsbeispiele) kommen mit diesem einen Thread aus. Realistisch komplexe Programme haben aber Threads, damit langlaufende Prozesse (zB NetzwerkKommunikation, Drucken, Berechnungen) die graphische Oberfläche nicht blockieren. Müssen Threads Änderungen an der GUI machen, dann müssen sie diese Aufgabe über die Klasse SwingUtilities an den GUI-Thread delegieren. Mit der Klasse SwingUtilities kann man zB: abfragen ob der aktuelle Thread der GUIThread ist (boolean isEventDispatchThread()) und eine Aufgabe an den GUIThread delegieren (void invokeLater(Runnable doRun), void invokeAndWait(Runnable doRun)). Swing-GUI Richtig Anlegen In vielen Tutorials steht, dass man die GUI in einem beliebigen Thread anlegen kann und man erst nachdem die GUI über setVisible(true) angezeigt (=realisiert) wurde mit dem GUI-Thread arbeiten muss. Es kann aber bereits während man die GUI anlegt durch Listener über den GUI-Thread auf die GUI zugegriffen werden, d.h. es wird gleichzeitig auf GUI-Komponenten aus mehreren Threads zugegriffen, was verboten ist. Daher muss man die GUI bereits im GUI-Thread aufbauen. Ein guter Artikel (Swing threading and the event-dispatch thread) von John Zukowski dazu ist: http://www.javaworld.com/javaworld/jw-08-2007/jw-08swingthreading.html. Hier ein Beispiel wie man eine GUI threadsicher aufbauen kann: public class GoodCreateGUI { // Use such a method to show GUIs. public static void show() { SwingUtilities.invokeLater(createInitTask()); } // Use such a method if you need the GUI to be ready // after the method call, e.g. in Applets. Markus Loeberbauer 2010, 2011 21 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen public static void showSynchronous() throws InvocationTargetException, InterruptedException { SwingUtilities.invokeAndWait(createInitTask()); } private static Runnable createInitTask() { return new Runnable() { @Override public void run() { new GoodCreateGUI().initAndShow(); } }; } // Use such a method to show GUIs where you // need the GUI object outside. public static GoodCreateGUI createAndShow() throws InvocationTargetException, InterruptedException { final GoodCreateGUI gui = new GoodCreateGUI(); SwingUtilities.invokeAndWait(createInitTask(gui)); return gui; } private static Runnable createInitTask(final GoodCreateGUI gui) { return new Runnable() { @Override public void run() { gui.initAndShow(); } }; } private GoodCreateGUI() { // nothing to do } private void initAndShow() { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(200, 60); frame.add(new JButton("Hello!")); frame.setVisible(true); } } Änderungen einer Swing-GUI aus einem Thread Läuft eine Aktion lange, dann soll man diese Aktion in einen eigenen Thread auslagern damit die GUI reaktionsfähig bleibt. Hier ein Beispiel wie man eine GUI threadsicher verändern kann: public class TaskGoodGUI { public static void show() { SwingUtilities.invokeLater(createInitTask()); } private static Runnable createInitTask() { return new Runnable() { @Override public void run() { new TaskGoodGUI().initAndShow(); } }; } private TaskGoodGUI() { Markus Loeberbauer 2010, 2011 22 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen // nothing to do } private void initAndShow() { final JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); final JButton startTaskButton = new JButton(); startTaskButton.setAction(createStartTaskAtion(startTaskButton)); frame.add(startTaskButton); frame.pack(); frame.setVisible(true); } private Action createStartTaskAtion(final JButton startTaskButton) { return new AbstractAction("Start Task!") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(final ActionEvent e) { startTaskButton.setText("Working ..."); final Runnable statusUpdater = new Runnable() { @Override public void run() { startTaskButton.setText("Start Task! (Set from GUI Thread: " + SwingUtilities.isEventDispatchThread() + ")"); } }; final Runnable worker = new Runnable() { @Override public void run() { doSomethingLong(); SwingUtilities.invokeLater(statusUpdater); } }; new Thread(worker).start(); } }; } private synchronized void doSomethingLong() { try { System.out.println("Start"); Thread.sleep(5000); System.out.println("Done"); } catch (final InterruptedException e) { Logger.getLogger(TaskGoodGUI.class.getName()).log(Level.SEVERE, e.getMessage(), e); } } } Langlaufende Aufgaben die die Swing-GUI verändern Läuft eine Aktion lange und man lagert sie in einen Thread aus, dann möchte man oft die GUI aktualisieren wenn Zwischenergebnisse hat. Hier ein Beispiel wie man aus einer langlaufenden Aufgabe eine GUI threadsicher verändern kann: Markus Loeberbauer 2010, 2011 23 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen public class WorkerComplexGUI { private static final int MAX_PARTS = 5; public static void show() { SwingUtilities.invokeLater(createInitTask()); } private static Runnable createInitTask() { return new Runnable() { @Override public void run() { new WorkerComplexGUI().initAndShow(); } }; } private WorkerComplexGUI() { // nothing to do } private void initAndShow() { final JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); final JProgressBar progressBar = new JProgressBar(); progressBar.setMaximum(MAX_PARTS); frame.add(progressBar, BorderLayout.SOUTH); final JButton startTaskButton = new JButton(); startTaskButton.setAction(createStartTaskAtion(startTaskButton, progressBar)); frame.add(startTaskButton); frame.pack(); frame.setVisible(true); } private Action createStartTaskAtion(final JButton startTaskButton, final JProgressBar progressBar) { return new AbstractAction("Start Task!") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(final ActionEvent e) { startTaskButton.setText("Working ..."); final SwingWorker<Void, Integer> worker = new SwingWorker<Void, Integer>() { @Override protected Void doInBackground() throws Exception { progressBar.setIndeterminate(true); for (int part = 1; part <= MAX_PARTS; ++part) { doSomethingLong(part); publish(part); } return null; Markus Loeberbauer 2010, 2011 24 Praktikum aus Softwareentwicklung 2 Graphische Oberflächen } @Override protected void process(List<Integer> chunks) { progressBar.setIndeterminate(false); progressBar.setValue(chunks.get(0)); } @Override protected void done() { startTaskButton.setText("Start Task! (Set from GUI Thread: " + SwingUtilities.isEventDispatchThread() + ")"); progressBar.setValue(0); } }; worker.execute(); } }; } private synchronized void doSomethingLong(final int part) { try { System.out.println("Start " + part); Thread.sleep(1000); System.out.println("Done " + part); } catch (final InterruptedException e) { Logger.getLogger(WorkerComplexGUI.class.getName()).log( Level.SEVERE, e.getMessage(), e); } } } Markus Loeberbauer 2010, 2011 25 Praktikum aus Softwareentwicklung 2 Dateien Dateien Über die Klasse File kann in Java auf das Dateisystem zugegriffen werden. File ist eine reine Verwaltungsklasse, ein Objekt enthält nur Informationen über eine Datei, aber keinen Inhalt. Mit File kann man beispielsweise feststellen ob eine Datei existiert, lesbar und schreibbar ist. Die Klasse File enthält die statischen Hilfsmethoden: listRoots, sie liefert die Laufwerke des Computers und createTempFile, mit ihr kann man temporäre Dateien anlegen. Markus Loeberbauer 2010, 2011 26 Praktikum aus Softwareentwicklung 2 Reflection Reflection Mit Reflection kann der Programmierer zur Laufzeit auf Typinformationen zugreifen, zB: Felder, Methoden und Konstruktoren. Über Reflection kann man aber auch Objekte erzeugen, Methoden aufrufen, Felder lesen und schreiben. Mit dynamischen Proxies (java.lang.reflect.Proxy) kann man zur Laufzeit Objekte erzeugen, die gegebene Interfaces implementieren. Die benötigten Klassen sind in den Packages java.lang.reflect und java.lang. Einsatzgebiete Die JavaVM nutzt Typinformation beispielsweise zur Speicherbereinigung (garbage collection). In der Java Klassenbibliothek wird Reflection benutzt um Klassen nachzuladen (java.util.ServiceLoader). Programmier können sie benutzen um Werkzeuge zu bauen, zB: Klassen-Browser, Test-Frameworks (zB: JUnit) und Debugger. Verwendet man Reflection sollte man sich aber der Nachteile bewusst sein zB: Geschwindigkeitsverlust durch Klassen-Analyse zur Laufzeit sowie Sicherheitsprobleme, über Reflection kann zB auf private Elemente einer Klasse zugegriffen werden. Class Der Einstiegspunkt in Reflection ist die Klasse Class. Ein Objekt dieser Klasse erhält man über: a. <Klassenname>.class, zB: ArrayList.class b. <Wrapper für einen primitiven Datentyp>.TYPE, zB Integer.TYPE entspricht int.class c. ein Objekt mit der Methode getClass() d. Class.forName("Klassenname als String"), zB: Class.forName("java.util.ArrayList") Markus Loeberbauer 2010, 2011 27 Praktikum aus Softwareentwicklung 2 Reflection Über das Class-Objekt kann man die Elemente in einer Klasse abfragen: Methoden (getMethods, getDeclaredMethods), Felder (getFields, getDeclaredFields), Konstruktoren (getConstructors, getDeclaredConstructors), innere Klassen (getClasses, getDeclaredClasses). Die Methoden mit den Namen get<…> liefer alle public-Elemente inklusive der geerbten. Die Methoden mit den Namen getDeclared<…> liefern alle Elemente einer Klasse, unabhängig von ihrer Sichtbarkeit, aber keine geerbten. Von allen Elementen kann man mit getDeclaringClass auf die deklarierende Klasse zugreifen. Elemente einer Klasse Reflektierte Elemente kann man weiter untersuchen, zB: Methoden auf Rückgabetyp (getReturnType) und Parametertypen (getParameterTypes); oder man kann damit arbeiten: Constructor, anlegen neuer Objekte (newInstance) Method, aufrufen einer Methode (invoke) Field, lesen und schreiben (get, set; und für primitive Datentypen get<Type>, set<Type>, zB: getInt, setInt) Auf private Elemente einer Klasse kann über Reflection zugegriffen werden, dazu muss man setAccessible(true) setzen. Ob ein Element zugreifbar ist kann über isAccessible abgefragt werden. Dynamischer Proxy Ein dynamischer Proxy ist eine Klasse, die eine Liste von Interfaces zur Laufzeit implementiert. Java leitet Aufrufe an einen dynamischen Proxy an ein HandlerObjekt (Interface InvokationHandler) weiter. Einsatzgebiete für dynamische Proxies sind zB: Methoden über das Netzwerk aufrufen (Remote Method Invokation), Testen und Loggen von Methodenaufrufen. Markus Loeberbauer 2010, 2011 28 Praktikum aus Softwareentwicklung 2 Reflection Dynamische Proxies können über die Methode Proxy.newProxyInstance angelegt werden. Annotationen Seit Java 1.5 haben Programmierer die Möglichkeit Metainformationen zu Elementen wie zB: Klassen und Methoden anzugeben. Annotationen sind eine Alternative zu den vorher benutzten Marker-Interfaces. Marker-Interfaces sind Interfaces ohne Methoden, sie werden Implementiert, um Aussagen über Klassen zu treffen. Beispiele für Marker-Interfaces sind Serializable und Cloneable. Annotation bieten aber mehr Möglichkeiten als Marker-Interfaces. Annotationen können auf Klassen, Konstruktoren, Methoden, Felder, lokale Variablen, Packages, Parameter und Annotationen angewendet werden. Diese Elemente werden unter dem Interface AnnotatedElement zusammengefasst. Pro Element können beliebig viele Annotationen angebracht werden, aber nur jeweils eine Annototation pro Annotationsart. Annotationen können mit Konstanten parametriert werden, folgende Typen sind erlaubt: primitive Datentypen, String, Class, Enums, Annotationen sowie eindimensionale Arrays dieser Typen. Arbeiten mit Annotationen Annotationen werden wie Interfaces definiert, allerdings wird dem Schlüsselwort interface ein "@" vorangestellt. Die Parameter der Annotationen werden auf Methoden mit Rückgabetyp abgebildet, wobei ein Default-Wert angegeben werden kann. Java bringt folgende Annotationen mit, mit denen man eigene Annotationen beschreiben kann: @Target: Welche Elemente können annotiert werden, zB: @Target(ElementType.TYPE) oder @Target({ElementType.TYPE, ElementType.METHOD}) @Documented: Soll die Annotation in die JavaDoc aufgenommen werden Markus Loeberbauer 2010, 2011 29 Praktikum aus Softwareentwicklung 2 Reflection @Inherited: Soll die Annotation an Subklassen vererbt werden. Das funktioniert nur für Annoationen die auf Klassen angebracht werden. @Retention: Wie lange soll die Annotation abfragbar sein, zB: nur im Quellcode @Retention(RetentionPolicy.SOURCE), in der Klassendatei @Retention(RetentionPolicy.CLASS) oder auch zur Laufzeut über Reflection @Retention(RetentionPolicy.RUNTIME) Beispiel-Annotation mit einem int-Wert und einem String-Wert: @interface SampleAnnotation { int value(); String stringProp() default "default"; } Anwenden der Annotation: @SampleAnnotation(value=1, stringProp = "test") public class Test {} Werte mit default-Klausel können ausgelassen werden: @SampleAnnotation(value=1) public class Test {} Muss nur ein Wert gesetzt werden und dieser Wert hat den Namen "value", dann kann dieser Wert ohne Namen gesetzt werden: @SampleAnnotation(1) public class Test {} Markus Loeberbauer 2010, 2011 30 Praktikum aus Softwareentwicklung 2 Threads Threads Threads sind parallele, oder auf Rechnern mit nur einer CPU quasi-parallele, Programmabläufe in Java. Sie können beispielsweise benutzt werden, um mehrere Anforderungen auf einem Server abzuarbeiten, Hintergrundtätigkeiten wie Animationen durchzuführen oder lang-laufende Aufgaben von der graphischen Benutzerschnittstelle zu entkoppeln. Basisklassen Die Java Klassenbibliothek enthält folgende Klassen zum Umgang mit Threads: java.lang.Thread Thread-Objekte bilden Threads ab; und bieten programmatischen Zugriff darauf, zB: starten (start), unterbrechen (interrupt), abgeben der Kontrolle (yield) und setzen der Priorität (setPriority). Die Klasse hat statische Hilfsmethoden, mit denen man auf den aktuellen Thread zugreifen kann. Auf Betriebssystemen die Threads unterstützen werden diese genutzt. java.lang.Runable Aufgaben die in einem Thread ausgeführt werden müssen in Objekte gekapselt werden. Diese Objekte müssen das Interface Runnable implementieren. Pro Thread kann eine Aufgabe im Konstruktor übergeben werden. java.lang.Object Implementiert einen Monitor, d.h. jedes Objekt kann zur ThreadSynchronisation genutzt werden. java.lang. InterruptedException Markus Loeberbauer 2010, 2011 31 Praktikum aus Softwareentwicklung 2 Threads Wird geworfen wenn ein Thread schläft oder wartet und von außen unterbrochen wird. Anlegen eines Threads Die Klasse Thread verwaltet Threads in Java. Will man einen Thread in Java starten muss man ein Objekt dieser Klasse anlegen und darauf die Methode start aufrufen. Ein Thread-Objekt kann nur einmal gestartet werden, sobald der Thread seine Aufgabe abgearbeitet hat ist er tot und kann nicht mehr verwendet werden. Das Interface Runnable ist die Schnittstelle für Aufgaben. Runnable enthält nur die Methode void run(). Benötigt man Parameter oder einen Rückgabewert, dann muss man diese als Felder im Objekt ablegen. Beispiel: Anlegen eines Threads der die Zahlen von 1 bis 100 ausgibt. Definieren der Aufgabe als Runnable: public class CounterTask implements Runnable { public void run() { for (int i = 1; i <= 100; ++i) { System.out.println(i); } } } Anlegen und starten des Threads: Thread counterThread = new Thread(new CounterTask()); counterThread.start(); Die Klasse Thread kann auch erweitert werden, wenn man eine spezielle Art von Threads braucht, zB Threads die Zeitmessungen machen oder Threads die Ereignisse auslösen. Diese Erweiterbarkeit kann auch verwendet werden, um einen Thread mit einer Aufgabe zu versehen. Allerdings ist diese Art der Erweiterung im Markus Loeberbauer 2010, 2011 32 Praktikum aus Softwareentwicklung 2 Threads objektorientierten Sinn falsch und aus diesem Grund in anderen Programmiersprachen, wie beispielsweise C#, unmöglich. Negativ-Beispiel: Anlegen einen Threads der die Zahlen von 1 bis 100 ausgibt, als Thread-Ableitung. public class CounterThread extends Thread { public void run() { for (int i = 1; i <= 100; ++i) { System.out.println(i); } } } CounterThread counterThread = new CounterThread(); counterThread.start(); Unterbrechen eines Threads Es gibt Threads die ihre Aufgabe so lange ausführen bis sie von außen unterbrochen werden. Zum Beispiel Server-Threads die Client-Anfrage abarbeiten. Einen Thread kann man zuverlässig und sicher abbrechen lassen, indem man in der Verarbeitungs-Schleife Thread.interrupted() prüft oder ein als volatile markiertes Feld ausliest. Reagiert der Thread auf Thread.interrupted(), dann kann der Thread von außen über die Methode interrupt beendet werden. Liest der Thread ein volatile Feld aus, dann kann man von außen auf dieses Feld schreiben um den Thread zu beenden. Es ist auch möglich einen Thread über die Methode stop zu beenden. Dabei wird der Thread allerdings ohne Vorwarnung gestoppt, ohne die Möglichkeit zu haben begonnene Aufgaben abzuschließen, was zu inkonsistenten Datenmodellen führt. Korrekter Umgang mit Thread.interrupted(): public class Exiter implements Runnable Markus Loeberbauer 2010, 2011 33 Praktikum aus Softwareentwicklung 2 Threads public void run() { while(!Thread.interrupted()) { // Endless loop } } } oder, falls in der Endlosschleife eine InterruptedException auftreten kann public class Exiter implements Runnable public void run() { while (!Thread.interrupted()) { try { // do something sleep(1000); // may throw an InterruptedException } catch (InterruptedException e) { // Call interrupt() to set interrupted() interrupt(); } // finish work } } } Korrekter Umgang mit einem volatile Feld: volatile boolean exit; private class Exiter implements Runnable { public void run() { while (!exit) { // Endless loop } } } Synchronisation In Java nutzen alle Threads einen gemeinsamen Speicherbereich, bei gemeinsam genutzten Objekten muss der Zugriff daher synchronisiert werden. Synchronisation kann auf Methoden- und Block-Ebene erfolgen. Synchronisiert man auf Blockebene, muss explizit ein Objekt angeben werden auf das Markus Loeberbauer 2010, 2011 34 Praktikum aus Softwareentwicklung 2 Threads synchronisiert werden soll. Synchronisiert man auf Methodenebene wird das thisObjekt benutzt. Handelt es sich um eine statische Methode wird das KlassenObjekt benutzt. Synchronisation auf Blockebene ist flexibler, weil man bestimmen kann welches Objekt zur Synchronisation benutzt werden soll; und sie ist sicherer, weil man das Synchronisationsobjekt lokal halten kann. Synchronisierter Block Synchronisierte Methode Object obj = new Object(); synchronized void bar() { // do critical stuff here } void foo() { // uncritical stuff synchronized(obj) { // do critical stuff here } // uncritical stuff } // equivalent to void bar() { synchronized(this) { // do critical stuff here } } Bedingtes Warten Muss in einem Thread auf eine Bedingung gewartet werden bevor weiter gearbeitet werden kann, muss man mit einem Monitor arbeiten. Threads können auf einen Monitor warten und wartende Threads benachrichtigen. Jedes Objekt in Java ist ein Monitor, dazu sind in Objekt die Methoden wait, wait(timeout), wait(timeout, nanos), notify und notifyAll vorhanden. Die Methode wait blockiert den Thread bis er über den Monitor notifiziert wird; oder der Thread mit interrupt unterbrochen wird. Möchte man maximal nur eine gewisse Zeit warten kann man die Methode wait(timeout) oder wait(timeout, nanos) benutzen. Die Methode notify benachrichtigt einen Thread der auf den Monitor wartet, die Auswahl des Threads erfolgt zufällig. Mit der Methode notifyAll werden alle wartenden Threads benachrichtigt. Markus Loeberbauer 2010, 2011 35 Praktikum aus Softwareentwicklung 2 Threads Beispiel: Überweisen eines Geldbetrags. Wobei am Quellkonto genug Geld vorhanden sein muss. public class Bank { private Object lock = new Object(); private Account[] accounts; // ... public void transfer(int from, int to, int amount) throws InterruptedException { synchronized(lock) { while (accounts[from] < amount) { lock.wait(); } accounts[from] -= amount; accounts[to] += amount; lock.notifyAll(); } } } In diesem Beispiel sieht man warum notifyAll wichtig ist. Bevor von einem Konto etwas abgebucht werden kann muss genügend Geld vorhanden sein. Das bedeutet eine Überweisung ist eventuell von einer anderen Überweisung abhängig. Würde man hier nur notify verwenden könnten die Threads in eine Blockierung geraten. Mit notifyAll haben alle Threads die Möglichkeit ihre Bedingung zu prüfen. Warten auf einen Thread Teilt man eine Aufgabe auf mehrere Threads auf, dann muss man, spätestens sobald man das Ergebnis braucht, warten bis alle Threads fertig sind. Dazu kann man auf Thread-Objekten die Methode join aufrufen. Beispiel: Markus Loeberbauer 2010, 2011 36 Praktikum aus Softwareentwicklung 2 Threads // start an extra thread Thread t = new Thread(...); t.start(); // concurrent execution t.join(); // thread t is dead Zustände eines Threads neu: erzeugt aber noch nicht gestartet lauffähig o aktiv: wird gerade ausgeführt o bereit: kann ausgeführt werden und wartet auf Zuteilung des Prozessors blockiert o schlafend: mit sleep schlafen gelegt o IO-blockiert: wartet auf Beendigung einer IO-Operation o wartend: wurde mit wait in den wartenden Zustand versetzt o gesperrt: Wartet auf die Aufhebung einer Objekt-Sperre o suspendiert: durch suspend vorübergehend blockiert Achtung: ist veraltet und sollte nicht verwendet werden tot: run-Methode ausgelaufen blockiert (blocked) d() en sp g un eis it-A nw e () su roniz yn ch um tify All tspe rre (s re s no Obje k p () rre tspe suspendiert (suspended) n otif y/ ben slee chen Obje k wartend (waiting) Aufh e aufwa Ende IO-Operation ed) gesperrt (locked) wa IO-blockiert (IO-blocked) IO-Opertion schlafend (sleeping) aktiv (active) bereit (ready) neu (new) run terminiert start() lauffähig (runnable) tot (dead) Markus Loeberbauer 2010, 2011 37 Praktikum aus Softwareentwicklung 2 Threads Singletons Threadsicher Anlegen Das Singleton-Muster ist eines der einfachsten Entwurfsmuster. Das Muster stellt sicher, dass von einer Klasse nur maximal ein Objekt existiert. In Anwendungen mit mehreren Threads kann man dieses Objekt threadsicher in einer statischen Feldinitialisierung oder im statischen Konstruktor (static Initializer) der Klasse anlegen. Will man das Objekt erst anlegen wenn es das erste Mal benötigt wird muss man es in einem synchronisierten Bereich anlegen. Beispiel für ein Singleton das in einer statischen Feldinitialisierung angelegt wird: public class FieldSingletonTest { private static FieldSingletonTest instance = new FieldSingletonTest(); private FieldSingletonTest() { /* keep private */ } // ... public static FieldSingletonTest getInstance() { return instance; } } Beispiel für ein Singleton das im statischen Konstruktor angelegt wird: public class StaticInitSingletonTest { private static StaticInitSingletonTest instance; private StaticInitSingletonTest() { /* keep private */ } // ... static { instance = new StaticInitSingletonTest(); // initialize the instance ... } public static StaticInitSingletonTest getInstance() { return instance; } } Beispiel für ein Singleton das bei der ersten Verwendung angelegt wird: public class SyncInitSingletonTest { private static final Object lock = new Object(); private static SyncInitSingletonTest instance; private SyncInitSingletonTest() { /* keep private */ } // ... public static SyncInitSingletonTest getInstance() { synchronized (lock) { if (instance == null) { instance = new StaticInitSingletonTest(); Markus Loeberbauer 2010, 2011 38 Praktikum aus Softwareentwicklung 2 Threads // initialize the instance ... } return instance; } } } Threading ab Java 5 Ab Version 5 gibt es in Java viele neue Klassen und Schnittstellen mit denen man Aufgaben parallelisieren kann, hier eine Kurze Übersicht: java.util.concurrent.Callable<V>: ein genisches Interface das eine Aufgabe mit Rückgabewert kapselt. Mit anderen Worten, ein Runnable mit Rückgabewert. java.util.concurrent.Future<V>: ein generische Interface, das eine asynchrone Berechnung kapselt. Mit einer Future kann man prüfen ob die Aufgabe beendet ist, sowie das Ergebnis der Berechung abfragen. java.util.concurrent.ExecutorService: ein Interface für Threadpools, an ein ExecutorService kann man mit submit und execute Aufgaben an den Threadpool übergeben. Mit submit bekommt man ein Objekt der Klasse Future zurück. java.util.concurrent.Executors: eine Hilfsklasse mit der man ExecutorServices anlegen kann. Mit der Methode ExecutorService newFixedThreadPool(int nThreads) bekommt man einen Threadpool mit einer fixen Anzahl von Threads. Mit der Methode ExecutorService newCachedThreadPool() bekommt man einen Threadpool bei dem Thread angelegt werden falls welche benötigt werden und Threads die über 60 Sekunden keine Aufgabe hatten werden zerstört. java.util.concurrent.CountDownLatch: eine Synchronisationsklasse mit der man Threads steuern kann. Eine Latch ist wie eine Tür, sobald die Tür offen ist können die Threads durchgehen. Ist eine Latch einmal offen, dann bleibt sie das für immer. Die CountDownLatch öffnet sobald ihr Zähler auf 0 geht. Ein Thread kann mit der Methode await() warten bis eine Latch öffnet. Mit Markus Loeberbauer 2010, 2011 39 Praktikum aus Softwareentwicklung 2 Threads der Methode countDown() kann man den Zähler verringern und mit der Methode getCount() abfragen. java.util.concurrent.locks.ReentrantLock: ein flexibler Locking-Mechanismus. Funktioniert wie ein synchronized-Block bietet aber mehr Möglichkeiten, wie zB tryLock mit diese Methoden kann man versuchen einen Lock zu erhalten. Achtung wenn man einen ReentrantLock anstelle eines synchronized-Block verwendet muss man sicherstellen, dass der Lock wieder freigegeben wird. Markus Loeberbauer 2010, 2011 40 Praktikum aus Softwareentwicklung 2 Streams Streams In Java sind die Aufgaben Lesen und Schreiben von Datenströmen getrennt. Weiters unterscheidet Java in Byte- und Character-Ströme. Wobei Byte-Ströme über InputStreams und OutputStreams abstrahiert werden und Character-Ströme mit Readern und Writern. Abbildung 7 zeigt wie Daten in einem Java-Programm gelesen und geschrieben werden können. InputStream Programm Reader OutputStrea m Writer Daten Daten quelle senke Abbildung 7) Lesen und Schreiben von Daten über Datenströme Lesen von Byte-Strömen Die abstrakte Klasse InputStream im Paket java.io ist die Basis aller Eingabeströme. Will man eine eigene Datenquelle in Java einbinden muss man InputStream beerben und zumindest die Methode int read() überschreiben. Alle anderen in der Klasse vorhandenen Methoden bauen auf int read() auf. Die Methode muss pro Aufruf ein Byte von der Datenquelle liefern, der Rückgabewert ist ein int, damit man den Wert -1 liefern kann, wenn der Datenstrom das Ende erreicht hat. Wichtige Ableitungen von InputStream sind: FileInputSteam (lesen einer Datei), ByteArrayInputStream (lesen aus einem Byte-Array), PipedInputStream (Kommunikation zwischen Threads), ObjectInputStream (lesen von primitiven Datentypen und Objekten) und FilterInputStream (Basis aller Dekoratoren für Eingabeströmen, zB für gepuffertes Lesen). Markus Loeberbauer 2010, 2011 41 Praktikum aus Softwareentwicklung 2 Streams Schreiben auf Byte-Ströme OutputStream im Paket java.io ist die Basisklasse aller Ausgabeströme. Will man eine eigene Datensenke in Java einbinden muss man OutputStream beerben und zumindest die Methode write(int) implementieren. Alle anderen Methoden in der Klasse bauen auf write(int) auf. Write hat aus Symmetriegründen zu int read() einen int-Parameter, schreibt aber nur das niederwertigste Byte in die Datensenke und ignoriert die restlichen drei Bytes. Wichtige Ableitungen von OutputStream sind: FileOutputStream (schreiben in eine Datei), ByteArrayOutputStream (schreiben in ein Byte-Array), PipedOutputStream (kommunizieren mit einem anderen Thread), ObjectOutputStream (schreiben von primitiven Datentypen und Objekten) und FilterOutputStream (Basis aller Dekoratoren für Ausgabeströme, zB für gepuffertes Schreiben). Lesen von Character-Strömen Die Basis aller Character-Eingabeströme ist Reader, Ableitungen von Reader müssen zumindest die Methoden close() und int read(char[] cbuf, int off, int len) implementieren. Die Methode füllt cbuf, ab Position off, für maximal len Zeichen; und liefert die Anzahl der tatsächlich gelieferten Zeichen zurück. Wichtige Ableitungen von Reader sind: InputStreamReader (lesen von einem InputStream) mit der Unterklasse FileReader (Komfort-Klasse, lesen von einer Datei), BufferedReader (gepuffertes Lesen), CharArrayReader und StringReader (lesen von einem Char-Array bzw. String), PipedReader (Kommunikation zwischen Threads). Schreiben von Character-Strömen Die Basis aller Character-Ausgabeströme ist Writer, Ableitungen von Writer müssen zumindest die Methoden close(), flush() und int read(char[] cbuf, int off, int len) überschreiben. Die Methode schreibt len Zeichen von cbuf ab Position off in die Datensenke. Markus Loeberbauer 2010, 2011 42 Praktikum aus Softwareentwicklung 2 Streams Wichtige Ableitungen von Writer sind: OutputStreamWriter (schreiben in einen OutputStream) mit der Unterklasse FileWriter (Komfort-Klasse, schreiben in eine Datei), BufferedWriter (gepuffertes Schreiben), CharArrayWriter und StringWriter (schreiben in einen Char-Array bzw. StringBuffer), PipedWriter (Kommunikation zwischen Threads). Standardströme Programme haben die Standardströme: Standard-Ausgabe-Strom, Standard-ErrorStrom und Standard-Eingabe-Strom. Diese kann man über die statischen Felder System.out, System.err bzw. System.in abrufen. Muster Ressourcen und Exceptions Klassen die mit Betriebssystem-Ressourcen arbeiten, müssen nach der Verwendung diese wieder freigeben. Das trifft auf Ströme, die zum Beispiel auf Dateien arbeiten, ebenfalls zu. Um das sicher zu stellen soll man das Muster in Abbildung 8 verwenden. Markus Loeberbauer 2010, 2011 43 Praktikum aus Softwareentwicklung 2 1. Variable deklarieren, mit null initialisieren 2. try-Block öffnen 1. Resource anlegen 2. Resource nutzen 3. catch-Block (optional) 4. finally-Block 1. Resource freigeben Streams FileInputStream fis = null; try { fis = new FileInputStream( "test.txt"); int c; while ((c = fis.read()) != -1) { char ch = (char) c; ... } } catch (IOException ioex) { ... } finally { if (fis != null) { try { fis.close(); } catch { /* log exception */ ... } } } Abbildung 8) Muster: Ressources und Exceptions Dekorieren von Datenströmen mit Filter-Streams Das Entwurfsmuster Decorator wird verwendet, um Klassen mit zusätzlichen Funktionen auszustatten. In Java wird das Decorator-Muster eingesetzt, um Einund Ausgabeströme mit zusätzlichen Funktionen zu versehen, zB Puffern, Verschlüsseln oder Komprimieren. Abbildung 9 zeigt schematisch wie FilterStreams verwendet werden können. Markus Loeberbauer 2010, 2011 44 Praktikum aus Softwareentwicklung 2 Input Filter Filter Strea Input Input Streams Programm Stream Stream Filter Filter Input Input Input Strea Stream Stream Daten Daten quelle senke Abbildung 9) Schematische Darstellung von Filter-Streams Markus Loeberbauer 2010, 2011 45 Praktikum aus Softwareentwicklung 2 Serialisierung Serialisierung Über Serialisierung kann man Objekte in Bytes verwandeln und Objekte aus Bytes aufbauen. In Java kann man das über den ObjectOutputStream bzw. ObjectInputStream machen. Java kann alle primitiven Datentypen und beliebige Objekte serialisieren, allerdings müssen Klassen mit dem Marker-Interface Serializable markiert werden. Wird ein solches Objekt serialisiert, dann serialisiert Java auch alle Objekte mit, die über Felder erreichbar sind (transitive Hülle). Zeigt ein Feld auf ein Objekt welches nicht Serializable implementiert wirft Java eine NotSerializableException. Felder die man bei der Serialisierung auslassen möchte muss man mit transient markieren. Klassen können weiterentwickelt werden, damit Änderungen in Klassen nicht zu korrupten Datenmodellen beim Deserialisieren führen kann man eine Versionsnummer als Konstante mit dem Namen serialVersionUID in der Klasse ablegen. Benutzerdefinierte Serialisierung Will man mehr Einfluss auf die Serialisierung nehmen kann man die Methoden writeObject, readObject und readObjectNoData; writerReplace und readResolve implementieren. Eine genaue Beschreibung dieser Methoden ist in der JavaDoc des Interfaces Serializable vorhanden. Über private void writeObject(ObjectOutputStream) kann man den Zustand eines Objekts speichern, dabei wird der Zustand der Basisklasse automatisch gespeichert. Mit private void readObject(ObjectInputStream) kann man den Zustand des Objekts wiederherstellen. Mit private void readObjectNoData() kann man einen Standard-Zustand herstellen wenn keine Daten für das Objekt vorhanden sind. Das kann passieren wenn der Datenstrom beschädigt ist oder der Datenstrom mit einer anderen Version des Objekts geschrieben wurde. Markus Loeberbauer 2010, 2011 46 Praktikum aus Softwareentwicklung 2 Serialisierung Über die Methode ANY-ACCESS-MODIFIER Object writeReplace() kann man ein Stellvertreterobjekt liefern, das anstelle des eigentlichen Objekts serialisiert werden soll. Mit der Methode ANY-ACCESS-MODIFIER Object readResolve() kann man beim Deserialisieren das ursprüngliche Objekt wieder liefern. Externalisieren Will man noch mehr Einfluss auf die Serialisierung nehmen kann man das Interface Externalizable mit den Methoden writeExternal und readExternal implementieren. Implementiert eine Klasse Externalizable, dann sichert Java nur eine Id für das Objekt, um die Daten des Objekts und die Daten der Superklassen muss sich der Programmierer kümmern. Externalizable implementiert man nur in Ausnahmefällen, eingeführt wurde es um die teure (und damals noch teurere) Reflection aus dem Serialisierungsprozess herausoptimieren zu können. Das kann notwendig sein wenn man sehr viele Objekte serialisieren muss, zB bei Methodenaufrufen über das Netzwerk. Markus Loeberbauer 2010, 2011 47 Praktikum aus Softwareentwicklung 2 Netzwerkprogrammierung Netzwerkprogrammierung Will man Programme schreiben die auf unterschiedlichen Rechnern laufen und miteinander kommunizieren müssen, dann muss man folgende Fragen klären: Wie finden sich die verteilten Programme? Wie wird die Verbindung aufgebaut? Wie werden Daten ausgetauscht? Diese Fragen werden in Java durch zwei Modelle abgedeckt, dem Socket-Streaming und dem Remoting. Socket-Streaming Sockets sind eine Programmierschnittstelle für stream-basierte Kommunikation. In Java wird Socket-Streaming über die Klassen Socket und ServerSocket umgesetzt. In diesem Modell finden sich Programme über IP-Adressen und Ports. Java unterstützt IP Version 4 (RFC 790 u.a.) und IP Version 6 (RFC 2373 u.a.). Die Kommunikation zwischen den Rechnern erfolgt über Ein- und Ausgabeströme. Clients bauen die Verbindung über die Klasse Socket auf, von diesem Socket kann man über die Methode getInputStream den Eingabestrom zum Lesen und über getOutputStream den Ausgabestrom zum Schreiben abrufen. Server öffnen einen Port über ServerSocket, auf den sich Clients verbinden können. Am Server wird ein Client über die Methode accept angenommen; accept liefert einen Socket zurück über den die Kommunikation abgewickelt werden kann. Häufig wird die Client-Anfrage in einem eigenen Thread abgearbeitet, damit mehrere Clients gleichzeitig bedient werden können. Markus Loeberbauer 2010, 2011 48 Praktikum aus Softwareentwicklung 2 Datenbanken Datenbanken Datenbanken werden in Java über JDBC (ist ein Eigenname, wird aber oft als Abkürzung von Java Database Connectivity API gesehen) angesprochen. JDBC ist eine Abstraktionsschicht über Datenbanktreibern. Ohne Abstraktion müsste man bei einem Datenbankwechsel die Anwendung umprogrammieren. Design von JDBC JDBC wird seit 1995 entwickelt, erste Überlegungen gingen in Richtung einer Spracherweiterung, diese Ideen wurden aber verworfen und eine Treiberschnittstelle für Drittanbieter gebaut. JDBC lehnt sich an ODBC an, ist aber im Stil von Java geschrieben: ODBC hat wenige Befehle aber sehr viele Optionen, JDBC hat viele einfache Methoden. Außerdem benutzt ODBC void-Zeiger und Java kennt keine Zeiger. Befehle an die Datenbank werden als String übergeben. Das erlaubt es Programmieren SQL-Befehle für eine Datenbank zu optimieren, aber man muss sich bewusst sein, dass eine solche Optimierung wieder eine Bindung an eine Datenbank bedeutet. Treiberarten in JDBC Es gibt vier Treiberarten in JDBC. Sie sind historisch bedingt: der Typ 1 Treiber ist eine Brücke von JDBC nach ODBC, Typ 2 Treiber leiten Aufrufe an native Treiber weiter, Typ 3 Treiber sind voll in Java implementiert und binden an eine Middleware und Typ 4 Treiber sind voll in Java implementiert und sprechen direkt eine Datenbank an. Typ 1: Brücke Typ 1 die Brücke von JDBC nach ODBC war ein pragmatischer Ansatz von Sun, um vom Start weg so viele Datenbanken wie möglich anzubinden. Für ODBC waren damals viele Datenbanktreiber verfügbar. Nachteile dieses Ansatzes sind: die Markus Loeberbauer 2010, 2011 49 Praktikum aus Softwareentwicklung 2 Datenbanken zusätzliche ODBC-Schicht kostet Leistung; höhere Wartung, es muss am Zielrechner ein ODBC-Treiber installiert und gepflegt werden. Typ 2: Partial Java Driver Gibt Aufrufe direkt an eine native Implementierung weiter. Da für Datenbanken native Treiber vorhanden waren, war dies eine Möglichkeit für DatenbankHersteller schnell Java-Treiber anzubieten. Der Nachteil dieses Ansatzes ist die Betriebssystemabhängigkeit der Treiber. Typ 3: Reiner Java Treiber zu einer Middleware Der Treiber ist völlig in Java implementiert und damit Betriebssystemunabhängig. Durch die Middleware ist das Programm auch Datenbankunabhängig. Nachteile: es muss einen Server geben wo diese Middleware installiert ist. Typ 4: Reiner Java Treiber zu einer Datenbank Der Treiber ist völlig in Java implementiert und dadurch Betriebssystemunabhängig. Typ 4 Treiber verbinden direkt auf die Datenbank und sind damit schnell. Ein kleiner Nachteil gegenüber Typ 3 ist die Abhängigkeit von der Datenbank. Installation von JDBC-Treibern Datenbanktreiber für JDBC kann man auf http://developers.sun.com/product/jdbc/drivers oder bei den Datenbankherstellerseiten finden. Zur Installation muss man den Treiber in den Klassenpfad aufnehmen. Will man einen Treiber benutzen, muss man ihn laden, dazu hat man die Möglichkeiten: a. Seit Java 6.0 (JDBC 4), Automatisches Laden als Java Service durch den DriverManager. Dazu muss der Treiber das Java Service java.sql.Driver anbieten, was heute üblich ist. b. System Property: jdbc.drivers, zB: Markus Loeberbauer 2010, 2011 50 Praktikum aus Softwareentwicklung 2 Datenbanken a. java -Djdbc.drivers=org.apache.derby.jdbc.EmbeddedDriver Xyz b. System.setProperty("jdbc.drivers", "org.apache.derby.jdbc.EmbeddedDriver"); c. Manuelles laden der Treiberklasse, zB: a. Class.forName("org.apache.derby.jdbc.EmbeddedDriver"); Aufbauen einer Verbindung Eine Verbindung (Connection) zu einer Datenbank kann man über DriverManager.getConnection aufbauen. Dazu muss man eine Datenbank-Url und optional einen Benutzernamen und ein Passwort angeben. DriverManager: Verwaltet registrierte Treiber, baut Verbindungen auf Connection getConnection(String url, String user, String password) Datenbank URL Aufbau: jdbc:<subprotrokoll>:<subname> jdbc:<Datenbanktreiber>:<treiberspezifische Angaben> Derby o jdbc:derby:/path/to/Database o jdbc:derby:Databasename MySQL o jdbc:mysql://<host>:<port>/<Database> Arten von Statements Über die Connection kann man die Verbindung zur Datenbank verwalten, zB schließen, Informationen über die Datenbank abfragen, Transaktionen verwalten und Statement-Objekte anfordern. JDBC unterscheidet die Statement-Arten: Statement, PreparedStatement und CallableStatement: Markus Loeberbauer 2010, 2011 51 Praktikum aus Softwareentwicklung 2 Datenbanken Statement (Connection.createStatement): Absetzen von beliebigen SQLBefehlen, einsetzbar für Werkzeuge, bei denen der Benutzer den Befehl eingeben kann und für vordefinierte Befehle. Beispiel: Statement stat = con.createStatement(); stat.executeUpdate("INSERT INTO test VALUES ('Hallo')"); PreparedStatement (Connection.prepareStatement): Absetzen von SQLBefehlen mit Parametern, einsetzbar wenn Benutzer Parameter eingeben können. Parameter werden mit einem „?“ in den SQL-String eingesetzt, und mit set-Methoden vor dem Ausführen über ihre Position gesetzt, Positionen werden von 1 ab gezählt. Verhindert SQL-Injection. Kann auf der Datenbank vorkompiliert werden und ist damit schneller bei der Ausführung. Beispiel: PreparedStatement stat; stat = con.prepareStatement("INSERT INTO test VALUES (?,?)"); stat.setString(1, "Hallo"); stat.setString(2, "Welt"); stat.executeUpdate(); stat.setString(2, "Jane"); stat.executeUpdate(); CallableStatement (Connection.prepareCall): Ausführen von DatenbankProzeduren. CallableStatements können wie PreparedStatements parametrisiert werden. Ausgangsparameter werden unterstützt, sie müssen aber registriert werden (registerOutParameter). Übergangsparameter müsse ebenfalls als Ausgangsparameter registriert werden. Beispiel: Markus Loeberbauer 2010, 2011 52 Praktikum aus Softwareentwicklung 2 Datenbanken CallableStatement cs; cs = con.prepareCall("{ CALL GET_NUMBER_FOR_NAME(?, ?) }"); cs.registerOutParameter(2, java.sql.Types.INTEGER); cs.setString(1, "Duke"); cs.execute(); int number = cs.getInt(2); Abfragen der Ergebnisse Statements liefern Abfrageergebnisse als ResultSet zurück. ResultSet ist ein Cursor, es funktioniert wie ein Iterator von dem man Werte abfragen kann. Am Anfang steht das ResultSet vor der ersten Zeile, mit boolean next() kann man die nächste Zeile anspringen, der Rückgabewert von next gibt an ob eine gültige Zeile erreicht wurde. Die Werte einer Zeile können mit Methoden der Art get<Typ>(int spalte) und get<Typ>(String spaltenName) abgefragt werden. Fragt man einen Wert ab, kann man mit wasNull abfragen ob der Wert in der Datenbank SQL-NULL ist. Folgende weitere Methoden stehen zur Verfügung: int findColumn(String spaltenName): sucht die Spaltennummer zu einer Spalte mit gegebenem Spaltennamen. boolean first(): Springt in die erste Zeile im ResultSet, liefert true wenn gültige Zeile erreicht wird. void beforeFirst(): Springt vor die erste Zeile im ResultSet. boolean last(): Springt in die letzte Zeile im ResultSet, liefert true wenn gültige Zeile erreicht wird. void afterLast(): Springt hinter die letzte Zeile im ResultSet. boolean absolute(int row): Spring in die Zeile mit der gegebenen Nummer: o row > 0 ... von oben (1 erste Zeile, 2 zweite Zeile, ...) o row < 0 ... von unten (-1 letzte Zeile, -2 vorletzte Zeile, ...) o liefert true wenn gültige Zeile erreicht wird. Markus Loeberbauer 2010, 2011 53 Praktikum aus Softwareentwicklung 2 Datenbanken int getRow(): Liefert die Nummer der aktuellen Zeile Markus Loeberbauer 2010, 2011 54 Praktikum aus Softwareentwicklung 2 Datenbanken Abbildung der SQL-Typen auf Java-Typen SQL-Typ Java-Typ CHAR, VARCHAR, LONGVARCHAR String NUMERIC, DECIMAL java.math.BigDecimal BIT boolean TINYINT byte SMALLINT short INTEGER int BIGINT long REAL float FLOAT, DOUBLE BINARY, VARBINARY, LONGVARBINARY DATE TIME TIMESTAMP … double byte[] java.sql.Date java.sql.Time java.sql.Timestamp siehe JSR-221, Appendix B, Date Type Conversion Tables Metadaten einer Datenbank Über DatabaseMetaData Connection.getMetadata() kann man auf Informationen der Datenbank zugreifen. Das ist wichtig wenn man Programme entwickelt, die die Datenbank nicht kennen, zB: Administrationsoberflächen; oder wenn man die Datenbank bei der ersten Verwendung initialisieren will. Markus Loeberbauer 2010, 2011 55 Praktikum aus Softwareentwicklung 2 Datenbanken Es kann auf Daten wie die Datenbank-Url (getURL), den Benutzernamen (getUserName) und Beschreibbarkeit (isReadOnly) zugegriffen werden. Man abfragen was eine Datenbank unterstützt, zB: Transaktionen (supportsTransactions), Gruppierung (supportsGroupBy). Welche Beschränkungen eine Datenbank hat, zB: Maximale Länge eines Statements (getMaxStatementLength), Maximale Anzahl der parallel absetzbaren Statements (getMaxStatements) und Maximale Anzahl geöffneter Verbindungen (getMaxConnections). Wird bei den Beschränkungen 0 geliefert, bedeutet das, dass keine Beschränkung gibt oder die Beschränkung unbekannt ist. Weiters kann man inhaltsbezogene Daten abfragen, zB: welche Tabellen in einer Datenbank liegen, welche Spalten in einer Tabelle existieren und welche Typen die Spalten haben. Daten über Ergebnistabellen kann man über ResultSetMetaData ResultSet.getMetaData() abfragen. Darüber kann man beispielsweise abfragen wie viele und welche Spalten geliefert wurden; welche Typen und Namen die Spalten haben und ob man in eine Spalte schreiben kann. Transaktionen Eine Transaktion wird in JDBC gestartet, sobald man ein Statement absetzt und noch keine Transaktion läuft. Standardmäßig wird in JDBC jedes Statement als eine Transaktion behandelt. Braucht man länger laufende Transaktionen, dann muss man die Eigenschaft autoCommit der Verbindung auf false setzen (Conncetion.setAutoCommit). Eine laufende Transaktion kann man mit Connection.commit abschließen und mit Connection.rollback rücksetzen. Zusätzlich kann man während einer Transaktion Sicherheitspunkte (Savepoints) angelegen auf die man mit einem Rollback zurückspringen kann. Markus Loeberbauer 2010, 2011 56 Praktikum aus Softwareentwicklung 2 Datenbanken Unterstützte Transaktions-Isolation Welcher Transaktions-Isolations-Level von einer Datenbank unterstützt wird kann man über DatabaseMetaData. supportsTransactionIsolationLevel abfragen. Die in JDBC bekannten Transaktionslevels sind in der Klasse Connection definiert: NONE: Kein Transaktionssupport => kein JDBC Treiber READ_UNCOMMITTED: dirty reads, non-repeatable reads und phantom reads können auftreten READ_COMMITTED: dirty reads sind verhindert; non-repeatable reads und phantom reads können auftreten REPEATABLE_READ: dirty reads und non-repeatable reads sind verhindert; phantom reads können auftreten SERIALIZABLE: dirty reads, non-repeatable reads und phantom reads sind verhindert. Beispiel: Connection con; ... try { con.setAutoCommit(false); Statement stat = con.createStatement(); stat.executeUpdate("INSERT ..."); stat.executeUpdate("INSERT ..."); stat.executeUpdate("UPDATE ..."); con.commit(); } catch (SQLException e) { con.rollback(); } Ausnahmebehandlung Alle JDBC-bezogenen Exceptions erben von SQLException, seit Java 6.0 gibt es eine feingranulare Aufteilung in die Fehlerklassen: SQLNonTransientException, SQLTransientException und SQLRecoverableException. Nicht-transient bedeutet, dass ein erneuter Versuch wieder fehlschlagen wird; transient bedeutet, dass ein erneuter Versuch durchgehen kann; und recoverable bedeutet, dass ein erneuter Versuch mit geänderten Daten durchgehen kann. Markus Loeberbauer 2010, 2011 57 Praktikum aus Softwareentwicklung 2 Datenbanken Java DB (Derby) Seit Java 6.0 wird die Datenbank Java DB mit dem JDK geliefert. Diese Datenbank ist auch unter dem Namen Derby oder Apache Derby bekannt. Derby ist eine kompakte (Kern: 2,5MB), in Java entwickelte, einfach zu nutzende (ohne Installation), standardkonforme (SQL 99, und Teile aus späteren Standards) Datenbank. Interessant ist, dass die erzeugten Daten-Dateien betriebssystemunabhängig sind. Will man von der Konsole aus mit Derby arbeiten muss man die Umgebungsvariablen JAVA_HOME, DERBY_HOME und PATH setzen: JAVA_HOME=Pfad zur Java JDK Installation DERBY_HOME=Pfad zur Derby Installation PATH um DERBY_HOME/bin erweitern In DERBY_HOME sind die jar-Dateien von Derby: derby.jar: Kern, genügt für embedded DB derbynet.jar: Netzzugriff, Serverseitig derbyclient.jar: Netzzugriff, Clientseitig derbytools.jar: Verwaltungs-Werkzeuge derbyrun.jar verweist auf: derby.jar, derbyclient.jar, derbytools.jar und derbynet.jar Auf die Verwaltungs-Werkzeuge in derbytools.jar kann man über Batch-Dateien zugreifen. Das Werkzeug sysinfo liefert Informationen über die Java-Installation auf dem System; mit dblook kann man das Datenbankschema exportieren. ij ist eine Konsole mit der man SQL-Befehle an eine Derby-Datenbank absetzen kann. Markus Loeberbauer 2010, 2011 58 Praktikum aus Softwareentwicklung 2 Remoting Remoting Über Remoting können Objekte über JavaVMs hinweg miteinander kommunizieren. Das ist auch mit Socket-Programmierung möglich. Aber Remoting abstrahiert die Kommunikation als Methodenaufrufe, während Sockets nur Byteströme übertragen. Remove-Methodenaufrufe unterscheiden sich von lokalen Methodenaufrufen dadurch, dass sie eine RemoteExceptions auslösen können. Das kann passieren wenn zB der Server abstürzt oder ein Netzwerkfehler auftritt. Remoting arbeitet mit dem Proxy-Muster um von der Netzwerkkommunikation zu abstrahieren. Das bedeutetet: auf der Client-Seite ist ein Proxy (Stub) der die Methodenaufrufe entgegennimmt und über das Netz überträgt. Auf der ServerSeite ist ein Proxy (Skeleton) der die Anfragen vom Netz liest und den gewünschten Methodenaufruf auf dem echten Server-Objekt macht. Hat die Methode einen Rückgabewert, dann überträgt der Server-Proxy diesen zurück an den Client-Proxy. Der Client-Proxy nimmt den Rückgabewert vom Netz und gibt ihn an den Rufer zurück. Siehe Abbildung 10. client server_stub server_skeleton server request request response response JVM 1 JVM 2 Abbildung 10) Remoting Kommunikation Die Kommunikation zwischen den JavaVMs erfolgt über die Netzwerk-Schicht des Betriebssystems, auch wenn die VMs auf demselben Rechner laufen. Die Netzwerkverbindung läuft über TCP/IP, der Standard-Port ist 1099, als Markus Loeberbauer 2010, 2011 59 Praktikum aus Softwareentwicklung 2 Remoting Kommunikationsprotokoll kann man zB: das Java Remote Method Protokol (JRMP) oder Internet Inter-ORB Protocol (IIOP) eingesetzten. Damit man eine Methode aufrufen kann muss ein Empfänger-Objekt existieren. Das Empfänger-Objekt am Server muss also existieren solange Clients darauf zugreifen. Damit das Objekt erhalten bleibt, auch wenn es am Server keine Referenz im Programm mehr gibt, gibt es einen Remote Reference Layer der auf Server-Objekte verweist. Seit Java 2 übernimmt der Remote Reference Layer die Aufgabe vom Skeleton am Server. Abbildung 11 zeigt die Schichten der Kommunikation, inklusive Remote Reference Layer. Abbildung 11) Schichten der Kommunikation bei Remoting Objektregistrierung und Objektsuche Eine Frage der Kommunikation zwischen zwei Objekten ist, wie finden sich die Objekte? In Java Remoting wird das über eine RMI-Registry gelöst. Server registrieren bei einer RMI-Registry die exportierten Remote-Objekte mit einem Namen; und Clients fragen über diese RMI-Registry Objekte mit dem Namen ab. Als RMI-Registry kann man das Kommandozeilenwerkzeug rmiregistry benutzen oder programmatisch eine über LocateRegistry.createRegistry erstellen. Registrieren und suchen kann man Objekte über die Klasse Naming, mit den Methoden bind und rebind bzw. lookup. Markus Loeberbauer 2010, 2011 60 Praktikum aus Softwareentwicklung 2 Remoting Stub und Skeleton Die Proxies für den Client und den Server kann man mit dem Kommandozeilenwerkzeug rmic erstellen. Gibt man den Parameter -keep an werden die erzeugten Klassen im Quellcode ausgegeben, sonst nur als classDateien. Da ab Java 2 die Aufgabe des Skeletons in der Remote Reference Schicht implementiert ist erzeugt rmic nur Stubs. Braucht man Skeletons muss man den Parameter -v1.1 angeben. Nutzt man Klasse UnicastRemoteObjekt fällt auch die Notwendigkeit für einen Stub weg. Die Klasse UnicastRemoteObjekt exportiert ein Objekt und erzeugt die nötige Remote-Reference. Diese Klasse kann beerbt werden, dann wird das Objekt im Konstruktor exportiert; oder man nutzt die statische Methode exportObject(Remote obj, int port), um ein beliebiges Remote-Objekt zu exportieren. Als port kann man 0 angeben, dann sucht Java selbst nach einem freien Port. Parameter und Rückgabewerte Sinnvoll werden Methodenaufrufe erst wenn man Parameter übergeben und Rückgabewerte erhalten kann. Bei Remoting werden alle serialisierbaren Datentypen als Parameter unterstützt. Das sind: die Basisdatentypen (int, boolean, double, …), Datentypen die das Interface Serializable implementieren und RemoteObjekte, diese implementieren das Interface Remote. Für Remote-Objekte wird anstelle des eigentlichen Objekts eine Remote-Referenz übertragen. Somit können Methodenaufrufe an das echte Objekt weitergeleitet werden. Achtung, bei den direkt serialisierten Objekten wird auf der anderen Seite eine Kopie aufgebaut, Änderungen sind also lokal zu der ausführenden JavaVM. Objekte aller anderen Klassen können nicht verwendet werden. Markus Loeberbauer 2010, 2011 61 Praktikum aus Softwareentwicklung 2 Remoting Übergibt man Remote-Objekte an ein Server-Objekt, kann der Server Methodenaufrufe am Client machen. Die Rolle des Servers und des Clients vertauscht sich für diesen Aufruf. Das kann zum Beispiel genutzt werden um Remote-Listener am Server zu installieren. Objektvergleich Remote-Objekte haben eine andere Gleichheitssemantik als lokale Objekte. Der Remote Reference Layer legt jedes Mal wenn ein Objekt abgefragt wird ein neues Stub-Objekt an. Damit schlägt der Referenzvergleich (==) auf der Clientseite fehl, auch wenn es sich um dasselbe Objekt auf Serverseite handelt. Will man feststellen ob zwei Objekt-Referenzen auf der Clientseite auf dasselbe Objekt der Serverseite zeigen muss man equals benutzen. Die Methode equals wird also in Remoting benutzt, um Referenzgleichheit am Server festzustellen. Braucht man eine Vergleichsmethode die Objektgleichheit (equals) am Server prüft, muss man sich eine eigene Remotemethode schreiben. Häufig wird dafür der Name remoteEquals benutzt. Distributed Garbage Collection Übergibt man ein Remote-Objekt an eine andere JavaVM, zB: als Listener oder als Arbeitsobjekt aus einer Factory, stellt sich die Frage wie lange man das reale Objekt am Leben erhalten muss. Sowohl Listener als auch Objekte die man aus einer Factory erzeugt, um sie einem Remote-Client zu übergeben speichert man Lokal nur selten. Der Garbage Collector würde diese Objekte also aufräumen. Damit die Objekte erhalten bleiben solange sie noch von Clients benötigt werden implementiert der Java Remote Reference Layer einen verteilten Garbage Collector. Der verteilte Garbage Collector arbeitet mit Reference Counting und einer Lease Time. Erst wenn der Referenz-Zähler auf null ist werden Objekte freigegeben. Ein Client gilt eine Zeit (Lease Time, Standard 10 Minuten) lang als aktiv, innerhalb Markus Loeberbauer 2010, 2011 62 Praktikum aus Softwareentwicklung 2 Remoting dieser Zeit muss er sich melden, um weiter als aktiv zu gelten. Das Erneuern der Lease übernimmt der Remote Reference Layer des Clients, der Programmierer ist davon unbehelligt. Die Lease Time kann über das System-Property java.rmi.dgc.leaseValue verändert werden. Je kürzer die Zeit, umso schneller werden unbenutzte Objekte freigegeben, aber die Last am Netz steigt. Einen idealen Wert gibt es nicht, normalerweise kann man den Standardwert von 10 Minuten beibehalten, in Spezialfällen muss man sich die Netz-Infrastruktur und die Objekt-Last am Server ansehen und den Wert anpassen. Nachladen von Klassen Übergibt man Objekte als Parameter oder Rückgabewerte die auf der Gegenstelle unbekannt sind muss Code nachgeladen werden. Zum Beispiel kennt ein Client nur das Interface eines Remote-Objekts, der Stub ist aber eventuell nur am Server bekannt. Damit der Client dennoch mit den Objekten umgehen kann muss er die Klassen nachladen. Remoting unterstützt nachladen von Klassen, dazu muss über das SystemProperty java.rmi.server.codebase eine Url angegeben werden; und ein SecurityManager installiert sein. Damit der Sicherheitsmanager den Zugriff auf die Codebase erlaubt muss über eine Policy-Datei Zugriff auf den Server erlaubt sein. Beispiel: public class MyClient { public static void main(String[] args) { System.setSecurityManager(new SecurityManager()); System.setProperty("java.security.policy", "client.policy"); ... client.policy: Markus Loeberbauer 2010, 2011 63 Praktikum aus Softwareentwicklung 2 Remoting grant { permission java.net.SocketPermission "server-url:1024-65535", "connect"; permission java.net.SocketPermission "server-url:80", "connect"; permission java.net.SocketPermission "server-url:8080", "connect"; } Aufteilung der Klassen Die Klassen können wie folgt aufgeteilt werden: Server: Klassen die für die Ausführung des Servers erforderlich sind Download-Bereich: Klassen die vom Client nachgeladen werden sollen, inklusive aller Basisklassen und Interfaces. Client: Klassen die unmittelbar am Client benötigt werden; und die PolicyDatei, die Zugriff auf den Download-Server erlaubt. Der Download-Bereich kann ein Web-Server sein, aber auch ein Verzeichnis auf einem FTP-Server oder ein Verzeichnis auf dem lokalen Rechner. Beispiel Remote Calculator Das Beispiel eines verteilten Rechners zeigt wie man mit Remoting einen verteilten Dienst implementieren kann. Als erstes muss eine Schnittstelle definiert werden die der Remote-Service implementieren soll, in unserem Fall ein Rechner mit den Grundrechnungsarten: public interface Calculator extends java.rmi.Remote { public long add(long a, long b) throws java.rmi.RemoteException; public long sub(long a, long b) throws java.rmi.RemoteException; public long mul(long a, long b) throws java.rmi.RemoteException; public long div(long a, long b) throws java.rmi.RemoteException; } Jede Methode im Interface muss die eine java.rmi.RemoteException werfen können. Markus Loeberbauer 2010, 2011 64 Praktikum aus Softwareentwicklung 2 Remoting Dazu eine Implementierung der Schnittstelle: public class CalculatorImpl implements Calculator public long add(long a, long b) { return a + b; public long sub(long a, long b) { return a - b; public long mul(long a, long b) { return a * b; public long div(long a, long b) { return a / b; } { } } } } Auf der Server-Seite kann hier keine Exception auftreten, also kann auf die Deklaration der RemoteException verzichtet werden. Der Client muss dennoch mit RemoteExceptions umgehen können, zB könnte der Server durch Netzwerkprobleme unerreichbar werden. Und einen Server der die Implementierung exportiert: public class CalculatorServer { public static void main(String args[]) throws RemoteException, MalformedURLException { Calculator c = new CalculatorImpl(); Remote calcStub = UnicastRemoteObject.exportObject(c, 0); Naming.rebind("rmi://localhost:1099/CalculatorService", calcStub); } } Dieser Server benutzt eine RMI-Registry am lokalen Rechner auf Port 1099 und exportiert den von UnicastRemoteObject generierten Stub unter dem Namen CalculatorService. Bevor der Server gestartet werden kann, muss eine RMI-Registry am lokalen Rechner gestartet werden, zB über das Kommandozeilenwerkzeugt rmiregistry. Markus Loeberbauer 2010, 2011 65 Praktikum aus Softwareentwicklung 2 Remoting Abschließend noch ein Test-Client der den Remote-Calculator benutzt: public class CalculatorClient { public static void main(String[] args) { try { Calculator c = (Calculator) Naming .lookup("rmi://localhost/CalculatorService"); System.out.println(c.sub(4, 3)); System.out.println(c.add(4, 5)); System.out.println(c.mul(3, 6)); System.out.println(c.div(9, 3)); } catch (MalformedURLException murle) { System.out.println("MalformedURLException"); System.out.println(murle); } catch (RemoteException re) { System.out.println("RemoteException"); System.out.println(re); } catch (NotBoundException nbe) { System.out.println("NotBoundException"); System.out.println(nbe); } catch (java.lang.ArithmeticException ae) { System.out.println("java.lang.ArithmeticException"); System.out.println(ae); } } } Markus Loeberbauer 2010, 2011 66 Praktikum aus Softwareentwicklung 2 XML XML XML (Extensible Markup Language) ist eine Auszeichnungssprache mit hierarchischer Struktur. Die Daten werden als Text abgelegt und mit Metaelementen strukturiert. Java unterstützt XML über die Java API for XML Processing (JAXP). Mit JAXP kann man XML-Dokumente lesen, schreiben, transformieren und erstellen. JAXP ist pluggable implementiert, Objekte werden über Factory-Methoden geliefert und über Interfaces benutzt, somit kann die Implementierung getauscht werden, siehe Abbildung 12. Client API Plugability-Layer Implementations Abbildung 12) JAXP, Plugability-Layer XML-Dokumente Bevor wir uns näher mit der XML-Unterstützung in Java beschäftigen, sehen wir uns XML-Dokumente im Überblick an. Abbildung 13 zeigt ein Adressbuch als Beispiel für ein XML-Dokument. In dem Beispiel sehen wir: XML-Dokumente bestehen aus Deklarationen und einem Baum von Elementen. In der Deklaration wird angegeben welche Version (version) von XML verwendet wird, wie die Zeichen kodiert (encoding) sind und ob auf weitere Dokumente verwiesen wird (standalone). Über das Element DOCTYPE werden das verwendete Schema (Aufbau der XML-Datei) und das Wurzelelement angegeben. Markus Loeberbauer 2010, 2011 67 Praktikum aus Softwareentwicklung 2 XML Jedes Element im Dokument hat einen Namen, optionalen Inhalt und beliebig viele Attribute. Davon optional eine eindeutige ID im Attribut id, der Wert der ID muss im Dokument eindeutig sein. Das erste Element im Dokument heißt root, in unserem Beispiel ist das das Element addressbook. Kinder (children) von addressbook sind Personen (person) und Firmen (companies). Kinder von Personen-Elementen sind zB: der Vorname (firstname) und der Nachname (lastname). Die Kinder und die Kindeskinder bezeichnet man als Nachfolger (descendents), zB die Nachfolger von addressbook sind person, companay, firstname, email, usw. Umgekehrt ist das Eltern-Element (parent) von firstname: person und von person: addressbook. Weitergehend sind alle Vorgänger (ancestors) von firstname: person und addressbook. Elemente auf gleicher Ebene sind Geschwister-Elemente (siblings), zB: firstname, lastname und email sind Geschwister. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE addressbook SYSTEM "addressbook.dtd"> <addressbook owner="p1" date="2005-03-12"> <person id="p1"> <firstname>Thomas</firstname> <lastname>Kotzmann</lastname> <email>[email protected]</email> </person> <company id="c1"> <companyname>Sun</companyname> <url>www.sun.com</url> </company> <person id="p2"> <firstname>Markus</firstname> <lastname>Loeberbauer</lastname> <email>[email protected]</email> Abbildung</person> 13) Beispiel: XML-Dokument Addressbook Markus Loeberbauer 2010, 2011 68 Praktikum aus Softwareentwicklung 2 XML Schemadefinition In der Schemadefinition wird die Struktur von XML-Dokumenten festgelegt. Hat man ein Schema, dann kann man XML-Dokumente gegen dieses Schema prüfen (validieren) und feststellen ob die Dokumente gültig (valide) sind. Die Struktur eines Dokuments besteht aus den Elementen, Attributen und der Verschachtelung der Elemente. Document Type Defnition Die Document Type Definition (DTD) ist eine Schemabeschreibungssprache. Element-Definitionen haben die Syntax: <!ELEMENT element_name (content_model)>; Attribut-Definitionen: <!ATTLIST target_elem attr_name attr_type default …>. Die DTD-Schemadefinition für unser Adressbuchbeispiel ist in Abbildung 14 gegeben. <!ELEMENT <!ATTLIST #IMPLIED> <!ELEMENT <!ATTLIST <!ELEMENT <!ELEMENT <!ELEMENT <!ATTLIST <!ELEMENT <!ATTLIST <!ELEMENT <!ELEMENT addressbook ((person | company)*)> addressbook owner IDREF #REQUIRED date CDATA person (firstname, lastname, email)> person id ID #REQUIRED> firstname (#PCDATA)> lastname (#PCDATA)> email (#PCDATA)> email type (home | business) "business"> company (companyname, url)> company id ID #REQUIRED> companyname (#PCDATA)> url (#PCDATA)> Abbildung 14) Document Type Definition für Beispiel: Addressbook Element-Definitionen haben einen Namen und beschreiben den Inhalt des Elements. Dabei werden folgende Meta-Symbole benutzt: Reihenfolgen: „,“, Alternativen: „|“, Gruppen: „()“, beliebige Wiederholung „*“, mindestens einmal: „+“, optional: „?“, ohne Inhalt: „EMPTY“, beliebiger Inhalt: „ANY“. Markus Loeberbauer 2010, 2011 69 Praktikum aus Softwareentwicklung 2 XML Attribut-Definitionen deklarieren den Namen des betroffenen Elements, den Namen des Attributes, den Attribut-Typ und optional einen Default-Wert. Als Typen gibt es: beliebige Zeichenketten ohne Leerzeichen „CDATA“, eindeutige IDs „ID“, Referenz auf ID „IDREF“ sowie mehrere Referenzen„IDREFS“ und Enumerationen „(…|…|…)“. Der Attribut-Wert kann optional (#IMPLIED), gefordert (#REQUIRED), unveränderlich (#FIXED "value") und vorgegeben ("value") sein. XML-Schema Neben DTD gibt es noch weitere Schemadefinitionssprachen, eine davon ist XMLSchema. XML-Schema beschreibt das Schema von XML-Dokumenten in XML, unterstützt Datentypen zB: einfache Datentypen wie string, integer und boolean, außerdem kann man komplexe (zusammengesetzte) Datentypen spezifizieren. Eine genaue Beschreibung kann man auf www.w3.org finden. DOM-Parser/-Builder Das Document Object Model (DOM) ist eine Hauptspeicherstruktur die den Inhalt einer XML-Datei darstellt. DOM hat eine Knotenstruktur, den DOM-Baum. Auf diesen Baum kann über eine objektorientierte Java-API zugegriffen werden. Abbildung 15 zeigt für einen Ausschnitt des Addressbook-Beispiels den DOMBaum. Markus Loeberbauer 2010, 2011 70 Praktikum aus Softwareentwicklung 2 Document DocumentType … Element: addressbook XML <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE addressbook SYSTEM "addressbook.dtd"> <addressbook Attr: owner = "p1" owner="p1" Attr: date = 2005-03-12 date="2005-03-12"> Element: person Attr: id = "p1" Element: firstname Text: Thomas Element: lastname Text: Kotzmann <person id="p1"> <firstname> Thomas</firstname> <lastname> Kotzmann</lastname> Abbildung 15) DOM-Baum für Beispiel: Addressbook Lesen eines DOM-Baumes Der DOM-Baum kann durch parsen aus einem XML-Dokument aufgebaut werden: Markus Loeberbauer 2010, 2011 71 Praktikum aus Softwareentwicklung 2 XML DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(true); factory.setIgnoringElementContentWhitespace(true); try { DocumentBuilder builder = factory.newDocumentBuilder(); File file = new File("addressbook.xml"); Document doc = builder.parse(file); ... } catch (ParserConfigurationException e) { … } catch (SAXException e) { … } catch (IOException e) { … } Im Dokument kann über Getter navigiert werden, mit getDocumentElement kann das Wurzelelement eines Dokuments geholt werden. Mit getNodeName der Name eines Elements. Attribute können mit Attr getAttribute(String) oder mit NamedNodeMap getAttributes() ausgelesen werden, von der NamedNodeMap kann man Attribute mit getNamedItem abfragen. Kinder kann man mit NodeList getChildNodes() oder NodeList getElementsByTagName() abfragen. Von TextKnoten kann man den Wert über String getNodeValue() auslesen. Der Direkte Zugriff auf ein Element im Dokument erfolgt über die Methode Element getElementById(String). Schreiben eines DOM-Baumes Ein DOM-Baum kann auch geschrieben werden, das ist interessant wenn man einen eingelesenen Baum verändern oder einen Baum programmatisch erzeugen will. Ein neues Dokument kann über die Methode Document DocumentBuilder.newDocument() erzeugt werden. Neue Elemente können über Element Document.createElement(String) erzeugt werden. Erzeugte Elemente können an Parent-Elemente über appendChild angehängt werden. Achtung, an ein Dokument darf nur ein Kind-Element gehängt werden, da das Markus Loeberbauer 2010, 2011 72 Praktikum aus Softwareentwicklung 2 XML Wurzelelement in XML eindeutig sein muss. Attribute von Elementen kann man über setAttribute setzen. Geschrieben wird der Baum über eine XSL-Transformation, wobei hier die identische Transformation erfolgt, also alles gleich bleibt. Es wird nur die Hauptspeicher-Darstellung in einen Datenstrom verwandelt: File file = new File(" ... "); TransformerFactory fact = TransformerFactory.newInstance(); Transformer t = fact.newTransformer(); t.setOutputProperty("doctype-system", "addressbook.dtd"); t.setOutputProperty("indent", "yes"); t.transform( new DOMSource(doc), // source new StreamResult(new FileOutputStream(file)) // target ); SAX-Parser Die Simple API for XML (SAX) ist eine API zum Lesen von XML-Dateien. Ein SAXParser läuft über ein XML-Dokument und ruft dabei Callback-Methoden des Benutzers auf (ereignisorientiert). Es gibt Callback-Methoden für zB den Start/Ende des Dokuments, Start/Ende eines Element und Zeichen-Daten in einem Element. Die Callback-Methoden sind im Interface org.xml.sax.ContentHandler definiert und werden leer in der Hilfsklasse DefaultHandler implementiert. Ein SAX-Parser kann wie folgt benutzt werden: SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setValidating(true); try { SAXParser saxParser = factory.newSAXParser(); File file = new File("addressbook.xml"); saxParser.parse(file, new PrintElementsHandler()); } catch (ParserConfigurationException e1) { … } catch (SAXException e1) { … } catch (IOException e) { … } Markus Loeberbauer 2010, 2011 73 Praktikum aus Softwareentwicklung 2 XML Da SAX ereignis-getrieben ist können mit der SAX-Schnittstelle XML-Dokumente nur gelesen werden. StAX-Reader/-Writer Die Streaming API for XML (StAX) ist eine Datenstrom-orientierte Schnittstelle zum Verarbeiten von XML-Dateien. Die Verarbeitung durch das Programm muss wie bei SAX on-the-fly erfolgen, aber das Parsen ist programmgetrieben. D.h. wie bei einem Iterator oder Cursor fordert das Programm das nächste Element an: FileInputStream fis = null; try { fis = new FileInputStream("addressbook.xml"); XMLInputFactory xmlInFact = XMLInputFactory.newInstance(); XMLStreamReader reader = xmlInFact.createXMLStreamReader(fis); while (reader.hasNext()) { reader.next(); … } } catch (IOException exc) { … } catch (XMLStreamException exc) { … } finally { … } Der StAX-Reader kann über Properties konfiguriert werden (XMLInputFactory.setProperty(String, Object)), zB ob die gelesenen Daten validiert werden sollen kann über das Property XMLInputFactory.IS_VALIDATING bestimmt werden; und ob Namespaces von Tags ausgewertet werden sollen kann über XMLInputFactory.IS_NAMESPACE_AWARE bestimmt werden. Mit StAX kann man auch XML-Dokumente schreiben, wie das Lesen erfolgt auch das Schreiben on-the-fly und programmgetrieben: Markus Loeberbauer 2010, 2011 74 Praktikum aus Softwareentwicklung 2 XML FileOutputStream fos = null; try { fos = new FileOutputStream("addressbook.xml"); XMLOutputFactory xmlOutFact = XMLOutputFactory.newInstance(); XMLStreamWriter writer = xmlOutFact.createXMLStreamWriter(fos); writer.writeStartDocument(); writer.writeStartElement("addressbook"); // … write addressbook … writer.writeEndElement(); writer.flush(); } catch (IOException exc) { … } catch (XMLStreamException exc) { … } finally { … } JAXB-InputStream/-OutputStream Die Java Architecture for XML Binding (JAXB) wird seit Java 6.0, in Version 2.0 mitgeliefert. Über JAXB ist es möglich Java-Objekte zu Serialisieren. Wie serialisiert werden soll kann über Annotationen festgelegt werden. Gelesen werden XMLDateien über das Interface javax.xml.bind.Unmarshaller einen solchen Unmarshaller kann man über JAXBContext.newInstance erzeugen. Damit die Klassen gelesen werden können muss der Kontext mit den benutzten Klassen initialisiert werden. Dabei genügt es allerding die Wurzelklassen anzugeben, JAXB erkennt selbstständig alle statisch referenzierten Klassen. Über Annotationen kann zB festgelegt werden welche Klassen Elemente sein sollen (@XmlRootElement), welche Felder als Elemente (@XmlElement) und welche als Attribute (@XmlAttribute) serialisiert werden sollen. Die Annotationen können über Attribute konfiguriert werden, zB kann der Name eines Elements in der XML Datei über das Attribut „name“ festgelegt werden, Standard ist der Feldname. Lesen eines Adressbuchs: Markus Loeberbauer 2010, 2011 75 Praktikum aus Softwareentwicklung 2 XML AddressBook adr = new AddressBook(); FileInputStream adrFile = null; try { adrFile = new FileInputStream("addressbook.xml"); JAXBContext ctx = JAXBContext.newInstance(AddressBook.class); Unmarshaller um = ctx.createUnmarshaller(); adr = (AddressBook) um.unmarshal(adrFile); } catch (IOException exc) { … } catch (JAXBException exc) { … } finally { … } Schreiben eines Adressbuchs: FileOutputStream adrFile = null; try { adrFile = new FileOutputStream("addressbook.xml"); JAXBContext ctx = JAXBContext.newInstance(AddressBook.class); Marshaller ma = ctx.createMarshaller(); ma.marshal(adr, adrFile); } catch (IOException exc) { … } catch (JAXBException exc) { … } finally { … } XSL-Transformation XML-Transformation mit XSLT ermöglicht die Transformation von XMLDokumenten über XSL-Stylesheets. In einem XSL-Stylesheet beschrieben wie ein XML-Dokument transformiert werden soll, zB kann aus einem XML-Dokument eine HTML-Seite erzeugt werden. Abbildung 16 zeigt ein Stylesheet mit dem Adressbücher in HTML-Seiten verwandelt werden können. In Abbildung 17 ist eine Beispiel-Transformation von einem Adressbuch auf eine HTML-Seite gezeigt. Will man ein XML-Dokument transformieren muss man einen Transformer, eine Source und ein Result anlegen, dann kann man mit dem Transformer das QuellMarkus Loeberbauer 2010, 2011 76 Praktikum aus Softwareentwicklung 2 XML Dokument in die Ziel-Darstellung verwandeln und gegebenenfalls dabei eine Transformation durchführen: // create Transformer TransformerFactory facty = TransformerFactory.newInstance(); File styleSheetFile = new File("addressbook.xsl"); StreamSource styleSheet = new StreamSource(styleSheetFile); Transformer t = facty.newTransformer(styleSheet); // create Source File inFile = new File("addressbook.xml"); Document doc = builder.parse(inFile); DOMSource source = new DOMSource(doc); // create Result File outFile = new File("addressbook.html"); StreamResult result = new StreamResult( new FileOutputStream(outFile)); // start transformation t.transform(source, result); Markus Loeberbauer 2010, 2011 77 Praktikum aus Softwareentwicklung 2 XML <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <html> <head> <title>XML Address Book</title> </head> <body> <table border="3" cellspacing="10" cellpadding="5"> <xsl:apply-templates/> </table> </body> </html> </xsl:template> <xsl:template match="addressbook"> <xsl:apply-templates select="person"/> </xsl:template> <xsl:template match="person"> <tr> <td> <xsl:value-of select="firstname"/> </td> <td> <b><xsl:value-of select="lastname"/></b> </td> <td> <xsl:value-of select="email"/> </td> </tr> </xsl:template> </xsl:stylesheet> Abbildung 16) XSL-Stylesheet von Addressbook.XML nach Addressbook.html Die Adressierung im Stylesheet erfolgt über XPath. Schritte (Steps) in einem XPath-Ausdruck sind über „/“ getrennt. Ein Schritt kann ein Element (element_name), ein Attribut (@attribute_name), eine Bedingung (condition) oder eine Position (index) beschreiben. Beispiele von XPath-Ausdrücken: "*" "/" "/addressbook/*" "/addressbook/person[1]" "/addressbook/*/firstname" Markus Loeberbauer 2010, 2011 Selektiert alle Knoten Wurzelknoten Selektiert alle Elemente unter dem addressbook-Element Liefert die ersten person-Elemente von addressbook-Elementen Liefert alle firstname-Elemente unter den addressbook-Elementen 78 Praktikum aus Softwareentwicklung 2 XML "/addressbook/person[@id="p1"]" Liefert den Personenknoten mit id="p1" "/*/email[@type="home"]" Liefert alle email-Knoten vom Typ "home" <?xml version='1.0' encoding="utf-8"?> <addressbook owner="1"> <person id="1"> <firstname>Thomas</firstname> <lastname>Kotzmann</lastname> <email>[email protected]</email> </person> <person id="2"> <firstname>Markus</firstname> <lastname>Loeberbauer</lastname> <email>[email protected]</email> </person> <html> <head> </addressbook> <META http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>XML-AddressBook</title> </head> <body> <table border="3" cellspacing="10" cellpadding="5"> <tr> <td>Thomas</td> <td><b>Kotzmann</b></td> <td>[email protected]</td> </tr> <tr> <td>Markus</td> <td><b>Loeberbauer</b></td> <td>[email protected]</td> </tr> </table> </body> </html> Abbildung 17) Beispiel Transformation: Addressbook.xml nach Addressbook.html Vergleich der XML-APIs Lesen Schreiben Ändern Speicherbedarf Einfachheit SAX Ja Nein Nein Niedrig Komplex StAX Ja Ja Nein Niedrig Einfach DOM Ja Ja Ja Hoch Einfach JAXB Ja Ja Markus Loeberbauer 2010, 2011 - - Einfach bis Komplex 79 Praktikum aus Softwareentwicklung 2 Java Servlet Java Servlet Java Servlets sind auf Java basierende Web-Komponenten. Sie werden von einem Container verwaltet und können dynamisch Inhalt erzeugen. Ein Container (Servlet-Engine) ist ein Teil eines Web-Servers, leitet Anfragen an die Servlets und liefert Antworten zurück. Der Container verwaltet die Servlets und ihren Lebenszyklus. Abbildung 18 zeigt die grobe Architektur von Webanwendungen die auf Java Servlets aufbauen. Die Spezifikation der Java Servlets kann in JSR 145 für die Version 2.5 und in JSR 315 für die Version 3.0 nachgelesen werden. Die Version 3.0 bringt einige interessante Neuerungen, zB Annotationen in Servlets und File Upload. Es ist aber interessant sich auch mit Version 2.5 zu beschäftigen um die Grundlagen zu verstehen und weil einige Web-Hoster nur Version 2.5 anbieten, zB Google App Engine. Client Firefox, Opera, Reques Web-Server & Servlet- Respons Engine Servlet 1 Servlet 2 Servlet 3 Ressourcen Dateien, Abbildung 18) Grobe Architektur von Webanwendungen mit ServletsDatenbanken, Servlets vs. Common Gateway Interface Java Servlets sind vergleichbar mit dem Common Gateway Interface (CGI) und proprietären Server-Erweiterungen, haben aber folgende Vorteile: Markus Loeberbauer 2010, 2011 80 Praktikum aus Softwareentwicklung 2 Java Servlet Schneller als CGI-Skripte, weil der Code im Speicher bleibt und der Prozess nach einer Anfrage weiterläuft. Bei CGI wird pro Aufruf ein Prozess gestartet Unterstützung für Sitzungen, CGI ist von sich aus zustandslos In Java entwickelt: o Nur von der JavaVM abhängig, aber sonst Systemunabhängig o Große Klassenbibliothek Entwickeln eines Servlets Will man ein Servlet entwickeln, muss man eine Klasse schreiben die das Interface Servlet im Packet javax.servlet implementiert. Servlets sind generisch gehalten, die Methode service(ServletRequest, ServletResponse) bekommt eine Anfrage als ServletReqest und beantwortet diese im ServletResponse. Dabei wird keine Aussage getroffen woher die Anfragen kommen. Meistens schreibt man aber Servlets für das Web, die Http-Anfragen beantworten. Dazu leitet man von HttpServlet ab, das ist eine abstrakte Basisklasse die von GenericServlet ableitet und Servlet implementiert. HttpServlet hat eine Methode für jede Http-Methode (GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT und OPTIONS), zB doGet für Get-Anfragen und doPost für Post-Anfragen. Je nachdem welche Http-Methoden man unterstützen will muss man die zugehörige Servlet-Methode überschreiben. Das Servlet in Abbildung 19 beantwortet Get-Anfragen mit dem Text „Hello!“. Markus Loeberbauer 2010, 2011 81 Praktikum aus Softwareentwicklung 2 Java Servlet public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/plain"); PrintWriter out = response.getWriter(); try { out.println("Hello!"); } finally { out.close(); } } } Abbildung 19) Beispiel Hello World Servlet Installieren von Web-Anwendungen Um Web-Anwendungen in einem Servlet-Container wie zB Tomcat zu installieren muss man einen Deployment Descriptor (web.xml) schreiben. Für das Beispiel in Abbildung 19 ist die web.xml in Abbildung 20 gegeben. Abbildung 21 zeigt die Verzeichnisstruktur von Tomcat 6 (Tomcat 7 hat die gleiche Verzeichnisstruktur) mit installiertem Hello-Beispiel. Im Verzeichnis webapps sind die installierten WebAnwendungen, in unserem Fall hello5. Im Verzeichnis der Web-Anwendung kann beliebiger Inhalt liegen zB: Bilder oder HTML-Dateien, dieser Inhalt kann über Web-Zugriffe abgefragt werden. Die einzige Ausnahme ist das Verzeichnis WEB- INF, das ist vor Zugriffen von außen geschützt. In diesem liegen die Servlets im Verzeichnis classes und falls nötig jar-Dateien im Verzeichnis lib. Markus Loeberbauer 2010, 2011 82 Praktikum aus Softwareentwicklung 2 Java Servlet <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class> at.jku.ssw.psw2.servlet.hello.HelloServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello.do</url-pattern> </servlet-mapping> <session-config> <session-timeout>30</session-timeout> </session-config> <welcome-file-list> <welcome-file>hello.do</welcome-file> </welcome-file-list> </web-app> Abbildung 20) web.xml für Beispiel Hello World Servlet Abbildung 21) Verzeichnisstruktur von Tomcat 6 Markus Loeberbauer 2010, 2011 83 Praktikum aus Softwareentwicklung 2 Java Servlet Lebenszyklus von Servlets Der Servlet-Container lädt die Klasse des Servlets, erzeugt ein (= 1) Objekt, d.h. die Felder werden einmal initialisiert und leben so lange das Servlet-Objekt lebt. Clients (Web-Browser) stellen Anfragen an den Web-Server, dieser ruft die ServletEngine auf und diese leitet die Anfrage an das Servlet weiter. Pro Anfrage erzeugt die Servlet-Engine einen Thread, d.h. mehrere Threads können gleichzeitig Methoden auf einem Servlet-Objekt ausführen. Die Servlet-Engine bestimmt wann ein Servlet weggeworfen wird, dabei muss das Servlet Ressourcen frei geben und eventuell seinen Zustand speichern. Der Lebenszyklus aus der Sicht eines Servlets ist in Abbildung 22 zu sehen. init(ServletConfig) Variablen initialisieren, Ressourcen anfordern service(HttpServletRequest, HttpServletResponse) doGet(HttpServletRequest, HttpServletResponse) doPost(HttpServletRequest, HttpServletResponse) doPut(HttpServletRequest, HttpServletResponse) ... Abbildung 22) Lebenszyklus eines Servlets Sitzungen (Sessions) Http ist ein nicht sitzungsorientiertes Protokoll. Sinnvolle Web-Anwendungen benötigen aber Sitzungen, zB: Warenkorb einer Shop-Anwendung oder Anmeldung einer E-Mail-Anwendung. Intern verwenden Servlets Cookies, URLParameter und versteckte Formular-Felder zur Sitzungs-Verwaltung. Für den Programmierer wird die Sitzungsverwaltung über die Klasse HttpSession abstrahiert. Ein Objekt dieser Klasse kann man von HttpServletRequest.getSession() abfragen. Markus Loeberbauer 2010, 2011 84 Praktikum aus Softwareentwicklung 2 Java Servlet Servlet 3.0 Am 10 Dezember 2009 hat Sun die Spezifikation für Servlet 3.0 veröffentlicht. Mit Version 3.0 werden Annotationen eingeführt mit denen man Informationen aus dem Deployment Descriptor direkt zu den Servlet-Klassen schreiben kann. Das Beispiel Hello World Servlet aus Abbildung 19 kann mit der Annotation @WebServlet angereichert werden, dadurch kann man die Information aus web.xml entfernen. Der neue Code und die neue web.xml ist in Abbildung 23 bzw. Abbildung 24 gegeben. @WebServlet(name = "HelloServlet", urlPatterns = {"/hello.do"}) public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/plain"); PrintWriter out = response.getWriter(); try { out.println("Hello from JavaEE 6!"); } finally { out.close(); } } } Abbildung 23) Beispiel Hello World Servlet mit @WebServlet Annotation Markus Loeberbauer 2010, 2011 85 Praktikum aus Softwareentwicklung 2 Java Servlet <?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <session-config> <session-timeout>30</session-timeout> </session-config> <welcome-file-list> <welcome-file>hello.do</welcome-file> </welcome-file-list> </web-app> Abbildung 24) web.xml für Beispiel Hello World Servlet mit @WebServlet Annotation Um dieses Servlet ausführen zu können braucht man einen Web-Container der Servlets in der Version 3.0 unterstützt, zB: GlassFish Server 3, jetty:// (Experimental) oder Tomcat 7. JavaServer Pages (Erstellen von HTML-Seiten) Servlets sind die Basis der Java Web-Technologie. Will man aber HTML-Seiten aus Servlets erstellen ist das sehr aufwendig. Dazu müsste man programmatisch, über den PrintWriter, den Text in den HttpServletResponse schreiben. Daher hat Sun die JavaServer Pages eingeführt, um diesen Standard-Anwendungsfall zu unterstützen. JavaServer Pages (JSP) werden vom Web-Container in Servlets übersetzt und dann wie alle anderen Servlets behandelt. Der Web-Container übersetzt JSPs bei Veränderungen automatisch neu. Schreiben von JSPs ist Servlet-Programmierung auf abstrakter Ebene. Man kann mit JSP alles machen was man auch mit Servlets machen kann. Am besten ist es jedoch Servlets und JSP als Team zu verwenden: wobei Servlets den Ablauf steuern, die Daten aufbereiten und die Geschäftslogik ansprechen; und JSPs die Darstellung übernehmen. Die Trennung entspricht dem Model-ViewMarkus Loeberbauer 2010, 2011 86 Praktikum aus Softwareentwicklung 2 Java Servlet Controller-Muster und heißt in der Java Servlet Welt „Model 2 Architecture“. Die selten verwendete „Model 1 Architecture“ benutzt JSPs zur Ablaufsteuerung und Darstellung; die Datenaufbereitung und Verbindung zur Geschäftslogik erfolgt in Java Beans. Heute wird fast nur die „Model 2 Architecture“ eingesetzt, weil sie klarer und einfacher ist. Abbildung 25 zeigt eine einfache Hello World JSP, Abbildung 26 zeigt dazu Ausschnitte aus dem generierten Java Servlet. Man kann sehen wie die Zeilen aus der JSP-Datei in einzelne write-Anweisungen im Code verwandelt werden. Will man für eine JSP das generierte Servlet sehen, kann man das in Tomcat unter: work/Catalina/localhost/<Web-Anwendung>/org/apache/jsp/<JSPName>_jsp.java. <%@page contentType="text/html" pageEncoding="UTF-8"%> <html> <head> <title>Hello Page</title> </head> <body> Hello! </body> </html> Abbildung 25) Beispiel Hello World JSP Markus Loeberbauer 2010, 2011 87 Praktikum aus Softwareentwicklung 2 Java Servlet public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent { // ... public void _jspService(HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, ServletException { PageContext pageContext = null; HttpSession session = null; ServletContext application = null; ServletConfig config = null; JspWriter out = null; Object page = this; JspWriter _jspx_out = null; PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out; out.write("\r\n"); out.write("<html>\r\n"); out.write(" <head>\r\n"); out.write(" <title>Hello Page</title>\r\n"); out.write(" </head>\r\n"); out.write(" <body>\r\n"); out.write(" Hello!\r\n"); out.write(" </body>\r\n"); out.write("</html>\r\n"); out.write("\r\n"); } catch (Throwable t) { /* ... */ } finally { _jspxFactory.releasePageContext(_jspx_page_context); } } } Abbildung 26) Generiertes Servlet für Hello World JSP Markus Loeberbauer 2010, 2011 88 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages Dynamischer Kontext in JavaServer Pages Mit JavaServer Pages (JSP) kann man dynamischen Inhalt erzeugen. Dazu kann man: Java-Code in die Seite schreiben, Ausdrücke in der Skriptsprache Expression Language (EL) auswerten und programmierte Tags einfügen. Skriptelemente Skriptelemente sind die älteste und unübersichtlichste Art Logik in JSP einzubringen. Skriptelemente enthalten Java-Code (Scriptlet) der direkt in die Handler-Methode kopiert wird wenn der Web-Container das Servlet aus der JSP erzeugt. Der JSP-Compiler erkennt Scriptlets an dem Klammern-Paar: <% und %>. Abbildung 27 zeigt ein einfaches Beispiel für Scriptlets. Man sieht, dass Scriptlets und HTML beliebig gemischt werden können. Das erste Scriptlet beginnt mit einer Schleife, dann kommt HTML-Code und das zweite Scriptlet schließt die Schleife ab. Der JSP-Compiler kopiert dabei den HTML-Code in out.write-Anweisungen und den Scriptlet-Code direkt in die generierte Handler-Methode. Vertippt man sich im Java-Code, kann die Fehlersuche mühsam sein. Je nach verwendetem Web-Container werden Fehlermeldungen vom Java-Compiler mehr oder weniger auf die eigentliche Fehlerstelle in der JSP-Seite zurückgeführt. Markus Loeberbauer 2010, 2011 89 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages <%@page contentType="text/html" pageEncoding="UTF-8"%> <html> <head><title>JSP Scriptlet Page</title></head> <body> <h1>Let's Count!</h1> <% for (int i = 0; i < 10; ++i) { out.println(i); %> , <% } %> ... </body> 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , ... Let's Count! </html> Abbildung 27) Beispiel-JSP mit Scriptlet In seltenen Fällen muss man in JSP Felder und Methoden deklarieren. Das ist in Deklarations-Elementen möglich, sie werden mit <%! eingeleitet und mit %> abgeschlossen. Abbildung 28 zeigt wie man in einer JSP ein Feld deklariert, hier die Konstante MESSAGE_FORMAT, und wie man Methoden deklariert, hier die Methode format. Wie bei programmierten Servlets muss man auch bei durch JSP generierten Servlets darauf achten, dass durch ein Objekt mehrere Benutzeranfragen gleichzeitig bearbeitet werden können. Also auch hier gilt, Felder sind potentiell ein Synchronisations-Problem. Markus Loeberbauer 2010, 2011 90 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages <%@page contentType="text/html" pageEncoding="UTF-8" import="java.util.Date"%> <html> <head><title>JSP Declarations Page</title></head> <body> <% String message = format("Hello World!"); out.println(message); %> </body> <%! private static final String MESSAGE_FORMAT = "%s : %s"; %> <%! %> </html> private String format(String message) { return String.format(MESSAGE_FORMAT, new Date(), message); } Fri Jun 04 08:27:27 CEST 2010 : Hello World! Abbildung 28) Beispiel-JSP mit Feld- und Methoden-Deklaration Im Beispiel in Abbildung 28 rufen wir eine Methode auf und geben das Ergebnis in der Web-Seite aus. Das wird in JSP mit Expression-Elementen unterstützt. Expression-Elemente werden mit <%= eingeleitet und mit %> abgeschlossen. Innerhalb eines Expression-Elements muss ein Ausdruck stehen, zB: eine Berechnung oder ein Methodenaufruf mit Rückgabewert. In Abbildung 29 werden Expression-Elemente verwendet, um das Ergebnis der Methode format und um die Zahlen 0 bis 9 in einer Schleife auszugeben. Kommentare in JSP werden in <%-- und --%> eingeschlossen, diese Kommentare kommen nur in der JSP vor. Will man Kommentare auch im generierten Servlet wiederfinden, dann muss man sie als normale Java-Kommentare in Scriptlets schreiben. Markus Loeberbauer 2010, 2011 91 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages <%@page contentType="text/html" pageEncoding="UTF-8" import="java.util.Date"%> <html> <head><title>JSP Expressions Page</title></head> <body> <%= format("Hello World!") %><br> <% for (int i = 0; i < 10; ++i) { %> <%= i %> <% } %> </body> <%! private static final String MESSAGE_FORMAT = "%s : %s"; %> <%! %> </html> private String format(String message) { return String.format(MESSAGE_FORMAT, new Date(), message); } Fri Jun 04 08:42:31 CEST 2010 : Hello World! 0123456789 Abbildung 29) Beispiel-JSP mit Expressions Weitere Befehle in JSP: <jsp:include>: Einfügen einer anderen Seite zur Laufzeit. <jsp:forward>: Weiterleiten an eine andere Seite. <jsp:param>: Parameter an eine andere Seite weitergeben, die mit <jsp:include> oder <jsp:forward> verwendet wird. <jsp:useBean>: Verwenden von JavaBean-Komponenten in JSP. Syntax: <jsp:useBean id="Instanzname" scope="Geltungsbereich" class="Klassenname"/> Markus Loeberbauer 2010, 2011 92 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages <jsp:getProperty> und <jsp:setProperty>: Abfragen bzw. Setzen eines Bean-Properties. Die JSP-Syntax der Skriptelemente mit dem Prozentzeichen passt nur schlecht zum XML-Stil von HTML. Deshalb gibt es für die Skriptelemente eine XML-Syntax. Dabei heißen die Tags: <jsp:scriptlet> für Scriptlets, <jsp:expression> für Ausdrücke und <jsp:declaration> für Deklarationen. Dabei ist zu beachten, dass innerhalb einer Seite entweder konsequent die JSP-Syntax oder die XML-Syntax verwendet werden muss. Einen Überblick über die gesamte Syntax von JSP kann man im JSR 152 oder unter http://java.sun.com/products/jsp/docs.html bekommen. Implizite Objekte in JSP In den Scriptlet-Beispielen haben wir mit out.println Text in die Web-Seite geschrieben. Aber wo kommt dieses out her? In JSP werden einige Objekte in der Handler-Methode implizit zur Verfügung gestellt: out, Klasse: JspWriter (Schreiben von Text in die Web-Seite) request, Klasse: HttpServletRequest response, Klasse: HttpServletResponse session, Klasse: HttpSession application, Klasse: ServletContext config, Klasse: ServletConfig exception, Klasse: JspException (Nur verfügbar in Error-Pages. Treten Fehler in Servlets oder JSP auf wird standardmäßig eine technische Fehlerseite erzeugt, diese hilft dem Entwickler, für den Anwender wirkt sie aber unprofessionell. Daher kann man in einer JSP page-Direktive eine JSP angeben die im Fehlerfall angezeigt werden soll, zB: <%@ page errorPage="/error.jsp" %>. Auch im Deployment Descriptor (web.xml) können mit error-page-Abschnitten Fehlerbehandlungsseiten angegeben werden. Mit error-code können http-Fehlercodes abgefangen Markus Loeberbauer 2010, 2011 93 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages werden (zB: 404); und mit exception-type können Exceptions abgefangen werden. pageContext, Klasse: PageContext (Kapselt die Impliziten Objekte, kann zB benutzt werden, um diese an eine Methode zu übergeben. page, Klasse: Object (Verweis auf das Seiten-Objekt = this-Pointer) Expression Language Skriptelemente verleiten dazu, zu viel Java-Code in eine JSP einzubetten, daher möchte man gerne auf Skriptelemente verzichten. Die Verwendung von Beans ist häufig zu aufwändig oder zu eingeschränkt. Aus diesen Gründen hat Sun mit JSP 2.0 die Expression Language (EL) eingeführt. Ausdrücke in EL beginnen mit einem Dollar-Zeichen und sind in geschwungene Klammern eingeschlossen, zB: ${42.0 / 23}, ${person.name} oder ${car.engine.power}. EL erlaubt den Zugriff auf Properties von Properties, usw. Damit ist es mächtiger als die JSP-Bean-Syntax. Und dabei wesentlich kompakter. Für jeden PropertyZugriff kommt ein Punkt gefolgt vom Namen des Properties. Existiert in einer Klasse eine Methode getName(), dann heißt das Property name. Der Zugriff erfolgt damit über ${objektname.name}. Der Aufbau von EL-Ausdrücken ist in Abbildung 30 zu sehen. Markus Loeberbauer 2010, 2011 94 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages ${ersterBezeichner.weitereBezeichne Implizite Objekte pageScope pageScope requestScope requestScope sessionScope sessionScope applicationScope applicationScope param Objekte werden paramValues zuerst im Page-, Schlüssel einer Map oder Property Muss sich an die Java Namenskonvention halten! dann im Request-, header dann im Session- headerValues und wenn sie dort auch nicht gefunden Abbildung 30) Aufbau eines EL-Ausdrucks Muss man auf Elemente in einer Map oder Liste zugreifen, dann verletzen die Zugriffsnamen häufig den Java-Namenskonventionen. In diesem Fall gibt es den Klammer-Operator ([]) als Alternative zum Dot-Operator(„.“). Die Syntax sieht dann wie folgt aus: ${objektname[bezeichner]} wobei objektname eine Map, ein Bean, eine List oder ein Array sein kann; bezeichner ist ein Schlüssel in der Map, ein Property, ein Listen-Index bzw. ein Array-Index. Der Zugriff kann auch verschachtelt sein zB: ${cars[favoiritCars[0]]}. Achtung, Property-Namen müssen unter Anführungszeichen gestellt werden, sonst wird der Name selbst als Objekt gesucht, zB: der Ausdruck im Dot-Stil ${person.name} entspricht ${person["name"]} im Klammer-Stil. Verwendet man nur ${person[name]}, dann wird zuerst name ausgewertet und das Ergebnis als Property-Name in person gesucht. Markus Loeberbauer 2010, 2011 95 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages Operationen in der Expression Language Mit EL kann man arithmetische und logische Operationen ausdrücken: Addition (+), Subtraktion (-), Multiplikation (*), Division (/, div) und Divisionsrest (%, mod) sowie logisches Und (&&, and), Oder (||, or) und Nicht (!, not). Vergleiche durchführen: Gleichheit (==, eq), Ungleichheit (!=, ne), Kleiner (<, lt), Grösser (>, gt), Kleiner gleich (<=, le) und Grösser gleich (>=, ge). Die folgenden Literale sind in EL definiert: true, false, null, und empty (zeigt an ob ein Attribut gesetzt ist, zB: ${not empty persons}. Das Schlüsselwort instanceof ist reserviert, hat aber noch keine Bedeutung. Java Standard Tag Libaray Mit EL kann man einfach auf einzelne Werte zugreifen. Häufig benötigt man aber auch einfache Kontrollstrukturen wie Iterationen oder Alternativen. Dazu kann die Java Standard Tab Library (JSTL) genutzt werden. Seit JSP 2.1 (JavaEE 5) ist die JSTL in Version 1.2 Teil der JavaEE-Spezifikation. Damit man die JSTL in Tomcat nutzen kann muss man die Dateien jstl.jar und standard.jar entweder in das lib-Verzeichnis von Tomcat oder seiner WebAnwendung kopieren. Die Dateien sind in der Standard-Installation von Tomcat in den Beispielanwendungen zu finden. Die JSTL ist nach Aufgaben in die Bereiche Core, Formatierung, Funktionen, SQL Datenbank-Zugriff und XML Verarbeitung geteilt. Wir werden kurzen Einblick auf den Core der JSTL geben; weitere Informationen können im JSR-52 nachgelesen werden. Wichtige Tags der Core Tag Library sind: <c:out>, <c:forEach>, <c:if>, <c:choose>, <c:when>, <c:otherwise>, <c:url> und <c:param>. <c:out> Mit dem Tag out kann man berechnete Ausgaben in die Webseite schreiben. Dabei wird null mit einem gegebenen Default-Wert ersetzt, ist keiner gegeben mit Markus Loeberbauer 2010, 2011 96 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages dem leeren String. Wichtig ist auch, dass folgende HTML-Sonderzeichen ersetzt werden: < gegen &lt; > gegen &gt; & gegen &amp; ' gegen &#039; " gegen &#034; <c:forEach> Mit dem forEach-Tag kann man über Sammlungen oder Zahlenbereiche iterieren, Abbildung 31 zeigt eine Beispielanwendung. In der page-Direktive wird isELIgnored auf false gesetzt, das stellt sicher, dass die EL-Ausdrücke richtig übersetzt werden. In Tomcat 6 und Glassfish 3 ist das bereits der Standardwert. In der Beispielanwendung wird davon ausgegangen, dass eine Sammlung mit dem Namen „Cars“ in einem der Scopes verfügbar ist. <%@ page isELIgnored="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> Print all cars: <c:forEach var="car" items="${Cars}"> ${car}<br /> </c:forEach> Print the numbers from 0 to 23 with a step of 5: <c:forEach begin="0" end="23" step="5" varStatus="counter"> ${counter.count}<br /> </c:forEach> </body> ... </html> Abbildung 31) JSTL Beispiel mit <c:forEach> Markus Loeberbauer 2010, 2011 97 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages <c:if> Der Tag if wird eingesetzt wenn optionale Teile in einer JSP vorhanden sind, Abbildung 32 zeigt ein Beispiel. Anders als in den meisten Programmiersprachen fehlt bei dem if-Tag in JSTL ein else-Zweig, benötigt man eine Auswahl aus mehreren Alternativen muss man entweder jede Alternative mit einem if-Tag eindeutig beschreiben oder einen choose-Tag benutzen. <%@ page isELIgnored="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" <html> <body> <c:if test="${car eq 'Smart'}"> Be smart drive Smart! </c:if> <c:if test="${car eq 'SUV'}"> Real men drive hard! </c:if> </body> ... </html> %> Abbildung 32) JSTL Beispiel mit <c:if> <c:choose>, <c:when>, <c:otherwise> Der choose-Tag entspricht der switch-Anweisung oder if-else-if-Kaskade in Programmiersprachen, Abbildung 33 zeigt ein Beispiel. Markus Loeberbauer 2010, 2011 98 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages <%@ page isELIgnored="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" <html> <body> <c:choose> <c:when test="${car eq 'Smart'}"> Be smart drive Smart! </c:when> <c:when test="${car eq 'SUV'}"> Real man drive hard! </c:when> <c:otherwise> Porsche, all a lady can expect. </c:otherwise> </c:choose> </body> ... </html> %> Abbildung 33) JSTL Beispiel mit <c:choose>, <c:when> und <c:otherwise> <c:url>, <c:param> Mit <c:url> kann man Urls mit Parametern in JSP zusammensetzen, siehe Abbildung 34. Dieser Tag ermöglicht Webanwendungen ohne Cookies, dazu fügt der url-Tag eine Session-ID als Parameter an die Url, falls der Client keine Cookies unterstützt. <%@ page isELIgnored="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> Please visit our <a href="<c:url value='exhibition.do'> <c:param name='color' value='${customer.favouritColor}' /> </c:url>"> car exhibition</a> to see your next vehicle! <a href="<c:url value='logout.jsp' />" >Logout</a> </body> </html> Abbildung 34) JSTL Beispiel mit <c:url> und <c:param> Markus Loeberbauer 2010, 2011 99 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages JSP aus Servlets Nutzen In sauber entwickelten Programmen sollte die Businesslogik von Servlets aus angesprochen und die Oberfläche in JSP gestaltet werden. Diese Vorgangsweise heißt bei JavaEE Model 2 Architektur und setzt das Model-View-Controller-Muster bei Webanwendungen um. Über die Klasse javax.servlet.RequestDispatcher kann man von einem Servlet auf ein anderes Servlet oder eine JSP umleiten. Einen RequestDispatcher kann man aus dem ServletContext über die Methode getRequestDispatcher(String absolutPath) aus einem Absoluten Pfad holen. Hier darf der Pfad auch in das Verzeichnis WEB-INF zeigen, obwohl dieses Verzeichnis gegen Zugriffe von außen geschützt ist. Oder über die Methode getNamedDispatcher(String name) aus einem Namen erzeugen, der Name entspricht dem Namen (servlet-name) aus dem Deployment Descriptor (web.xml). Relativ zum aktuellen Servlet kann man sich auch aus dem ServletRequest über die Methode getRequestDispatcher(String path) einen RequestDispatcher holen. Hat man einen RequestDispatcher, dann kann man den Request über die Methoden include oder forward weiterleiten. Nutzt man die Methode include behält man die Kontrolle und kann nachdem das gerufene Servlet fertig ist noch etwas ausgeben oder aufräumen. Meistens ist das aber unnötig und man verwendet forward, dabei gibt man die Kontrolle an das gerufene Servlet ab. Die aufbereiteten Daten gibt man an das gerufene Servlet als Attribute im HttpServletRequest mit. Attribute kann man in den Request über die Methode setAttribute(String name, Object o) stellen und über getAttribute(String name) abfragen. In einer JSP kann man Attribute über EL abfragen, wobei man den Namen des Attributes angeben muss, zB: ${name}. Abbildung 35 zeigt ein Servlet, das den Parameter name aus dem Request ausliest, den gelesenen Wert in Großbuchstaben umwandelt, das Ergebnis als Markus Loeberbauer 2010, 2011 100 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages Attribut ablegt und den Request an eine JSP weiterleitet. Wobei die JSP im Verzeichnis WEB-INF liegt (und damit vor direkten Zugriffe von außen geschützt ist). Die zugehörige JSP ist in Abbildung 36 gezeigt. @WebServlet(name = "Greeter", urlPatterns = {"/Greeter"}) public class Greeter extends HttpServlet { public static final String NAME_PARAMETER = "name"; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter(NAME_PARAMETER); String upperCaseName; upperCaseName = name != null ? name.toUpperCase() : "???"; request.setAttribute(NAME_PARAMETER, upperCaseName); RequestDispatcher rd = getServletContext() .getRequestDispatcher("/WEB-INF/jsp/namePrinter.jsp"); rd.forward(request, response); } } Abbildung 35) Beispiel: Servlet mit Request-Weiterleitung <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Name Printer Page</title> </head> <body> <h1>Hello <c:out value="${name}"/>!</h1> </body> </html> Abbildung 36) Beispiel: JSP das einen Namen über EL und <c:out> ausgibt. Markus Loeberbauer 2010, 2011 101 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages Schemadefinitionen für web.xml Der Deployment Descriptor (web.xml) ist eine XML-Datei, hier sind die Rahmen für die gängigen Versionen abgebildet. Version 2.3 wird noch über DTD beschrieben, d.h. die Elemente innerhalb der web.xml müssen in der richtigen Reihenfolge angegeben werden, zB: servlet vor servlet-mapping. Version 2.3 <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <!-- ... --> </web-app> Version 2.4 <?xml version="1.0" encoding="ISO-8859-1"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <!-- ... --> </web-app> Version 2.5 <?xml version="1.0" encoding="ISO-8859-1"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <!-- ... --> </web-app> Version 3.0 <?xml version="1.0" encoding="UTF-8"?> Markus Loeberbauer 2010, 2011 102 Praktikum aus Softwareentwicklung 2 Dynamischer Kontext in JavaServer Pages <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <!-- ... --> </web-app> Markus Loeberbauer 2010, 2011 103 Praktikum aus Softwareentwicklung 2 JSP Custom Tags JSP Custom Tags EL und die JSTL ersetzen Scriptlets in weiten Teilen von Web-Anwendungen, manchmal braucht man aber Funktionen die in der JSTL fehlen. Da wir auf Scriptlets verzichten wollen, um die JSP-Seiten sauber zu halten brauchen wir eine Lösung eigenen Code in Tags zu packen. In JSP können wir dazu Custom Tags implementieren. Seit JSP 2.0 (zeitgleich mit Servlet API 2.4 und J2EE 1.4) gibt es mit Simple Tags und Tag Files bequeme Möglichkeiten eigene Tags zu erzeugen. Vor JSP 2.0 war es komplexer eigene Tags zu implementieren. Sollte es notwendig sein die alte API zu verwenden findet man dazu Dokumentation wenn man nach JSP classic Tags sucht. Tag Files Tag Files sind die einfachste Art eigene Tags zu erstellen. Mit Tag Files kann man Teile einer JSP-Seite auslagern, zB: Header oder Footer die auf allen Seiten einer Webanwendung gleich sind. Tag Files können parametriert werden, zB der Titel der Web-Seite im Header. Größere Text- und Html-Blöcke können als Tag-Body übergeben werden. Installiert werden Tag Files indem man sie im Verzeichnis WEB-INF/tags/ (oder einem Unterverzeichnis davon) ablegt, um sie zu nutzen muss man eine taglibDirektive in die JSP-Seite einfügen. Über die taglib-Direktive muss angegeben werden welcher Präfix verwendet werden soll und wo die Tags abgelegt sind. In Abbildung 37 ist eine JSP-Seite zu sehen die Tag Files nutzt. In der taglibDirektive ist angegeben, dass die Tags mit dem Präfix psw2tf angesprochen werden, und dass die Tags direkt im Verzeichnis WEB-INF/tags liegen. Das Beispiel nutzt die Tags header, footer und disclaimer. Mit dem Tag header wird der HTMLSeiten-Header eingebunden, er benötigt den Parameter title, dieser wird als Titel der Webseite ausgegeben. Parameter in einem Tag-File werden mit der Direktive attribute bekannt gemacht. Die Implementierung ist in Abbildung 38 (Datei: Markus Loeberbauer 2010, 2011 104 Praktikum aus Softwareentwicklung 2 JSP Custom Tags header.tag) gegeben. Weiter nutzt die Seite den Tag footer der die Webseite abschließt (siehe Abbildung 39, Datei: footer.tag), er ist parameterlos. Und den Tag disclaimer (siehe Abbildung 40) der den übergebenen Body als Parameter verwendet und als Disclaimer-Text ausgibt. Will man Tag-Files in mehreren Anwendungen verwenden, dann lohnt es sich diese in eine JAR-Datei zu verpacken. Tag-Files in JAR-Dateien müssen im Verzeichnis META-INF/tags/ (oder einem Unterverzeichnis davon) liegen und eine Beschreibung der Tags muss als Tag Library Descriptor vorliegen. Der Aufbau dieser Beschreibungen ist im Abschnitt Simple Tags beschrieben. <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="psw2tf" tagdir="/WEB-INF/tags/" %> <psw2tf:header title="Hello Page"/> <h1>Hello PSW2 Students!</h1> <p> Welcome to the shiny world of JSP programming. In this course you will learn everything you&#39;ll ever need! </p> Hello PSW2 <psw2tf:disclaimer> Everything written on this Students! page may be exaggerated, or just plain wrong. Welcome to the shiny world of JSP programming. In this course you will learn everything you'll ever </psw2tf:disclaimer> need! <psw2tf:footer /> Abbildung 37) JSP Seite mit Custom Tags aus Tag Files Markus Loeberbauer 2010, 2011 105 Praktikum aus Softwareentwicklung 2 JSP Custom Tags <%@tag description="standard header tag for our web application" pageEncoding="UTF-8"%> <%@attribute name="title" required="true" description="title of the page" rtexprvalue="true"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>${title}</title> </head> <body> Abbildung 38) Beispiel Tag mit Parameter (Seiten-Header) <%@tag description="standard footer for our web application" pageEncoding="UTF-8"%> </body> </html> Abbildung 39) Beispiel Tag File ohne Parameter (Seiten-Footer) <%@tag description="web site disclaimer" pageEncoding="UTF-8"%> <p> Disclaimer<br> <em> <jsp:doBody /> </em> </p> Abbildung 40) Beispiel Tag File das den Body des Tags nutzt (Disclaimer). Simple Tags Tag-Files vermeiden Code-Verdopplung in JSP-Seiten, aber wenn man Programmlogik braucht sind Tag Files zu wenig. Dann kommen Custom-Tags ins Spiel. Die einfachste Möglichkeit Custom-Tags zu programmieren sind Simple Tags. Jeder Simple-Tag muss das Interface SimpleTag implementieren, in der Praxis leitet man dafür von der Klasse SimpleTagSupport ab. Markus Loeberbauer 2010, 2011 106 Praktikum aus Softwareentwicklung 2 JSP Custom Tags <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="psw2tag" uri="http://ssw.jku.at/Teaching/Lectures/PSW2/2010/" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head><title>Students</title></head> <body> Active <h1>Active Students</h1> <ol> Students <psw2tag:StudentFilter activeOnly="true" students="${students}" 1. Susi Brain var="student"> 2. Max Power <li>${student.name}</li> </psw2tag:StudentFilter> All Students </ol> <h1>All Students</h1> 1. Susi Brain <ol> <psw2tag:StudentFilter students="${students}"> <li>${var.name}</li> </psw2tag:StudentFilter> </ol> </body> </html> Abbildung 41) Beispiel-JSP-Seite mit Simple Tags Abbildung 41 zeigt eine JSP-Seite die Studenten, mit Hilfe des Custom Tags StudentFilter, anzeigt. Mit der Direktive taglib wird die Tag-Library in die Seite eingebunden. Abbildung 42 zeigt den Tag StudentFilterTag. Markus Loeberbauer 2010, 2011 107 Praktikum aus Softwareentwicklung 2 JSP Custom Tags public class StudentFilterTag extends SimpleTagSupport { private String controlVariable = "var"; private boolean activeOnly; private Iterable<Student> students; @Override public void doTag() throws JspException, IOException { JspFragment body = getJspBody(); if (body == null) { return; } JspContext context = getJspContext(); for (final Student student : students) { if (activeOnly && !student.isActive()) { continue; } context.setAttribute(controlVariable, student); body.invoke(null); } } public void setStudents(Iterable<Student> students) { this.students = students; } public void setActiveOnly(boolean activeOnly) { this.activeOnly = activeOnly; } public void setVar(String controlVariable) { this.controlVariable = controlVariable; } } Abbildung 42) Beispiel Simple Tag; Liefert die Studenten der Reihe nach, gefiltert nach Aktivität Simple Tags erben von SimpleTagSupport und überschreiben die Methode doTag, diese wird zum Rendern des Tags aufgerufen. Der Lebenszyklus eines Tags ist wie folgt: 0. Der Web-Container lädt die Tag-Klasse (bei der ersten Verwendung eines Tags). 1. Erzeugt ein Objekt des gewünschten Tags über den Default-Konstruktor. 2. Setzt den JSP-Context über setJSPContext. 3. Setzt den Eltern-Tag, falls der Tag in einem anderen Tag geschachtelt ist. 4. Setzt alle Attribute über die Setter-Methoden. Markus Loeberbauer 2010, 2011 108 Praktikum aus Softwareentwicklung 2 JSP Custom Tags 5. Setzt den Body über setJSPBody, falls der Tag einen Body hat. 6. Ruft die Methode doTag auf. 7. Verwirft das Tag-Objekt. Aus dem Lebenszyklus sehen wir, dass eine Tag-Klasse einen einen parameterlosen Default-Konstruktor benötigt und ein Tag-Objekt nur einmal benutzt wird, d.h. man kann sich darauf verlassen, dass die Attribute die richtigen Werte haben; und manuelle Bereinigung des inneren Zustands unnötig ist. Damit der Web-Container die Tags findet muss man eine Tag Library Description (TLD) schreiben. Abbildung 43 zeigt den TLD für unseren StudentFilter, darin ist festgelegt welche Klasse für den Tag verwendet werden soll und welche Attribute der Tag hat. Ein TLD hat einen eindeutigen Namen (uri), eine Kurzbezeichnung (short-name), eine Version (tlib-version) und optionale Daten wie zB: eine Beschreibung (description) und ein Icon (icon). In einer TLD können beliebig viele Tags (tag) und Tag Files (tag-file) beschrieben werden. Die TLD-Dateien (Endung .tld) muss im Verzeichnis WEB-INF oder einem Unterverzeichnis davon liegen. Häufig legt man sie in das Verzichnis WEB-INF/tlds oder in das Verzeichnis wo die geschützten JSPs liegen (häufig WEB-INF/jsp). Abbildung 44 zeigt die Datenklasse Student für das Beispiel. Ein Student hat einen Namen und einen Status ob er aktiv ist. Abbildung 45 zeigt das Servlet mit der Anbindung an die Geschäftslogik und dem Aufruf der Seite showStudents.jsp. Anmerkung: Alle notwendigen Klassen um Tags zu implementieren liegen im Paket javax.servlet.jsp.tagext. Markus Loeberbauer 2010, 2011 109 Praktikum aus Softwareentwicklung 2 JSP Custom Tags <?xml version="1.0" encoding="UTF-8"?> <taglib version="2.1" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"> <short-name>psw2tag</short-name> <tlib-version>1.0</tlib-version> <uri>http://ssw.jku.at/Teaching/Lectures/PSW2/2010/</uri> <tag> <name>StudentFilter</name> <tag-class> at.jku.ssw.psw2.jsptutorial.tag.StudentFilterTag </tag-class> <body-content>scriptless</body-content> <attribute> <name>students</name> <required>true</required> <rtexprvalue>true</rtexprvalue> <type>java.util.Iterable</type> </attribute> <attribute> <name>activeOnly</name> <required>false</required> <rtexprvalue>true</rtexprvalue> <type>boolean</type> </attribute> <attribute> <name>var</name> <required>false</required> <rtexprvalue>true</rtexprvalue> <type>java.lang.String</type> <description>control variable</description> </attribute> </tag> </taglib> Abbildung 43) Tag Library Descriptor für den Beispiel-Tag StudentFilterTag Markus Loeberbauer 2010, 2011 110 Praktikum aus Softwareentwicklung 2 JSP Custom Tags package at.jku.ssw.psw2.jsptutorial.model; public class Student { private final String name; private final boolean active; public Student (final String name, final boolean active) { this.name = name; this.active = active; } public String getName() { return name; } public boolean isActive() { return active; } } Abbildung 44) Beispiel-Datenklasse: Student public class ShowStudentsServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setAttribute("students", getStudents()); RequestDispatcher rd = request.getRequestDispatcher( "/WEB-INF/jsp/showStudents.jsp"); rd.forward(request, response); } private Iterable<Student> getStudents() { // read students, e.g. from a database // ... return students; } } Abbildung 45) Beispiel-Servlet: Liest Studenten und leitet an die Beispiel-JSP-Seite weiter Markus Loeberbauer 2010, 2011 111 Praktikum aus Softwareentwicklung 2 Applets Applets Mit Applets hat Sun die Möglichkeit geschaffen Java Programme in Webseiten einzubetten. Applets können mit AWT und mit Swing entwickelt werden, dazu muss man eine Klasse von java.applet.Applet bzw. javax.swing.JApplet ableiten. Damit Applets auf dem lokalen Rechner keinen Schaden anrichten können werden sie in einer sicheren Umgebung (Sandbox) ausgeführt. Dadurch wird verhindert, dass: Applets auf das lokale Dateisystem zugreifen, Netzwerkverbindungen aufbauen, gefährliche System-Aufrufe ausführen (zB: System.exit), die Zwischenablage auslesen oder sicherheitskritische System-Properties abfragen. Benötigt man Zugriff auf diese sicherheitskritischen Dinge muss man sein Applet signieren, dann fragt Java den Benutzer ob er dem Applet die Zugriffe erlaubt. Der Lebenszyklus von Applets besteht aus vier Methodenaufrufen: 1. init, wird aufgerufen sobald das Applet geladen wird, hier kann zB: die GUI aufgebaut, Threads gestartet oder Ressourcen geladen werden 2. start, wird jedes Mal aufgerufen wenn das Applet angezeigt wird, zeigt ein Applet eine Animation, kann diese hier gestartet werden 3. stop, der Browser ruft diese Methode wenn das Applet nicht mehr angezeigt wird, hier kann die Animation wieder gestoppt werden 4. destroy, hier können Ressourcen freigegeben werden Wir beschäftigen uns hier mit der Swing-Version von Applets, und wie Swing allgemein sind auch JApplets nicht threadsicher. Das heißt, Änderungen an der GUI müssen im GUI-Thread erfolgen. Abbildung 46 zeigt eine JSP-Seite mit einem Applet, im Tag applet wird festgelegt welche Ausmaße das Applet haben soll, wo die Jar-Datei liegt und wie die AppletKlasse heißt. Im Body des Applet-Tags können Parameter angegeben werden, in unserem Fall wird der Parameter name auf Alex gesetzt. Außerdem ist im Body Markus Loeberbauer 2010, 2011 112 Praktikum aus Softwareentwicklung 2 Applets ein Text hinterlegt der angezeigt werden soll falls der Browser nicht mit Applets umgehen kann. Die zugehörige Applet-Klasse ist in Abbildung 47 gegeben. <%@page contentType="text/html" pageEncoding="UTF-8"%> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>Applet Test</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <applet width="200" height="150" code="at.jku.ssw.psw2.applet.TestApplet" codebase="<c:url value="/"/>" archive="TestApplet.jar"> <param name="name" value="Alex"/> Please enable Applets in your browser. </applet> </body> </html> Abbildung 46) Beispiel JSP-Seite mit Applet Markus Loeberbauer 2010, 2011 113 Praktikum aus Softwareentwicklung 2 Applets package at.jku.ssw.psw2.applet; public class TestApplet extends JApplet { private String name; private JLabel label; @Override public void init() { name = getParameter("name"); try { SwingUtilities.invokeAndWait(new Runnable() { public void run() { label = new JLabel("Name: " + name); getContentPane().add(label, BorderLayout.CENTER); } }); } catch (Exception ex) { Logger.getLogger(TestApplet.class.getName()) .log(Level.SEVERE, null, ex); } // allocate resources here } @Override public void start() { // start animations here } @Override public void stop() { // stop animations here } @Override public void destroy() { // cleanup resources here } } Abbildung 47) Beispiel Applet, zeigt den Wert des Parameters "name" in einem Label an Markus Loeberbauer 2010, 2011 114 Praktikum aus Softwareentwicklung 2 Java Service Java Service Ein Java Service ist ein Interface, Objekte die dieses Interface implementieren können zur Laufzeit angefordert werden. Implementierungen eines Java Services (Java Service Provider) müssen das Service-Interface implementieren, in einer JarDatei liegen und dort als Service Provider registriert werden. Dazu muss man im Verzeichnis META-INF/services/ eine Text-Datei mit dem Namen des Interfaces anlegen, als Inhalt der Datei muss der voll qualifizierte Name der Klasse angegeben werden. Es können in der Datei auch mehrere Klassen angegeben werden, diese müssen jeweils in einer eigenen Zeile stehen. Für Verwender gibt es seit Java 6 mit der Klasse java.util.ServiceLoader eine bequeme Möglichkeit Service Provider zu erzeugen. In Java Versionen vor 6 muss man die Dateien im Verzeichnis services auslesen (zB mit Class.getResourceAsStream) und Service Provider über Reflection (zB mit Class.forName und Class.newInstance) anlegen. Services können verwendet werden, um Anwendungen erweiterbar oder Teile davon austauschbar zu machen; oder um Anwendungen klar zu strukturieren. In der Praxis werden Java Services (zB in Java selbst) eingesetzt, um XML-Provider und JDBC-Treiber einzubinden. NetBeans nutzt Java Services als Basis des Plug-inMechanismus. Hinweis! Als Services werden häufig Factories geliefert mit denen man dann die gewünschten Objekte erzeugen kann. Beispiel Abbildung 47 zeigt eine Beispiel-Klasse die Services für das Interface Runnable lädt und die run-Methoden der gefunden Services ausführt. In Abbildung 48 ist ein Service abgebildet, das das Interface Runnable implementiert. Markus Loeberbauer 2010, 2011 115 Praktikum aus Softwareentwicklung 2 Java Service public class Executor { public static void main(String[] args) { ServiceLoader<Runnable> runnableLoader = ServiceLoader.load(Runnable.class); for (Runnable runnable : runnableLoader) { runnable.run(); } } } Abbildung 47) Beispiel: Java Service, Executor lädt alle Services die Runnable implementieren und führt ihre run-Methode aus HelloService.java: package at.jku.ssw.psw2.javaservice.provider; public class HelloService implements Runnable { @Override public void run() { System.out.println("Hello!"); } } META-INF/services/java.lang.Runnable: Abbildung 48) Beispiel Java Service: HelloService implementiert das Interface Runnable at.jku.ssw.psw2.javaservice.provider.HelloService und ist im Verzeichnis META-INF/services/ registriert Damit der Executor das Hello-Service findet muss das Hello-Service in eine JarDatei verpackt werden und die Jar-Datei in den Klassenpfad von Executor eingefügt werden. Jar-Dateien können mit dem Kommandozeilenwerkzeug jar erstellt werden. In unserem Beispiel mit dem Befehl: "jar cf HelloService.jar – C bin/ ." wenn wir davon ausgehen, dass die Klassen und das Verzeichnis METAINF in bin liegen. Der Executor kann mit dem Befehl: "java –cp .;HelloService.jar at.jku.ssw.psw2.javaservice.Executor" ausgeführt werden. Markus Loeberbauer 2010, 2011 116 Praktikum aus Softwareentwicklung 2 Java Service Dabei gehen wir davon aus, dass der Klassenpfad von Executor das lokale Verzeichnis ist und die Datei HelloService.jar ebenfalls im lokalen Verzeichnis liegt. Liegen die Klassen oder die Jar-Datei woanders, dann muss der erste Teil (".") bzw. der zweite Teil ("HelloService.jar") im Klassen-Pfad ("-cp") angepasst werden. Java Services in Entwicklungsumgebungen erstellen Entwickeln wir diese Anwendung in einer Entwicklungsumgebung wie zB Eclipse, dann haben wir zwei unabhängige Projekte. Eines für den Executor und eines für das HelloService. Das Verzeichnis META-INF geben wir in das src-Verzeichnis des HelloService-Projekts. Eclipse kopiert es in das bin-Verzeichnis wenn es das Projekt übersetzt, damit kann das Service gefunden werden. Führen wir das Executor-Projekt aus, dann kann es das HelloService aber nicht finden. Erst müssen wir in der Run Configuration für das Executor-Projekt das HelloServiceProjekt in den Classpath eintragen. D.h. wir stellen eine Laufzeitabhängigkeit zwischen den Projekten her. Andere Entwicklungsumgebungen haben ähnliche Einstellungsmöglichkeiten um den Laufzeit-Classpath zu setzen. In NetBeans beispielsweise kann man den Laufzeit-Classpath in den Properties eines Projekts in der Kategorie Libraries im Karteireiter Run einstellen. In IntelliJ IDEA zum Beispiel kann man in der Project Structure in den Modul-Einstellungen die Abhängigkeiten unter Dependencies einstellen. Die Einstellung Scope bestimmt ob es sich um eine Übersetzungszeitoder Laufzeit-Abhänigkeit handelt. Markus Loeberbauer 2010, 2011 117 Praktikum aus Softwareentwicklung 2 Sicherheit Sicherheit Das Thema Sicherheit war vom Start an Bestandteil von Java. Wobei Sicherheit in Java bedeutet, dass der Code keinen Schaden anrichten darf. Auf Sprachebene werden dazu Bereichsprüfungen bei Arrays durchgeführt, auf Zeigerarithmetik verzichtet und Typ-Casts nur erlaubt wenn das gecastete Objekt den Typ des Casts erfüllt. Auf Ebene der Klassenbibliothek werden Zugriffe auf Ressourcen wie Dateien und das Netz über einen Sicherheitsmanager geschützt. Den Sicherheitsmanager kann man konfigurieren, wobei man Code Rechte zuteilt, das kann abhängig zB vom Speicherort oder der Signatur geschehen. Das Sicherheitsmanagement verteilt sich auf: die Virtuell Maschine (Arrayzugriffe, Typ-Casts, Bytecode-Prüfung), den Klassenlader (lädt die Klassen), Sicherheitsmanager (prüft ob ein Zugriff erlaubt ist), Verschlüsselung (Codesignierung) und der Java Authentication and Authorization Service (Autorisierung von Benutzern). Die Konfiguration des Sicherheitsmanagers (java.lang.SecurityManager) erfolgt über Policy-Dateien. Java liest standardmäßig zwei Policy-Dateien aus, eine im Verzeichnis der Java-Installation "jre/lib/security/java.policy" und eine im Basisverzeichnis des Benutzers ".java.policy". Zusätzlich kann man über das System-Property java.security.policy eine Policy-Datei angeben. Das funktioniert als Kommandozeilenparameter (zB: java.-Djava.security.policy=MyPolicy.policy MyApp) oder im Programm (zB: System.setProperty("java.security.policy", "MyPolicy.policy");). Damit die Policy-Datei ausgewertet wird muss ein Sicherheitsmanager aktiv sein. Ein Sicherheitsmanager kann durch Angabe des System-Properties java.security.manager beim Programmstart installiert werden; oder progammatisch über System.setSecurityManager(new SecurityManager()); gesetzt werden. Policy-Dateien und SecurityManager können aus Markus Loeberbauer 2010, 2011 118 Praktikum aus Softwareentwicklung 2 Sicherheit Sicherheitsgründen nur gesetzt werden, wenn bisher kein SecurityManager installiert ist oder die aktive Policy das erlaubt (policy.allowSystemProperty=true). Policy-Dateien Die Policies werden in Text-Dateien abgelegt. Der Inhalt besteht aus grantEinträgen, pro Eintrag kann der betroffene Code-Quelle und beliebig viele Permissions angegeben werden: grant Codesource { Permission_1; Permission_2; } Die Codesource besteht aus einer URL der Code-Basis und vertrauenswürdigen Zertifikaten: grant codebase codebase-URL certificate-name { ... Eine Permission besteht aus einer Permission-Klasse, einem Zielwert und einer Aktion: { permission className target action, ...; ... } Beispiel einer Policy-Datei: grant codeBase "file:${java.home}/lib/ext/*" { permission java.security.AllPermission; Markus Loeberbauer 2010, 2011 119 Praktikum aus Softwareentwicklung 2 Sicherheit }; grant { permission java.lang.RuntimePermission "stopThread"; permission java.net.SocketPermission "localhost:1024-", "listen"; permission java.util.PropertyPermission "java.version", "read"; permission java.util.PropertyPermission "java.vendor", "read"; }; grant { permission javax.crypto.CryptoPermission "DES", 64; permission javax.crypto.CryptoPermission "DESede", *; }; grant codeBase "http://www.ssw.uni-linz.ac.at/classes/" { permission java.net.SocketPermission "*:1024-65535", "connect"; permission java.io.FilePermission "${user.home}${/}-", "read,write,execute“; }; Regeln und Meta-Zeichen in Policy-Dateien • Die Code-Basis ist eine URL, daher muss immer "/" und niemals "\" verwendet werden, zB: "file:/C:/bla/" • Mit ${…} kann auf System-Properties zugegriffen werden. Als Kürzel für ${file.separator} ist ${/} definiert. • Pfad-Endungen haben folgende Bedeutung: • "/": alle class-Dateien in der angegeben URL • "/*": alle class- und jar-Dateien in der angegebenen URL • "/-": alle class- und jar-Dateien in der angegebenen URL und Unterverzeichnissen Permissions Damit Code sicherheitskritische Methoden aufrufen darf muss er die entsprechende Permission haben; oder eine Permission, die die gewünschte Permission impliziert. Beispiel: Will ein Programm die Datei c:\autoexec.bat lesen, dann muss es die Permission java.io.FilePermission (oder mehr zB java.security.AllPermission) mit dem Target "file:/C:${/}autoexec.bat" (oder mehr zB: Markus Loeberbauer 2010, 2011 120 Praktikum aus Softwareentwicklung 2 Sicherheit "file:${/}*" oder "<<ALL_FILES>>") und der Action "read" (oder mehr zB: "read,write") haben. Die Permissions aus den Policy-Dateien werden zu Laufzeit auf Java-Objekte abgebildet. Alle Permissions erben von java.security.Permission, eine beispielhafte Auswahl von Permissions ist in Abbildung 49 gegeben. Permission java.io.FilePermission java.net.SocketPermission Target Action Dateipfad read, write, execute, <<ALL FILES>> delete Host:Portrange accept, connect, listen, resolve java.util.PropertyPermission Name des read, write Systemproperties java.lang.RuntimePermission createClassLoader setSecurityManager exitVM stopThread … java.net.NetPermission setDefaultAuthenticator setCookieHandler setResponseCache … java.awt.AWTPermission accessClipboard watchMousePointer readDisplayPixels … Markus Loeberbauer 2010, 2011 121 Praktikum aus Softwareentwicklung 2 java.security.SecurityPermission Sicherheit getPolicy setPolicy … java.security.AllPermission Abbildung 49) Beispielhafte Aufzählung von Security Permissions Signieren von Jar-Dateien Damit man Code in Jar-Dateien, unabhängig von ihrem Speicherort vertrauen kann ist es möglich Jar-Dateien zu signieren. Signierte Dateien haben die Vorteile vor nachträglichen Veränderungen geschützt zu sein und an ihren Urheber zuordenbar zu sein. Damit kann man zB firmeninternen Jar-Dateien alle Rechte geben und Code von außen einschränken. Mit dem Kommandozeilenwerkzeug jarsigner kann man Jar-Dateien signieren, zB: jarsinger demo.jar MyKeyStore dabei ist demo.jar die zu signierende Jar-Datei und MyKeyStore der zu verwendende Keystore. Bevor man mit jarsigner arbeiten kann muss man einen Schlüssel im Keystore haben. Mit dem Kommandozeilenwerkzeug keytool kann man einen Schlüssel erstellen, zB: keytool -genkey. Ablauf eine Berechtigungsprüfung Wird eine Sicherheitskritische Methode (zB System.exit) aufgerufen, dann fragt diese den SecurityManager ob das erlaubt ist. Der SecurityManager untersucht den Methoden-Stack (Aktivierungssätze) und prüft ob jede Methode in der Aufrufkette zumindest eine Permission hat die diese Methode impliziert. Eine Methode hat die Permissions ihrer Klasse, einer Klasse sind Permissions über eine ProtectionDomain zugeordent. Eine ProtectionDomain speichert die Herkunft (URL), die Zertifikate und eine Sammlung von Permissions (PermissionCollection). Bespiel: System.exit Markus Loeberbauer 2010, 2011 122 Praktikum aus Softwareentwicklung 2 Sicherheit Die Methode System.exit ruft Runtime.exit auf: public static void exit(int status) { Runtime.getRuntime().exit(status); } Runtime.exit prüft ob ein Sicherheitsmanager installiert ist, wenn ja prüft sie mit checkExit ob exit erlaubt ist. public void exit(int status) { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkExit(status); } Shutdown.exit(status); } SecurityManager.checkExit erzeugt ein Objekt der Klasse RuntimePermission und prüft diese Permission über SecurityManager.checkPermission. public void checkExit(int status) { checkPermission(new RuntimePermission("exitVM."+status)); } SecurityManager.checkPermission läuft über den Stack (stack walk) und prüft ob jede Methode am Stack zumindest eine Permission hat die exit impliziert. Markus Loeberbauer 2010, 2011 123 Praktikum aus Softwareentwicklung 2 Java Native Interface (JNI) Java Native Interface (JNI) Über das Java Native Interface können Java-Programme Funktionen aus nativen Bibliotheken aufrufen. Das ist beispielsweise notwendig wenn man: plattformabhängige Bibliotheken (zB GUI) an Java anbinden will, mit Legacy-Systemen kommunizieren muss oder wenn man langlaufende, zeitkritische Teile von Algorithmen in maschinennahen Sprachen wie Assembler programmieren muss. Details über das JNI kann man nachlesen unter: http://download.oracle.com/javase/6/docs/technotes/guides/jni/ Beispiel Als Beispiel verwenden wir einen Rechner für die Grundrechenarten, Abbildung 50 zeigt das C++-Interface, Abbildung 52 die Implementierung. Der Rechner zählt die Anzahl der ausgeführten Operationen im Feld calcCount mit. class Calculator { private: long calcCount; void incCalcCount(); public: long add(long x, long y); long sub(long x, long y); long mul(long x, long y); long div(long x, long y); long getCalculationCount(); }; Abbildung 50) Beispiel: C++-Interface eines Rechners mit den Grundrechenarten. #include "Calculator.hpp" long long long long long void Calculator::add(long x, long y) { incCalcCount(); return Calculator::sub(long x, long y) { incCalcCount(); return Calculator::mul(long x, long y) { incCalcCount(); return Calculator::div(long x, long y) { incCalcCount(); return Calculator::getCalculationCount() { return calcCount; } Calculator::incCalcCount() { calcCount++; } x x x x + * / y; y; y; y; } } } } Abbildung 52) Beispiel: C++-Implementierung eines Rechners mit den Grundrechenarten. Damit wir die C++-Klasse nutzen können müssen einen Java-Proxy erstellen. Dieser Proxy muss die gewünschten Methoden als native Methoden deklarieren. Abbildung 53 zeigt die Java Klasse at.jku.ssw.psw2.jni.calculator.Calculator. In der Markus Loeberbauer 2010, 2011 124 Praktikum aus Softwareentwicklung 2 Java Native Interface (JNI) Java-Klasse Calculator habe ich die Methoden gleich benannt wie in der C++Klasse, damit wir uns leicht zurechtfinden. Wir könnten die Methoden aber beliebig benennen, weil wir auf der nativen Seite ohnehin eine C-Zwischenschicht implementieren müssen. Neben den Methoden für den Rechner enthält die Klasse einen Konstruktor der die native Methode initNative aufruft, einen Finalizer der die native Methode destroyNative aufruft und das Feld nativeObject. Diese Dinge brauchen wir weil wir ein zustrandsbehaftetes C++-Objekt an Java binden und nicht nur zustandslose C-Bibliotheksfunktionen aufrufen wollen. Die Methode initNative schreibt den Zeiger auf das C++-Objekt in das Feld nativeObject damit die nativen Methoden des Rechners darauf zugreifen können. Würden wir das nicht haben, dann würde die C-Zwischenschicht immer ein neues C++-Objekt anlegen müssen und wir würden den Zustand verlieren, d.h., die Zählfunktion würde nicht mehr funktionieren. Die Methode destroyNative gibt das C++-Objekt wieder frei. Unserer Java-Klasse implementiert zusätzlich noch das Interface Cloasable, damit das native Objekt explizit freigegeben werden kann und man nicht auf den Finalizer warten muss. Bei unserem kleinen C++-Objekt wäre das nicht nötig, aber bei größeren Objekten, oder bei Objekten die knappe Betriebssystemressourcen verwenden ist das sinnvoll. Markus Loeberbauer 2010, 2011 125 Praktikum aus Softwareentwicklung 2 Java Native Interface (JNI) package at.jku.ssw.psw2.jni.calculator; import java.io.Closeable; import java.io.IOException; public class Calculator implements Closeable { static { System.loadLibrary("CalculatorAdapter"); } private long nativeObject; public Calculator() { initNative(); } private native void initNative(); protected void finalize() throws Throwable { close(); }; @Override public void close() throws IOException { if (nativeObject != 0) { destroyNative(); nativeObject = 0; } } private native void destroyNative(); public public public public public native native native native native int int int int int add(int x, int y); sub(int x, int y); mul(int x, int y); div(int x, int y); getCalculationCount(); } Abbildung 53) Beispiel: Java-Implementierung der Anbindung an den C++-Rechner Die C-Zwischenschicht muss Funktionen für jede Methode der Java-Klasse implementieren. Die Schnittstelle für diese Zwischenschicht muss man mit javah erstellen. In unserem Beispiel also mit dem Kommandozeilenaufruf javah at.jku.ssw.psw2.jni.calculator.Calculator. Abbildung 54 zeigt die generierte CSchnittstelle. Wir sehen, Java nutzt eigene Datentypen (zB jobject, jint), um die Kompatibilität zwischen Java und C zu gewährleisten. Diese Datentypen sind in der JNI-Spezifikation in Kapitel 3 beschrieben (siehe http://download.oracle.com/javase/6/docs/technotes/guides/jni/spec/types.html). Damit man diese Datentypen benötigte Hilfsfunktionen nutzen kann ist jni.h inkludiert. Markus Loeberbauer 2010, 2011 126 Praktikum aus Softwareentwicklung 2 Java Native Interface (JNI) /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class at_jku_ssw_psw2_jni_calculator_Calculator */ #ifndef _Included_at_jku_ssw_psw2_jni_calculator_Calculator #define _Included_at_jku_ssw_psw2_jni_calculator_Calculator #ifdef __cplusplus extern "C" { #endif JNIEXPORT void JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_initNative (JNIEnv *, jobject); JNIEXPORT void JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_destroyNative (JNIEnv *, jobject); JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_add (JNIEnv *, jobject, jint, jint); JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_sub (JNIEnv *, jobject, jint, jint); JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_mul (JNIEnv *, jobject, jint, jint); JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_div (JNIEnv *, jobject, jint, jint); JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_getCalculationCount (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif Abbildung 54) Beispiel: C++-Interface eines Rechners mit den Grundrechenarten. Die Imlementierung der C-Schicht ist in Abbildung 55 gegeben. Alle Funktionen die in der generierten Schnittstelle sind werden hier auf die native Implementierung des Rechners umgesetzt. In den Hilfsmethoden getCalcField und setCalcField sehen wir ausserdem wie man von C aus auf Felder des Java-Objekts zugreifen kann. Die dafür notwendigen Funktionen sind in der JNIEnv-Klasse, siehe Kapitel 4 der JNI-Spezifikation http://download.oracle.com/javase/6/docs/ technotes/guides/jni/spec/functions.html. Dort ist zB auch beschriben wie man von C aus Objekte auf der Java-Seite erstellen und Java-Methoden aufrufen kann. Anmerkung: Aus Performanzgründen kann man das native Feld auf der C-Seite in einen Cache (zB: in eine Abbildung von dem jobject auf das Feld) geben, die reflektiven Zugriffe über JNIEnv sind zeitintensiv. Markus Loeberbauer 2010, 2011 127 Praktikum aus Softwareentwicklung 2 Java Native Interface (JNI) #include <jni.h> #include "Calculator.hpp" #include "at_jku_ssw_psw2_jni_calculator_Calculator.h" jfieldID getCalcFieldId(JNIEnv *env, jobject obj) { jclass calcClass = env->GetObjectClass(obj); return env->GetFieldID(calcClass, "nativeObject", "J"); } void setCalcField(JNIEnv *env, jobject obj, Calculator *calc) { env->SetLongField(obj, getCalcFieldId(env, obj), (jlong) calc); } Calculator* getCalcField(JNIEnv *env, jobject obj) { return (Calculator*) env->GetLongField(obj, getCalcFieldId(env, obj)); } JNIEXPORT void JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_initNative (JNIEnv *env, jobject obj) { setCalcField(env, obj, new Calculator()); } JNIEXPORT void JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_destroyNative (JNIEnv *, jobject) { Calculator *calc = getCalcField(env, obj); delete calc; } JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_add (JNIEnv *env, jobject obj, jint x, jint y) { Calculator *calc = getCalcField(env, obj); return calc->add(x, y); } JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_sub (JNIEnv *env, jobject obj, jint x, jint y) { Calculator *calc = getCalcField(env, obj); return calc->sub(x, y); } JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_mul (JNIEnv *env, jobject obj, jint x, jint y) { Calculator *calc = getCalcField(env, obj); return calc->mul(x, y); } JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_div (JNIEnv *env, jobject obj, jint x, jint y) { Calculator *calc = getCalcField(env, obj); return calc->div(x, y); } JNIEXPORT jint JNICALL Java_at_jku_ssw_psw2_jni_calculator_Calculator_getCalculationCount (JNIEnv *env, jobject obj) { Calculator *calc = getCalcField(env, obj); return calc->getCalculationCount(); } Abbildung 55) Beispiel: C++-Implementierung der Anbindung and den C++- Rechners. Markus Loeberbauer 2010, 2011 128 Praktikum aus Softwareentwicklung 2 Java Native Interface (JNI) Damit unser Programm funktioniert müssen wir die C-Bibliotheken noch kompilieren. Achtung, damit Java die Bibliotheken findet müssen die Namenskonventionen der Ziel-Plattform eingehalten werden, zB muss in Windows eine Bibliothek X den Namen X.dll haben in unserem Fall also CalculatorAdapter.dll, unter Mac Os X muss man den Prefix lib vor den Bibliotheksnamen stellen und die Endung dylib oder jnilib verwenden in unserem fall also libCalculatorAdapter.dylib. Unter anderen Unixen muss man häufig den Prefix lib und die Endung so verwenden in unserem Fall also libCalculatorAdapter.so. Die kompilieren Bibliotheken müssen in den Bibliothekspfad eingefügt werden, damit sie Java finden kann. In Windows müssen sie dafür im PATH liegen. Plattformübergreifend kann man den Ordner mit den Bibliotheken als Kommandozeilenparameter der Java-Runtime -Djava.library.path übergeben. In einem Eclipse-Projekt kann man das in der Run-Configuration machen, zB als -Djava.library.path=${project_loc}/NativeCalculator Damit das Betribssystem abhängige Bibliotheken (in uneserem Fall Calculator) findet müssen diese im Bibliothekspfad des Betribssystems liegen. Der Parameter java.library.path nutzt dafür nichts. Für unser Testprogramm ist es am einfachsten wenn wir die Bibliotheken in das Ausführungsverzeichnis geben, da sowohl Windows als auch Mac Os X dort nach Bibliotheken suchen. In Eclipse kann man das Ausführungsverzeichnis in der Run Configuration im Karteireiter Arguments im Bereich Working Directory einstellen. Markus Loeberbauer 2010, 2011 129 Praktikum aus Softwareentwicklung 2 Eclipse-Tastenkürzel Eclipse-Tastenkürzel 1. Ctrl+1: Kontextabhängige Vorschläge, zB: Code-Erzeugung und Korrekturen 2. Ctrl+Space: Code-Vervollständigung 3. Alt+Shift+M: Extrahieren einer Methode 4. Alt+Curser-Up, Alt+Cursor-Down: Verschiebt die aktuelle Zeile nach oben bzw. unten. Funktioniert auch mit markierten Bereichen. 5. Alt+Shift+L: Erstellt eine lokale Variable aus einem markierten Teilausdruck. 6. Ctrl+7: Aus-/Ein-Kommentieren einer Zeile oder eines markierten Bereichs. 7. Ctrl-Shift-F: Code formatieren. 8. Ctrl-Shift-O: Fehlende Import-Statements einfügen, überflüssige entfernen. 9. Ctrl-Shift-R: Umbenennen eines Elements, zB: Variable, Klasse oder Methode. 10. F3 oder Ctrl-Click: zur Deklaration des Elements gehen 11. F4: Vererbungshierarchie einer Klasse anzeigen 12. Ctrl-Shift-G: Verwendungen einer Klasse, Methode, etc. suchen 13. Ctrl-G: Implementierungen einer Klasse oder Methode suchen. Markus Loeberbauer 2010, 2011 130