Skip to content

WW-5791 Struts Boot: run Struts from main() on embedded Tomcat with a fluent action registry - #2053

Merged
lukaszlenart merged 15 commits into
mainfrom
WW-5791-struts-boot
Oct 24, 2026
Merged

lukaszlenart merged 15 commits into
mainfrom
WW-5791-struts-boot

Conversation

@lukaszlenart

Copy link
Copy Markdown
Member

Fixes WW-5791

What

Struts Boot: run a Struts application from main() on embedded Tomcat, with actions registered in Java instead of struts.xml. Aimed at newcomers. Standalone, no Spring dependency, no changes under core/.

public class HelloApp implements StrutsBootApplication {
    public static void main(String[] args) {
        StrutsBoot.run(HelloApp.class, args);
    }

    @Override
    public void actions(ActionRegistry actions) {
        actions.action("hello", HelloAction.class).success("hello.ftl");
    }
}
  • boot/ (struts2-boot-starter) contains:
    • StrutsBoot / StrutsBootServer: embedded Tomcat that registers StrutsPrepareAndExecuteFilter in code.
    • The fluent ActionRegistry API.
    • RegistryConfigurationProvider, which turns the registry into ordinary PackageConfigs through the existing configProviders init param.
    • BootSettings, which reads struts.boot.port, struts.boot.address and struts.boot.context-path from struts.properties or --key=value arguments.
  • Tomcat follows core's Jakarta line: 10.1.59 by default, 11.0.27 under -Pjakartaee11. The version is managed in the parent POM, and the BOM lists the starter.
  • apps/boot-hello is an example app shaded into a runnable jar. It is not deployed (maven.deploy.skip), because shading merges the dependencies' LICENSE/NOTICE files.
  • THREAT_MODEL.md gets a Boot section. The starter's container defaults are now in the model, and §14 has a new question, Q-boot, for the PMC.

Behaviour worth reviewing

  • Registry config is never broader than the equivalent struts.xml.
    • Packages extend struts-default and Strict Method Invocation stays on.
    • Action classes are added to the OGNL allowlist the same way the XML provider adds them.
    • Each action allows only its registered method. That is narrower than XML, which also inherits the package's global allowed methods.
  • Configuration errors fail at startup with a ConfigurationException for:
    • slashes in an action name (use namespaces instead);
    • non-.ftl views;
    • missing templates or unknown interceptor stacks;
    • non-public action classes, or classes without a no-arg constructor;
    • duplicate actions, within the registry or against struts.xml.
  • Container defaults:
    • listens on all interfaces, port 8080 (struts.boot.address restricts the interface);
    • error pages carry no stack trace or server version;
    • DefaultServlet serves an empty per-run docBase;
    • Tomcat's base directory is a UUID-named temp directory, deleted on shutdown;
    • devMode is never switched on;
    • a failed start stops Tomcat and removes the temp directory.
  • jakarta.servlet.jsp-api is a runtime dependency of the starter. FreeMarker's TaglibFactory needs it, and tomcat-embed-core doesn't ship it.

Not in this PR (parked in WW-5791)

Not covered here:

  • WAR-to-jar migration
  • JSP views
  • Gradle plugin
  • other containers
  • a fluent API for interceptor stacks
  • global results, global exception mappings and wildcards
  • a Convention bridge
  • dev-mode reload

The struts-site documentation and the 7.5.0 Version Notes entry follow after merge.

Testing

  • ./mvnw clean install -DskipAssembly: BUILD SUCCESS, with and without -Pjakartaee11.
  • boot has 49 tests covering:
    • the registry, the provider and the settings;
    • end to end on embedded Tomcat:
      • FreeMarker rendering;
      • POST-only rejects GET;
      • !method and method: blocked with DMI on, with positive controls;
      • XML and the registry working side by side;
      • context path and redirects;
      • busy port and config-error cleanup;
      • error-page content;
      • bind address.
  • java -jar on the shaded boot-hello jar serves /hello.action with 200.

Design and plan: docs/superpowers/specs/2026-10-23-WW-5791-struts-boot-design.md, docs/superpowers/plans/2026-10-23-WW-5791-struts-boot.md.

🤖 Generated with Claude Code

lukaszlenart and others added 13 commits October 23, 2026 12:24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n registry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… configs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and arguments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it failure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… settings

Error pages no longer carry stack traces or the Tomcat version, the
connector is opened as an explicit step after the Struts filter started,
struts.boot.address restricts the listening interface (all interfaces
stay the default), and struts.boot.context-path is normalised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…artup

Mirror XmlDocConfigurationProvider#verifyAction: a non-public action
class or one without a public no-arg constructor fails startup. A null
action class or an empty redirectAction target is rejected with a
ConfigurationException.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elease

The shaded example jar merges dependency LICENSE/NOTICE files, so it is
not deployed. The manifest declares Multi-Release because log4j-api and
Tomcat are multi-release jars. The redundant version is inherited.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: struts.boot.address, error pages, the jar name, BOM usage, a
logging backend note, and non-Spring packaging alternatives. Spec: fix
the .method(name) row and state that registry config is never broader
than the equivalent struts.xml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java Fixed
@lukaszlenart
lukaszlenart requested a balanced review from Copilot October 23, 2026 13:28
The Tomcat base directory is created owner-only (rwx------) where the
file system supports POSIX permissions, and the shutdown hook is
registered inside the guarded start block so a failure there still
closes the server. Settings keys get a _KEY suffix, the empty
register() says why it is empty, and test lambdas keep a single
throwing call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

8 open issues
What changed in this PR

Adds a new “Struts Boot” starter module that runs Struts from main() on embedded Tomcat and allows registering actions via a fluent Java registry (no web.xml/struts.xml required), including an example runnable app and threat-model documentation updates.

Changes:

  • Introduces new boot/ module (struts2-boot-starter) with embedded Tomcat launcher, fluent ActionRegistry, and a ConfigurationProvider translating registry entries into standard Struts configs.
  • Adds apps/boot-hello example app shaded into a runnable JAR.
  • Updates BOM/parent POMs and threat model to manage/describe the new starter and its defaults.
File Description
pom.xml Adds boot module and manages embedded Tomcat version via properties (EE10/EE11 profiles).
parent/​pom.xml Adds dependencyManagement entries for tomcat-embed-core and struts2-boot-starter.
bom/​pom.xml Publishes struts2-boot-starter via the Struts BOM.
boot/​pom.xml Defines the new starter module and its runtime/test dependencies.
boot/​src/​main/​java/​org/​apache/​struts2/​boot/​StrutsBoot.java Provides run() / start() entry points for starting embedded Struts.
boot/​src/​main/​java/​org/​apache/​struts2/​boot/​StrutsBootServer.java Implements embedded Tomcat lifecycle, filter wiring, temp dirs, and shutdown behavior.
boot/​src/​main/​java/​org/​apache/​struts2/​boot/​BootSettings.java Reads struts.boot.* from struts.properties and --key=value args.
boot/​src/​main/​java/​org/​apache/​struts2/​boot/​RegistryConfigurationProvider.java Converts registry definitions into Struts PackageConfig/ActionConfig at startup.
boot/​src/​main/​java/​org/​apache/​struts2/​boot/​ActionRegistry.java Holds namespaces/actions registered in code.
boot/​src/​main/​java/​org/​apache/​struts2/​boot/​NamespaceActions.java Per-namespace action registration + optional interceptor stack.
boot/​src/​main/​java/​org/​apache/​struts2/​boot/​ActionDefinition.java Fluent per-action configuration (method/results/http methods/interceptors).
boot/​src/​main/​java/​org/​apache/​struts2/​boot/​HttpMethods.java Convenience constants for HttpMethod to support static imports.
boot/​src/​main/​java/​org/​apache/​struts2/​boot/​StrutsBootApplication.java Public app interface for supplying actions to the registry.
boot/​src/​test/​java/​org/​apache/​struts2/​boot/​ActionRegistryTest.java Unit tests for registry behavior and validation.
boot/​src/​test/​java/​org/​apache/​struts2/​boot/​BootSettingsTest.java Unit tests for settings precedence and normalization.
boot/​src/​test/​java/​org/​apache/​struts2/​boot/​RegistryConfigurationProviderTest.java Tests translation/validation (allowlist, results, clashes, stacks, templates).
boot/​src/​test/​java/​org/​apache/​struts2/​boot/​StrutsBootTomcatTest.java End-to-end tests on embedded Tomcat (rendering, DMI, context path, errors, cleanup).
boot/​src/​test/​java/​org/​apache/​struts2/​boot/​fixture/​* Fixtures for integration tests (apps/actions).
boot/​src/​test/​resources/​templates/​* FreeMarker templates used by tests.
boot/​src/​test/​resources/​struts.xml Test XML config to validate coexistence with registry packages.
boot/​src/​test/​resources/​struts.properties Enables DMI in tests to validate method restrictions.
boot/​src/​test/​resources/​struts-boot-clash.xml XML fixture to test startup failure on action name clashes.
apps/​pom.xml Adds the boot-hello example module under apps.
apps/​boot-hello/​pom.xml Builds a runnable shaded JAR for the example app (deploy skipped).
apps/​boot-hello/​src/​main/​java/​org/​apache/​struts2/​boot/​hello/​HelloApp.java Example main() and registry-based action registration.
apps/​boot-hello/​src/​main/​java/​org/​apache/​struts2/​boot/​hello/​HelloAction.java Example action for hello page.
apps/​boot-hello/​src/​main/​resources/​templates/​hello.ftl Example FreeMarker view using Struts tags.
apps/​boot-hello/​src/​test/​java/​org/​apache/​struts2/​boot/​hello/​HelloAppTest.java Smoke test verifying the example app serves /hello.action.
apps/​boot-hello/​README.md Documents how to build/run the example and starter defaults/limitations.
docs/​superpowers/​specs/​2026-10-23-WW-5791-struts-boot-design.md Design/spec document for Struts Boot behavior and constraints.
docs/​superpowers/​plans/​2026-10-23-WW-5791-struts-boot.md Implementation plan and test matrix for the feature.
THREAT_MODEL.md Adds Struts Boot deployment shape and default container behavior to the threat model.

Review effort: Lite

Comment on lines +80 to +82
server.openConnector();
} catch (Exception e) {
ConfigurationException failure = e instanceof ConfigurationException configurationException
Comment on lines +145 to +149
if (created.getState() != LifecycleState.STARTED || created.getLocalPort() < 0) {
throw new ConfigurationException("Struts Boot failed to start: connector state "
+ created.getState() + " on port " + settings.port());
}
}
Comment on lines +52 to +55
public ActionDefinition method(String methodName) {
this.methodName = methodName;
return this;
}
Comment on lines +61 to +68
public ActionDefinition result(String name, String templatePath) {
if (templatePath == null || !templatePath.endsWith(".ftl")) {
throw new ConfigurationException("Action '" + this.name + "' result '" + name + "' uses '" + templatePath
+ "': Struts Boot renders FreeMarker templates only, the path must end with .ftl");
}
results.put(name, new ResultDefinition(ResultDefinition.Kind.TEMPLATE, templatePath));
return this;
}
Comment on lines +100 to +106
private static int parsePort(String value) {
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
throw new ConfigurationException("Invalid " + PORT + ": '" + value + "' is not a number", e);
}
}
private final AtomicBoolean closed = new AtomicBoolean();
private final Thread shutdownHook = new Thread(this::close, "struts-boot-shutdown");
private Connector connector;

Comment on lines +113 to +127
if (!closed.compareAndSet(false, true)) {
return;
}
try {
tomcat.stop();
tomcat.destroy();
} catch (Exception e) {
throw new ConfigurationException("Struts Boot failed to stop Tomcat", e);
} finally {
deleteRecursively(baseDir);
removeShutdownHook();
}
}

private void closeAfterFailure(Throwable failure) {
Comment on lines +211 to +226
try {
Files.delete(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private static String rootMessage(Throwable throwable) {
Throwable root = throwable;
while (root.getCause() != null && root.getCause() != root) {
root = root.getCause();
}
Comment thread boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java Fixed
… start failure

The filter and Tomcat are created before the base directory, so nothing
can fail between creating the directory and the guarded start block. On
file systems without POSIX permissions the base directory is restricted
to its owner after creation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lukaszlenart
lukaszlenart marked this pull request as ready for review October 24, 2026 07:44
@sonarqubecloud

sonarqubecloud Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Comment thread boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java Dismissed
@lukaszlenart
lukaszlenart merged commit 9a13f55 into main Oct 24, 2026
9 checks passed
@lukaszlenart
lukaszlenart deleted the WW-5791-struts-boot branch October 24, 2026 07:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants