Add wicketstuff-spring-boot-starter: Apache Wicket on Spring Boot 4 - #1568
danielbartl wants to merge 35 commits into
Conversation
…ed dependencies, and static resources integration
The starter module was added while the root reactor was at 10.10.0-SNAPSHOT; the root has since moved to 10.12.0-SNAPSHOT but the starter, examples, and their aggregator parent POM were left pointing at the old version, breaking parent resolution. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Import spring-boot-dependencies as a BOM instead of hardcoding spring-boot.version on every individual Spring Boot dependency, so versions stay consistent and are managed from one place. - Guard WicketAutoConfiguration with @ConditionalOnWebApplication(SERVLET) so it doesn't try to register a servlet FilterRegistrationBean in a non-servlet (e.g. reactive) application. - Add spring-boot-maven-plugin's repackage goal to the examples module so it produces a runnable jar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- wicket-spring-boot-starter: WicketAutoConfigurationTest exercises the autoconfiguration in isolation via ApplicationContextRunner (default bean wiring, custom wicket.* property binding, @ConditionalOnMissingBean back-off, and the SERVLET-only guard); DefaultWebApplicationTest uses WicketTester to verify the default home page renders and the mounted CSS/PNG resources are actually servable. - wicket-spring-boot-starter-examples: WicketPropertiesBindingTest proves application.properties values reach the registered filter; SpringBeanInjectionQuickstartTest is an end-to-end proof of the README's Quickstart pattern (custom WebApplication + @SpringBean injection) over real HTTP through an embedded servlet container. Also fixes dependency issues surfaced by actually running this suite: - wicket-spring-boot-starter needs spring-web (test scope) for WebApplicationContextRunner; nothing else on its classpath supplies it since spring-boot-autoconfigure's own deps are optional and wicket-spring excludes spring-web. - wicket-spring-boot-starter-parent pins the JUnit Platform/Jupiter family and logback-core to the versions the root pom already manages, ahead of the spring-boot-dependencies BOM import: the BOM's own versions of those artifacts were binary-incompatible with root-managed sibling artifacts (junit-jupiter-api/junit-platform-commons, logback-classic/logback-core). - wicket-spring-boot-starter-examples needs spring-boot-restclient (test scope): spring-boot-resttestclient's own autoconfiguration needs RestTemplateBuilder from it but doesn't declare it as a dependency. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Replace the dependency snippet's undefined \${wicketstuff.version}
property (not declared in any pom) with the actual current version and
a pointer to Maven Central for the released version.
- Fix the Spring Initializr versionRange, which was copy-pasted from an
unrelated example (a stray 4.2.0-M1 patch-level bound). Left it
open-ended (4.0.0 and later) rather than guessing an unconfirmed future
Spring Boot major version number.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the copy inherited from Wicket's old quickstart archetype with content specific to this starter: drop the dead Freenode IRC link and outdated JIRA bug-report walkthrough, explain why the page is shown, and link to the starter's own README Quickstart and the Wicket docs instead. Also fixes the static "1.5-SNAPSHOT" placeholder text (cosmetic only -- always overwritten at runtime by the version Label). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The root pom.xml already declares Apache License 2.0 (inherited by this module and its children), but there was no physical LICENSE file anywhere in the repo to make that unambiguous to someone browsing this module's directory directly, e.g. Spring Initializr maintainers reviewing the submission. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WicketProperties.configuration was left null unless explicitly set, with the DEVELOPMENT fallback only applied manually inside WicketAutoConfiguration. This meant the property's generated IDE metadata (spring-configuration-metadata.json) had no defaultValue, unlike the other two wicket.* properties, contradicting both its own Javadoc and the README's documented default. Initializing the field directly fixes the metadata and lets the now-dead null-check in WicketAutoConfiguration go away, along with its now-unused RuntimeConfigurationType import. Also cleans up DefaultWebApplication to use proper imports and Java 17 pattern-matching instanceof instead of inline fully-qualified names and a manual cast. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ConditionalOnMissingBean(WicketFilter.class) was declared on a method producing a FilterRegistrationBean<WicketFilter>, so the condition never examined what the method actually creates. A user registering their own FilterRegistrationBean<WicketFilter> -- the customization path the README points at -- ended up with two Wicket filters, ours silently mapped at /*. Conversely a raw WicketFilter bean made us back off entirely, leaving Boot to auto-register it with no init parameters and no SpringComponentInjector, quietly breaking @SpringBean. Using a bare @ConditionalOnMissingBean lets Boot deduce the bean type from the return value including its generics, so a user-supplied FilterRegistrationBean<WicketFilter> now correctly wins. Covered by a new regression test. Also removes two pieces of dead/fragile code found while reviewing: - DefaultWebApplication re-added "+*.css"/"+*.png" resource guard patterns that SecurePackageResourceGuard's constructor already registers by default. - The @nonnull on a private helper's return type came from org.jspecify, which this module never declared and only got transitively via Wicket. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
The injector was registered inside an anonymous WicketFilter subclass's init(). Now that the filter registration correctly backs off for an application-supplied FilterRegistrationBean, that back-off took Spring injection with it: requesting a page in that setup returned HTTP 500, because @SpringBean fields were never injected and the page NPE'd. Wicket's own documentation registers the injector on the WebApplication rather than the filter, and SpringComponentInjector's constructor only sets application metadata and binds, so it has no dependency on filter initialisation. Moving it to its own bean keyed off the WebApplication keeps injection working whoever owns the filter registration, and drops the anonymous subclass entirely. Exposing it as a @ConditionalOnMissingBean bean also gives applications migrating from a hand-wired Wicket/Spring setup a way to suppress it, instead of silently ending up with two injectors when their own WebApplication.init() already registers one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
Third-party starters are generally expected to offer an opt-out, and there was no way to keep the starter on the classpath while disabling it. @ConditionalOnProperty sits on the auto-configuration class rather than on individual beans so the default WebApplication, the injector and the filter registration all switch off together instead of leaving a half-configured application. matchIfMissing keeps it on by default, so existing applications are unaffected. The property is read only by the condition and never bound to a field, so it is documented through additional-spring-configuration-metadata.json rather than by adding an unused field to WicketProperties. The annotation processor merges it into the generated metadata, so IDE completion covers it alongside the other wicket.* properties. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
A project whose only dependency is this starter got no embedded servlet container, no spring-web, and jakarta.servlet-api only at provided scope. Spring Boot therefore booted it as a non-web application, @ConditionalOnWebApplication(SERVLET) declined, and the application exited having served nothing. That is precisely the project Spring Initializr generates when a user selects Wicket and nothing else, and Initializr metadata has no way to express "also add Spring Web", so every generated project would have been dead on arrival. Depending on spring-boot-starter-web fixes it: the same minimal project now starts Tomcat, registers the WebApplication and serves the default page. The examples module drops its own spring-boot-starter-web and spring-boot-starter-logging declarations as a result, so it now depends on nothing but the starter and demonstrates that one dependency is genuinely enough. Also drops the Google Fonts stylesheet from the default page. It was fetched on every render of a page shipped into every generated project, which breaks in offline and air-gapped environments and makes the page hotlink a third-party service by default, for no benefit given the CSS already declared fallback fonts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
The packaging bug fixed in the previous commit -- the starter not bringing a servlet container, so a project depending only on it booted as a non-web application -- was invisible to the whole test suite, because both modules used to declare the web stack themselves. StarterSelfSufficiencyTest closes that gap. This module declares nothing but the starter at compile scope, so the container it boots can only have come transitively from the starter. A bannedDependencies enforcer rule keeps that precondition honest: declaring a Spring Boot starter here would satisfy the test without the starter supplying anything, so the rule fails the build and points at the starter instead. Only direct declarations are checked, leaving the starter's own transitives alone. Both guards were verified by deliberately breaking them. Note that when the starter stops supplying the web stack the build fails during test compilation, in the test that legitimately needs jakarta.servlet.Filter, rather than with this test's assertion message. The regression is caught either way, and the enforcer blocks the tempting local fix. Also documents a footgun this surfaced: Wicket keys its application registry by filter name and Spring caches test contexts, so tests that each start a container must set distinct wicket.filter-name values. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
The README explained what to type but never what the starter does. It did not mention the beans the auto-configuration contributes, that each steps aside when you define your own, the conditions under which it applies at all, or that a project gets a placeholder home page until it registers a WebApplication -- which is the first thing a generated project shows. Adds How It Works (activation conditions and the three beans), The Default Page, Overriding the Defaults, and a migration note for applications that already register a SpringComponentInjector by hand and would otherwise end up with two. Every claim was checked against WicketAutoConfiguration rather than against the previous wording. Also answers a question the previous commit created. Depending on spring-boot-starter-web means every application now has Spring MVC's DispatcherServlet alongside Wicket's filter at /*, so whether @RestController endpoints still work is the obvious thing to ask. WicketAndSpringMvcCoexistenceTest establishes that they do -- Wicket forwards what it does not handle down the filter chain -- so the README documents verified behaviour with a regression test behind it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
The artifact followed Spring's documented convention for third-party
starters ({name}-spring-boot-starter), but so does the established
com.giffing.wicket.spring.boot.starter:wicket-spring-boot-starter, so
both landed on an identical artifactId differing only by groupId. That
is legal and causes tooling no trouble, but it is confusing when
searching Maven Central, and claiming an existing project's name reads
as territorial when the two are better described as complementary.
Using "wicketstuff" as the name segment stays within the convention,
matches the sibling wicketstuff-* modules in this repo, and says where
the artifact comes from. The pom display names move with it, since
leaving them identical to the other project would have defeated the
point. The Java package was already org.wicketstuff.springboot.starter
and is unchanged.
Also adds a Motivation section to the README covering why a deliberately
small starter is worth having: it supplies the three pieces every Wicket
application on Spring Boot needs and stops there, which keeps it cheap to
re-verify across major versions, and it releases with wicketstuff-core so
the version matching your Wicket version is the one to use. It names the
broader alternative and describes what each is good for, so readers can
choose deliberately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
The module is called "examples" but its main source is a bare @SpringBootApplication, so a reader browsing it learns nothing and the emptiness looks like an oversight rather than the point. Meanwhile the patterns the README documents -- a custom WebApplication, @SpringBean injection, overriding the filter registration, REST controllers beside Wicket pages -- are all demonstrated in the integration tests, where nobody thinks to look. Rather than move those demonstrations into src/main, where they would be unverified sample code free to drift, this documents where they already are: a module README mapping each pattern to the test that shows it, a pom description saying what the module is for, javadoc on ExampleApplication explaining that having nothing in it is the demonstration, and comments in application.properties noting that none of those settings are required. Also records the compatibility result behind the Initializr metadata's versionRange of 4.0.0: an application built on spring-boot-starter-parent 4.0.3 resolves Spring Boot 4.0.3 rather than being pulled to the 4.1.0 this starter is built against, and boots and serves correctly on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
spring-boot-starter-web is deprecated as of Spring Boot 4; its own pom description says "deprecated in favor of spring-boot-starter-webmvc". The replacement carries the same four dependencies plus spring-boot-starter itself, so logging arrives explicitly rather than incidentally, and it is what Vaadin's Initializr-listed starter depends on. Also documents WAR deployment. The starter ships an embedded Tomcat but does not commit anyone to it: declaring spring-boot-starter-tomcat at provided scope demotes the whole embedded chain even though it arrives transitively through the starter. Verified by packaging a war against the installed artifact -- no tomcat jars in WEB-INF/lib, while wicket-core, the starter and spring-boot-webmvc are packaged as normal. That also settles whether to adopt the conventional split between an autoconfigure module and a dependency-only starter. Supporting an external servlet container was the concrete reason to want it, and it turns out not to need one, so the single combined module stays -- which Spring's documentation explicitly allows, and which can still be split later without changing any consumer's coordinates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
4.1.1 is the current stable release; the starter was pinned to 4.1.0. This is the one dependency version the module pins itself rather than inheriting from the root pom, so it does not move with the reactor and needs bumping deliberately. Tomcat moves 11.0.22 to 11.0.24 with it. Found while sweeping for deprecated dependencies before a first release. That sweep came up clean otherwise: all 83 resolved artifacts checked for deprecation notices (the four hits were false positives, matching only their own showDeprecation compiler settings), and a clean recompile of all sources with deprecation warnings enabled produced none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012N3ZXf1gtmcbQ2jWcqyDTF
Replace the placeholder "WicketStuff" author and add @author tags across the module, matching the convention used elsewhere in the repo of crediting the actual contributor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ConditionalOnMissingBean only matched a FilterRegistrationBean<WicketFilter>, so an application declaring a plain WicketFilter bean got Wicket mapped twice. @ConditionalOnMissingFilterBean covers both forms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…izr metadata Add an invoker integration test: a project shaped like one generated by Spring Initializr, inheriting from spring-boot-starter-parent, so it runs against exactly the library versions Spring Boot manages rather than the ones wicketstuff-core pins. -Dspring-boot.version=... checks other Boot lines; it passes on 4.0.8 and 4.1.1. The Initializr metadata in the README used the old versionRange key, an open-ended range and no starter version, which would generate a build that does not resolve. Replace it with a bounded compatibilityRange and a version mapping, and state the verified Boot lines in the compatibility matrix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Versions pinned by wicketstuff-core win over the imported Spring Boot BOM, so the starter was built and tested against e.g. logback 1.6 and jackson-databind 3.2 next to jackson-core 3.1. Override those properties in the starter parent with Spring Boot's values, drop the JUnit/logback-core workaround this made necessary, and add BuildAlignedWithSpringBootTest to fail the build as soon as the overrides drift from the BOM. Also drop the redundant provided jakarta.servlet-api dependency; the embedded Tomcat already supplies the servlet API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spring Boot initialises the embedded container's filters, and with them the Wicket application, while the context is still refreshing. The injector was an ordinary singleton created afterwards, so Injector.get() returned null in WebApplication#init() and nothing could be injected there. Register the injector from a BeanPostProcessor as soon as a WebApplication bean is created instead. It still backs off for a user-defined SpringComponentInjector bean and still works with custom filter registrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
spring-boot-autoconfigure-processor writes META-INF/spring-autoconfigure-metadata.properties, which lets Spring Boot evaluate the class conditions without loading the auto-configuration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s at root - Add the Apache 2.0 license header to all sources, markup and CSS. - Indent WicketAutoConfiguration and DefaultHomePage with tabs like the rest. - Link the default page's stylesheet and logo with <wicket:link> as package resources instead of mounting them at the application root, where they could shadow an application's own static files. - Show the release version, not a snapshot, in the README's Maven snippet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Defaults to Ordered.LOWEST_PRECEDENCE, the order the registration already had, so Wicket keeps running after every other filter, including Spring Security's. Changing it no longer requires replacing the whole filter registration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With DevTools, Wicket's jars and a stale copy of the application classes stay in the base classloader while the application runs in the restart classloader. Restoring a stored page (the back button, or any page after a restart) resolved the page class from the base classloader but rebuilt its @SpringBean proxies for the restart classloader, failing with a ClassCastException. Ship META-INF/spring-devtools.properties to load all Wicket and wicketstuff jars in the restart classloader, guard its patterns with a test against the Wicket jars on the classpath, and document DevTools usage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The examples module is excluded from publishing, so the BOM must not reference it either; its exclusion list is kept in sync with the publishing one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Quickstart: define the injected service and show the page's markup and where it goes, so the example actually runs. - List Spring MVC coexistence, DevTools support and the filter order among the features, mention the BOM, and describe accurately what the integration test verifies. - Fix the examples' run command, which failed to resolve the spring-boot plugin prefix, and list SpringBeanInjectionDuringInitTest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
State the goal in the motivation and outline the steps: merge, release to Maven Central, then request the listing with the prepared Initializr entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hey, thanks a lot for all the effort. I have just a question regarding the module itself. There is already a Spring Boot integration: https://github.com/MarcGiffing/wicket-spring-boot so, what is the advantage of this one over the one I linked? |
| @Bean | ||
| @ConditionalOnMissingBean(WebApplication.class) | ||
| public WebApplication webApplication() { | ||
| return new DefaultWebApplication(); |
There was a problem hiding this comment.
From what I can see https://github.com/MarcGiffing/wicket-spring-boot is not creating such a useless WebApplication stub ...
Maybe it worth to create PR against https://github.com/MarcGiffing/wicket-spring-boot with spring-boot version update to 4.1.1 ?
There was a problem hiding this comment.
I explained the motivation in the readme file of the module:
Running Wicket on Spring Boot always needs the same handful of pieces: a WebApplication, a filter registration for it, and a SpringComponentInjector so that @SpringBean works. None of it is difficult, but it is the same every time, it is easy to get subtly wrong, and it is the first thing standing between someone and a running Wicket page.
This starter supplies exactly those pieces and then gets out of the way. Everything past that point is ordinary Wicket and ordinary Spring, documented in their own projects.
Staying small is the point, not a limitation. A deliberately narrow starter is cheap to keep alive: when Wicket or Spring Boot publishes a new major version, there is very little surface to re-verify. It also lives inside wicketstuff-core and is released with it, so the version you need is simply the one matching your Wicket version, and there is no second release cycle to wait for.
The goal is also to make Apache Wicket selectable on start.spring.io, the way Vaadin already is, so that a new Wicket project is one click away. See Road to start.spring.io for the plan.
Is this the right starter for you?
There is an established alternative, MarcGiffing/wicket-spring-boot (com.giffing.wicket.spring.boot.starter), which is a much broader integration: Spring Security, native WebSockets, bean validation, CSRF protection, several serializers, session datastores, monitoring and a set of development-mode helpers.
Pick that one if you want those batteries included. Pick this one if you would rather start from the smallest thing that works and add what you need yourself. They solve the same problem with opposite philosophies, and neither is a replacement for the other.
There was a problem hiding this comment.
makes sense :)
@martin-g could you please take a look at this PR? :)
There was a problem hiding this comment.
I am not using Java for 7 years now, so I am not the right person to ask to review :-/
I was going to suggest to add the explanation above in the README, so that the users are aware why there are two similar projects but it is already there:
https://github.com/wicketstuff/core/pull/1568/changes#diff-bd8b825bb0a64ace15552bf450f0f28e38f97386eb73946cf1eafb140b77788eR26-R37
Side note:
The PR description says "Generated with Claude Code". IMO this is more than a disclaimer! The modern way to develop these days is to ask the LLM to do it for you. So, I am not sure there is much value is such starters anymore. Just write the prompt with the your requirements and then tune it (with more prompts :-) ).
Summary
Adds
wicketstuff-spring-boot-starter, a small Spring Boot starter that runs Apache Wicket on Spring Boot 4 with a single dependency. Adding it and starting the application gives a running Wicket app: the starter registers theWicketFilter, wires@SpringBeaninjection, and exposes the Wicket settings as Spring properties.The goal after this merge is to get Wicket listed on start.spring.io, the way Vaadin already is. That needs a released version on Maven Central, so the plan is: merge, ship with the next wicketstuff release, then request the listing from the Spring Initializr team. The Initializr entry is already written up in the module README.
wicketstuff-spring-boot-starter-parent/README.mdgh-pagesbranch will follow, so the page can be served at wicketstuff.org/core/spring-boot-starter/)What the starter provides
One auto-configuration,
WicketAutoConfiguration. It is active for servlet web applications with Wicket on the classpath, and can be switched off withwicket.enabled=false.WebApplicationbean, or a built-in default page ("It works!") if there is none.@SpringBeanworks in pages and components, and already inside the application'sinit().WicketFilteris registered with the embedded container. It backs off when the application registers its ownWicketFilterorFilterRegistrationBean<WicketFilter>.wicket.configuration,wicket.filter-path,wicket.filter-nameandwicket.filter-order, with IDE autocompletion.@RestControllerendpoints keep working next to Wicket pages.META-INF/spring-devtools.propertiesthat moves the Wicket and wicketstuff jars into DevTools' restart classloader. Without it, restoring a stored page (e.g. on the back button) fails with aClassCastExceptionon@SpringBeanfields.Changes outside the new module
pom.xml: adds the module, and addswicketstuff-spring-boot-starter-examplesto thecentral-publishing-maven-pluginexclusions.wicketstuff-bom/pom.xml: adds the same examples module to the BOM exclusions, keeping the two lists in sync. The BOM will containwicketstuff-spring-boot-starteritself.Notes for reviewers
BuildAlignedWithSpringBootTestfails as soon as an override drifts from the BOM. Dependabot PRs that bump these overrides will fail on purpose; they should be closed rather than merged, and the overrides updated together withspring-boot.version. This only affects the two starter modules.wicketstuff-spring-boot-starter/src/itholds a small project built onspring-boot-starter-parent, run bymaven-invoker-pluginduringverify. It checks the starter against exactly what a generated Spring Boot project gets. It downloads Spring Boot's parent pom on first use.-DskipTestsskips it, and-Dspring-boot.version=…runs it against another Spring Boot line.Testing
mvn clean verifyof the starter modules on Java 17 via the root toolchain: 18 unit tests, the integration test and 11 example tests, all passing.-Dspring-boot.version=4.0.8.🤖 Generated with Claude Code