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 ( |
Published since 6.0.0; did not work in my test, see The Async Plugin |
This page |
Bean Validation ( |
Published |
|
CDI ( |
Published |
|
Config Browser ( |
Published; development tool, see The Config Browser Plugin |
This page |
Convention ( |
Published |
|
JasperReports ( |
Published, targets JasperReports 6.x (built against 6.21.5, |
|
JasperReports 7 ( |
Published since 7.1.1 on Central (the index says 7.1.0+) |
|
Javatemplates ( |
Published |
The Javatemplates Plugin; the themes are in Themes and Templates |
JFreeChart ( |
Published |
|
JSON ( |
Published |
|
JUnit ( |
Published |
The JUnit and TestNG Plugins; the Testing page of this part goes further |
REST ( |
Published |
|
Spring ( |
Published |
|
Tiles ( |
Published (Tiles 3 code bundled since 6.3.0) |
|
Velocity ( |
Published since 6.0.0; the index page is a three-line stub |
|
XSLT ( |
Published since 6.2.0, not on the plugin index; the result is documented under the Core Developer Guide |
|
Embedded JSP, OSGi, Plexus, Portlet Tiles, GXP, DWR, OVal, Portlet, SiteMesh |
Not published for 7.x; last version 6.12.0 |
|
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) |
Source: Apache Struts — 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.
(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 |
|---|---|---|
|
|
How actions, results and interceptors are created (this is how the Spring plugin takes over) |
|
|
Dedicated factories, lighter than a new object factory |
|
|
Creation of the action proxy |
|
|
How a URL maps to an action |
|
|
The file upload parser |
|
|
Loading and processing of FreeMarker templates |
|
|
The validation framework |
|
|
The value stack and the expression language plumbing |
|
|
Text lookup and the locale |
|
|
Serving of |
|
|
The |
|
|
Restrictions on OGNL evaluation |
|
|
Newer extension points: the |
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=<classic>
newest=<new>
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 |
|---|---|
|
Redirects to |
|
The namespaces on the left (here: the default, |
|
One action: its class, method, default URL ( |
|
All constants with their effective values (it begins with |
|
Every bean of the container by type, alias, implementation and constant name |
|
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") |
|
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) |
|---|---|---|
|
The location of the stylesheet |
A path inside the web application, read with |
|
"true by default"; when false the location is not parsed for expressions |
The default in the 7.4.0 source is |
|
An OGNL expression, shown as |
Evaluated as a plain OGNL expression, without |
|
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 |
|
The character encoding used in the XML, |
Applied to the HTTP response ( |
|
Not documented |
Exists in 7.4.0: the HTTP status ( |
|
Deprecated, dropped in 2.5 |
Gone: use |
|
Disables the template cache; good for development |
Read by the result; the constant is |
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, |
|
A one-page PDF of 1197 bytes. |
|
|
The delimiter came from |
|
|
About 4 KB of positioned HTML; images are served from |
|
|
A real XLSX file. |
|
|
Both produced |
|
|
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 |
Rename in |
Lower-case format values; |
|
|
Use the constants or |
|
Configure the exporter in code, in a custom exporter provider |
The PDF exporter is no longer bundled |
Add |
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 |
|---|---|---|
|
404 instead of the page |
|
The same filter mapped for |
No response; the client timed out |
|
|
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 |
GXP |
Not on the index |
6.12.0 |
The BOM lists |
Source: Apache Struts — Plugins
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 |
Long-running work without holding the user |
|
Not the async plugin in 7.4.0 |
Faster |
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 |
|
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
A report request fails with |
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 |
|
JasperReports 7 reads the new JRXML syntax; |
Convert the report with the version 7 designer and compile at build time |
|
The 7 plugin has no |
Use |
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 |
The XSLT output has an empty element where the list should be |
A list is exposed as |
Use |
|
7.4.0 has no pattern parameters; |
Remove the patterns, write |
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 |
An extension point bean is |
The injection used |
Use |
The config browser is reachable in the deployed application |
The plugin jar is in the WAR and no constraint covers |
Build it into development profiles only, or protect it with a |
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 |
Put them under |
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 |
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 ( |
7.3.0 |
The XSLT template cache is a |
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 |
7.4.0 |
|
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.xwork2packages and stops well short of the more than 60 aliases of 7.4.0; the classes are now inorg.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,IMyPlugInandIMyInterfacein different places, calls the plugin descriptorstruts-default.xmland uses the XWorkContainer. -
The XSL result page documents
matchingPatternandexcludingPattern(absent from 7.4.0), aparsedefault oftrue(it isfalse) and anexposedValuewritten 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-*,itextandjdt-compilerjars intoWEB-INF/lib, compiles the template inside the action and usesnew 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
chartproperty is fixed; thevalueparameter 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.Actionandjavax.servlet.ServletException, and its mapping setsstruts.objectFactorytospringfor a test that does not use Spring.