Good ol' split install

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.

Why serve static resources through the application?

  1. It simplifies the build and deployment process
    You don't need the additional step of "installing" your webserver resources to the webserver. Deplyoment becomes just copying the application and restarting it.
  2. It simplifies the deployment environment
    With the webserver no longer serving any content directly, it becomes nothing more than a pure proxy for your app. This makes the deployment environment easier to understand and maintain.
  3. It ensures consistency between dev and production environments
    Resources are served the same way, using the same method and same code in all environments.
  4. You get application-level control over the serving of resources
    If you need to modify caching, control access, operate on a resource before serving it etc. — everything is controlled from the application. You'll never have to mess with webserver configuration.
  5. It simplifies URLs and URL generation
    It sometimes feels like half of Wonder is hacks and tricks for locating static resources and generating the appropriate resource URLs. Serving resources through the application means URLs are simple, readable and uniform. And they're standard WO application URLs. No separate completely different looking "resource URLs".

Performance

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.

How?

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

Work to do in slim's implementation

Even if already used in production, wonder-slim's implementation still needs some work:

  1. Webserver resources are cached in-memory forever in production
    This is fine for my own projects since static resources are usually at most 10-20MB of data. Not a lot of memory to sacrifice for the performance gained. But server-side caching should be configurable.
  2. Cache headers (for client side caching) are hardcoded
    Only applies in production, no client-side caching in development mode. Client-side caching should ideally be configurable all the way down to individual resource level.
  3. No support for localized resources
    Currently planning on adding a languages parameter to resource URLs to handle those.
  4. Needs a way to force clients to reload cached resources
    The old ERXResourceManager had a clever trick, allowing the adding of a 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.
  5. I'm sure there are other edge cases to address. But after some time using this method in a dozen applications, I haven't hit them. Pointers appreciated if you can think of any.

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.

Installing

  1. Download the JetBrains runtime. The release I use is osx-aarch64/JBRSDK
  2. Expand the downloaded file
  3. Download and add hotswap-agent
  4. Do the xattr dance
  5. Move the JDK to /Library/JavaVirtualMachines (or wherever you keep your JVMs)
  6. Add and configure the JDK in Eclipse

In shell language, assuming you're running on macOS X

% 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.

My Default VM arguments in Eclipse

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.

pom.xml

<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 -->

Application.java

public Application() {
	parsley.Parsley.register();
	parsley.Parsley.showInlineRenderingErrors( isDevelopmentModeSafe() ); // For enabling inline error reporting in dev mode
}

What's Parsley?

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.

A fix for WO's KVC

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.

A more acceptable/correct fix

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).

JDK 25 and WO

September 21, 2025

Now building/deploying on JDK 25

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.

Caveat when moving from JDKs older than 24

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.

Why upgrade?

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.

Still doing development on JDK 21 though

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.

Development activity feed

September 19, 2025

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.

WebObjects 5.4.3 released!

September 15, 2008

Exciting times, we have a new WO version! Issues fixed include:

  • EOF Database snapshot not updating
  • Webassistant not available for D2W apps
  • Exceptions when using WOTextField with formatters
  • Duplicate primary keys generated by FrontBase JDBC Adaptor under load
  • Additional issue fixes

What's happenin'

📚 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