Struts 2 (Basics)
Inhalt:
Konfiguration
Action "GeometricModelAction"
struts.xml
JSP
Dieses Beispiel baut auf der gleichen Logik auf wie die JSP-Beispiel, es existiert also nur eine Web-Anwendung mit minimaler Programmlogik.
Bereits durchgeführte Berechnungen werden in der Session gespeichert.
Hier gibt es das Projekt als WAR-Export-Datei: StrutsBasics.war.
Die Seite des Struts-Projekts: http://struts.apache.org/
Ein Struts 2-Tutorial: http://www.roseindia.net/struts/struts2/index.shtml.
Konfiguration
Wir benötigen Struts 2.1.6.
Folgende Dateien müssen in WEB-INF\lib kopiert werden damit die simple Anwendung sich deployen läßt:
- freemarker-2.3.13.jar
- ognl-2.6.11.jar
- struts2-core-2.1.6.jar
- xwork-2.1.2.jar
- commons-fileupload-1.2.1.jar
Außerdem muss folgendes in web.xml eingetragen werden:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<display-name>StrutsBasics</display-name>
<welcome-file-list>
...
</welcome-file-list>
<filter>
<filter-name>struts</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
Hinweis: im Vorjahresbeispiel hatte ich hier einen Init-Parameter zusätzlich angegeben:
...
<filter>
<filter-name>struts</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
<init-param>
<param-name>actionPackages</param-name>
<param-value>de.fhw.komponentenarchitekturen.knauf.strutsbasics.actions</param-value>
</init-param>
</filter>
...
Dies führt leider beim JBoss5 zu diesem Fehler:
17:30:17,156 INFO [ResolverUtil] Scanning for classes in [/C:/temp/jboss-5.0.0.GA/server/default/deploy/StrutsBasics.war/WEB-INF/classes/de.fhw.komponentenarchitekturen.knauf.strutsbasics.actions/] matching criteria: org.apache.struts2.config.ClasspathConfigurationProvider$1@dc9c10
17:30:17,156 ERROR [ResolverUtil] Could not search jar file 'C:\temp\jboss-5.0.0.GA\server\default\deploy\StrutsBasics.war\WEB-INF\classes\de.fhw.komponentenarchitekturen.knauf.strutsbasics.actions' for classes matching criteria: org.apache.struts2.config.ClasspathConfigurationProvider$1@dc9c10 due to an IOException
java.io.FileNotFoundException: C:\temp\jboss-5.0.0.GA\server\default\deploy\StrutsBasics.war\WEB-INF\classes\de.fhw.komponentenarchitekturen.knauf.strutsbasics.actions (Das System kann den angegebenen Pfad nicht finden)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(Unknown Source)
at com.opensymphony.xwork2.util.ResolverUtil.loadImplementationsInJar(ResolverUtil.java:341)
at com.opensymphony.xwork2.util.ResolverUtil.findInPackage(ResolverUtil.java:288)
at com.opensymphony.xwork2.util.ResolverUtil.find(ResolverUtil.java:240)
at org.apache.struts2.config.ClasspathConfigurationProvider.loadPackages(ClasspathConfigurationProvider.java:234)
at org.apache.struts2.config.ClasspathConfigurationProvider.loadPackages(ClasspathConfigurationProvider.java:402)
at com.opensymphony.xwork2.config.impl.DefaultConfiguration.reload(DefaultConfiguration.java:152)
at com.opensymphony.xwork2.config.ConfigurationManager.getConfiguration(ConfigurationManager.java:52)
at org.apache.struts2.dispatcher.Dispatcher.init_PreloadConfiguration(Dispatcher.java:395)
at org.apache.struts2.dispatcher.Dispatcher.init(Dispatcher.java:452)
at org.apache.struts2.dispatcher.FilterDispatcher.init(FilterDispatcher.java:205)
....
Dieser Parameter ist eigentlich auch nicht nötig (siehe
http://www.informit.com/guides/content.aspx?g=java&seqNum=468).
Er ist nur nötig, wenn die Action-Klassen nicht in struts.xml explizit konfiguriert werden (siehe unten).
Action "GeometricModelAction"
Zuallererst fügen wir die Struts-Action "GeometricModelAction" zu die die Anwendungslogik enthält.
Ihr Klassenrumpf sieht so aus:
import org.apache.struts2.interceptor.SessionAware;
import com.opensymphony.xwork2.ActionSupport;
public class GeometricModelAction extends ActionSupport implements SessionAware
{
private Map<String, Object> session;
@SuppressWarnings("unchecked")
public void setSession(Map session)
{
this.session = session;
}
...
}
Die Parentklasse com.opensymphony.xwork2.ActionSupport
markiert die Klasse als Struts-Action, d.h. eine Klasse die
bei einem Form-Submit aufgerufen wird. Anmerkung: diese Parentklasse muss nicht zwingend angegeben werden.
Das Interface org.apache.struts2.interceptor.SessionAware
wird implementiert damit die Session-Daten als Map mittels
der Interface-Methode setSession
in die Klasse geschrieben wird. Wir benutzen sie um die Historie der Berechnungen zu speichern.
Jetzt werden der Klasse Properties zugefügt:
private double dblA = 0;
private double dblB = 0;
private double dblC = 0;
private double dblOberflaeche = 0;
private double dblVolumen = 0;
private Historie historieGesamt = null;
public double getA()
{
return dblA;
}
public void setA(double dblA)
{
this.dblA = dblA;
}
public double getB()
{
return dblB;
}
public void setB(double dblB)
{
this.dblB = dblB;
}
public double getC()
{
return dblC;
}
public void setC(double dblC)
{
this.dblC = dblC;
}
Es gibt get-Properties für die aktuell berechneten Werte sowie die Historie:
private double dblOberflaeche = 0;
private double dblVolumen = 0;
private Historie historieGesamt = new Historie();
public Historie getHistorieGesamt()
{
if (this.session == null)
{
throw new NullPointerException("session nicht gesetzt !");
}
if (this.session.get("historieGesamt") == null)
{
this.session.put("historieGesamt", new Historie() );
}
this.historieGesamt = (Historie) this.session.get("historieGesamt");
return this.historieGesamt;
}
public double getVolumen()
{
return this.dblVolumen;
}
public double getOberflaeche()
{
return this.dblOberflaeche;
}
getHistorieGesamt
hat eine Besonderheit: beim ersten Abrufen innerhalb des Requests wird die Historie aus der Session
geholt. Ist dieses nicht vorhanden (erster Aufruf der Seite), so wird es gesetzt. Struts 2 bietet leider keine so eleganten Wege wie JSF, Daten in der Session zu speichern.
Schließlich wird die Methode execute
der Basisklasse ActionSupport
überladen, die beim Klick auf "Submit" aufgerufen wird und
die Berechnung durchführt sowie die aktuelle Berechnung der Historie zufügt.
public String execute() throws Exception
{
//Ausrechnen:
this.dblVolumen = this.dblA * this.dblB * this.dblC;
this.dblOberflaeche = 2 * (this.dblA * this.dblB) + 2 * (this.dblA * this.dblC) + 2 * (this.dblB * this.dblC);
//Zufügen zur Historie !
Seitenlaengen seitenlaengeAktuell = new Seitenlaengen();
seitenlaengeAktuell.setA(this.dblA);
seitenlaengeAktuell.setB(this.dblB);
seitenlaengeAktuell.setC(this.dblC);
//Die Historie per Get-Methode abrufen damit sie bei Bedarf erzeugt wird.
this.getHistorieGesamt().addSeitenlaenge( seitenlaengeAktuell );
return Action.SUCCESS;
}
Zur Rückgabe einer Execute-Methode wird in der Config-Datei die zugehörige Zielseite deklariert.
Zur Info: falls man auf den eigentlichen Servlet-Request zugreifen will, so muss man das Interface
org.apache.struts2.interceptor.ServletRequestAware
implementieren, das zu einer Methode setServletRequest
führt:
public class GeometricModelAction extends ActionSupport implements ServletRequestAware
{
private HttpServletRequest request;
public void setServletRequest(HttpServletRequest httpServletRequest)
{
this.request = httpServletRequest;
}
...
}
Falls man nur Daten in den Request stecken will, die z.B. auf der Zielseite verfügbar sein sollen, so kann man das Interface
org.apache.struts2.interceptor.RequestAware
implementieren, das analoy zum Servlet-Request die Attribute in einer Map ablegt.
Vorteil dieser Maps ist, dass dadurch die Actions unabhängig vom Webserver werden, und z.B. in Unit-Tests angesprochen werden können.
struts.xml
Die Konfiguration des Struts-Frameworks erfolgt primär in der Datei "struts.xml". Diese liegt NICHT in WEB-INF, sondern
muss ins Verzeichnis "src" (im Eclipse: "Java Resources:src") gepackt werden damit sie beim Compilieren und Deploy in "classes" landet, wo Struts sie sucht.
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<-- <constant name="struts.devMode" value="true"/> -->
<package name="strutsbasics" extends="struts-default">
<action name="geometricmodel" class="de.fhw.komponentenarchitekturen.knauf.strutsbasics.actions.GeometricModelAction">
<result>/geometricmodel.jsp</result>
<result name="input">/geometricmodel.jsp</result>
</action>
</package>
</struts>
Hier wird ein Konfigurationspaket deklariert, das seine nicht explizit gesetzten Werte von einer Struts-internen Konfiguration "struts-defaults" erbt
und "strutsbasics" heißen soll.
Es wird die Action GeometricModelAction
registriert und es wird angegeben zu welcher Seite man von der execute
-Methode springen soll.
Im Element result
gibt man normalerweise den Rückgabewert der execute
-Methode an, für die diese Regel gilt. In unserem Fall gibt
es nur die Rückgabe "success", dies ist gleichzeitig Default-Wert des Elements. Die Langversion würde so aussehen:
<result name="success">/geometricmodel.jsp</result>
Mit der (hier auskommentierten) Konstantendeklaration "struts.devMode" aktivieren wir den Developer-Modus, der strenger auf z.B. Konfigurationsfehler reagiert und uns zu sauberem Arbeiten zwingt ;-).
Das führt z.B. dazu, dass bei Fehleingaben (Buchstaben in Textfeldern) mit obiger Konfiguration eine Exception fliegt, statt einfach wieder die Eingabeseite
anzuzeigen:
No result defined for action de.fhw.komponentenarchitekturen.knauf.strutsbasics.actions.GeometricModelAction and result input
Deshalb wird ein Result namens "input" deklariert ("input" bedeutet hier: "wegen Fehlern in der Seite erneut zur Eingabe springen"). Bei nicht aktiviertem
"devMode" springt die Anwendung automatisch zur Seite mit dem fehlerhaften Formular, im "devMode" sind wir gezwungen, diesen Fall sauber zu konfigurieren.
Außerdem führt der "devMode" allgemein zu mehr Konsolenausgaben bei Fehlern, die Struts intern abfängt, deshalb sollte er immer aktiviert werden.
JSP
Wir fügen eine JSP-Seite "geometricmodel.jsp" zu.
Sie sieht so aus:
<?xml version="1.0" encoding="ISO-8859-1" ?>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!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=ISO-8859-1" />
<title>Test für Struts2</title>
</head>
<body>
<s:form action="geometricmodel">
<s:if test="volumen > 0.0">
Volumen: <s:property value="volumen"/> <br/>
Oberfläche: <s:property value="oberflaeche"/> <br/>
</s:if>
<s:textfield name="a" label="Kante a"></s:textfield> <br/>
<s:textfield name="b" label="Kante b"></s:textfield> <br/>
<s:textfield name="c" label="Kante c"></s:textfield> <br/>
<br/>
<s:submit value="Berechnen"></s:submit>
</s:form>
<%--Historie ausgeben: --%>
<s:if test="historieGesamt.size > 0">
Historie:<br/>
<s:iterator value="historieGesamt.iterator" status="currentStatus">
<s:property value="#currentStatus.index"/>: a=<s:property value="a"/>, b=<s:property value="b"/>, c=<s:property value="c"/> <br/>
</s:iterator>
</s:if>
</body>
</html>
Die Elemente im Einzelnen:
- Es wird die Struts-Taglibrary eingebunden:
<%@ taglib prefix="s" uri="/struts-tags" %>
Die Doku zur Taglib findet man übrigens im Verzeichnis "docs\struts2-core\apidocs\org\apache\struts2\components" des Struts-Pakets.
- Es wird ein HTML-Form deklariert, das beim Submit auf die Action mit dem Namen "geometricModel" verweist:
<s:form action="geometricmodel">
...
</s:form>
- Zuallererst wird geprüft ob beim aktuellen Aufruf der Seite eine Berechnung durchgeführt wurde. Hierzu wird das
Struts-Tag
s:if
genutzt. Im Attribut "test" können wir einen EL-ähnlichen Ausdruck nutzen, den Struts allerdings wohl selbst parst.
Obwohl wir hier außerhalb der Form sind, steht uns die Action und deren gesamte Felder zur Verfügung.
Die Ausgabe der einzelnen Properties wird mittels des Tags s:property
gesteuert, hier gibt das Attribut value
an, welcher
Wert der Action ausgegeben werden soll.
<s:if test="volumen > 0.0">
Volumen: <s:property value="volumen"/> <br/>
Oberfläche: <s:property value="oberflaeche"/> <br/>
</s:if>
- Jetzt folgt das eigentliche Formular. Zuerst drei Ein-/Ausgabefelder. Diese werden mit den Properties a, b und c der Action
verdrahtet, d.h. beim Erzeugen der HTML-Seite werden die aktuellen Werte in die Felder geschrieben, und beim Auswerten des Requests
werden die Request-Parameter "a", "b" und "c" (durch das Attribut
name
definiert) in die entsprechenden Felder geschrieben.
<s:textfield name="a" label="Kante a"></s:textfield> <br/>
<s:textfield name="b" label="Kante b"></s:textfield> <br/>
<s:textfield name="c" label="Kante c"></s:textfield> <br/>
Durch das Attribut "label" können wir einen Text angeben der direkt vor dem Feld ausgegeben wird (einschließlich angehängtem ":").
Der Submit-Button wird durch ein Element s:submit
abgebildet.
&<s:submit value="Berechnen"></s:submit>
- Die Ausgabe der Historie erfolgt nur, wenn überhaupt Werte berechnet sind.
<s:if test="historieGesamt.size > 0">
Historie:<br/>
<s:iterator value="historieGesamt.iterator" status="currentStatus">
<s:property value="#currentStatus.index"/>: a=<s:property value="a"/>, b=<s:property value="b"/>, c=<s:property value="c"/> <br/>
</s:iterator>
</s:if>
Das Tag s:iterator
läuft über eine Liste oder einen Iterator. Mittels dem Attribut status
können wir eine
Statusvariable vom Typ org.apache.struts2.views.jsp.IteratorStatus
(http://struts.apache.org/2.0.14/struts2-core/apidocs/org/apache/struts2/views/jsp/IteratorStatus.html)
deklarieren, die uns z.B. den aktuellen Iterator-Index liefert (wird hier genutzt, um eine laufende Nummer an den Anfang der Zeile zu setzen).
Eine Besonderheit ist hier, dass dem Wert von value
ein # vorangestellt wird. Dies ist in diesem Fall nötig, eine Begründung kann ich leider nicht liefern.
Für die Ausgabe der Seitenlängen a, b und c wird wieder ein s:property
-Tag genutzt. Dieses sucht sich hier automatisch ein Objekt mit Properties
a, b und c, und landet da scheinbar als erstes beim aktuellen Iterator-Element (das beim Iterator-Lauf auf einem Struts-internen Stack landet und dort gefunden wird).
Stand 21.01.2009
Historie:
01.01.2009: Erstellt aus Vorjahresbeispiel, Struts 2.0.14, Anpassungen an JBoss5 und Eclipse 3.4
21.01.2009: Struts 2.1.6 (geänderte JARs, kein "devMode")