Repository navigation
WW-5791 Struts Boot: run Struts from main() on embedded Tomcat with a fluent action registry - #2053
Merged
Merged
Conversation
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>
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>
Contributor
There was a problem hiding this comment.
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
openConnector()creates and attaches the TomcatConnectoraftertomcat.start(), but never… · NewopenConnector()creates and attaches the TomcatConnectoraftertomcat.start(), but never… · New The public fluent API allowsmethod(null)andresult(null, "x.ftl")(or blank names). Those… · New The public fluent API allowsmethod(null)andresult(null, "x.ftl")(or blank names). Those… · NewparsePortaccepts negative values or values > 65535, which will later fail during socket bind… · Newclose()can throw aConfigurationException, but it is invoked directly as a JVM shutdown hook… · Newclose()can throw aConfigurationException, but it is invoked directly as a JVM shutdown hook… · New Cleanup currently throwsUncheckedIOExceptionif any file deletion fails (e.g., transient Windows… · New
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, fluentActionRegistry, and aConfigurationProvidertranslating registry entries into standard Struts configs. - Adds
apps/boot-helloexample 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(); | ||
| } |
Copilot stopped reviewing on behalf of
lukaszlenart due to an error
October 23, 2026 15:37
… 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
marked this pull request as ready for review
October 24, 2026 07:44
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes WW-5791
What
Struts Boot: run a Struts application from
main()on embedded Tomcat, with actions registered in Java instead ofstruts.xml. Aimed at newcomers. Standalone, no Spring dependency, no changes undercore/.boot/(struts2-boot-starter) contains:StrutsBoot/StrutsBootServer: embedded Tomcat that registersStrutsPrepareAndExecuteFilterin code.ActionRegistryAPI.RegistryConfigurationProvider, which turns the registry into ordinaryPackageConfigs through the existingconfigProvidersinit param.BootSettings, which readsstruts.boot.port,struts.boot.addressandstruts.boot.context-pathfromstruts.propertiesor--key=valuearguments.-Pjakartaee11. The version is managed in the parent POM, and the BOM lists the starter.apps/boot-hellois 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.mdgets 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
struts.xml.struts-defaultand Strict Method Invocation stays on.ConfigurationExceptionfor:.ftlviews;struts.xml.struts.boot.addressrestricts the interface);DefaultServletserves an empty per-run docBase;devModeis never switched on;jakarta.servlet.jsp-apiis a runtime dependency of the starter. FreeMarker'sTaglibFactoryneeds it, andtomcat-embed-coredoesn't ship it.Not in this PR (parked in WW-5791)
Not covered here:
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.boothas 49 tests covering:!methodandmethod:blocked with DMI on, with positive controls;java -jaron the shadedboot-hellojar serves/hello.actionwith 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