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.
| Attribute | Required | Meaning |
|---|---|---|
|
yes |
The key other packages use to refer to this one. It has no meaning to URLs: a package called |
|
no |
One or more package names, comma separated, whose interceptors, interceptor stacks, result types, global results, global exception mappings and actions this package inherits |
|
no |
The URL prefix under which the package’s actions are reachable. Omitted means the default namespace, the empty string |
|
no |
|
|
no |
|
Source: Struts Configuration DTD 6.0
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.
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>
<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.
| Inherited | Notes |
|---|---|
Result types |
Including the default result type that decides what |
Interceptors and interceptor stacks |
By name; |
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 |
|
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:
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.
| Kind | Written as | Meaning |
|---|---|---|
Default namespace |
|
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 |
|
The namespace of a request directly under the context path, such as |
Named namespace |
|
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.
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.
| URL under the context | 7.x defaults (g8pkg) |
fallbackToEmptyNamespace=true (g8pkgfb) |
alwaysSelectFullNamespace=false (g8pkgnofull) |
What it shows |
|---|---|---|---|---|
|
200 |
same |
same |
Root namespace, action in the root package |
|
200 |
same |
same |
Named namespace, exact match |
|
200 |
same |
same |
Root request, action only in the default namespace: the root always falls back |
|
404 |
200 |
404 |
Named namespace falls back only when the constant is |
|
404 |
404 |
200 |
Longest-prefix selection only with |
|
not probed |
200 |
not probed |
Fallback needs no configured namespace at all |
|
404 |
200 |
not probed |
A namespace nobody declared still falls back when the constant is |
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.
Which Setting to Keep
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:
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:
<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:
<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.
Namespaces and Generated URLs
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 /:
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:
<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.
Source: Struts Convention Plugin
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.
| Package | Namespace | Purpose |
|---|---|---|
|
none |
Default interceptor stack, global results ( |
|
|
Public pages: the catalogue, search, help, the home page |
|
|
Back-office actions, protected by |
|
|
Per-user pages, only if namespace patterns are really wanted |
Pitfalls
| 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 |
Declare the action where it is used, link with |
A link generated by |
No |
Give the namespace explicitly for actions outside the current area |
|
Namespaces are one level, not folders, and |
Declare |
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" |
|
Fix the name, or |
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 |
|
Use it only for an explicit "not found" or "under construction" action |
Upgrading from 6.x, actions that used to fall back return 404 |
|
Fix the links, or restore the old values while migrating |
Mixing |
|
Choose one; named variables replace the wildcard matcher |
What Changed Recently
| Version | Change |
|---|---|
Struts 2.1 |
Namespace patterns ( |
Struts 6.0 |
The newest DTD (6.0) declares |
Struts 7.0 |
|
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 |
Source: Struts 6.x.x to 7.x.x migration
Source: Struts Version Notes 7.4.0
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.
| In the books | Today |
|---|---|
Struts 1 modules, each with its own |
Namespaces of one |
Packages without an explicit |
Extend |
Names such as |
Struts 7 repackaged them under |
URLs ending in |
7.4.0 default is |
Namespace fallback to the default namespace from any namespace (also the behaviour in the official guide’s example) |
Only from the root namespace unless |
|
On by default since 7.0 |
|
|
Dynamic method invocation |
|
Source: Struts Configuration DTD 6.0
References
-
Struts Core Developers Guide — Package Configuration, Packages
-
Struts Core Developers Guide — Package Configuration, Simple usage
-
Struts Core Developers Guide — Package Configuration, Inherit from more than one package
-
Struts Core Developers Guide — Namespace Configuration, Default Namespace
-
Struts Core Developers Guide — Namespace Configuration, Root Namespace
-
Struts Core Developers Guide — Namespace Configuration, Namespace Example
-
Struts Core Developers Guide — Namespace Configuration, How the code works
-
Struts Core Developers Guide — Action Configuration, default action
-
Struts Core Developers Guide — Wildcard Mappings, Parameters in namespaces