Convention Plugin & Annotations

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 (the current 7.x release, verified on 2026-10-08) with the struts2-convention-plugin 7.4.0, running on Apache Tomcat 11.0.26 and Java 21; Struts 7 needs Java 17 and Jakarta Servlet 6 at least. The facts were checked against the plugin page of the Struts site, the Core Developers Guide annotations reference, the Getting Started "Annotations" tutorial and the 7.4.0 plugin jar itself (struts-plugin.xml, javap on the annotations and on ConventionConstants), and every example was built into a Maven WAR with the struts2-bom 7.4.0, deployed to the scratch Tomcat and exercised with curl; the outputs shown are the outputs that run produced. The Convention plugin removes the need to write one <action> element per action: it scans the class path for action classes, derives each URL from the class name and the package name, finds the view page by file name and lets annotations override any default. The running scenario is the Bookshelf catalogue of Part II: a list of titles, a detail page, an /admin area, a CSV export and a report. The other pages of this part (for instance Spring Integration, the JSON and REST plugins and Testing) are linked where they come up.

The Idea: Configuration by Convention

In Struts 2 the plain route from a URL to a page is three pieces of configuration: an action name, an action class and a result (see Actions and Results and Result Types). The XML of struts.xml writes the three pieces down. The Convention plugin derives them instead:

  • the class is found by scanning: every class in a package whose name ends with one of a few agreed words (action, actions, struts, struts2) and that either implements org.apache.struts2.action.Action or has a name ending in Action is an action;

  • the action name and namespace come from the class name and the package name: BookListAction in org.example.bookshelf.actions is /book-list, and ImportBooksAction in org.example.bookshelf.actions.admin is /admin/import-books;

  • the result is found by file name: the view of /book-list for the result code success is /WEB-INF/content/book-list.jsp (or book-list-success.jsp), and for input it is book-list-input.jsp.

Everything that does not fit the convention is declared with annotations placed on the class or on its methods: a different URL (@Action), a different view (@Result), a different namespace (@Namespace), a different parent package (@ParentPackage), interceptors (@InterceptorRef), exception mappings (@ExceptionMapping) and the location of the views (@ResultPath). The plugin is the successor of two older mechanisms: the zero configuration actionPackages of Struts 2.0 and the Codebehind plugin. The Struts documentation says that the Zero Config and Codebehind plugins are deprecated from Struts 2.1 on, and the plugin list of the site marks Codebehind as removed since 2.5; the 7.4.0 struts2-core jar does not contain the string actionPackages at all.

The plugin does not replace the rest of Struts. The filter is still declared in web.xml, the generated mappings still run the defaultStack interceptors, parameters still need @StrutsParameter (see Parameter Injection and @StrutsParameter) and a package declared in struts.xml can still coexist with the generated ones. What the plugin changes is who writes the <package> and <action> elements: at start-up the plugin’s ClasspathPackageProvider builds them in memory from the scan.

flowchart LR subgraph start["Start-up, once"] S1["Scan packages that end with a locator"] --> S2["Class implements Action or name ends with Action"] S2 --> S3["Name builder: BookListAction becomes book-list"] S3 --> S4["Generated package with action book-list and results from WEB-INF/content"] end subgraph req["Each request"] R1["GET /book-list"] --> R2["Namespace slash, action book-list"] R2 --> R3["Action runs and returns success"] R3 --> R4["Result /WEB-INF/content/book-list.jsp"] end S4 -.-> R2

Setup and the Bookshelf Used on This Page

The plugin is one dependency, and its version is managed by the struts2-bom that the Bookshelf POM already imports:

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-convention-plugin</artifactId>
</dependency>

The artifact is published at 7.4.0 on Maven Central (Maven Central listing). Its struts-plugin.xml has order="20", so its constants and beans are loaded after the core ones; no file of the application has to name it. The verification application used by every example of this page, g11conv, has this layout. It contains no struts.xml in its first form: Struts starts, scans and serves the actions below from the plugin alone. A struts.xml is added later only where an example needs a package or a constant.

src/main/java/org/example/bookshelf/
  model/Catalogue.java                         not scanned (package 'model' is no locator)
  actions/BookListAction.java                  /book-list
  actions/BookDetailsAction.java               /book, /book-info, /book-form, /go ...
  actions/Hello.java                           /hello (implements Action, no suffix)
  actions/ISBNLookupAction.java                /i-sbnlookup (see the naming rules)
  actions/admin/ImportBooksAction.java         /admin/import-books
  actions/admin/IndexAction.java               /admin/index, /admin/
  actions/export/BookExportAction.java         namespace /csv by annotation
  actions/reports/package-info.java            @Namespace and @ResultPath for the package
  actions/reports/SalesAction.java             /reports/sales
  actions/timed/package-info.java              @ParentPackage and @DefaultInterceptorRef
  actions/timed/StatsAction.java               /timed/stats
src/main/webapp/WEB-INF/
  web.xml                                      the StrutsPrepareAndExecuteFilter on /*
  content/about.jsp                            a view with no action class at all
  content/book-list.jsp, hello-success.jsp ... the views, by file name
  reports/reports/sales.jsp                    views of the reports package (see @ResultPath)

The Catalogue class is the same thread-safe in-memory list that the other Part II pages use: two titles, Dune and Neuromancer. The filter declaration of web.xml is the one of Getting Started with Struts; the plugin does not change it.

The Maven dependencies of the verification application are struts2-core and struts2-convention-plugin (both from the struts2-bom 7.4.0), the Servlet 6.1 and Pages 4.0 APIs as provided, and log4j-slf4j2-impl. The Convention plugin’s class files are built against the org.apache.struts2 package names, so the javax-era and com.opensymphony.xwork2 imports that appear in the plugin’s documentation (see Where the Books Are Outdated) do not compile in 7.4.0.

A View With No Action Class

The smallest use of the plugin is a page with no code at all. A JSP placed under /WEB-INF/content/ answers the URL that has its name, even though no action exists for that URL:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<html><body><p>About the Bookshelf (no action class)</p></body></html>

saved as src/main/webapp/WEB-INF/content/about.jsp. The plugin installs an UnknownHandler (ConventionUnknownHandler, registered in struts-plugin.xml) that Struts calls when no action matches the URL; the handler looks for a result file with that name and, if there is one, builds the action configuration on the fly.

$ curl -s -w '%{http_code}\n' http://localhost:18080/g11conv/about
<html><body><p>About the Bookshelf (no action class)</p></body></html>
200
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g11conv/about.action
200

Both /about and /about.action work because the 7.4.0 default is struts.action.extension=action,,. The files are under WEB-INF, so they are not reachable directly: the only way to a view is through Struts. The page of the official documentation that shows this is the "Hello world" section; the Getting Started tutorial uses the same default location.

How Action Classes Are Found: Package Locators

A class is turned into an action in two steps. First the plugin decides which packages to scan; then it decides which classes of those packages are actions.

Which Packages: Locators and Root Packages

The plugin looks for packages whose name ends with one of the package locators, the words of the constant struts.convention.package.locators; the default is action,actions,struts,struts2. A package found that way is a root package, and the plugin scans it and all of its sub-packages. The part of the package name that follows the root package becomes the namespace. For the Bookshelf:

Table 1. Packages of the Bookshelf, with the default locators
Java package Scanned? Namespace of its actions

org.example.bookshelf.actions

Yes, it ends with actions (root package)

/

org.example.bookshelf.actions.admin

Yes, sub-package of a root package

/admin

org.example.bookshelf.actions.export

Yes, sub-package; the class carries @Namespace("/csv")

/csv (annotation wins, see @Namespace)

org.example.bookshelf.model

No: no locator matches

none

org.example.bookshelf

No: the name does not end with a locator

none

The verification run printed the decision of the scan at DEBUG level (the logger org.apache.struts2.convention; see Debugging), one line for each action created:

Creating action config for class [class org.example.bookshelf.actions.BookListAction], name [book-list] and package name [org.example.bookshelf.actions#convention-default#] in namespace []
Creating action config for class [class org.example.bookshelf.actions.admin.ImportBooksAction], name [import-books] and package name [org.example.bookshelf.actions.admin#convention-default#/admin] in namespace [/admin]

The root namespace is shown as an empty string in this log line and is / in the URL. The generated package name, <java-package>#<namespace>#<parent-package>, is explained in The Generated Package.

A package that is not a locator match can still be scanned by naming it in struts.convention.action.packages, a comma-separated list. The verification run set struts.convention.package.locators.disable to true and struts.convention.action.packages to org.example.bookshelf.actions.admin: only the two admin classes were configured, and their namespace was the root, because the namespace is computed from the package below the named one (here nothing):

<constant name="struts.convention.package.locators.disable" value="true"/>
<constant name="struts.convention.action.packages" value="org.example.bookshelf.actions.admin"/>

The result was name [import-books] and package name [org.example.bookshelf.actions.admin#convention-default#] in namespace [], so /import-books and not /admin/import-books. Setting struts.convention.package.locators.basePackage instead (to the same package) kept the locator search but limited it to packages that start with that value, and the namespace stayed /admin. Replacing the locator words (struts.convention.package.locators=screens) scanned only org.example.bookshelf.screens.* and ignored the actions packages. Three more constants shape the scan:

Table 2. Constants that limit or extend the scan
Constant Default (7.4.0 struts-plugin.xml) Effect

struts.convention.package.locators

action,actions,struts,struts2

Package-name endings that mark a root package

struts.convention.package.locators.disable

false

true turns the locator search off; then only action.packages is scanned

struts.convention.package.locators.basePackage

empty

If set, only packages that start with its value are searched for locators

struts.convention.action.packages

not set

Packages scanned whether or not they match a locator

struts.convention.exclude.packages

org.apache.struts.*,org.apache.struts2.*,org.springframework.web.struts.*,org.springframework.web.struts2.*,org.hibernate.*

Packages (and sub-packages) never scanned; an excluded package cannot be re-included by another constant

struts.convention.action.disableScanning

false

true turns the class path scan off

Which Classes: Suffix or the Action Interface

Inside a scanned package a class is an action if it implements org.apache.struts2.action.Action (constant struts.convention.action.checkImplementsAction, default true) or its simple name ends with the suffix of struts.convention.action.suffix (default Action). ActionSupport implements Action, so every class that extends it qualifies, whatever its name. The verification application shows both rules and a class that is neither:

package org.example.bookshelf.actions;

import org.apache.struts2.action.Action;

public class Hello implements Action {
    @Override
    public String execute() {
        return SUCCESS;
    }
}

Hello has no suffix but implements Action, so it is configured as /hello and answered with hello-success.jsp:

$ curl -s http://localhost:18080/g11conv/hello
<html><body><p>Hello, reader (hello-success.jsp)</p></body></html>

A plain class NotAnAction (no interface, name ends with Action) was configured as /not-an, because the plugin removes the suffix from the end of the name, so a word the author meant as part of the name is cut. The class Catalogue (package model, no locator) never appears in the log. With the constant struts.convention.action.checkImplementsAction set to false, /hello disappeared from the config browser; with struts.convention.action.suffix set to Controller, a class ReaderController became /reader, while the ActionSupport subclasses stayed mapped under names that keep the old suffix (book-list-action), because they still implement Action.

Actions in jar files are not scanned unless the jar URL matches one of the regular expressions of struts.convention.action.includeJars (default: no jar is scanned; DEBUG log: "struts.convention.action.includeJars" is not defined, excluding all JAR files!). The verification built a one-class jar bookshelf-extras-0.1.jar with org.example.extras.actions.ExtrasReportAction, put it in WEB-INF/lib and set:

<constant name="struts.convention.action.includeJars" value=".*?/bookshelf-extras.*?jar(!/)?"/>

Without the constant the class was ignored; with it the log printed Creating action config for class [class org.example.extras.actions.ExtrasReportAction], name [extras-report]. The expression is matched against the jar URL (it can contain the path and a trailing !/), not the file name. The plugin page says a match on external jars can fill the log with errors such as "Could not load …​/ClasspathResourceLoader.class"; keep the expression as narrow as the example.

Naming Rules: From Class Name to URL

The URL of an action is built from the namespace (the package, see above) and an action name derived from the class name by the ActionNameBuilder bean, SEOActionNameBuilder by default ("SEO" because my-action is a better URL than MyAction). The rules, which were checked by calling the builder directly with each name below:

  1. the suffix (Action by default) is removed from the end of the name; a class named exactly Action fails with IllegalStateException: The action name cannot be the same as the action suffix [Action];

  2. the camel-case name is split into words at each upper-case letter, and the words are joined with the separator (- by default, constant struts.convention.action.name.separator);

  3. the result is converted to lower case (constant struts.convention.action.name.lowercase, default true).

Table 3. Class name to action name (default constants, checked with SEOActionNameBuilder)
Class name Action name Comment

BookListAction

book-list

Suffix removed, camel case split

ShowCompanyDetailsAction

show-company-details

The example of the plugin page

ExportCSVAction

export-csv

A trailing acronym stays together

Book2PdfAction

book2-pdf

Digits do not start a word

ISBNLookupAction

i-sbnlookup

A leading acronym is split after its first letter: the pitfall below

NotAnAction

not-an

The suffix is removed from the end of the name, An is left

bookListAction

book-list

The first letter’s case does not matter

BookAction2

book-action2

The suffix is removed only from the end, so it stays

With the constants changed (lowercase=false, separator _, suffix Controller) the same builder gave Book-List for BookListAction, book_list, and book-list for BookListController; with the suffix Controller the class BookListAction kept its suffix (book-list-action). The same separator is the one used between the action name and the result code in view file names (next section), so changing it changes the view names too.

The acronym case is a real pitfall for the Bookshelf: ISBNLookupAction is published as /i-sbnlookup. Two fixes were verified. The simple one is an explicit name with @Action("isbn-lookup") on the method (an earlier version of the verification class carried that annotation and /isbn-lookup answered with the view isbn-lookup.jsp). The general one replaces the name builder, which the plugin supports through its extension points:

package org.example.bookshelf.convention;

import org.apache.struts2.convention.SEOActionNameBuilder;
import org.apache.struts2.inject.Inject;

public class AcronymAwareNameBuilder extends SEOActionNameBuilder {
    @Inject
    public AcronymAwareNameBuilder(
            @Inject("struts.convention.action.name.lowercase") String lowerCase,
            @Inject("struts.convention.action.name.separator") String separator) {
        super(lowerCase, separator);
    }

    @Override
    public String build(String className) {
        String name = super.build(className);
        return name.replace("i-sbnlookup", "isbn-lookup");
    }
}

registered in struts.xml as a bean and selected by constant (the bean name is the value of the constant):

<bean type="org.apache.struts2.convention.ActionNameBuilder" name="bookshelf"
      class="org.example.bookshelf.convention.AcronymAwareNameBuilder"/>
<constant name="struts.convention.actionNameBuilder" value="bookshelf"/>

After the restart the DEBUG line read name [isbn-lookup] for ISBNLookupAction and curl /isbn-lookup answered 200 with the page isbn-lookup.jsp. The plugin’s other replaceable beans are listed in Replacing the Plugin’s Beans. The Struts constructor injection of the two constants uses org.apache.struts2.inject.Inject, which is the 7.x package of the container annotation.

Finding the View: The WEB-INF/content Convention

When an action returns a result code, the plugin looks for a file in the result path (constant struts.convention.result.path, default /WEB-INF/content/) followed by the namespace and the action name, and optionally the result code:

<result path><namespace>/<action name>[<separator><result code>].<extension>

For a request to /admin/import-books that returns success, either /WEB-INF/content/admin/import-books.jsp or /WEB-INF/content/admin/import-books-success.jsp is the view. The Bookshelf BookListAction returns success normally and the string empty when the filter parameter is blank; with two files the plugin chose the view by the code:

package org.example.bookshelf.actions;

import java.util.List;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
import org.example.bookshelf.model.Catalogue;

public class BookListAction extends ActionSupport {
    private String filter;

    public String getFilter() {
        return filter;
    }

    @StrutsParameter
    public void setFilter(String filter) {
        this.filter = filter;
    }

    public List<String> getTitles() {
        return Catalogue.titles();
    }

    @Override
    public String execute() {
        return filter != null && filter.isBlank() ? "empty" : SUCCESS;
    }
}
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html><body><h1>Bookshelf</h1><ul><s:iterator value="titles"><li><s:property/></li></s:iterator></ul><p>filter=<s:property value="filter"/></p></body></html>

(WEB-INF/content/book-list.jsp; the file book-list-empty.jsp holds <p>Empty filter</p>.)

$ curl -s http://localhost:18080/g11conv/book-list
<html><body><h1>Bookshelf</h1><ul><li>Dune</li><li>Neuromancer</li></ul><p>filter=</p></body></html>
$ curl -s 'http://localhost:18080/g11conv/book-list?filter=x'
<html><body><h1>Bookshelf</h1><ul><li>Dune</li><li>Neuromancer</li></ul><p>filter=x</p></body></html>
$ curl -s 'http://localhost:18080/g11conv/book-list?filter=%20'
<html><body><p>Empty filter</p></body></html>

So book-list.jsp serves success and book-list-empty.jsp serves empty. The result type comes from the file extension:

Table 4. File extension to result type (verified with the Bookshelf FormatsAction)
Extension Result type Observed

.jsp, .htm, .html

dispatcher

/formats?code=html returned the static formats-html.html; .jsp served as usual; .htm is listed by the plugin page and was not run

.ftl

freemarker

/formats?code=ftl returned <p>Hello from the action (formats-ftl.ftl)</p>

.vm

velocity

Ignored by default (DEBUG: No result type defined for file suffix : [vm]. Ignoring file formats-vm.vm); works after struts.convention.default.parent.package points at velocity-default (see below)

The .vm case is a trap that no document mentions: the plugin builds its result types from the parent package, and the default parent convention-default extends only struts-default, which has the dispatcher and freemarker types and not velocity. The Velocity plugin declares the type in its own package velocity-default (from its struts-plugin.xml). With the constant <constant name="struts.convention.default.parent.package" value="velocity-default"/> the request /formats?code=vm returned <p>Hello from the action (formats-vm.vm)</p>. See FreeMarker and Velocity Views for the views themselves.

Four more rules complete the lookup. They were all observed:

  • Unknown result code with no matching file. /formats?code=zzz has no formats-zzz.* file, and the plugin answered with the plain formats.jsp (the view without a code) and not with a 404.

  • No view at all. An action whose view file is missing is mapped but fails when rendered: Struts logs ConfigurationException: No result defined for action org.example.bookshelf.actions.reports.SalesAction and result success at WARN and the browser receives a 404. (That is the situation of the first attempt at /reports/sales, before the file was in the right directory, see @ResultPath.)

  • Chaining. If an action returns the name of another action of the same package and has no result for that code, the plugin chains to that action. The Bookshelf /go method returns "go-home"; the action go-home exists, so the answer was its view (go-home.jsp) with no result declared. The chain result is the one used; see Action Chaining, Sessions and Scopes for what chaining copies.

  • Nested layout. With <constant name="struts.convention.result.flatLayout" value="false"/> the file for book-list and success is /WEB-INF/content/book-list/success.jsp (a directory per action, a file per result code). With the flat layout removed and the nested files in place, /book-list answered 200 from book-list/success.jsp.

The table that follows shows the default layout and its variants (the nested layout and the result path were run with the Bookshelf; the plugin page has a longer table of the same lookup).

Table 5. Where a result is looked up, for the action /admin/import-books and the code input
Layout File

Flat (default), code in the name

/WEB-INF/content/admin/import-books-input.jsp

Flat, no code (the default result of the action)

/WEB-INF/content/admin/import-books.jsp

flatLayout=false

/WEB-INF/content/admin/import-books/input.jsp

result.path=/WEB-INF/pages

/WEB-INF/pages/admin/import-books-input.jsp

The Getting Started "Annotations" tutorial follows the same rule: the action hello.action runs HelloAction.execute, and the view for success is hello-success.jsp in WEB-INF/content (hello-input.jsp for input). The struts.convention.relative.result.types constant (default dispatcher,velocity,freemarker) lists the result types whose location is resolved against the result path and the namespace when it is relative; a location that starts with / is used as written.

The Annotations of the Plugin

All annotations of the plugin are in the package org.apache.struts2.convention.annotation and have RUNTIME retention. The plugin jar of 7.4.0 contains these: Action, Actions, AllowedMethods, DefaultInterceptorRef, ExceptionMapping, ExceptionMappings, InterceptorRef, InterceptorRefs, Namespace, Namespaces, ParentPackage, Result, ResultPath and Results. Their targets and attributes, read with javap on the 7.4.0 jar:

Table 6. Annotations of the Convention plugin (7.4.0)
Annotation Where Attributes

@Action / @Actions

Method or class

value (the name), results, interceptorRefs, params, exceptionMappings, className; @Actions holds several @Action

@Result / @Results

Class, or inside @Action

name (a string array since 2.5), location, type, params

@Namespace / @Namespaces

Class or package

value (the namespace)

@ParentPackage

Class or package

value (the name of a package declared in struts.xml)

@InterceptorRef / @InterceptorRefs

Class, or inside @Action

value (interceptor or stack name), params

@DefaultInterceptorRef

Package (in package-info.java)

value

@ExceptionMapping / @ExceptionMappings

Class, or inside @Action

exception, result, params

@ResultPath

Class or package

value, property

@AllowedMethods

Class or package

value (a string array of method names or patterns)

The next sections go through them with Bookshelf examples. @DefaultInterceptorRef, @AllowedMethods and @Namespaces are in the 7.4.0 jar but are not described on the plugin page or on the Core Developers Guide annotations page.

@Action and @Actions: Names and Several Actions per Class

@Action on a method makes that method the action. The value is the action name. A name that starts with / is absolute: its namespace is the part before the last slash and the plugin ignores the package. A name without a leading slash is relative to the namespace of the class. The Bookshelf BookDetailsAction has four relevant methods:

@Action("book")
public String show() {
    if (title == null || !Catalogue.titles().contains(title)) {
        message = "No such title";
        return "failure";
    }
    return SUCCESS;
}

@Actions({
    @Action("book-info"),
    @Action("/legacy/book-details")
})
public String info() {
    return SUCCESS;
}

show() is /book. info() is two actions: the relative book-info in the namespace of the class, and the absolute /legacy/book-details in the namespace /legacy. The views come from the result lookup of the previous section:

$ curl -s 'http://localhost:18080/g11conv/book?title=Dune'
<html><body><p>Book: Dune</p></body></html>
$ curl -s 'http://localhost:18080/g11conv/book?title=Nope'
<html><body><p>failure: No such title</p></body></html>
$ curl -s http://localhost:18080/g11conv/book-info
<html><body><p>info</p></body></html>
$ curl -s http://localhost:18080/g11conv/legacy/book-details      # needs content/legacy/book-details.jsp
<html><body><p>legacy book details</p></body></html>

The absolute action found its view in /WEB-INF/content/legacy/book-details.jsp: the namespace is part of the file path, so a name with a slash needs a matching directory (the first attempt, with the file in WEB-INF/content/, returned a 404 with No result defined).

Three behaviours of @Action are worth knowing:

  • The execute method is mapped too. By default (struts.convention.action.alwaysMapExecute, true) a class that has other @Action methods is also mapped under the name derived from the class name, using execute (inherited from ActionSupport if you do not write one). The config browser listed book-details for BookDetailsAction for that reason. Setting the constant to false removed the extra mapping

    1. Keep the default to catch the unexpected URL in review, or set false to expose only what is annotated.

  • Annotated methods are allowed methods. The 6.x line made method invocation strict (an action may be called only for the methods listed in allowed-methods; see Wildcard Mappings and Method Invocation). The plugin adds the annotated methods to that list itself: /book (the method show, not execute) worked without any allowed-methods element. The class-level annotation @AllowedMethods (below) is for the dynamic method invocation case.

  • Duplicate names are an error. Two methods of one class annotated with the same name stop the whole application at start-up (since 7.2.0, WW-4421). The verification run answered every URL with 404 and logged The action class [class org.example.bookshelf.actions.DupAction] contains two methods with an action name annotation whose value is the same.

The plugin page also allows @Action and @Actions at the class level (useful with dynamic method invocation: the execute method is used if it exists, otherwise the method is decided by the request), and says the className attribute is mainly useful when Spring creates the actions, which belongs to Spring Integration. Neither was run here.

Wildcard names. An @Action value may contain a wildcard, as an XML action name can. The Bookshelf GenreAction maps any genre and gives the single genre fiction its own view; the wildcard is declared first on purpose:

public class GenreAction extends ActionSupport {
    @Action(value = "genre-*", results = @Result(location = "genre.jsp"))
    public String any() {
        return SUCCESS;
    }

    @Action(value = "genre-fiction", results = @Result(location = "genre-fiction.jsp"))
    public String fiction() {
        return SUCCESS;
    }
}

/genre-poetry answered <p>any genre</p> and /genre-fiction answered <p>fiction</p>: the more specific name wins even though it was declared second. That ordering is a 7.x change (WW-3784, in 7.3.0; WW-5743 in 7.4.0 ranks ** patterns after single-segment ones), so a Struts 6 application that relied on declaration order must be re-read; for XML wildcard mappings the rules of the wildcard page apply.

The Getting Started tutorial uses @Action("register-input") on a method input() that returns INPUT so that the link register-input.action shows the form (view register-input.jsp) while register.action runs execute(); the Bookshelf /book-form follows the same pattern below.

@Result and @Results: Local and Class-Level Results

@Result declares a result. Inside @Action(results = ...) it is local: it applies to that action only. On the class (alone or in @Results) it is global to the class: every action method of the class inherits it. The Bookshelf BookDetailsAction shares one failure view on the class, and the form action has local results, one with a name array (since 2.5) and one that redirects:

@Results({
    @Result(name = "failure", location = "book-failure.jsp")
})
public class BookDetailsAction extends ActionSupport {

    @Action(value = "book-form", results = {
        @Result(name = {"error", "input"}, location = "book-form-input.jsp"),
        @Result(name = "success", type = "redirectAction", params = {"actionName", "book-list"})
    })
    public String form() {
        return title == null ? INPUT : SUCCESS;
    }
    // ...
}
$ curl -s http://localhost:18080/g11conv/book-form
<html><body><p>input form</p></body></html>
$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' 'http://localhost:18080/g11conv/book-form?title=Dune'
302 http://localhost:18080/g11conv/book-list.action

Notes on the attributes, all confirmed in the config browser (showConfig for book-form: input and error to book-form-input.jsp, success to ServletActionRedirectResult with actionName = book-list, and the class-level failure inherited):

  • location is relative to the result path and the namespace when the result type is in struts.convention.relative.result.types; an absolute location (/WEB-INF/content/export-unavailable.jsp) is used as it is.

  • type is the name of a result type of the parent package (redirectAction, httpheader, chain, stream…​). Without a type the plugin chooses it from the file extension.

  • params is a string array of key and value pairs, in an even number: {"actionName", "book-list"} above, {"status", "503"} below.

  • Class-level results are inherited by sub-classes, and a result defined at the method level overrides one with the same name at the class level (documented since 2.1.7).

  • The redirect target was book-list.action: a redirect to an action name gets the action extension.

@Namespace: A Class or a Whole Package

@Namespace replaces the namespace that the package name gives. On a class it applies to every action of the class that is not absolute. The Bookshelf CSV export lives in the package ...actions.export but is published under /csv:

@Namespace("/csv")
public class BookExportAction extends ActionSupport {
    @Action("absolute")
    public String absolute() {
        return SUCCESS;
    }
    // ...
}

/csv/absolute answered <p>csv absolute</p> from WEB-INF/content/csv/absolute.jsp, and /export/books (the namespace of the Java package) was a 404 with the WARN There is no Action mapped for namespace [/export]. In a package, the annotation goes in package-info.java and changes the default namespace of every action in that Java package (not its sub-packages):

@org.apache.struts2.convention.annotation.Namespace("/reports")
@org.apache.struts2.convention.annotation.ResultPath("/WEB-INF/reports")
package org.example.bookshelf.actions.reports;

@ResultPath: Where the Views Live

@ResultPath replaces the result path of the class or package (the constant gives the global default). The package-info above sets /WEB-INF/reports for SalesAction; the namespace of the class is /reports, so the view is not /WEB-INF/reports/sales.jsp but /WEB-INF/reports/reports/sales.jsp: the namespace is still appended to the result path. The first attempt with the file at /WEB-INF/reports/sales.jsp was answered with a 404 and the WARN No result defined for action org.example.bookshelf.actions.reports.SalesAction and result success; after the file was moved:

$ curl -s http://localhost:18080/g11conv/reports/sales
<html><body><p>Sales report</p></body></html>

The second attribute, property, names a property of the action that supplies the path; this page did not exercise it.

@ParentPackage, @InterceptorRef and @DefaultInterceptorRef

A generated package extends convention-default, which extends struts-default. To use interceptors, result types or global results declared in your struts.xml, the action must live in a package that extends the one that declares them: that is what @ParentPackage selects (the equivalent of extends). The Bookshelf declares a stack in XML:

<struts>
    <package name="bookshelf-timed" extends="struts-default">
        <interceptors>
            <interceptor-stack name="timedStack">
                <interceptor-ref name="logger"/>
                <interceptor-ref name="defaultStack"/>
            </interceptor-stack>
        </interceptors>
    </package>
</struts>

(The stack uses logger because there is no timer interceptor in the 7.4.0 struts-default.xml; see the errata of Interceptors.) A class that applies the stack must also extend the package:

@ParentPackage("bookshelf-timed")
@InterceptorRefs({@InterceptorRef("timedStack")})
public class StockOkAction extends ActionSupport {
    @Override
    public String execute() {
        return SUCCESS;
    }
}

@InterceptorRef takes value (an interceptor or a stack) and params (pairs, for example params = {"programmatic", "false"}). On a class, @InterceptorRefs applies to every action of the class; inside @Action(interceptorRefs = ...) it is added for that action. The plugin page states that the interceptors given on the method are added to those of the class, and that "if interceptors are not specified, the default stack is applied". To apply a default to a whole package, package-info.java takes @ParentPackage plus @DefaultInterceptorRef:

@org.apache.struts2.convention.annotation.ParentPackage("bookshelf-timed")
@org.apache.struts2.convention.annotation.DefaultInterceptorRef("timedStack")
package org.example.bookshelf.actions.timed;

With the logger interceptor raised to INFO (org.apache.struts2.interceptor.LoggingInterceptor), the requests to /timed/stats (package default) and to /stock-ok (class annotations) both logged Starting execution stack for action /timed/stats and .../stock-ok and the matching Finishing execution stack, so both routes applied the stack. The generated package of the first was org.example.bookshelf.actions.timed#bookshelf-timed#/timed: the third part of the name is the parent package.

The failure mode is the most dangerous one of the plugin. The Bookshelf class StockAction had @InterceptorRefs({@InterceptorRef("timedStack")}) and no @ParentPackage. The generated package extends convention-default, which does not know timedStack, so the start-up failed for the whole application:

ERROR Dispatcher - Dispatcher initialization failed
Caused by: org.apache.struts2.config.ConfigurationException: Unable to find interceptor class referenced by ref-name timedStack
	at org.apache.struts2.convention.DefaultInterceptorMapBuilder.buildInterceptorList(DefaultInterceptorMapBuilder.java:87)

and every URL of the application answered 404 (including /book-list). The plugin page gives the two fixes: use @ParentPackage (or struts.convention.default.parent.package) with the package that declares the interceptor, or declare an XML package that extends it and point at that.

See Interceptors and Custom Interceptors for the interceptors themselves.

@ExceptionMapping and @ExceptionMappings

An exception mapping turns an exception into a result code, as the XML exception-mapping element does (see Exception Handling and Global Results). On a class it applies to every action of the class; in @Action(exceptionMappings = ...) to one. The Bookshelf export fails when the exporter is offline, and two ways of answering were verified:

@Namespace("/csv")
@ExceptionMappings({
    @ExceptionMapping(exception = "java.lang.IllegalStateException", result = "unavailable")
})
public class BookExportAction extends ActionSupport {
    @Action(value = "books", results = @Result(name = "unavailable", location = "/WEB-INF/content/export-unavailable.jsp"))
    public String books() {
        throw new IllegalStateException("exporter offline");
    }

    @Action(value = "titles", exceptionMappings = {
        @ExceptionMapping(exception = "java.lang.IllegalStateException", result = "offline")
    }, results = @Result(name = "offline", type = "httpheader", params = {"status", "503"}))
    public String titles() {
        throw new IllegalStateException("exporter offline");
    }
    // ...
}
$ curl -s -w '%{http_code}\n' http://localhost:18080/g11conv/csv/books
<html><body><p>export unavailable</p></body></html>
200
$ curl -s -o /dev/null -w '%{http_code}\n' http://localhost:18080/g11conv/csv/titles
503

/csv/books used the class-level mapping and a result for the code unavailable; /csv/titles used a method-level mapping and a httpheader result with status 503. The exception-mapping annotations need the exception interceptor of the stack (part of defaultStack), and the result name must exist for the action, as in XML. The params attribute (not used here) passes values to the result, as the plugin page’s example does with params = {"param1", "val1"}.

@AllowedMethods and Dynamic Method Invocation

The allowed-methods of the generated action covers the annotated method. Dynamic method invocation (/book-details!info) is off by default (struts.enable.DynamicMethodInvocation=false) and is discouraged (see Wildcard Mappings and Method Invocation). When it is switched on for a class that uses the plugin, @AllowedMethods lists the methods that may be called. The Bookshelf BookDetailsAction carries @AllowedMethods({"info", "go"}); with the constant struts.enable.DynamicMethodInvocation set to true:

$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:18080/g11conv/book-details!info'     # 200
$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:18080/g11conv/book-details!go'       # 200
$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:18080/g11conv/book-details!show'     # 404, not listed

Before the annotation was added, every !method request was a 404 with the WARN Could not find action or result. Leave the constant false unless there is a reason; the annotation is the narrow way to open a method when there is one.

The Other Annotations of the Reference

The Core Developers Guide page "Annotations" is a catalogue of annotations across the framework; only the Action group belongs to this plugin. The rest are documented on the pages that use them:

Table 7. Annotation groups of the Core Developers Guide and where they are treated
Group Annotations (7.4.0 package) Page

Security

@StrutsParameter (org.apache.struts2.interceptor.parameter)

Parameter Injection and @StrutsParameter

Action

The table above (org.apache.struts2.convention.annotation)

This page

Workflow

@InputConfig (org.apache.struts2.interceptor.annotations)

Validation

Interceptor

@Before, @BeforeResult, @After (same package, run by the annotationWorkflow interceptor, which is not in the defaultStack)

Interceptors

Validation

@Validations, @RequiredStringValidator, @EmailValidator…​ (org.apache.struts2.validator.annotations)

Validation

Type conversion

@Conversion, @TypeConversion (org.apache.struts2.conversion.annotations), @Element, @Key, @KeyProperty, @CreateIfNull (org.apache.struts2.util)

Type Conversion

Tiles

@TilesDefinition, @TilesPutAttribute…​ (plugin org.apache.struts2.tiles.annotation)

Tiles and Page Layouts

The Generated Package

Each combination of Java package, namespace and parent package becomes one generated Struts package, so actions never collide with those of other packages. The name is <java-package>#<namespace>#<parent-package>, where the parent defaults to the value of struts.convention.default.parent.package (convention-default). From the Bookshelf run:

Table 8. Generated package names (from the DEBUG log and the config browser)
Action Namespace Package name

/book-list

root

org.example.bookshelf.actions#convention-default#

/admin/import-books

/admin

org.example.bookshelf.actions.admin#convention-default#/admin

/legacy/book-details (absolute @Action)

/legacy

org.example.bookshelf.actions#convention-default#/legacy

/timed/stats

/timed

org.example.bookshelf.actions.timed#bookshelf-timed#/timed

The root namespace is written as an empty string inside the name, not as /, so the plugin page’s example com.example.actions#/#conventionDefault differs from what 7.4.0 produces. The namespace rules of Packages and Namespaces apply to these packages unchanged. The plugin sets struts.mapper.alwaysSelectFullNamespace to true in its own struts-plugin.xml (it is the default in 7.0 and later too).

Discovering Mappings: The Config Browser

The question "what did the plugin actually generate?" is answered by the Config Browser plugin, a development tool that lists the loaded configuration. The Getting Started tutorial recommends it with the Convention plugin. Add the dependency (version from the BOM):

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-config-browser-plugin</artifactId>
</dependency>

and open /config-browser/index.action of the application (the Config Browser page says /config-browser/index also works with the default action mapper). In the verification run the request was redirected (302) to /g11conv/config-browser/actionNames.action, which listed the namespaces of the Bookshelf (default, /config-browser, /admin, /timed, /legacy, /csv, /reports) and the actions of each; showConfig.action?namespace=&actionName=book-form printed the class (BookDetailsAction), the method (form), the default location (/g11conv/book-form.action) and the results shown earlier; showConstants.action listed the struts.convention.* constants in effect, and showBeans.action the beans. The tool answered without any authentication. The plugin page says it "should be used only during development phase and access to it should be strictly restricted"; do not ship the jar to production.

Debugging What the Scan Does

The scan logs through the category org.apache.struts2.convention. With Log4j 2 (the Struts 7 default), a file log4j2.xml in src/main/resources shows every decision:

<Configuration status="WARN">
  <Appenders>
    <File name="F" fileName="/tmp/bookshelf-convention.log" append="false">
      <PatternLayout pattern="%-5p %c{1} - %m%n"/>
    </File>
  </Appenders>
  <Loggers>
    <Logger name="org.apache.struts2.convention" level="debug"/>
    <Root level="warn"><AppenderRef ref="F"/></Root>
  </Loggers>
</Configuration>

(The file name here is a placeholder; the verification wrote to a scratch file.) The DEBUG lines used throughout this page come from it: Processing class [...] in package [...], Creating action config for class [...], name [...] and package name [...] in namespace [...], No result type defined for file suffix : [vm]. Ignoring file ... and "struts.convention.action.includeJars" is not defined, excluding all JAR files!. Source: Struts Core Developers Guide — Logging

The plugin page’s tip for Log4j 1 (log4j.logger.org.apache.struts2.convention=DEBUG) is the same category. The common errors of the plugin page map to the following:

Table 9. Common errors of the plugin and what they mean
Message Cause Fix

There is no Action mapped for namespace [/export] and action name [books] (WARN, HTTP 404)

The URL does not match a mapping: wrong namespace or name (the verification used /export/books while @Namespace("/csv") is in force)

Check the config browser; fix the URL or the namespace

No result defined for action ...SalesAction and result success (WARN, 404)

The action is mapped but no view file was found for the code

Create the file at result path plus namespace plus name, or declare a @Result

Unable to find interceptor class referenced by ref-name timedStack (start-up failure)

A class references an interceptor that its generated package does not inherit

@ParentPackage (see above)

The action class ... contains two methods with an action name annotation whose value is the same (start-up failure)

Two @Action with one name in a class

Rename one

Every URL answers 404 after the start

Start-up failed; look for Dispatcher initialization failed

Read the first ERROR of the log

java.lang.Exception: Could not load .../ClasspathResourceLoader.class flood

includeJars matches third-party jars

Narrow the expression

Replacing the Plugin’s Beans

Five beans do the work, all registered under the name convention in the plugin’s struts-plugin.xml, and each selected by a constant of the same name. Any of them can be replaced by defining a bean of the same type under another name and pointing the constant at it, exactly as the name builder was replaced above:

Table 10. Replaceable beans of the plugin (7.4.0 struts-plugin.xml)
Constant Bean type Default implementation

struts.convention.actionConfigBuilder

org.apache.struts2.convention.ActionConfigBuilder

PackageBasedActionConfigBuilder: finds actions and builds the packages

struts.convention.actionNameBuilder

org.apache.struts2.convention.ActionNameBuilder

SEOActionNameBuilder: class name to action name

struts.convention.resultMapBuilder

org.apache.struts2.convention.ResultMapBuilder

DefaultResultMapBuilder: finds view files and builds the results

struts.convention.interceptorMapBuilder

org.apache.struts2.convention.InterceptorMapBuilder

DefaultInterceptorMapBuilder: builds interceptors from annotations

struts.convention.conventionsService

org.apache.struts2.convention.ConventionsService

ConventionsServiceImpl: the conventions and their annotation overrides

Two more beans are registered but are not extension points of the same kind: the UnknownHandler named convention and the two package providers convention.packageProvider and convention.containerProvider (ClasspathPackageProvider, ClasspathConfigurationProvider). The ConventionUnknownHandler is the class that answers the zero-action URLs, the fallback to the plain view and the redirect of /admin to /admin/.

The Redirect to the Slash

When no action answers /admin but one answers /admin/ (the default IndexAction of the package admin), the plugin redirects with a 302, as a web server does for a directory. The Bookshelf admin/IndexAction produced:

$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:18080/g11conv/admin
302 http://localhost:18080/g11conv/admin/
$ curl -s http://localhost:18080/g11conv/admin/
<html><body><p>admin index</p></body></html>

The constant struts.convention.redirect.to.slash (true) controls it; the Bookshelf action named index of the package answered the URL that ends with a slash.

All the Constants

The 7.4.0 struts-plugin.xml declares the values below; the class ConventionConstants has one more name (eagerLoading). Defaults are those of the jar, which differ from the plugin page in the places marked.

Table 11. The struts.convention.* constants of 7.4.0
Constant Default Meaning

struts.convention.actionConfigBuilder, actionNameBuilder, resultMapBuilder, interceptorMapBuilder, conventionsService

convention

Names of the replaceable beans (see above)

struts.convention.result.path

/WEB-INF/content/

Directory of the views; trailing slashes are handled

struts.convention.result.flatLayout

true

false: one directory per action and one file per result code

struts.convention.action.suffix

Action

Class-name suffix that marks an action and is removed from the name

struts.convention.action.disableScanning

false

Turns the class path scan off

struts.convention.action.mapAllMatches

false

Create mappings for matching classes even when they have no @Action (REST setups set it to true)

struts.convention.action.checkImplementsAction

true

A class that implements Action is an action even without the suffix

struts.convention.default.parent.package

convention-default

Parent of the generated packages (the page’s table shows the same)

struts.convention.action.name.lowercase

true

Lower-case action names

struts.convention.action.name.separator

-

Word separator of names and between the name and the result code

struts.convention.package.locators

action,actions,struts,struts2

Locator words (see above)

struts.convention.package.locators.disable

false

Turn the locator search off

struts.convention.package.locators.basePackage

empty

Limit the locator search to this package

struts.convention.exclude.packages

org.apache.struts.*, org.apache.struts2.*, org.springframework.web.struts.*, org.springframework.web.struts2.*, org.hibernate.*

Never scanned

struts.convention.relative.result.types

dispatcher,velocity,freemarker

Result types whose relative location is resolved against path plus namespace

struts.convention.redirect.to.slash

true

302 from /foo to /foo/ when only the latter exists

struts.convention.action.alwaysMapExecute

true

Also map execute when other methods are annotated

struts.convention.action.includeJars

not set

Regular expressions of jar URLs to scan

struts.convention.action.fileProtocols

jar

URL protocols the scan accepts (JBoss and embedded Jetty change it)

struts.convention.classes.reload

false

Reload the actions when classes change (experimental)

struts.convention.exclude.parentClassLoader

true

Exclude URLs of the parent class loader from the scan; the plugin page’s table writes it struts.convention.classLoader.excludeParent

struts.convention.enable.smi.inheritance

false

Not described by the plugin page; declared in the jar (strict method invocation and inheritance of allowed methods)

struts.convention.action.eagerLoading

not declared

Instantiate found classes early; the page says it "can clash with Spring managed beans"

The constant struts.mapper.alwaysSelectFullNamespace=true is also set by the plugin. For the two platform notes of the plugin page: JBoss needs struts.convention.exclude.parentClassLoader=true and struts.convention.action.fileProtocols=jar,vfsfile,vfszip; embedded Jetty needs exclude.parentClassLoader=false and fileProtocols=jar,code-source. Neither platform was used here (the application ran on Tomcat 11), so those two lines are quoted from the plugin page. The plugin page also describes automatic reload (struts.devMode=true plus struts.convention.classes.reload=true), which it calls experimental and "strongly advised not to use …​ in production environments"; this was not exercised either.

XML or Annotations: Choosing for the Bookshelf

The first Struts 2 tutorial of every book asks the question "XML or annotations?". The answer of the verification is that the two are not exclusive: a single application can keep the struts.xml for the cross-cutting things (the package with the interceptor stack, global results, constants) and let the plugin generate the actions. The table compares the concerns:

Table 12. XML mappings against the Convention plugin for the Bookshelf
Concern struts.xml Convention plugin

Where the URL is declared

One <action name> per action, in one file

The class and package name, or @Action; read from the code

Discoverability

The file is the list of actions

The class path is the list; use the config browser or the DEBUG log

View location

<result> with a path

File name under /WEB-INF/content, or @Result

Cross-cutting configuration

Packages, stacks, global results, constants

Still in XML; selected with @ParentPackage and @DefaultInterceptorRef

Refactoring

A rename leaves the XML out of date

A rename of a class changes the URL: a silent API change

Failure mode

An error in the file stops start-up with its line

A scan error (unknown interceptor, duplicate @Action) stops start-up too, with a stack trace in the log

Review

One place to read

Annotations are spread over the classes

The Convention plugin is also the base of the REST plugin and of the Getting Started "Annotations" tutorial, and it is a good fit for new applications with many small actions; XML stays the better tool when an auditor needs one file that lists every URL. A rename of BookListAction to ShelfAction moves /book-list to /shelf, so pin the URLs that other systems call with an explicit @Action.

Pitfalls

Table 13. Convention plugin pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Struts 7.4.0 and Tomcat 11.0.26)
Symptom Cause Fix

An action class is ignored (observed)

The package does not end with a locator, or the class neither implements Action nor ends with the suffix

Move it under an action(s) package, set action.packages, or implement Action

The whole application answers 404 after a change (observed)

A start-up failure of the scan: unknown interceptor, duplicate @Action name

Read the first ERROR of the log; fix the annotation

Unable to find interceptor class referenced by ref-name X (observed)

The generated package does not extend the package that declares the interceptor

@ParentPackage or default.parent.package

/i-sbnlookup for ISBNLookupAction (observed)

Acronym splitting of SEOActionNameBuilder

@Action("isbn-lookup") or a custom ActionNameBuilder

A class named NotAnAction becomes /not-an (observed)

The suffix is removed from the end of the name

Rename the class or give it an @Action name

404 with No result defined for action ... and result success (observed)

The view file is missing, or it is in a directory that omits the namespace

Create the file at path, namespace, name; mind that @ResultPath keeps the namespace

.vm views are ignored (observed)

The parent package has no velocity result type

default.parent.package=velocity-default (or a package that extends it)

An unexpected URL answers (observed)

alwaysMapExecute also maps the class name for classes that use @Action

Set alwaysMapExecute=false, or accept it deliberately

A view for an absolute @Action is not found (observed)

The file must be under the namespace: content/legacy/book-details.jsp for /legacy/book-details

Put the file in the directory of the namespace

A parameter is never set

The setter lacks @StrutsParameter (the plugin does not change the Struts 7 rule)

Annotate the setter

A rename of a class moves a public URL

The URL derives from the class name

Pin with @Action("name")

!method returns 404 (observed)

DynamicMethodInvocation is off, or the method is not in @AllowedMethods

Prefer one @Action per method; enable DMI only with @AllowedMethods

The config browser is reachable in production

The jar was left in WEB-INF/lib

Remove it from the production build; the plugin page says access must be strictly restricted

What Changed Recently

Table 14. Changes that affect the Convention plugin
Version Change

Struts 2.1.6

The plugin is introduced; the Codebehind and Zero Config plugins are deprecated from 2.1 and Codebehind is removed in the 2.5 line

Struts 2.1.7

Class-level @Result results are inherited by subclasses and a method-level result overrides a class-level one of the same name

Struts 2.5

@Result(name = {...}) takes several names; strict method invocation introduced (annotated methods are allowed methods)

Struts 6.0

The newest configuration DTD is struts-6.0.dtd; the plugin’s struts-plugin.xml still declares it (7.4.0 too)

Struts 7.0

com.opensymphony.xwork2 classes move to org.apache.struts2 (Action to org.apache.struts2.action); @StrutsParameter becomes mandatory; the OGNL allowlist is on (WW-5440, July 2024, fixed the allowlist compatibility with the plugin during the 7.0 milestones)

Struts 7.2.0

Duplicate @Action names on methods are detected at start-up (WW-4421); the scan handles NoClassDefFoundError (WW-5593) and root-package classes with wildcard patterns (WW-5594); class-loader leaks on Tomcat hot deployment fixed (WW-5537)

Struts 7.3.0

Annotated wildcard actions are ordered most-specific-first (WW-3784)

Struts 7.4.0

** path-spanning patterns rank after single-segment ones (WW-5743); the plugin is unchanged otherwise

The release of each WW ticket was found by comparing the commit with the release tags of the source repository (STRUTS_7_2_0, STRUTS_7_3_0, STRUTS_7_4_0).

Where the Books Are Outdated

The Struts 2 book of the bibliography introduces annotations in its Hello World chapter and again in the chapter on configuration; the Struts 1 books have no equivalent (everything is in struts-config.xml).

Table 15. What to adjust when reading the books
In the books Today

Zero configuration with the filter parameter actionPackages and the @Result annotation of the Zero Config plugin

Both are gone: the Zero Config plugin was deprecated in 2.1, and actionPackages is not in the 7.4.0 core. Use the Convention plugin and struts.convention.action.packages

The Codebehind plugin (conventions on class names and views)

Removed in the 2.5 line; the Convention plugin took its place with different defaults (/WEB-INF/content)

@Result(value = "success", ...) and similar attribute names of the Zero Config annotations

@Result(name = ..., location = ..., type = ..., params = ...)

Imports of com.opensymphony.xwork2.ActionSupport and com.opensymphony.xwork2.Action (also in the plugin page’s examples)

org.apache.struts2.ActionSupport and org.apache.struts2.action.Action; the old names do not compile in 7.x

Result file hello-world-zero.jsp, .vm views without a parent package

The .vm extension is ignored unless the parent package has the velocity result type

Namespace and class names taken from the books' examples in com.example.actions

Keep the locator word (actions) in the package

The name FilterDispatcher

org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter

Setters bound from any request parameter

@StrutsParameter on every injection point (see the parameter page)

URLs ending in .action or .do

7.4.0 accepts .action or no extension; .do does not work

Inconsistencies in the Official Documentation

Collected while verifying this page against the 7.4.0 jar:

  • The plugin page and its examples import com.opensymphony.xwork2.Action and com.opensymphony.xwork2.ActionSupport; the 7.4.0 names are org.apache.struts2.action.Action and org.apache.struts2.ActionSupport. The checkImplementsAction row also names the old interface.

  • The configuration table writes struts.convention.classLoader.excludeParent; the constant of the jar (and of the JBoss and Jetty sections of the same page) is struts.convention.exclude.parentClassLoader.

  • The table omits struts.convention.action.fileProtocols, struts.convention.classes.reload and struts.convention.enable.smi.inheritance, which the 7.4.0 struts-plugin.xml declares, and lists struts.convention.action.eagerLoading, which it does not declare.

  • The XWork package example is com.example.actions#/#conventionDefault; 7.4.0 builds com.example.actions#convention-default# (an empty namespace for the root).

  • The result-extension sentence reads "jsp,ftl,vm,html,html" and does not say that .vm needs a parent package with the velocity result type.

  • The @AllowedMethods, @DefaultInterceptorRef and @Namespaces annotations of the 7.4.0 jar are described neither on the plugin page nor on the Core Developers Guide annotations page, which also lists a Validation marker annotation that the 7.4.0 jar does not contain and omits some validators of the jar (CreditCardValidator, LongRangeFieldValidator, ShortRangeFieldValidator, ConditionalVisitorFieldValidator).

  • The includeJars example <constant name="..." value="..." is not closed.

  • The plugin page does not say that an unknown result code falls back to the view without a code.

  • The Config Browser page footer still reads "Struts (c) 2003 - 2015" and the tool uses a Tigris template; it works with 7.4.0.

Summary

  • Add struts2-convention-plugin; an action is a class in a package that ends with action, actions, struts or struts2, that implements Action or whose name ends with Action.

  • The URL is the namespace (package below the root) plus the class name without the suffix, in lower-case words joined by -; check acronyms.

  • The view is /WEB-INF/content/<namespace>/<action>[-<code>].<ext>; the extension selects the result type; .vm needs velocity-default as parent.

  • Annotations override: @Action(s), @Result(s), @Namespace, @ParentPackage, @InterceptorRef(s), @DefaultInterceptorRef, @ExceptionMapping(s), @ResultPath, @AllowedMethods.

  • A bad annotation stops the whole application; the config browser and the DEBUG log show what was generated.

References