Other Plugins

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Struts 7.4.0 (released 2026-10-02) running on Apache Tomcat 11.0.26 and Java 21; the facts were checked on 2026-10-08 and 2026-10-10 against the plugin index and the plugin pages of https://struts.apache.org/plugins/, the Core Developer Guide page on the XSL result, the Getting Started JasperReports tutorial, the contents and the source jars of the 7.4.0 plugin artifacts on Maven Central, and the Version Notes and the 6 to 7 migration guide on the Struts wiki. Struts is a small core with a large set of optional parts: nearly everything that is not the request cycle itself (the Convention, Spring, CDI, JSON, REST and Bean Validation integrations, Tiles, Velocity) is a plugin, a jar that is dropped on the class path and that the framework finds by itself. The major plugins have pages of their own in this part of the section; this page is about the rest: how a plugin is put together and how to write one, the map of every plugin of the official index with its 7.4.0 status, the plugins that did not get a page of their own (config browser, XSLT, JasperReports 6 and 7, JFreeChart, async, javatemplates, JUnit and TestNG) and the list of plugins that were removed or are no longer published. The running scenario is the Bookshelf, a catalogue of books; everything below was built as a Maven reactor (an API jar, a plugin jar and a WAR), deployed to the scratch Tomcat and requested with curl, except where the text says that an example was not run. One result of that work deserves to be read first: in my test the async plugin of 7.4.0 did not complete a single request (see the async section), and the JasperReports 7 plugin needs one extra import in the POM’s dependency management to start at all.

The Plugin Map for 7.4.0

The plugin index lists 30 entries. Whether an entry can be used with Struts 7.4.0 is a different question from whether it is listed: the index keeps the removed and the deprecated plugins, and the struts2-bom 7.4.0 still has entries for artifacts that do not exist at 7.4.0. The table below was built by reading the index, the BOM and the maven-metadata.xml of every struts2-*-plugin artifact on Maven Central. A plugin whose latest Central version is not 7.4.0 cannot be used with Struts 7.4.0 as it stands.

Plugin (artifact struts2-*-plugin) 7.4.0 status Where it is described

Async (async)

Published since 6.0.0; did not work in my test, see The Async Plugin

This page

Bean Validation (bean-validation)

Published

Bean Validation Plugin

CDI (cdi)

Published

Dependency Injection and CDI

Config Browser (config-browser)

Published; development tool, see The Config Browser Plugin

This page

Convention (convention)

Published

Convention Plugin and Annotations

JasperReports (jasperreports)

Published, targets JasperReports 6.x (built against 6.21.5, provided)

JasperReports: the 6.x and the 7 Plugins

JasperReports 7 (jasperreports7)

Published since 7.1.1 on Central (the index says 7.1.0+)

JasperReports: the 6.x and the 7 Plugins

Javatemplates (javatemplates)

Published

The Javatemplates Plugin; the themes are in Themes and Templates

JFreeChart (jfreechart)

Published

The JFreeChart Plugin

JSON (json)

Published

JSON Plugin and Ajax

JUnit (junit) and TestNG (testng)

Published

The JUnit and TestNG Plugins; the Testing page of this part goes further

REST (rest)

Published

REST Plugin

Spring (spring)

Published

Spring Integration

Tiles (tiles)

Published (Tiles 3 code bundled since 6.3.0)

Tiles and Page Layouts

Velocity (velocity)

Published since 6.0.0; the index page is a three-line stub

FreeMarker and Velocity Views

XSLT (xslt)

Published since 6.2.0, not on the plugin index; the result is documented under the Core Developer Guide

The XSLT Result

Embedded JSP, OSGi, Plexus, Portlet Tiles, GXP, DWR, OVal, Portlet, SiteMesh

Not published for 7.x; last version 6.12.0

Removed, Retired and Deprecated Plugins

Codebehind, JSF, SiteGraph, Struts 1, Tiles 3, Java 8 Support

Gone since 2.5 (last Central versions 2.3.37, 2.3.37, 2.5.33, 2.3.37, 2.5-BETA2 and 2.5.2)

Removed, Retired and Deprecated Plugins

All plugin versions are managed by the BOM, so a dependency is written without a version once the BOM is imported (the pom.xml of the first page of this part shows the import). A dependency on a BOM entry that has no 7.4.0 artifact (the embedded JSP, GXP, OSGi and Plexus plugins) fails to resolve; that is the first sign that a plugin is no longer maintained.

The Bookshelf Used on This Page

All examples below are classes and mappings of one scratch application, built as a Maven reactor and deployed as g11other.war on Tomcat 11.0.26, Java 21, Struts 7.4.0 (BOM). It has three modules: bookshelf-badge-api (two interfaces), bookshelf-badge-plugin (a Struts plugin jar written in the section on writing a plugin) and bookshelf-app (the WAR). The WAR holds a Book bean with title, author and year, and an action BookListAction that exposes three books and the shelf name:

public class BookListAction extends ActionSupport {
    public List<Book> getBooks() {
        return List.of(
                new Book("Dune", "Frank Herbert", 1965),
                new Book("Neuromancer", "William Gibson", 1984),
                new Book("Hyperion", "Dan Simmons", 1989));
    }

    public String getShelfName() {
        return "Main shelf";
    }
}

The dependencies of the WAR, without versions (the BOM manages the Struts ones), are the seven plugins this page uses plus the two libraries that two of them need:

<dependency><groupId>org.apache.struts</groupId><artifactId>struts2-core</artifactId></dependency>
<dependency><groupId>org.apache.struts</groupId><artifactId>struts2-config-browser-plugin</artifactId></dependency>
<dependency><groupId>org.apache.struts</groupId><artifactId>struts2-xslt-plugin</artifactId></dependency>
<dependency><groupId>org.apache.struts</groupId><artifactId>struts2-async-plugin</artifactId></dependency>
<dependency><groupId>org.apache.struts</groupId><artifactId>struts2-jasperreports7-plugin</artifactId></dependency>
<dependency><groupId>org.apache.struts</groupId><artifactId>struts2-jfreechart-plugin</artifactId></dependency>
<dependency><groupId>org.apache.struts</groupId><artifactId>struts2-javatemplates-plugin</artifactId></dependency>
<dependency><groupId>net.sf.jasperreports</groupId><artifactId>jasperreports</artifactId><version>7.0.8</version></dependency>
<dependency><groupId>net.sf.jasperreports</groupId><artifactId>jasperreports-pdf</artifactId><version>7.0.8</version></dependency>
<dependency><groupId>org.jfree</groupId><artifactId>jfreechart</artifactId><version>1.5.6</version></dependency>

How a Plugin Is Built

A Struts plugin is an ordinary jar. The framework looks for jars on the class path that contain a struts-plugin.xml at their root, loads that file with the same parser and format as struts.xml, and may also find a struts-deferred.xml. The official page names what the two files make possible: a plugin can define new packages with results, interceptors and actions, override framework constants, and introduce implementations of extension points. The order matters, and the page of this part on configuration files and constants shows it with a measured table; in short, the framework loads its own defaults, then every struts-plugin.xml found, then the application’s struts.xml, then every struts-deferred.xml.

flowchart TD core["struts-default.xml
(struts2-core jar)"] --> sp["struts-plugin.xml
(every plugin jar)"] sp --> app["struts.xml
(your application)"] app --> df["struts-deferred.xml
(every plugin jar)"] df --> run["One configuration at runtime"] sp -. "can override core constants" .-> core app -. "has the last word over plugin constants" .-> sp df -. "bean-selection providers read the final constants" .-> app

Two behaviours of that order were verified on the scratch application. The plugin of the next section sets a constant to bracket in its struts-plugin.xml, and the application’s struts.xml sets the same constant to angle; the application’s value won (the output of the request is shown in the next section), which is the rule that the application has the last word. And a bean-selection declared in the plugin’s struts-deferred.xml saw the application’s value, because it ran after struts.xml. The bundled plugins are good examples of what a plugin contains. This is the content of the Config Browser and XSLT plugin descriptors of 7.4.0 (abbreviated, with the license comments removed), which show the two extremes: a plugin made only of actions, and a plugin made of a single package and one result type.

<!-- struts2-xslt-plugin-7.4.0.jar!/struts-plugin.xml -->
<struts>
    <package name="xslt-default" extends="struts-default">
        <result-types>
            <result-type name="xslt" class="org.apache.struts2.result.xslt.XSLTResult"/>
        </result-types>
    </package>
</struts>

<!-- struts2-config-browser-plugin-7.4.0.jar!/struts-plugin.xml (abbreviated) -->
<struts>
    <bean class="org.apache.struts2.config_browser.ConfigurationHelper" />
    <package name="config-browser" extends="struts-default" namespace="/config-browser">
        <default-action-ref name="index" />
        <action name="index" class="org.apache.struts2.config_browser.ActionNamesAction" method="redirect">
            <result type="redirectAction">actionNames</result>
        </action>
        <action name="actionNames" class="org.apache.struts2.config_browser.ActionNamesAction">
            <result type="freemarker" name="success">/config-browser/actionNames.ftl</result>
        </action>
        <!-- showConfig, showConstants, showBeans, showJars, showValidators, validatorDetails -->
    </package>
</struts>

The recurring pattern is a package that extends struts-default and registers a result type. A plugin does not appear in the application’s own configuration: the application’s package extends the plugin’s package (extends="xslt-default") to get the result type, or names the type directly in a result-type element. The descriptors of all the 7.4.0 plugins still declare the Struts 6.0 DTD (struts-6.0.dtd), because a 7.0 DTD was never published (the page on configuration files records that the URL answers 404); the descriptors you write should do the same. The plugin can also ship static files: everything under /static in the jar is served at /static/... by the framework’s static content loader, and the example below shows it.

Writing a Plugin: Beans, Actions, Templates and Static Files

The official tutorial on extending an application with custom plugins shows the idea in four steps: an interface in a jar that both the application and the plugin see, a plugin jar with an implementation registered as a bean, an action that asks the container for all beans of that interface, and a JSP that lists them. The tutorial’s code has typos and old package names (see Where the Official Pages and the Older Books Are Outdated), so this section does the same job with the current API on the Bookshelf: a badge for a book ("classic", "new").

The interface lives in its own jar so the application and the plugin do not depend on each other. It is the only thing both sides compile against:

package org.example.badge;

/** Extension interface: every plugin jar contributes beans of this type. */
public interface BadgeProvider {
    String badgeFor(String title);
}

The plugin jar has two implementations, a Struts action that lists them, a FreeMarker template, a static file and the descriptor. The class of the action asks the container for every bean of the interface by name; the container is the Struts dependency injection container that the page on configuration files describes, and its package in 7.4.0 is org.apache.struts2.inject, not the com.opensymphony.xwork2.inject of the older documentation:

package org.example.badge.plugin;

import org.apache.struts2.ActionSupport;
import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;
import org.example.badge.BadgeFormatter;
import org.example.badge.BadgeProvider;

public class BadgeAction extends ActionSupport {
    private Container container;
    private BadgeFormatter formatter;

    @Inject
    public void setContainer(Container container) {
        this.container = container;
    }

    @Inject(required = false)
    public void setFormatter(BadgeFormatter formatter) {
        this.formatter = formatter;
    }

    public Map<String, String> getBadges() {
        Map<String, String> result = new LinkedHashMap<>();
        for (String name : container.getInstanceNames(BadgeProvider.class)) {
            String badge = container.getInstance(BadgeProvider.class, name).badgeFor("Neuromancer");
            result.put(name, formatter == null ? badge : formatter.format(badge));
        }
        return result;
    }
}

The descriptor registers the beans by type and unique name, defines one constant, a package in its own namespace and a result that uses a template inside the jar (the FreeMarker result loads templates from the class path too):

<struts>
    <bean type="org.example.badge.BadgeProvider" name="classic" class="org.example.badge.plugin.ClassicBadge"/>
    <bean type="org.example.badge.BadgeProvider" name="newest" class="org.example.badge.plugin.NewBadge"/>
    <bean type="org.example.badge.BadgeFormatter" name="bracket" class="org.example.badge.plugin.BracketFormatter"/>

    <constant name="struts.badge.formatter" value="bracket"/>

    <package name="badge-default" extends="struts-default" namespace="/badges">
        <action name="list" class="org.example.badge.plugin.BadgeAction">
            <result type="freemarker">/badges/list.ftl</result>
        </action>
    </package>
</struts>

The template badges/list.ftl and a stylesheet static/badge.css are in the same jar. After dropping the jars into the WAR, the plugin’s action and its static file answered without a line in the application’s struts.xml:

$ curl http://localhost:18080/g11other/badges/list
classic=[classic]
newest=[new]

$ curl -i http://localhost:18080/g11other/static/badge.css
HTTP/1.1 200
Cache-Control: public
Content-Type: text/css

body { font-family: sans-serif; }

The formatter is the second kind of extension: a plugin-defined extension point. The next section explains how it is wired.

Extension Points

An extension point is a name (a constant) that locates a bean by name and injects it where the framework or a plugin asks for it. The official page puts it as a bridge between an interface and its final implementation, which "is not a bean" and cannot be injected by itself. The core defines its own in StrutsBeanSelectionProvider: the 7.4.0 source has more than 60 alias calls, each tying a class to a constant such as struts.objectFactory, struts.mapper.class, struts.multipart.parser or struts.date.formatter. The application selects an implementation by registering a bean and pointing the constant at its name, as the plugins page shows:

<bean type="org.apache.struts2.components.date.DateFormatter"
      name="myDateTimeFormatter"
      class="com.company.date.MyDateTimeFormatterAdapter"
      scope="singleton"/>
<constant name="struts.date.formatter" value="myDateTimeFormatter"/>

The table lists a selection of the extension points of 7.4.0 with the class they are declared against. The official page has the same table, but every type in it from the XWork days is out of date; the current packages were read from the imports of StrutsBeanSelectionProvider in the 7.4.0 sources jar.

Constant Type in 7.4.0 What it replaces

struts.objectFactory

org.apache.struts2.ObjectFactory

How actions, results and interceptors are created (this is how the Spring plugin takes over)

struts.objectFactory.actionFactory, resultFactory, interceptorFactory, converterFactory, validatorFactory, unknownHandlerFactory

org.apache.struts2.factory.*Factory

Dedicated factories, lighter than a new object factory

struts.actionProxyFactory

org.apache.struts2.ActionProxyFactory

Creation of the action proxy

struts.mapper.class

org.apache.struts2.dispatcher.mapper.ActionMapper

How a URL maps to an action

struts.multipart.parser

org.apache.struts2.dispatcher.multipart.MultiPartRequest

The file upload parser

struts.freemarker.manager.classname

org.apache.struts2.views.freemarker.FreemarkerManager

Loading and processing of FreeMarker templates

struts.actionValidatorManager

org.apache.struts2.validator.ActionValidatorManager

The validation framework

struts.valueStackFactory, struts.reflectionProvider

org.apache.struts2.util.ValueStackFactory, org.apache.struts2.util.reflection.ReflectionProvider

The value stack and the expression language plumbing

struts.localizedTextProvider, struts.localeProviderFactory

org.apache.struts2.text.LocalizedTextProvider, org.apache.struts2.locale.LocaleProviderFactory

Text lookup and the locale

struts.staticContentLoader

org.apache.struts2.dispatcher.StaticContentLoader

Serving of /static files (the plugin static resources)

struts.date.formatter

org.apache.struts2.components.date.DateFormatter

The s:date tag; the example of the official page

struts.ognlGuard, struts.securityMemberAccess

org.apache.struts2.ognl.OgnlGuard, org.apache.struts2.ognl.SecurityMemberAccess

Restrictions on OGNL evaluation

struts.executor.provider, struts.csp.nonce.reader, struts.parameterAuthorizer

org.apache.struts2.interceptor.exec.ExecutorProvider, org.apache.struts2.interceptor.csp.CspNonceReader, org.apache.struts2.interceptor.parameter.ParameterAuthorizer

Newer extension points: the execAndWait executor, the CSP nonce and the @StrutsParameter authorizer

A plugin can define its own extension point, and the Bookshelf plugin does. It declares an interface (BadgeFormatter), a bean-selection provider that ties a constant name to it, and an entry in struts-deferred.xml. The provider extends AbstractBeanSelectionProvider and has a no-argument constructor, as the official page says; alias uses the constant’s value as the bean name and makes that bean the default instance of the type:

public class BadgeBeanSelectionProvider extends AbstractBeanSelectionProvider {
    public static final String STRUTS_BADGE_FORMATTER = "struts.badge.formatter";

    @Override
    public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
        alias(BadgeFormatter.class, STRUTS_BADGE_FORMATTER, builder, props);
    }
}
<!-- struts-deferred.xml, in the plugin jar -->
<struts>
    <bean-selection name="badgeBeans" class="org.example.badge.plugin.BadgeBeanSelectionProvider"/>
</struts>

The same arrangement is in the Velocity plugin of 7.4.0: its struts-deferred.xml holds <bean-selection name="velocityBeans" class="org.apache.struts2.views.velocity.VelocityBeanSelectionProvider"/>, as the official page shows it. The application now replaces the plugin’s formatter by registering a bean of the interface and pointing the constant at it; the plugin’s own default (bracket) is overridden because struts.xml is loaded after struts-plugin.xml:

<!-- struts.xml of the application -->
<bean type="org.example.badge.BadgeFormatter" name="angle" class="org.example.bookshelf.AngleFormatter"/>
<constant name="struts.badge.formatter" value="angle"/>
$ curl http://localhost:18080/g11other/badges/list
classic=&lt;classic&gt;
newest=&lt;new&gt;

The angle brackets are escaped because the FreeMarker result of 7.4.0 escapes HTML by default (see FreeMarker and Velocity Views). One mistake cost me a failed first run and is worth remembering: the injection point of a bean that stands behind an extension point is injected without a name. I first wrote @Inject("struts.badge.formatter"), which asks the container for a bean called struts.badge.formatter, found none (the parameter is optional) and printed the badges without brackets; a plain @Inject(required = false) receives the bean the constant selected. A plugin written like this can also be controlled by the same constants as the framework: any constant its struts-plugin.xml defines is overridden by the application’s struts.xml.

The Config Browser Plugin

The config browser is a read-only view of the configuration the framework loaded, served by actions in the /config-browser namespace. Its official page says it should be used only during development and that access to it "should be strictly restricted". Installing it is a dependency and nothing else; the descriptor above shows the package. In the scratch application http://localhost:18080/g11other/config-browser/index redirects to actionNames and shows the namespaces and the actions of each one:

Action What it shows in 7.4.0

index

Redirects to actionNames

actionNames

The namespaces on the left (here: the default, /config-browser, /badges, /, /xml, /xmlmore, /xmldump, /reports, /charts) and the actions of each

showConfig?namespace=/xml&actionName=books

One action: its class, method, default URL (/g11other/xml/books.action), results, exception mappings, interceptors, properties and validators

showConstants

All constants with their effective values (it begins with struts.url.queryStringParser, struts.multipart.maxFiles, struts.serve.static and the rest of the list)

showBeans

Every bean of the container by type, alias, implementation and constant name

showJars

The jars on the class path with artifact id, group id and version (read from the jars' Maven metadata; the menu says "requires Maven 2 data")

showValidators, validatorDetails

The registered validators; the list was empty for this application

The reason for the warning is plain: everything on those pages is information that an attacker would otherwise have to guess, including the effective security constants and the version of every library. The plugin does not check struts.devMode (it worked with the default false) and has no authentication of its own, so the application has to keep it out of production. There are two ways. The simplest is a Maven profile that adds the dependency only for development builds, so the jar is not in the WAR that is deployed. The second is a standard security constraint in web.xml that requires a role nobody holds in production, or one that only an operations realm grants; with this constraint in the scratch application every configuration browser URL answered 403:

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Struts configuration browser</web-resource-name>
        <url-pattern>/config-browser/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>ops</role-name>
    </auth-constraint>
</security-constraint>
<security-role>
    <role-name>ops</role-name>
</security-role>
$ curl -s -o /dev/null -w "%{http_code}\n" http://localhost:18080/g11other/config-browser/showConstants
403

For debugging during development the plugin is a convenient look at "why does this URL not map" and "which constant won", and the debugging page of this part discusses it next to struts.devMode and the debugging interceptor. The pages show their age (a footer that reads "2003 - 2015" and a menu entry about Maven 2), but they worked on Struts 7.4.0 and Tomcat 11.

The XSLT Result

The xslt result serialises the action into an XML document and runs a stylesheet over it. It was part of the core up to 6.1.x (the 6.1.2 jar holds org.apache.struts2.views.xslt.XSLTResult and struts-default.xml registers the type); since 6.2.0 (WW-5264) it is the struts2-xslt-plugin, with the package org.apache.struts2.result.xslt and the package xslt-default that registers the type. Without the plugin on the class path, type="xslt" is an unknown result type; with the plugin, the application’s package extends xslt-default. That is the only change needed to port an older application, apart from the stylesheet, which is untouched.

The first thing to see is what the stylesheet receives. An identity transform shows the document built from the Bookshelf action: the root element is always result, every property the action exposes (including the inherited ones: actionErrors, actionMessages, fieldErrors, locale, texts) becomes an element, in alphabetical order, and a list becomes a sequence of item elements (not elements named after the class):

<package name="xmldump" extends="xslt-default" namespace="/xmldump">
    <action name="books" class="org.example.bookshelf.BookListAction">
        <result type="xslt">/WEB-INF/xsl/dump.xsl</result>
    </action>
</package>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/"><xsl:copy-of select="."/></xsl:template>
</xsl:stylesheet>
$ curl http://localhost:18080/g11other/xmldump/books
<?xml version="1.0" encoding="UTF-8"?><result>
    <actionErrors/>
    <actionMessages/>
    <books>
        <item>
            <author>Frank Herbert</author>
            <title>Dune</title>
            <year>1965</year>
        </item>
        ... (two more items)
    </books>
    <fieldErrors/>
    <locale> ... about a dozen children ... </locale>
    <shelfName>Main shelf</shelfName>
    <texts>null</texts>
</result>

With that document in mind a real stylesheet is short. The next one turns the shelf into a small XML catalogue; the first version of it that I wrote used books/book, which matched nothing and produced an empty element, because the elements are called item:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/result">
        <catalogue shelf="{shelfName}">
            <xsl:for-each select="books/item">
                <entry year="{year}"><xsl:value-of select="title"/> by <xsl:value-of select="author"/></entry>
            </xsl:for-each>
        </catalogue>
    </xsl:template>
</xsl:stylesheet>
$ curl -i http://localhost:18080/g11other/xml/books
HTTP/1.1 200
Content-Type: text/xml;charset=UTF-8

<?xml version="1.0" encoding="UTF-8"?><catalogue shelf="Main shelf">
    <entry year="1965">Dune by Frank Herbert</entry>
    <entry year="1984">Neuromancer by William Gibson</entry>
    <entry year="1989">Hyperion by Dan Simmons</entry>
</catalogue>

The parameters of the result were checked against the setXxx methods of XSLTResult in the 7.4.0 sources, and each one was tried. The table compares them with the official page, which has three differences:

Parameter or constant The official page says What 7.4.0 does (verified)

stylesheetLocation (default parameter)

The location of the stylesheet

A path inside the web application, read with ServletContext.getResource; a missing file fails with "Stylesheet …​ not found in resources". Templates are cached per path in a ConcurrentHashMap (WW-5647, 7.3.0)

parse

"true by default"; when false the location is not parsed for expressions

The default in the 7.4.0 source is false. /WEB-INF/xsl/%{style}.xsl was used literally (500, "not found") until parse was set to true, and then it resolved to dump.xsl

exposedValue

An OGNL expression, shown as ${user}

Evaluated as a plain OGNL expression, without %{...} or ${...}: books[1] exposed the second book as the document (<result><author>William Gibson</author>...), while %{books[1]} answered 500

matchingPattern, excludingPattern

Regular expressions on element paths, to keep a large object graph small

Not in the 7.4.0 class: no setter exists, the parameters are silently ignored and the whole tree is built (verified: the locale element was still there with excludingPattern set to .*(locale).*)

encoding

The character encoding used in the XML, UTF-8 by default

Applied to the HTTP response (Content-Type: text/xml;charset=ISO-8859-1 for the value ISO-8859-1); the XML declaration is written by the stylesheet’s xsl:output, so it can still say UTF-8

status

Not documented

Exists in 7.4.0: the HTTP status (202 was returned for 202)

location

Deprecated, dropped in 2.5

Gone: use stylesheetLocation

struts.xslt.nocache

Disables the template cache; good for development

Read by the result; the constant is XsltConstants.STRUTS_XSLT_NOCACHE

The page on results shows the xslt result in its overview of result types; its example with a matchingPattern parameter produces the same output with or without the parameter in 7.4.0 (see Where the Official Pages and the Older Books Are Outdated). Two more notes on the XSLT result. It uses the XSLT engine of the JDK (XSLTC), not Apache Xalan, so the long explanation on the official page of how Xalan walks the object graph and the loop eliminator is background; the plugin has no Xalan dependency. And the result exposes every public property of the action, so a stylesheet is a view of the action’s data, not of a DTO: a property you would not put in a page (a password, an internal id) is in the XML too. For an API that needs a stable document, expose a purpose-built object with exposedValue, or use the JSON or REST plugin.

JasperReports: the 6.x and the 7 Plugins

JasperReports is an LGPL reporting library that turns a compiled report (.jasper) and a data source into PDF, HTML, CSV, XLSX and other formats. Struts has two plugins for it, and they are independent: struts2-jasperreports-plugin (package jasperreports-default, result type jasper, class JasperReportsResult) targets the JasperReports 6.x line and is built against 6.21.5, and struts2-jasperreports7-plugin (package jasperreports7-default, result type jasperReport7, class JasperReport7Result) was added for the 7.x line and first appears on Maven Central as 7.1.1. The official page says plainly that the two are not meant to be used together. Neither plugin ships the JasperReports library: it is LGPL licensed and cannot be distributed in an Apache release. The reporting itself (the page layout, the PDF engines, fonts, signatures and forms) is outside this documentation set’s Struts pages; the PDF Generation section of this site covers JasperReports and iText (see PDF Export Configuration for the exporter side), and this page only shows how the Struts side of an integration works.

Setup and a Trap

The Struts 7 plugin documents net.sf.jasperreports:jasperreports and jasperreports-pdf as dependencies you add yourself, with the same version, and says that since 7.4.0 the library is no longer transitive (WW-5735). On Struts 7.4.0 that is not enough. With exactly those dependencies the build is green and the first request fails, because Maven resolves Jackson from two places: the Struts core brings jackson-databind 2.22.2, JasperReports 7.0.8 brings jackson-core, jackson-annotations and jackson-dataformat-xml 2.18.8, and the mix is not loadable:

java.lang.NoClassDefFoundError: Could not initialize class com.fasterxml.jackson.databind.ObjectMapper
  at net.sf.jasperreports.jackson.util.JacksonUtil.getXmlMapper(JacksonUtil.java:164)
Caused by: java.lang.ExceptionInInitializerError:
  Exception java.lang.NoClassDefFoundError: com/fasterxml/jackson/annotation/JsonSerializeAs

The fix is to align all Jackson artifacts with the import of the Jackson BOM at the version Struts uses (checked: with it Maven resolves jackson-core, -databind and -dataformat-xml 2.22.2 and jackson-annotations 2.22):

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson</groupId>
            <artifactId>jackson-bom</artifactId>
            <version>2.22.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!-- plus the struts2-bom import -->
    </dependencies>
</dependencyManagement>

JasperReports 6 and 7 do not read the same report templates either. The template of the Getting Started tutorial (a jasperReport with a DOCTYPE, reportElement and textElement children) and the same file with the 6.x namespace both failed to compile under JasperReports 7.0.8 with "Unable to load report", while a report in the JasperReports 7 syntax compiled. An existing report therefore has to be converted (open and save it with the designer of version 7) before it can be used with the 7 plugin. This is the 7 syntax of the Bookshelf report (abbreviated: the column header band is left out); there is no xmlns, and elements carry a kind:

<jasperReport name="books" language="java" pageWidth="595" pageHeight="842" columnWidth="515"
              leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
    <parameter name="ShelfTitle" class="java.lang.String"/>
    <field name="title" class="java.lang.String"/>
    <field name="author" class="java.lang.String"/>
    <field name="year" class="java.lang.Integer"/>
    <title height="40">
        <element kind="textField" x="0" y="0" width="515" height="30" fontSize="18.0" bold="true">
            <expression><![CDATA[$P{ShelfTitle}]]></expression>
        </element>
    </title>
    <detail>
        <band height="20">
            <element kind="textField" x="0" y="0" width="200" height="20"><expression><![CDATA[$F{title}]]></expression></element>
            <element kind="textField" x="200" y="0" width="200" height="20"><expression><![CDATA[$F{author}]]></expression></element>
            <element kind="textField" x="400" y="0" width="115" height="20"><expression><![CDATA[$F{year}]]></expression></element>
        </band>
    </detail>
</jasperReport>

The tutorial’s action compiles the .jrxml in execute(), and the tutorial itself says not to do that in production. Compile at build time instead, with whatever the build offers. The 6.x plugin ships a small command-line class for it (org.apache.struts2.views.jasperreports.CompileReport), and for 7 a few lines of Java do the same; the compiled file books.jasper in the scratch WAR came from this helper:

import net.sf.jasperreports.engine.JasperCompileManager;

public class Comp {
    public static void main(String[] args) throws Exception {
        JasperCompileManager.compileReportToFile(args[0], args[1]);
    }
}

The Result and Its Formats

The action only has to supply the data. The Bookshelf ReportAction returns the three books and, as a map, a report parameter with the title; it also reads the requested format from the request (a String property with @StrutsParameter, because the format comes from a request parameter, see Parameter Injection and @StrutsParameter):

public class ReportAction extends ActionSupport implements JasperReport7Aware {
    private String format = "pdf";

    public String getFormat() { return format; }

    @StrutsParameter
    public void setFormat(String format) { this.format = format; }

    public List<Book> getBooks() { return new BookListAction().getBooks(); }

    public Map<String, Object> getReportParameters() { return Map.of("ShelfTitle", "Bookshelf catalogue"); }

    @Override
    public String getCsvDelimiter(ActionInvocation invocation) { return ";"; }
}
<package name="reports" extends="jasperreports7-default" namespace="/reports">
    <action name="books" class="org.example.bookshelf.ReportAction">
        <result type="jasperReport7">
            <param name="location">/WEB-INF/reports/books.jasper</param>
            <param name="dataSource">books</param>
            <param name="reportParameters">reportParameters</param>
            <param name="format">${format}</param>
        </result>
    </action>
</package>

Requesting the same URL with the different format values gave the following (all with HTTP 200; the sizes are of the Bookshelf report):

format Content-Type Remarks

none, pdf, PDF

application/pdf

A one-page PDF of 1197 bytes. PDF works in 7.4.0 (the lookup is retried in lower case, WW-5732); in 7.3.0 and earlier it fails

csv, CSV

text/csv

The delimiter came from getCsvDelimiter of JasperReport7Aware: Bookshelf catalogue;;, Title;Author;Year, Dune;Frank Herbert;1965 and so on

html

text/html

About 4 KB of positioned HTML; images are served from struts.jasperReport7.html.imageServletUrl (default /images/)

xlsx

application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

A real XLSX file. xls fails: "No exporter found for format: xls"

xml, rtf

text/xml, application/rtf

Both produced

txt

text/plain

Not shipped: a custom exporter provider, see below

The other parameters of the result were checked with a second mapping: contentDisposition set to attachment and documentName set to bookshelf produced Content-disposition: attachment; filename=bookshelf.xlsx, so the file name takes the extension from the format.

The extra hook is JasperReport7Aware, a set of default methods an action can override: beforeReportGeneration, afterReportGeneration, getCsvDelimiter and getReportLocale. The example above used getCsvDelimiter and the output shows it in effect. An exporter for another format is a bean of type JasperReport7ExporterProvider registered under the new format’s name; the provider has to set the content type and attach the exporter to the response stream. The official page uses a DOCX exporter that needs further libraries, so I wrote a text exporter with a class that JasperReports 7 includes. It failed once with "Character width in pixels or page width in characters must be specified and must be greater than zero", because the text exporter needs a page size in characters, which the provider has to configure:

public class TextExporterProvider implements JasperReport7ExporterProvider<JRTextExporter> {
    @Override
    public JRTextExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
        HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
        response.setContentType("text/plain;charset=UTF-8");
        JRTextExporter exporter = new JRTextExporter();
        exporter.setExporterInput(new SimpleExporterInput(jasperPrint));
        SimpleTextReportConfiguration configuration = new SimpleTextReportConfiguration();
        configuration.setPageWidthInChars(80);
        configuration.setPageHeightInChars(40);
        exporter.setConfiguration(configuration);
        try {
            exporter.setExporterOutput(new SimpleWriterExporterOutput(response.getOutputStream()));
        } catch (IOException e) {
            throw new StrutsException(e);
        }
        return exporter;
    }
}
<bean type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"
      name="txt" class="org.example.bookshelf.TextExporterProvider"/>
$ curl "http://localhost:18080/g11other/reports/books?format=txt"
     Bookshelf catalogue

     Title                      Author                     Year
     Dune                       Frank Herbert              1965
     Neuromancer                William Gibson             1984
     Hyperion                   Dan Simmons                1989

The remaining parts of the 7 plugin are the two constants (struts.jasperReport7.csv.defaultDelimiter, , by default, and struts.jasperReport7.html.imageServletUrl, /images/) and the timeZone parameter, which is passed to the report as REPORT_TIME_ZONE. The official page also documents the migration from the 6.x plugin; it matters when a report is ported:

Change from the 6.x plugin What to do

Package jasperreports7-default, result type jasperReport7

Rename in struts.xml

Lower-case format values; xls is gone, use xlsx

PDF still resolves in 7.4.0 (WW-5732); 7.3.0 and earlier reject it

delimiter and imageServletUrl result parameters are gone

Use the constants or JasperReport7Aware.getCsvDelimiter

exportParameters is gone

Configure the exporter in code, in a custom exporter provider

The PDF exporter is no longer bundled

Add net.sf.jasperreports:jasperreports-pdf with the same version as jasperreports

The 6.x plugin (jasper result) takes the same location, dataSource, parse, format, contentDisposition, documentName, reportParameters and connection parameters, and also delimiter, imageServletUrl, exportParameters and wrapField; its format constants are upper case (PDF, XML, HTML, XLS, CSV, RTF, from JasperReportConstants). That plugin was not run on this page; the facts come from its official page, the descriptor and the class listing of the 7.4.0 jar. Since 7.4.0 both plugins can fill a report from report parameters alone when neither dataSource nor connection is set (WW-3245), with the object the query executer expects (a Hibernate session under HIBERNATE_SESSION, a CSV or JSON stream, a ready data source) in reportParameters, and a report parameter that the map does not hold is looked up on the value stack again (WW-5729).

The JFreeChart Plugin

The JFreeChart plugin returns a chart as an image. The package jfreechart-default registers the result type chart (org.apache.struts2.dispatcher.ChartResult) with default width 200 and height 150; the action only has to expose a JFreeChart. As with JasperReports, the charting library is LGPL and is not bundled: the application adds org.jfree:jfreechart itself (1.5.6 is the latest on Central, the version the example used). The Bookshelf action builds a bar chart of the publication years:

public class BookChartAction extends ActionSupport {
    private JFreeChart chart;

    @Override
    public String execute() {
        DefaultCategoryDataset data = new DefaultCategoryDataset();
        for (Book b : new BookListAction().getBooks()) {
            data.addValue(b.getYear(), "Year", b.getTitle());
        }
        chart = ChartFactory.createBarChart("Publication year", "Book", "Year", data,
                PlotOrientation.VERTICAL, false, false, false);
        return SUCCESS;
    }

    public JFreeChart getChart() {
        return chart;
    }
}
<package name="charts" extends="jfreechart-default" namespace="/charts">
    <action name="years" class="org.example.bookshelf.BookChartAction">
        <result type="chart">
            <param name="width">400</param>
            <param name="height">240</param>
        </result>
    </action>
</package>
$ curl -D - -o chart.png http://localhost:18080/g11other/charts/years
HTTP/1.1 200
Content-Type: image/png

$ file chart.png
chart.png: PNG image data, 400 x 240, 8-bit/color RGBA, non-interlaced

Three details were checked in addition to the official example. A result with no parameters uses the defaults of the result type (<result type="chart"/> gave a PNG of 200 x 150). The result has more parameters than the page documents: type (png, the default, or jpeg/jpg, which gave Content-Type: image/jpg and a JPEG) and value, the name of the action property that holds the chart. The page’s "Future Work" section says the chart property is hard coded; with value set to yearChart the result read getYearChart(), so it is not. A chart is generated on every request, so for a page that is displayed often, render it once and cache it (the stream result of the results page serves a stored image). The chart is an image: for charts in a page, an img tag whose src is the chart action is the usual way to place it.

The Async Plugin

The async plugin promises asynchronous actions on top of the Servlet asynchronous processing: the action method returns a Callable<String> instead of a result code, Struts starts an asynchronous context, runs the callable on another thread and, when it completes, dispatches the request back to the container to execute the result. The plugin has existed since 6.0.0, its documentation is the page with three examples (a plain Callable, a callable with a timeout and a callable that returns another callable), and it is still in 7.4.0: AsyncAction (a Callable wrapper with constructors that take a timeout in milliseconds and an Executor), AsyncManager (the core interface) and DefaultAsyncManager, registered as a prototype bean in the plugin’s descriptor. The Bookshelf version of the example is a slow action:

public class SlowBooksAction {
    public Callable<String> execute() {
        return new AsyncAction(5000, () -> {
            Thread.sleep(500);
            return "success";
        });
    }
}
<action name="slow" class="org.example.bookshelf.SlowBooksAction">
    <result>/WEB-INF/views/slow.jsp</result>
</action>

The Servlet side is mandatory: the filter or servlet that serves Struts must declare async-supported (as with any asynchronous servlet, see Asynchronous Processing in Brief), and the official page shows the StrutsServlet with the flag. The result was not what the page promises. I ran the same action in three configurations, with a counter in execute() and a log line per call; all three used Tomcat 11.0.26 and Struts 7.4.0:

Configuration Result What happened

StrutsPrepareAndExecuteFilter with async-supported and the default dispatcher (REQUEST)

404 instead of the page

execute() ran once; the dispatch back to the application after the callable finished did not reach a filter mapped only for REQUEST

The same filter mapped for REQUEST and ASYNC

No response; the client timed out

execute() ran ten times within about five seconds, each time starting a new thread

StrutsServlet mapped to / with async-supported (the official page’s configuration)

No response; the client timed out

Same loop: ten calls within about five seconds

The cause is visible in the 7.4.0 sources. DefaultAsyncManager is a prototype bean, so every ActionInvocation gets its own instance, and the result of the callable is stored in that instance. After AsyncContext.dispatch() the container calls Struts again, the framework creates a new invocation with a new manager, hasAsyncActionResult() is false, the action method runs again and starts another asynchronous task, and so on until the application is stopped. I found nothing in the core that carries the first invocation over to the dispatched request, and I could not verify the plugin on any other configuration, so the honest summary is limited to this: on Struts 7.4.0 with Tomcat 11.0.26, the three configurations above did not complete a request, and the page’s own examples cannot be recommended. Do not use the async plugin in an application until this is fixed or documented. The alternatives that work today are the servlet’s own asynchronous API for one endpoint (the page on Servlet Fundamentals shows it), a normal executor with a polling page, and the execAndWait interceptor with its wait page, which is the Struts-native tool for long-running actions (see Execute and Wait).

The Javatemplates Plugin

The javatemplates plugin is a faster implementation of the tags of the simple theme: instead of rendering a FreeMarker template, a Java class writes the markup. It is switched on by two constants, which the plugin documentation lists, and the plugin’s descriptor registers a template engine called java and adds it to struts.templateEngines (ftl,jsp,vm,java):

<constant name="struts.ui.templateSuffix" value="java"/>
<constant name="struts.ui.theme" value="simple"/>
<%@ taglib prefix="s" uri="/struts-tags" %>
<s:form action="books" method="get">
    <s:textfield name="filter" label="Title"/>
    <s:submit value="Search"/>
</s:form>

The same page rendered with the plugin and with the default templates (struts.ui.templateSuffix ftl), from the same JSP, shows that the markup is not identical byte for byte, although it is equivalent:

<!-- templateSuffix = java -->
<form name="books" id="books" action="/g11other/books.action" method="get">
    <input type="text" name="filter" id="books_filter"></input>
    <input type="submit" value="Search" id="books_0"></input>
</form>

<!-- templateSuffix = ftl -->
<form id="books" name="books" action="/g11other/books.action" method="get">
    <input type="text" name="filter" value="" id="books_filter"/>
    <input type="submit" value="Search" id="books_0"/>
</form>

The differences are attribute order, a value="" that the FreeMarker template writes and the Java version leaves out, and <input></input> instead of self-closing elements; the last one is valid in HTML but not in XML. The plugin is a drop-in replacement for the 19 tags it supports (a, actionerror, actionmessage, checkbox, datetextfield, div, fielderror, file, form, head, hidden, label, password, select, reset, submit, textfield, textarea and token), only in the simple theme; the performance benchmark section of its page still reads "TODO", so there is no official number, and the performance tuning page of the Core Developer Guide only suggests it. The constant struts.ui.templateSuffix is global, so every tag that is not in the list is rendered by its own FreeMarker template, as before. The page on themes and templates explains the template engines, and the page on UI and form tags notes that s:datetextfield needs this plugin and has a quirk with the simple theme.

The JUnit and TestNG Plugins

Both testing plugins are test-scoped dependencies, and neither belongs in WEB-INF/lib. The JUnit plugin offers base classes that start a full Struts instance and simulate a request: StrutsTestCase (JUnit 3 style), StrutsJUnit4TestCase, StrutsRestTestCase, StrutsSpringTestCase, StrutsSpringJUnit4TestCase and XWorkJUnit4TestCase, all in org.apache.struts2.junit; the TestNG plugin has TestNGStrutsTestCase and TestNGXWorkTestCase in org.apache.struts2.testng. The JUnit plugin brings Spring’s mock servlet classes (spring-test, spring-web) and JUnit 4.13.2 as transitive test dependencies, which was verified in the dependency tree of the scratch project. A test of the Bookshelf shelf, run with Maven, passed:

public class BookListActionTest extends StrutsJUnit4TestCase<BookListAction> {

    @Test
    public void shelfHasThreeBooks() throws Exception {
        ActionProxy proxy = getActionProxy("/books");
        assertEquals("success", proxy.execute());
        BookListAction action = (BookListAction) proxy.getAction();
        assertEquals(3, action.getBooks().size());
    }
}
<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-junit-plugin</artifactId>
    <scope>test</scope>
</dependency>

The test extends a JUnit 4 base class, so on a project that uses JUnit Jupiter it needs the JUnit Vintage engine to run (it was run that way here). executeAction("/books") runs the full request including the result, but the JSP result is not rendered by the mock environment (the response body was empty), so rendered-page tests belong in an integration test against a real container. The TestNG plugin is declared the same way (struts2-testng-plugin, test scope) and subclassing TestNGStrutsTestCase; it was not run on this page. The Testing page of this part builds on both plugins.

Removed, Retired and Deprecated Plugins

The plugin index is a historical list. This table separates what disappeared, with the version of removal read from the Version Notes, the 6 to 7 migration guide and the Maven Central metadata (the last published version of the artifact is the evidence for a plugin whose removal is not documented). If an application of an older version uses one of them, the migration notes below are where the replacement is.

Plugin Status in the index Last Central version Notes and replacement

Codebehind

Removed since 2.5

2.3.37

Use the Convention plugin

JSF

Removed since 2.5

2.3.37

No replacement in Struts

SiteGraph

Removed since 2.5

2.5.33

No replacement is named in the index

Struts 1

Removed since 2.5

2.3.37

The Struts 1 migration is covered by the page on Struts 1 in this part

Tiles 3 (old plugin name)

Removed since 2.5

2.5-BETA2

Replaced by the Tiles plugin

Java 8 Support

2.3.21 to 2.5.2 only

2.5.2

Obsolete: Struts 7 needs Java 17

Embedded JSP

Deprecated since 6.0.0

6.12.0

No 7.x artifact; the BOM still lists it. Use a servlet container’s JSP engine

OVal

Deprecated since 6.0.0, "use Bean Validation"

6.12.0

Use the Bean Validation plugin

OSGi

Deprecated since 6.0.0

6.12.0

No 7.x artifact; the BOM still lists it

Plexus

Deprecated since 6.0.0

6.12.0

No 7.x artifact; the BOM still lists it

Portlet and Portlet Tiles

Deprecated since 6.0.0

6.12.0

Removed in 7.0.0 (WW-5306)

DWR

Listed as a plugin

6.12.0

Removed in 7.0.0 (6 to 7 migration guide), no straight replacement

Pell Multipart

Not on the index; deprecated in 6.2.0 (WW-5274)

6.12.0

Removed in 7.0.0 (6 to 7 migration guide)

SiteMesh

Listed as a plugin

6.12.0

Deprecated, un-deprecated in 6.4.0 (WW-5344) and removed in 7.0.0 (WW-5472); use SiteMesh 3 directly as a filter, see the sitemesh3 example of the Struts examples repository

GXP

Not on the index

6.12.0

The BOM lists struts2-gxp-plugin, but there is no 7.x artifact

The migration guide’s wording for the plugins removed in 7.0.0 is: the Portlet plugins, the Pell Multipart plugin, DWR and SiteMesh; "there is no straight replacement" for them, and SiteMesh is replaced by direct integration with SiteMesh 3. The page of this part on Tiles and Page Layouts shows how Tiles, a tag-file layout and SiteMesh 3 compare.

Which Plugin When

Need Plugin Condition and caution

Understand what Struts loaded and why a URL does not map

Config Browser

Development only; never reachable in production

An XML document from an action, shaped by a stylesheet

XSLT result

Every property of the action is exposed; for a stable API prefer the JSON or REST plugin

A PDF, CSV or XLSX report from a data list

JasperReports 7 (or 6 for old reports)

Add the library and Jackson BOM yourself; the report must be in the 7 syntax

A chart image

JFreeChart

Add org.jfree:jfreechart; cache the image if it is requested often

Long-running work without holding the user

execAndWait interceptor (core)

Not the async plugin in 7.4.0

Faster simple theme markup

Javatemplates

Only the 19 listed tags; markup differs slightly from the FreeMarker templates

Testing actions inside a Struts invocation

JUnit or TestNG plugin

Test scope only

Your own reusable extension for several applications

A custom plugin jar

struts-plugin.xml plus an interface jar, and struts-deferred.xml for extension points

Pitfalls

Symptom Cause Fix

A report request fails with NoClassDefFoundError: Could not initialize class com.fasterxml.jackson.databind.ObjectMapper, JsonSerializeAs missing, while the build is green

Struts core brings Jackson 2.22.2 and JasperReports 7.0.8 brings 2.18.8 of the other Jackson artifacts; Maven mixes them

Import com.fasterxml.jackson:jackson-bom at 2.22.2 in dependencyManagement

JRException: Unable to load report for a template that worked with JasperReports 6

JasperReports 7 reads the new JRXML syntax; reportElement or a DOCTYPE or 6.x namespace are not accepted

Convert the report with the version 7 designer and compile at build time

ServletException: No exporter found for format: xls

The 7 plugin has no xls; formats are lower case (pdf, csv, html, xlsx, xml, rtf)

Use xlsx, or register a custom JasperReport7ExporterProvider

A custom exporter fails with a message about page width in characters

The exporter needs a configuration that the provider must supply

Set the exporter configuration in createExporter, as in the text exporter above

The XSLT output has an empty element where the list should be

A list is exposed as item elements, not book elements

Use books/item in the stylesheet; dump the document with an identity transform first

matchingPattern or excludingPattern has no effect, a 500 error occurs with %{...} in exposedValue, or the stylesheet path with %{...} is used literally

7.4.0 has no pattern parameters; exposedValue is plain OGNL; parse defaults to false

Remove the patterns, write exposedValue as books[1], set parse to true for dynamic stylesheet names

An async action never returns, or returns 404, or runs repeatedly

The async plugin of 7.4.0 loses the invocation when the container dispatches back to the request, see The Async Plugin

Do not use it; use execAndWait or the servlet’s own async API

An extension point bean is null in the action

The injection used @Inject("constant.name"), which asks for a bean with that name

Use @Inject (default name) and let the constant select the bean

The config browser is reachable in the deployed application

The plugin jar is in the WAR and no constraint covers /config-browser/*

Build it into development profiles only, or protect it with a security-constraint

A BOM-managed plugin dependency does not resolve

The BOM lists artifacts that were never published at 7.x (embedded JSP, GXP, OSGi, Plexus)

Remove the dependency; see the table of removed plugins

Static files in a plugin are not served

The files are not under /static in the jar, or the page links them with another prefix

Put them under static/ in the jar and link /static/...

What Changed Recently

Version Change

6.0.0

The async and Velocity plugins are first published as separate artifacts on Maven Central

6.2.0

The XSLT result is extracted from the core into struts2-xslt-plugin (WW-5264); the Pell Multipart plugin is deprecated (WW-5274)

6.4.0

The SiteMesh plugin is un-deprecated and upgraded (WW-5344); the JasperReports 6 plugin moves to 6.21 (WW-5397)

7.0.0

Portlet, Pell Multipart, DWR and SiteMesh plugins are removed (WW-5306, WW-5472, WW-5485)

7.1.1

The JasperReports 7 plugin (jasperReport7) is the first release on Maven Central (the plugin page says 7.1.0)

7.3.0

The XSLT template cache is a ConcurrentHashMap (WW-5647)

7.4.0

JasperReports: reports can be filled from report parameters (WW-3245); value-stack parameters reach the report again (WW-5729); the 7 plugin’s CSV record delimiter, upper-case format and response-stream bugs are fixed (WW-5731, WW-5732, WW-5733); the 7 plugin no longer brings jasperreports transitively (WW-5735)

7.4.0

bean-validation and REST default stacks add the coep, coop and fetchMetadata interceptors (WW-5718); relevant when a plugin’s own stack is used

Where the Official Pages and the Older Books Are Outdated

The older books on Struts 2 (the 2.0 generation) describe a plugin world that has changed in four ways. First, the plugin list: Codebehind, SiteMesh, Tiles 2, JFreeChart and the JasperReports result were the common ones; SiteMesh and Codebehind are gone, JasperReports has a new plugin for version 7 with a different template syntax, and the XSLT result moved from the core into a plugin. Second, the framework classes: every com.opensymphony.xwork2.* name in a book (ObjectFactory, ActionProxyFactory, Container, @Inject, ActionContext) is org.apache.struts2.* in 7.4.0. Third, the namespace: javax.servlet code of the book is jakarta.servlet. Fourth, the Struts 1 book’s PlugIn interface, configured with <plug-in> in struts-config.xml, has no counterpart in Struts 2; the equivalent is a servlet context listener or a bean with a startup method. The Struts 1 migration is on the page on Struts 1 Classic and Migration in this part.

The official pages have their own inconsistencies, found while verifying the examples of this page:

  • The extension points table of the plugin architecture page lists the types with com.opensymphony.xwork2 packages and stops well short of the more than 60 aliases of 7.4.0; the classes are now in org.apache.struts2. Its examples show the 2.5 and 2.6 DTD, while the plugins of 7.4.0 declare the 6.0 DTD.

  • The tutorial on custom plugins names the interface IMyPlugin, IMyPlugIn and IMyInterface in different places, calls the plugin descriptor struts-default.xml and uses the XWork Container.

  • The XSL result page documents matchingPattern and excludingPattern (absent from 7.4.0), a parse default of true (it is false) and an exposedValue written as ${user} (it is plain OGNL); the status parameter is missing and Xalan is mentioned although the JDK engine is used.

  • The async plugin page uses a user class called AsyncAction, the same name as the plugin class that the timeout example needs, and, as shown above, did not work in my test.

  • The JasperReports tutorial describes copying jasperreports, commons-*, itext and jdt-compiler jars into WEB-INF/lib, compiles the template inside the action and uses new Long(1); the 6.x template syntax no longer loads in version 7, and the 6.x plugin’s own page now says to use the 7 plugin for 7.x.

  • The JFreeChart page’s "Future Work" says that the chart property is fixed; the value parameter exists. The plugin index lists the JasperReports 7 plugin as "7.1.0+", but the first artifact on Central is 7.1.1.

  • The JUnit plugin page’s example imports com.opensymphony.xwork2.ActionProxy, com.opensymphony.xwork2.Action and javax.servlet.ServletException, and its mapping sets struts.objectFactory to spring for a test that does not use Spring.

References