WO has an interesting way of handling static resources. Traditionally, resources like images, CSS-files etc. are put into a project's WebServerResources folder, and on application deployment that folder gets copied to the webserver and served from there, bypassing the application entirely.
This "split install" made sense a while back since it freed the application from serving static resources, saving CPU cycles and bandwidth, more valuable resources at the time. Today however, serving static stuff is a comparatively lightweight task making split installs something of an unnecessary hassle.
So, for a while I've been skipping the split install and serving my webserver resources from my applications rather than the web server.
Serving static resources from WO is fast. Fast enough that for my purposes, I don't even care exactly how fast, but for reassurance I did some quick and dirty benchmarking. The test is done on a ~230Kb resource on my own private website, hosted on a Hetzner CAX31 with -Xmx512M (not chosen specifically, just happens to be the heap size I've got set, should be fine with just about any value). Did the benchmarking locally using ab, speaking directly to a single instance of the application in production mode (meaning caching is enabled).
# Single request thread, 10.000 requests
# Completes in 6 seconds, ~0.6ms per request
# Transfer rate 375MB/sec
ab -n 10000 http://localhost:2001/Apps/WebObjects/Hugi.woa/res/app/ZillaSlab-Light.ttf
# 32 request threads, 10.000 requests
# Completes in 1.3 seconds, avg. ~4ms per request/~0.13ms across all concurrent requests
# Transfer rate 1703MB/sec
ab -c 32 -n 10000 http://localhost:2001/Apps/WebObjects/Hugi.woa/res/app/ZillaSlab-Light.ttf
This purely measures WO's request handling performance (no SSL, no network latency no mod_WebObjects etc). And yes, I'm fetching the same resource repeatedly so probably getting some performance benefits from that. But this is fast and would still be fast at even half the speed. And note I'm not using ab with -k (HTTP keep-alive) meaning each request initiates a new connection. Using keep-alive roughly doubles the performance in both cases.
I've been doing something like this through a private framework for a while, but I recently started cleaning it up, making it more generic and added it to wonder-slim. The implementation can be found in ERXAppBasedResourceManager and ERXAppBasedResourceRequestHandler. Same code/method should work in any WO/Wonder app, it's simple and just means static resources are always served from your app using URLs that follow a simple format:
## URL format
.../App.woa/res/[frameworkName]/[resourceName]
## Example
.../App.woa/res/app/main.css
Even if already used in production, wonder-slim's implementation still needs some work:
languages parameter to resource URLs to handle those.version query parameter to resource URLs, forcing clients to update potentially cached resources on the client side. This is useful so I'd like to add something like that.I've deleted most of the code in wonder-slim for supporting traditional split installs, most or all of it for generating URLs during development. It's a lot of logic to maintain and slim is all about reducing complexity and development effort by having a "single, proper, well understood and maintained way" to do stuff. And I think serving resources through the application is most definitely the single proper way.
Static resources are just as much a part of the application as anything dynamic.
JetBrains released a JDK 25 version of it's runtime today. It's marked "pre-release" but that doesn't scare me much since I only use it locally during development.
Quick testing showed it seemingly works fine with WO on Eclipse 2025-09 with the current hotswap-agent v2.0.1, so I changed all my own apps and libraries from targeting JDK 21 to JDK 25. Since my build and deployment environments are already running JDK 25, each project's "migration process" consisted of updating the JDK version in the pom.xml and doing a commit/push/build/deploy. Easy upgrade, everything works like a charm and I'm a happy man. Now giving it a few days in testing in my own projects before updating client projects.
I haven't migrated any of my public libraries or frameworks yet but probably will in the next few days. I'll be keeping Parsley and Vermilingua on JDK 21 for a while though, since they're used by others and I'm aware not everyone is in quite the same hurry to upgrade as I am.
For your WO application to work with JDK 25, you must use the workaround for the JDK's removal of GetPropertyAction or use wonder-slim which includes it. See previous article on JDK 25 for more info.
hotswap-agentxattr dance/Library/JavaVirtualMachines (or wherever you keep your JVMs)% curl -LO https://cache-redirector.jetbrains.com/intellij-jbr/jbrsdk-25-osx-aarch64-b176.4.tar.gz
% curl -LO https://github.com/HotswapProjects/HotswapAgent/releases/download/RELEASE-2.0.1/hotswap-agent-2.0.1.jar
% tar -xzf jbrsdk-25-osx-aarch64-b176.4.tar.gz
% mkdir jbrsdk-25-osx-aarch64-b176.4/Contents/Home/lib/hotswap
% mv hotswap-agent-2.0.1.jar jbrsdk-25-osx-aarch64-b176.4/Contents/Home/lib/hotswap/hotswap-agent.jar
% sudo xattr -r -d com.apple.quarantine jbrsdk-25-osx-aarch64-b176.4
% sudo mv jbrsdk-25-osx-aarch64-b176.4 /Library/Java/JavaVirtualMachines
Then add the JDK in Eclipse, which I assume you know how to do. And don't forget to configure the JDK with default arguments for WO and DCEVM/hotswap-agent.
Note that the set of packages you need to open may differ from these, based on which classes you actually use. But this basic set has served me well.
-XX:+AllowEnhancedClassRedefinition
-XX:HotswapAgent=fatjar
--add-opens java.base/java.time=ALL-UNNAMED
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
🌿 Parsley the template parser has been released to maven.wocommunity.org so it's now easy to try out. Just add it to your pom.xml.
Parsley is not "automatically enabled" when added as a dependency, since it contains utility functionality you might want to use without having it as your default template parser. You must activate it somewhere during application initialization to use it as your default parser, for example in your Application class constructor as shown below.
<dependency>
<groupId>is.rebbi</groupId>
<artifactId>parsley</artifactId>
<version>1.2.0</version>
</dependency>
<!-- make sure to also remove WOOgnl from the pom if present -->
public Application() {
parsley.Parsley.register();
parsley.Parsley.showInlineRenderingErrors( isDevelopmentModeSafe() ); // For enabling inline error reporting in dev mode
}
Parsley is an evolution of Project Wonder's WOOgnl template parser. It's largest currently visible "new feature" is inline display of some common templating errors during development, replacing the beloved meter long stack trace pages.
Parsley's largest changes are behind the scenes though. It parses templates to an AST before converting them to a WO Dynamic Element template. This is what enables the inline error messages and will allow for some more fun functionality as we extend it. Parsley's template parser is also used in ng-objects, meaning templates that work in WO under Parsley are syntactically guaranteed to carry over.
Note that Parsley does not support all of WOOgnl's features. Specifics can be found in the section "Differences from WOOgnl" in the README
Using good ol' Java collections usually works great in WO templates.
However, in the past years the JDK has been increasingly introducing the pattern of factory methods that return private implementions of public interfaces. Examples of commonly used methods that do this are List.of() and Collectors.toList().
This poses a problem for us WO-folks, especially in templating, since when KVC encounters a class like this (a private class implementing a public interface) it will fail to locate the correct method to invoke, causing an exception.
Examples:
var size1 = NSKeyValueCoding.Utility.valueForKey( new ArrayList<>(), "size" );
var size2 = NSKeyValueCoding.Utility.valueForKey( List.of(), "size" );
var filteredList = people()
.stream()
.filter( p -> p.name().startsWith( "H" ) )
.toList();
var size3 = NSKeyValueCoding.Utility.valueForKey( filteredList, "size" );
Obtaining size1 will work fine since ArrayList is a public class - but the same will fail for size2 and size3 with an IllegalAccessException.
I occasionally worked around this by wrapping collections in a public class like ArrayList when returning from a method I expected to use in a WO template - but obviously, that's no solution and quite the bother.
To make life easier, I've added a hack in wonder-slim to fix this: ERXKVCReflectionHack.
This is a small modification to KVC's default way of invoking methods, making an inaccessible method accessible on it's first invocation. If you're not using wonder-slim, you can drop this class into your own project to fix this - or add it to the actual original Project Wonder if you're brave enough. I have a general policy of not changing Wonder myself since I rarely use it, meaning I can't contribute much actual testing (and unfortunately, the best way to know if a hacky fix works is to see it running in production without problems for a while (this one's been in use in all of my projects for a couple of months)).
While the patch works we'll probably have to update it soon since we're using terminally deprecated functionality from sun.misc.Unsafe to replace a private static final field in KVC's default implementation. But that's for later.
ng-objects has a more generic fix in progress that attempts to properly locate an interface method to invoke when encountering an inaccessible method. Using this method in KVC would involve some hacking, but I'll probably look into it once the time arrives (as in; when the functionality we're using in Unsafe gets removed).
In early August I upgraded the build and deployment environments of most of my WO apps from JDK 24 to JDK 25 (release candidate). Happily, I didn't experience anything problematic or unexpected so when JDK 25 was actually released last week, I finished the job and updated the customer deployments I control as well. So; everything up and running on JDK 25, including JavaMonitor and wotaskd from wonder-slim-deployment.
This comes with the traditional warning that my setup differs from that of most WO users. Of the 12 apps upgraded, only one uses EOF and the original Project Wonder, the rest use wonder-slim and Cayenne, so YMMV depending on which WO/Wonder frameworks/features you use.
WO (NSTimeZone) references the class sun.security.action.GetPropertyAction which was removed in JDK 24, meaning a plain WO/Wonder app will fail to start on JDK 24 and later.
I added a fix for this in wonder-slim by putting in a "replacement" class. Works fine and shouldn't interfere with functionality since GetPropertyAction doesn't really do anything anymore, with Java's Security Manager now more or less removed/disabled. If you have an application you'd like to upgrade and aren't using wonder-slim, you can drop that replacement class into your project and it should run fine. That's what I did with my one remaining Project Wonder/EOF project.
There are a number of great reasons to keep your Java installation up to date — that I won't go into. Plenty of folks out there talking about Java in general. But the main reason for my own early adoption was I'd already started using Compact Object Headers as a preview feature in JDK 24. Since that was finalized and released in JDK 25, I felt it was worth the experiment.
Although I haven't done any formal benchmarking suitable for publishing, Compact Object Headers resulted in reduced memory use and well perceivable performance improvements in my apps, so I can recommend trying it out if performance is something you care about. Just add -XX:+UseCompactObjectHeaders to your application's arguments and you're good to go.
Although my apps are now built and run on JDK 25 I still target and do development on JDK 21, mostly because I can't live without DCEVM, now released as a part of the Jetbrains Runtime. Now eagerly waiting for JetBrains to release a JDK 25 runtime so I can upgrade my development environment as well.
We now have a page showing a quick overview of WO/ng related development activity on GitHub, access it by clicking Dev feed in the main navigation bar.
For obvious reasons the list of repos is a little skewed towards my own repos, mostly because these are currently the repos I know of, use and care about. Let me know if you'd like to see some repos added; your own, someone else's or WOCommunity's. If many more repos get added, I'll probably enhance the functionality a little to allow some customization and filtering of the commit list.
Note that the list is a little skewed since it only shows the 20 most recent commits to each repo's main branch (what we get from github's RSS feed). So this isn't all development activity back to 2023, for that I'd have to use a different method.
Suggestions for more useful stuff to add to the list or the site in general very welcome.
Exciting times, we have a new WO version! Issues fixed include:
| 📚 whoacommunity.com | Renamed wo-jetty-adaptor to wo-adaptor-jetty | Nov 24 |
| 🔌 wo-adaptor-jetty | Renamed wo-jetty-adaptor to wo-adaptor-jetty | Nov 24 |
| 🔌 wo-adaptor-jetty | Logger cleanup | Nov 19 |
| 🔌 wo-adaptor-jetty | Allow delegation of Jetty server creation to the Application class | Nov 16 |
| 🔌 wo-adaptor-jetty | Phrasing | Nov 16 |
| 🤸♀️ wonder-slim | Add note on ETag headers | Nov 16 |
| 🔌 wo-adaptor-jetty | Invoke WOApplication.setPort() rather than setting the port property | Nov 16 |
| 🔌 wo-adaptor-jetty | Hardly think we need to both log and throw | Nov 16 |
| 🚀 ng-objects | Don't send a server header from the Jetty adaptor | Nov 16 |
| 🤸♀️ wonder-slim | Add note on WOHost | Nov 15 |