Packages and Namespaces

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 with Jakarta Servlet 6.1 and Java 21 (Struts 7 needs Java 17 or later). Everything was verified on 2026-10-08: the facts against the Struts Core Developers Guide, the 7.4.0 default.properties and the 7.4.0 source tree, and every example by building it into a Maven WAR (with struts2-bom 7.4.0) and running it on Tomcat 11.0.26 with curl; the outputs shown are the outputs that run produced. A package is the unit in which struts.xml groups actions, results, interceptors and exception mappings, and a namespace is the part of the URL that selects one group of actions. Together they answer two questions that every Struts request raises: which configuration does this action inherit, and which action does this URL mean. The running scenario is the Bookshelf, which starts with one package and grows an /admin area, a shared help action and a per-owner shelf.

What a Package Is

The Core Developers Guide describes packages as a way to group actions, results, result types, interceptors and interceptor stacks into a logical configuration unit, and compares them to objects: they can be extended, and a "sub" package can override parts of what it inherits. A package is declared by a <package> element directly under <struts> in struts.xml (or in a file that struts.xml includes) and it has exactly four attributes of interest plus one added in Struts 6.

Table 1. The attributes of the package element (Core Developers Guide, Package Configuration, and the struts-6.0 DTD)
Attribute Required Meaning

name

yes

The key other packages use to refer to this one. It has no meaning to URLs: a package called default is not special, and the name never appears in a request

extends

no

One or more package names, comma separated, whose interceptors, interceptor stacks, result types, global results, global exception mappings and actions this package inherits

namespace

no

The URL prefix under which the package’s actions are reachable. Omitted means the default namespace, the empty string

abstract

no

true declares a base package that need not (and, as shown below, does not by itself) expose actions

strict-method-invocation

no

true by default in the 6.0 DTD: only methods listed in allowed-methods can be invoked through the method attribute or a wildcard. The whole mechanism belongs to the wildcard-mappings page of this section

Besides actions a package can declare result-types (see Results and Result Types), interceptors (see Interceptors), default-interceptor-ref, default-action-ref, default-class-ref, global-results, global-allowed-methods and global-exception-mappings (see Exception Handling and Global Results), in that order, followed by the action elements; the DTD fixes the order and Struts refuses a file that breaks it. Each of those elements has a page of its own in this section; this page is about what the package does with them: it is the scope in which they are defined, the unit that is inherited, and the owner of a namespace.

The Bookshelf Used on This Page

All the examples below are the same WAR, deployed on Tomcat 11.0.26 under context paths that start with g8pkg (one WAR per configuration variant, described where it is used). It uses Struts 7.4.0 through struts2-bom, the filter org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter mapped to /*, and one action class that reports where it was found, so every URL below can be probed with curl and the answer shows the namespace and name Struts resolved.

The action class and the result page that every example shares
public class WhereAmIAction extends ActionSupport {
    private String label = "unset";
    private String owner = "-";

    public void setLabel(String label) { this.label = label; }
    public String getLabel() { return label; }
    public void setOwner(String owner) { this.owner = owner; }
    public String getOwner() { return owner; }

    private ActionProxy proxy() {
        return ActionContext.getContext().getActionInvocation().getProxy();
    }

    public String getNamespace() { return "'" + proxy().getNamespace() + "'"; }
    public String getName() { return proxy().getActionName(); }
}
<%@ page contentType="text/plain; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
label=<s:property value="label"/> namespace=<s:property value="namespace"/> action=<s:property value="name"/> owner=<s:property value="owner"/>
link-help=<s:url action="help"/>
link-books=<s:url action="books"/>
link-admin-books=<s:url action="books" namespace="/admin"/>

The label values (root-books, admin-books and so on) are set from struts.xml with <param name="label">, a static parameter that needs no @StrutsParameter annotation because it does not come from the request.

The Bookshelf Configuration: Four Packages

The configuration below is the one the base WAR (g8pkg) runs. It has an abstract base package that holds what every Bookshelf package shares, and three concrete packages: the root namespace /, an /admin namespace, and a package that declares no namespace at all, so its namespace is the default one.

struts.xml: an abstract base package and three packages that extend it
<struts>
    <package name="bookshelf-base" extends="struts-default" abstract="true">
        <global-results>
            <result name="notfound">/WEB-INF/views/notfound.jsp</result>
        </global-results>
        <action name="ghost" class="org.example.bookshelf.WhereAmIAction">
            <param name="label">ghost-in-abstract</param>
            <result>/WEB-INF/views/whereami.jsp</result>
        </action>
    </package>

    <package name="root" extends="bookshelf-base" namespace="/">
        <action name="books" class="org.example.bookshelf.WhereAmIAction">
            <param name="label">root-books</param>
            <result>/WEB-INF/views/whereami.jsp</result>
        </action>
    </package>

    <package name="admin" extends="bookshelf-base" namespace="/admin">
        <action name="books" class="org.example.bookshelf.WhereAmIAction">
            <param name="label">admin-books</param>
            <result>/WEB-INF/views/whereami.jsp</result>
        </action>
        <action name="stats" class="org.example.bookshelf.WhereAmIAction">
            <param name="label">admin-stats</param>
            <result>/WEB-INF/views/whereami.jsp</result>
        </action>
    </package>

    <package name="shared" extends="bookshelf-base">
        <action name="help" class="org.example.bookshelf.WhereAmIAction">
            <param name="label">default-help</param>
            <result>/WEB-INF/views/whereami.jsp</result>
        </action>
    </package>
</struts>

Inheritance: extends and abstract

extends is how a package receives the behaviour that makes Struts useful. Every real package ultimately extends struts-default, the package declared in struts-default.xml (inside the struts2-core jar), because that package holds the result types (dispatcher, redirect, redirectAction and the rest), the whole catalogue of interceptors and the defaultStack, the default interceptor reference defaultStack, and the default class org.apache.struts2.ActionSupport. The package without extends has none of that. In 7.4.0 struts-default is itself declared abstract="true", which is why an application never calls its own package struts-default and never expects to reach an action in it.

Table 2. What a package inherits from the packages it extends
Inherited Notes

Result types

Including the default result type that decides what <result> without a type means

Interceptors and interceptor stacks

By name; default-interceptor-ref is inherited too, and a child can declare its own

Global results and global exception mappings

A child adds to them or overrides one by name

Actions

Every action of a parent is also an action of the child, in the child’s namespace

default-action-ref, default-class-ref, global-allowed-methods

Inherited when the child does not declare its own

More Than One Parent

extends takes a comma separated list, which is how a package combines struts-default with the package of a plugin. The guide’s example extends struts-default, json-default so that the json result type becomes available next to the standard ones. The same attribute in the Bookshelf would read extends="bookshelf-base, json-default" once the JSON plugin is on the class path.

What abstract Really Does

The guide says abstract lets a base package omit action configurations. The run shows the other half: an abstract package does not expose its own actions, but every concrete package that extends it inherits them. The base package of the listing above declares the action ghost. Nothing is mapped for the abstract package itself, yet each of the three concrete packages answers to it in its own namespace:

Probing the inherited action (base WAR, curl with a cookie jar)
GET /g8pkg/ghost        200  label=ghost-in-abstract namespace='/' action=ghost owner=-
GET /g8pkg/admin/ghost  200  label=ghost-in-abstract namespace='/admin' action=ghost owner=-

So an abstract package is the right place for a stack, a result type or a global result that many packages share, and the wrong place for an action that should exist in one namespace only. Putting an action in an abstract base is a way of publishing it everywhere that base is extended, which is easy to do by accident.

Parent Packages and the Order of the File

The guide warns that the configuration file is processed sequentially and a package referenced by extends should be defined above the package that extends it. That is the safe habit, but Struts 7.4.0 does not insist on it for packages of one file: when a package names a parent that is declared later in the same document, the provider adds the parent first (the 7.4.0 source tracks "declared but not yet added" packages for exactly this). A WAR variant (g8pkgorder) whose root package extends a later-base package declared at the end of the file starts normally and answers GET /g8pkgorder/books with 200. A parent that is declared nowhere is another matter: the variant g8pkgbad has extends="nowhere", and the context does not start. The log reports Parent package is not defined: nowhere, and every URL of that application answers 404 because the filter never initialised. A parent that is marked final in the Struts configuration API cannot be extended either (Parent package is final and unextendable). Keep parents above children anyway: it reads better, and a parent that lives in another file loaded later is not covered by the lookup shown here.

Namespaces

A namespace is the part of the request path between the context path and the action name. For GET /bookshelf/admin/books.action the context path is /bookshelf, the namespace is /admin and the action name is books; the extension (.action, or none, because 7.4.0 defaults to struts.action.extension=action,,) is removed before the split. The Core Developers Guide calls namespaces the equivalent of Struts 1 modules, but more flexible, and defines three kinds.

Table 3. The three kinds of namespace (Core Developers Guide, Namespace Configuration)
Kind Written as Meaning

Default namespace

namespace="" or no attribute

The "catch-all" namespace. If an action is not found in the namespace of the request, the guide says the default namespace is searched too; see the next section for what 7.x really does

Root namespace

namespace="/"

The namespace of a request directly under the context path, such as /books.action. It always falls back to the default namespace if the action is not found in the root

Named namespace

namespace="/admin"

The namespace of requests whose path before the action name equals it exactly

Namespaces are not hierarchical. The guide states that there is one namespace level: for /barspace/myspace/bar.action the framework looks for the namespace /barspace/myspace, and if the action is not there the search falls straight to the default namespace; it never tries /barspace as a "parent folder". The Bookshelf run confirms it: with the base configuration GET /g8pkg/admin/x/books answers 404, although /admin/books is a perfectly good action.

A namespace prefix can be registered with the container’s declarative security, which is the reason to give an administrative area its own namespace; see the security section below.

How a URL Becomes a Namespace and an Action

Resolution happens in two steps and two different classes. The ActionMapper (by default org.apache.struts2.dispatcher.mapper.DefaultActionMapper) splits the URL into a namespace and an action name. Then the runtime configuration looks the pair up and, if allowed, retries in the default namespace. The first step is controlled by struts.mapper.alwaysSelectFullNamespace and the second by struts.actionConfig.fallbackToEmptyNamespace.

flowchart TD A["Request path after context path and extension, e.g. /admin/x/books"] --> B{"Where is the last slash?"} B -- "none" --> C["namespace empty, name = whole path"] B -- "at position 0" --> D["namespace /, name = rest"] B -- "later" --> E{"alwaysSelectFullNamespace"} E -- "true (7.x default)" --> F["namespace = everything before the last slash: /admin/x"] E -- "false" --> G["namespace = longest configured namespace that prefixes the path: /admin"] C --> H["Look up name in namespace"] D --> H F --> H G --> H H -- "found" --> I["Execute the action"] H -- "not found, after trying namespace patterns" --> J{"Namespace is / or fallbackToEmptyNamespace is true?"} J -- "yes" --> K["Look up name in the default namespace"] J -- "no" --> L["404, no action mapped"] K -- "found" --> I K -- "not found" --> L

struts.mapper.alwaysSelectFullNamespace

With the value true, the default in 7.x, the namespace is simply everything before the last slash. With false, which was the default before Struts 7, the mapper compares the path with the namespaces that exist in the configuration and takes the longest one that is a prefix (on a / boundary); whatever is left after it is the action name, from which a leading directory part is dropped unless struts.enable.SlashesInActionNames is true (its default is false). If no configured namespace matches, the namespace is the default one, or the root one when some package declared /.

struts.actionConfig.fallbackToEmptyNamespace

Once the namespace is chosen, an action that is not found in it can be looked up in the default (empty) namespace. In 7.x the answer depends on the namespace: a request in the root namespace / always falls back, which is the behaviour the guide describes; a request in any other namespace falls back only when struts.actionConfig.fallbackToEmptyNamespace is true, and its 7.0 default is false. The guide’s prose and its namespace example (which has /barspace/foo.action fall back to /foo.action) describe the older, more permissive behaviour, so on a 7.x default configuration that example returns 404.

Table 4. The same URLs against four configurations of the Bookshelf (status and what answered)
URL under the context 7.x defaults (g8pkg) fallbackToEmptyNamespace=true (g8pkgfb) alwaysSelectFullNamespace=false (g8pkgnofull) What it shows

/books

200 root-books, namespace '/'

same

same

Root namespace, action in the root package

/admin/books

200 admin-books, namespace '/admin'

same

same

Named namespace, exact match

/help

200 default-help, namespace '/'

same

same

Root request, action only in the default namespace: the root always falls back

/admin/help

404

200 default-help, namespace '/admin'

404

Named namespace falls back only when the constant is true

/admin/x/books

404

404

200 admin-books, namespace '/admin'

Longest-prefix selection only with false; with true the namespace is /admin/x

/admin/x/help

not probed

200 default-help, namespace '/admin/x'

not probed

Fallback needs no configured namespace at all

/nothing/help

404

200 default-help, namespace '/nothing'

not probed

A namespace nobody declared still falls back when the constant is true

In the fallback case the namespace the action reports is the requested one ('/admin'), not the namespace of the package that actually supplied the action. The constants are set with <constant name="struts.actionConfig.fallbackToEmptyNamespace" value="true"/> as a child of <struts>; the variants differ from the base WAR only by that one line.

The two 7.0 defaults are the safer ones, and the sensible policy is to keep them. A strict resolution means a typo in a URL is a 404 rather than another action that happens to share the name, and it means an administrative namespace cannot be reached by an action that was meant for the public area. Turn fallbackToEmptyNamespace on only for an application whose links were written against the old behaviour and cannot yet be corrected, and treat it as a migration crutch. Turn alwaysSelectFullNamespace off only when URLs carry variable path segments after a fixed prefix and you cannot use a namespace pattern (described below) instead.

Several Packages in One Namespace, and default-action-ref

Namespaces belong to the URL space, not to a package: the runtime configuration groups the actions of every concrete package by namespace, so two packages that declare the same namespace contribute to the same namespace. The variant g8pkgmore2 adds a second package, admin-reports, with namespace="/admin" and one action reports; both are served:

Two packages, one namespace (variant g8pkgmore2)
GET /g8pkgmore2/admin/books    200  label=admin-books   namespace='/admin' action=books
GET /g8pkgmore2/admin/reports  200  label=admin-reports namespace='/admin' action=reports

A package can also name a fallback inside its namespace with default-action-ref, which must come before the actions:

A catch-all action for the admin namespace (variant g8pkgmore2)
<package name="admin" extends="bookshelf-base" namespace="/admin">
    <default-action-ref name="books"/>
    <action name="books" class="org.example.bookshelf.WhereAmIAction">
        <param name="label">admin-books</param>
        <result>/WEB-INF/views/whereami.jsp</result>
    </action>
    ...
</package>
GET /g8pkgmore2/admin/whatever  200  label=admin-books namespace='/admin' action=whatever
GET /g8pkgmore2/admin/help      200  label=admin-books namespace='/admin' action=help
GET /g8pkgmore2/nothing         404

The default action answers every unknown name of that namespace and reports the requested name (whatever), not the name of the action that ran, so it hides typos inside the namespace; the guide’s own use for it is an "under construction" page. It applies only to the namespace of its package, so /nothing, which falls in the root namespace where no default is declared, stays 404. Because names collide in a shared namespace, two packages should not both declare the same action name there: whichever is registered later silently wins.

Namespace Parameters

From Struts 2.1 a namespace can contain variables that are extracted from the URL and set on the action. They need struts.patternMatcher set to namedVariable, which replaces the default wildcard matcher (the two cannot be mixed in one application, so the * wildcards of the Wildcard Mappings and Method Invocation page stop working under it). The variant g8pkgnv adds this package to the Bookshelf, one shelf per owner:

A namespace with a variable (variant g8pkgnv)
<constant name="struts.patternMatcher" value="namedVariable"/>

<package name="shelf" extends="bookshelf-base" namespace="/shelf/{owner}">
    <action name="books" class="org.example.bookshelf.WhereAmIAction">
        <param name="label">shelf-books</param>
        <result>/WEB-INF/views/whereami.jsp</result>
    </action>
</package>
GET /g8pkgnv/shelf/alice/books  200  label=shelf-books namespace='/shelf/alice' action=books owner=alice
GET /g8pkgnv/shelf/bob/books    200  label=shelf-books namespace='/shelf/bob'   action=books owner=bob
GET /g8pkgnv/shelf/books        404

The owner property was set from the namespace without any @StrutsParameter annotation, because the matcher adds the variables to the action configuration as static parameters rather than as request parameters. The guide warns that some tags do not cooperate with variables in a namespace: they may write the literal pattern (/shelf/{owner}) into the generated HTML instead of the path of the request, so build such links as plain HTML with explicit paths.

A package’s namespace also shapes the URLs that Struts writes. The url tag and the other tags that build the address of an action, and when no namespace attribute is given they use the namespace of the action that rendered the page. The result page of the base WAR contains <s:url action="help"/> and prints this when it is rendered from an action in /admin and from an action in /:

The same tag in two namespaces (base WAR, with the session identifier left out)
link-help=/g8pkg/help.action              (rendered by /books, namespace '/')
link-help=/g8pkg/admin/help.action        (rendered by /admin/books, namespace '/admin')
link-admin-books=/g8pkg/admin/books.action  (with namespace="/admin" given explicitly)

On the 7.x default configuration the second link is a dead link, because help exists only in the default namespace and /admin/help is a 404 (table above). That is the most common namespace bug in a migrated application, and the cure is one of three: write <s:url action="help" namespace="/"/> (the root always falls back to the default namespace), declare the action in the namespace that needs it, or set the fallback constant. The first is the least invasive. A request made without a cookie also carries ;jsessionid=... in these links on the first page, which is URL rewriting and is not a namespace matter.

Namespaces and Container Security

Because the namespace is a path prefix, the container’s declarative security can protect a namespace without Struts knowing about it. A <security-constraint> with the URL pattern /admin/* guards every action of /admin, because the filter that runs Struts sits behind the container’s security check. The variant g8pkgmore adds this to web.xml:

web.xml: only the librarian role may reach the /admin namespace (variant g8pkgmore)
<security-constraint>
    <web-resource-collection>
        <web-resource-name>admin</web-resource-name>
        <url-pattern>/admin/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>librarian</role-name>
    </auth-constraint>
</security-constraint>
<login-config>
    <auth-method>BASIC</auth-method>
</login-config>
<security-role>
    <role-name>librarian</role-name>
</security-role>

On that WAR, GET /g8pkgmore/books answers 200 and every URL under /admin/ answers 401 without credentials (the run had no user with the role, so the successful case is not shown). The pattern must match what a client can actually ask for: /admin/* covers both /admin/books and /admin/books.action, but a second package in another namespace that exposes the same action by a fallback (see above) escapes the constraint, one more reason to keep fallback off. Struts also offers its own authorization through interceptors, which the security pages of this section cover (Security Hardening and Interceptors).

The Same Concepts With Annotations

The Convention plugin does not need <package> elements. It derives a package for each class and exposes its configuration through annotations: @Namespace fixes the namespace of a class, @ParentPackage names the package it extends (the counterpart of extends), and @Action names the action. The namespace rules of this page (single level, fallback, patterns) apply to the generated packages unchanged. The plugin is the topic of its own page in this section; the guide to the same configuration in XML is this page.

A Package Layout for the Bookshelf

The experiments suggest a layout that scales. One abstract base package holds the stack, global results and result types that every package shares. One concrete package per public area owns a namespace; a second package may reuse a namespace only to split a long file. Shared actions that must be reachable from everywhere live in the root namespace, because the root is the only namespace that always falls back, not in the default one.

Table 5. A package plan for a growing Bookshelf
Package Namespace Purpose

bookshelf-base (abstract)

none

Default interceptor stack, global results (notfound, login), global exception mappings; no actions

root

/

Public pages: the catalogue, search, help, the home page

admin

/admin

Back-office actions, protected by <security-constraint> on /admin/*

shelf

/shelf/{owner}

Per-user pages, only if namespace patterns are really wanted

Pitfalls

Table 6. Common mistakes with packages and namespaces
Symptom Cause Fix

An action works in one area and is a 404 in another, although the name is right

7.x resolves in one namespace and falls back to the default namespace only from the root, so /admin/help does not find the default help

Declare the action where it is used, link with namespace="/", or set struts.actionConfig.fallbackToEmptyNamespace knowingly

A link generated by s:url or s:form is dead in some pages

No namespace attribute, so the tag uses the namespace of the current action

Give the namespace explicitly for actions outside the current area

/admin/x/books is a 404

Namespaces are one level, not folders, and alwaysSelectFullNamespace is true in 7.x

Declare /admin/x, use a namespace pattern, or avoid deep URLs

An action appears under every namespace

It was declared in an abstract base package that the concrete packages extend

Move it to one concrete package

The application does not start: "Parent package is not defined"

extends names a package that exists in no loaded file, or is misspelt

Fix the name, or <include> the file that declares it

Every URL returns 404 after a configuration edit

The context failed to start, so the Struts filter never initialised

Read the Tomcat log for the configuration exception

Two packages in one namespace override each other

Namespaces merge across packages and the later registration wins

Give actions of a shared namespace unique names

A typo inside a namespace runs an unrelated action

default-action-ref answers every unknown name

Use it only for an explicit "not found" or "under construction" action

Upgrading from 6.x, actions that used to fall back return 404

alwaysSelectFullNamespace and fallbackToEmptyNamespace changed defaults in 7.0

Fix the links, or restore the old values while migrating

Mixing * wildcards and {var} namespace patterns

struts.patternMatcher allows only one matcher

Choose one; named variables replace the wildcard matcher

What Changed Recently

Table 7. Changes that affect packages and namespaces
Version Change

Struts 2.1

Namespace patterns (namedVariable) can supply request-time parameters to the action

Struts 6.0

The newest DTD (6.0) declares strict-method-invocation on the package element, true by default, together with allowed-methods and global-allowed-methods; wildcard and method invocation need an allowed method

Struts 7.0

struts.mapper.alwaysSelectFullNamespace defaults to true and struts.actionConfig.fallbackToEmptyNamespace defaults to false, as the 6.x to 7.x migration guide lists under "Constants default value changed"; the com.opensymphony.xwork2 classes (ActionSupport, ActionContext, ActionProxy) moved to org.apache.struts2

Struts 7.4.0

No change to package or namespace semantics; a fix for the REST action mapper, which put URIs with an id into the empty namespace so actions at / returned 404 (WW-5688), is listed in the Version Notes

A Struts 7 struts.xml keeps the 6.0 document type declaration, as the Bookshelf configuration does (why 6.0).

Where the Books Are Outdated

The Struts 2 book of the bibliography devotes a chapter to actions, packages and namespaces, and the Struts 1 books use the module as the unit of separation. The concepts still hold; the details have moved.

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

Struts 1 modules, each with its own struts-config.xml and its own prefix

Namespaces of one struts.xml replace modules; the guide itself calls them the equivalent of modules

Packages without an explicit extends="struts-default", or with extends of a parent from a plug-in that no longer exists (for example Codebehind)

Extend struts-default plus the package of a current plugin; Codebehind was removed in the 2.5 line and the Convention plugin took its role

Names such as com.opensymphony.xwork2.ActionSupport in actions of the package

Struts 7 repackaged them under org.apache.struts2 (org.apache.struts2.ActionSupport)

URLs ending in .action as the only form, or .do after migrating from Struts 1

7.4.0 default is struts.action.extension=action,,, so both /books and /books.action work and .do does not

Namespace fallback to the default namespace from any namespace (also the behaviour in the official guide’s example)

Only from the root namespace unless fallbackToEmptyNamespace is true

alwaysSelectFullNamespace off by default (longest-prefix selection)

On by default since 7.0

struts.xml with a DOCTYPE of struts-2.0.dtd or struts-2.3.dtd

struts-6.0.dtd is the newest

Dynamic method invocation /admin/books!save.action

struts.enable.DynamicMethodInvocation is false by default; use wildcard mappings

References