Struts 7 (Basics)


Inhalt:

Konfiguration
Action "GeometricModelAction"
struts.xml
JSP
Anwendung starten

Für WildFly 33, JakartaEE10 und Struts 7: 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 (vermutlich ziemlich veraltetes) Struts 2-Tutorial: http://www.roseindia.net/struts/struts2/index.shtml.


Konfiguration

Wir benötigen Struts 7.0 für die JakartaEE-Unterstützung. Diese Version findet man mit Stand September 2024 nicht auf den offiziellen Downloadservern, für das aktuell letzte Release "7.0 M9" muss man den SourceCode von Github herunterladen und selbst compilieren: https://github.com/apache/struts.

Folgende Dateien müssen nach WEB-INF\lib kopiert werden damit die simple Anwendung sich deployen läßt (falls man die Dateien nicht über Eclipse kopiert, dann muss anschließend im Project Explorer-Contextmenü "Refresh" gewählt werden, damit Eclipse die Dateien sieht):

Das Projekt muss mit Java 17 erstellt werden, dies ist die minimale Java-Version, die Struts 7 unterstützt.


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="https://jakarta.ee/xml/ns/jakartaee" 
    xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" 
    version="6.0">
  
  <display-name>StrutsBasics</display-name>
  <welcome-file-list>
    ...
  </welcome-file-list>
  <filter>
    <filter-name>struts</filter-name>
    <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>struts</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

Um Varianten aus diversen Tutorials abzugrenzen: die Deklaration des Filters hat sich über verschiedene Versionen geändert.

Vor Struts 2.5 war folgender Filter zu deklarieren:
  <filter>
    <filter-name>struts</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>struts</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
Vor Struts 2.1.3 war folgender Filter zu deklarieren:
  <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>

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.action.SessionAware;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
import com.opensymphony.xwork2.ActionSupport;

public class GeometricModelAction extends ActionSupport implements SessionAware
{
  private Map<String, Object> session;

  @Override
  public void withSession(Map<String, Object> session)
  {
    this.session = session;
  }
  
  ...
} 
In früheren Versionen wurde die Methode setSession des Interface org.apache.struts2.interceptor.SessionAware verwendet:
  @Override
  public void setSession(Map<String, Object>  session)
  {
    ...
  }
Seit Struts 6 ist dies durch das Interface org.apache.struts2.action.SessionAware zu ersetzen:
  public void withSession(Map<String, Object> 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;
  
  public Double getA()
  {
    return dblA;
  }
  @StrutsParameter
  public void setA(Double dblA)
  {
    this.dblA = dblA;
  }

  public Double getB()
  {
    return dblB;
  }
  @StrutsParameter
  public void setB(Double dblB)
  {
    this.dblB = dblB;
  }

  public Double getC()
  {
    return dblC;
  }
  @StrutsParameter
  public void setC(Double dblC)
  {
    this.dblC = dblC;
  } 

Warum die Annotation @StrutsParameter?

Dies ist eine Neuerung in Struts 7. Es sei auf den Migration Guide verwiesen:
https://cwiki.apache.org/confluence/display/WW/Struts+6.x.x+to+7.x.x+migration.
In der neuen Version werden Setter nur noch aufgerufen, wenn sie eine entsprechende Annotation haben.


Alternativ kann in "struts.xml" folgende Property definiert werden, um das pauschal zu setzen:
<struts>
   <constant name="struts.parameters.requireAnnotations" value="false"/>
   ..
</struts>
Diese Lösung ist aber nicht empfohlen.


Warum Double statt double?
Die Seite geometricmodel.jsp bzw. die zugehörige Action werden auch aufgerufen, wenn die Seite zum ersten Mal aufgerufen wird. Dabei sind natürlich keine Formularfelder vorhanden, deren Werte in die entsprechenden Properties gesetzt werden. Trotzdem wird die execute-Methode aufgerufen, die (siehe unten) einen Eintrag in die Historie erzeugen würde.

Um diese Situation zu erkennen, habe ich den Datentyp Double verwendet: solange der Setter nicht aufgerufen wurde, behält sie ihren Wert null. Dadurch kann ich in execute diesen Fall erkennen und in diesem Fall keinen Eintrag in der Historie vornehmen. Dies wäre zwar auch mit double und dem Wert "0" möglich gewesen, aber vielleicht hat der User ja wirklich für alle drei Werte "0" angegeben.



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.
  @Override
  public String execute() throws Exception
  {
    if (this.dblA != null && this.dblB != null && this.dblC != null)
    {
      //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.action.ServletRequestAware (vor Struts 6: org.apache.struts2.interceptor.ServletRequestAware mit einer Methode setServletRequest) implementieren, das zu einer Methode withServletRequest führt:
public class GeometricModelAction extends ActionSupport implements ServletRequestAware
{
  private HttpServletRequest request;

  public void withServletRequest(HttpServletRequest httpServletRequest)
  {
    this.request = httpServletRequest;
  }
  
  ...
}


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 6.0//EN"
    "http://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
    <constant name="struts.devMode" value="true"/>
    <constant name="struts.allowlist.classes" value="de.hsrm.jakartaee.knauf.strutsbasics.Historie"/>
	
    <package name="strutsbasics" extends="struts-default">
        <default-action-ref name="geometricmodel" />
        <action name="geometricmodel" class="de.hsrm.jakartaee.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>
Ein zweites Element result namens input gilt für den Fehlerfall: wenn man z.B. Buchstaben ins Zahlenfeld eingibt, dann erkennt Struts, dass es diese Eingabe nicht in die entsprechende Feldproperty (die den Datentyp java.lang.Integer hat) setzen kann, und wird erneut das Eingabeformular anzeigen - mit einer Fehlermeldung versehen.
   <result name="input">/geometricmodel.jsp</result>

Zusätzlich wird eine default-action-ref definiert: diese Default Action wird aufgerufen, wenn keine Unter-URL der Seite angegeben wird. Hier soll also auf die URL /geometricmodel.jsp weitergeleitet werden.
Ohne Angabe einer Default Action kommt folgende Fehlermeldung beim Aufruf der Webadresse ohne Sub-URL:
18:37:38,728 ERROR [org.apache.struts2.dispatcher.Dispatcher] (default task-1) Could not find action or result: /StrutsBasics/: There is no Action mapped for 
    namespace [/] and action name [] associated with context path [/StrutsBasics]. - [unknown location]
	at deployment.StrutsBasics.war//com.opensymphony.xwork2.DefaultActionProxy.prepare(DefaultActionProxy.java:183)
	at deployment.StrutsBasics.war//org.apache.struts2.factory.StrutsActionProxy.prepare(StrutsActionProxy.java:57)
	at deployment.StrutsBasics.war//org.apache.struts2.factory.StrutsActionProxyFactory.createActionProxy(StrutsActionProxyFactory.java:32)
	at deployment.StrutsBasics.war//com.opensymphony.xwork2.DefaultActionProxyFactory.createActionProxy(DefaultActionProxyFactory.java:60)
	at deployment.StrutsBasics.war//org.apache.struts2.dispatcher.Dispatcher.createActionProxy(Dispatcher.java:781)
	at deployment.StrutsBasics.war//org.apache.struts2.dispatcher.Dispatcher.prepareActionProxy(Dispatcher.java:767)
	at deployment.StrutsBasics.war//org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:730)
	at deployment.StrutsBasics.war//org.apache.struts2.dispatcher.ExecuteOperations.executeAction(ExecuteOperations.java:79)
	at deployment.StrutsBasics.war//org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.handleRequest(StrutsPrepareAndExecuteFilter.java:163)
	at deployment.StrutsBasics.war//org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.tryHandleRequest(StrutsPrepareAndExecuteFilter.java:146)
	at deployment.StrutsBasics.war//org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:134)
	at io.undertow.servlet@2.3.15.Final//io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)


Hinweis: "devMode":
Das Struts-Framework mittels der Konstantendeklaration "struts.devMode" in den Entwicklermodus versetzt werden, 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) ohne eine Konfiguration des result name="input" eine Exception fliegt, statt einfach wieder die Eingabeseite anzuzeigen:
No result defined for action de.hsrm.jakartaee.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.
Mehr Infos:
https://struts.apache.org/core-developers/development-mode


Deklaration struts.allowlist.classes:
Ohne struts.allowlist.classes=... erscheint folgende Ausgabe auf der Serverkonsole - und die JSP-Seite gibt keine Historie aus:
18:30:02,073 WARN  [com.opensymphony.xwork2.ognl.SecurityMemberAccess] (default task-1) Declaring class [class de.hsrm.jakartaee.knauf.strutsbasics.Historie] of member type 
  [public int de.hsrm.jakartaee.knauf.strutsbasics.Historie.getSize()] is not allowlisted!

Erklärung: ich laufe in einem OGNL über eine Liste von Historie-Elementen. Struts 7 verbietet hier bei Verwendung von "unbekannten" Klassen. Diese müssen explizit deklariert werden, siehe https://cwiki.apache.org/confluence/display/WW/Struts+6.x.x+to+7.x.x+migration#Struts6.x.xto7.x.xmigration-OGNLallowlistcapability.

Alternativ könnte man die "allowList" komplett abschalten, was aber nicht empfohlen ist:
<constant name="struts.allowlist.enable" value="false"/>

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:


Anwendung starten

Die Anwendung wird durch Aufruf von http://localhost:8080/StrutsBasics/ gestartet (oder direkt durch Aufruf der Subseite ".../geometricmodel.action").

Der direkte Aufruf von ".../geometricmodel.jsp" ist in Struts 7 nicht mehr möglich - das führt in diesem Beispiel zu einem Laufzeitfehler.

Struts selbst betrachtet den direkten Aufruf von JSPs als "verboten": https://struts.apache.org/security/#never-expose-jsp-files-directly.

Stand 20.10.2024
Historie:
07.10.2024: Erstellt aus JakartaEE8/Struts2-Beispiel, Anpassungen an WildFly 33, JakartaEE10 und Struts 7M7
20.10.2024: Struts 7M9