Startup is where a WebObjects application has traditionally been hardest to reason about: properties loaded twice, frameworks initializing on notifications at points nobody could quite name, and logging that only started working partway through. wonder-slim makes it a fixed sequence, in which the configuration is complete before anything else runs, and every framework and the application itself come in at named points.
This guide goes through that sequence, what's in place at each step, and how a framework takes part in it. It describes wonder-slim as of 8.0.12.
1. The sequence
- Before
main(): the JVM initializes the application class and its superclasses, so their static initializers run first. No configuration exists yet (see section 8). ERXApplication.main()starts console logging in a default layout, then finds the plugins (section 5) and orders them.- The configuration is composed from all its sources (section 2) and written to the system properties. Every property now has the value the application will run with.
- Logging is configured from it. From here on, everything logged reaches the configured log (see the logging guide).
- Each plugin's
beforeApplicationConstruction()runs. - The application is constructed. WebObjects' constructor, then
ERXApplication's, then yourApplication's. All of them see the composed configuration. At the end ofERXApplication's constructor, the configuration's files start being watched and the startup report is printed. - Each plugin's
finishInitialization( application )runs, then your application'sfinishInitialization(). The application is complete, and nothing is listening for requests yet. - The adaptors start listening. Requests may arrive from this moment.
- Each plugin's
didFinishLaunching( application )runs, then your application'sdidFinishLaunching(), possibly while the first requests are already being handled.
2. Where the configuration comes from
The configuration is composed from an ordered list of sources, each overriding the ones before it. From lowest precedence to highest:
| Source | Read |
|---|---|
| The JVM's own properties | Beneath everything: the system properties as they were at launch. |
Each framework's Properties | From a jar or, in development, the framework's project. A framework in a project directory adds its Properties.dev (in development) and Properties.<user>. The order between frameworks is in section 7. |
The application's Properties | Always. |
~/WebObjects.properties | In the home directory of the user the application runs as. |
| Optional configuration files | Each file er.extensions.ERXProperties.OptionalConfigurationFiles lists: an absolute path, or a resource in the application. |
| Optional variants | The application's Properties.log4j, .database and the like, only with er.extensions.ERXProperties.loadOptionalProperties=true. Off by default; it costs a couple of lookups per framework. |
The machine's Properties | /etc/WebObjects/Properties, or if there's none, /etc/WebObjects/<App>/Properties. The directory is er.extensions.ERXProperties.machinePropertiesPath. |
The application's Properties.dev | In development only. |
The application's Properties.<user> | For the user the application runs as. |
| JVM options | The -Dkey=value options on the java command line. |
| Arguments | The application's arguments: -Key value after the main class, as JavaMonitor passes them. |
| Set on this instance | Properties set on the running application (section 4). |
Properties that decide which files are read, such as OptionalConfigurationFiles or machinePropertiesPath, can be set in any of these sources: after composing, the files are looked for again, and the configuration composed again if they changed.
Every value keeps the source it came from. The startup report prints each property with it:
============== PROPERTIES FILES ================
(applied in this order - a later source overrides an earlier one)
ERExtensions.framework : jar:file:///…/ERExtensions-8.0.12.jar!/Resources/Properties
MyApp.app : /…/MyApp.woa/Contents/Resources/Properties
{$user.home}/WebObjects.properties : /home/webobjects/WebObjects.properties (no file present)
Application-Machine Properties : /etc/WebObjects/Properties
Arguments : (22)
================= PROPERTIES ===================
WOPort Arguments = 2001
er.logging.level.root ERExtensions.framework = INFO
file.encoding = UTF-8
Files that were looked for and aren't there are listed too, as "no file present", since a missing /etc/WebObjects/Properties is as much a fact about the machine as a present one. The admin console's Configuration page (/wonder/admin/configuration) shows the same, live: the plugins, the sources with how many of each one's values are in effect, and each property with its source and the sources it overrides. Passwords, tokens and keys are masked in both, including where they appear inside another value, such as the java command line.
3. Reading properties
The configuration lives in the system properties, so any way of reading those works. The typed readers convert as well:
final int timeout = ERXProperties.intForKeyWithDefault( "com.example.timeout", 30 );
final boolean enabled = ERXProperties.booleanForKey( "com.example.feature.enabled" );
final NSArray<String> hosts = ERXProperties.arrayForKey( "com.example.hosts" ); // (a, b, c)
NSProperties offers the same methods (NSProperties.intForKeyWithDefault and so on), and returns the same values; ERXProperties hands them to it. Values are read as they are at the time, never from a cache.
Read a property where it's used, rather than once into a static field, and a changed value (section 4) takes effect by itself. When something has to react to a change, listen for it:
ERXConfigurationManager.onChange( "com.example.cacheSize", change -> {
cache.resize( Integer.parseInt( change.newValue() ) );
} );
A listener hears every change in the value in effect, with propertyName(), oldValue() and newValue() (null where the property had no value, or has none any more). A predicate listens to a family of keys: onChange( key -> key.startsWith( "com.example." ), … ). onChange() returns the listener, to remove() it when its owner goes away.
4. When the configuration changes
The configuration is composed again when one of its files changes, and a listener hears only the properties whose value actually changed:
- In development, the files the configuration was read from are watched, and so are those it looked for where no file is present, so creating one is noticed too.
- In deployment, files aren't watched, since much of the configuration is only read at startup and a reload would seem to change what it can't. A touch file can be: set
er.extensions.ERXConfigurationManager.PropertiesTouchFile, and touching it reloads. A path with/{AppName}/in it names one file per application, plus one for every application on the machine. - On a running instance, a property can be set from the admin console's Configuration page, or in code:
ERXConfigurationManager.setProperty( "com.example.feature.enabled", "true" );
ERXConfigurationManager.unsetProperty( "com.example.feature.enabled" );
A property set this way is a source of its own, above all the others, the arguments included. It survives a reload, shows where it came from, and when it's unset, the property gets back the value the other sources give it. Nothing is written to disk; it lasts until the instance stops.
5. Plugins
A framework, or part of an application, takes part in startup as a plugin: a class implementing ERXPlugin, listed in the jar's META-INF/services/er.extensions.ERXPlugin. Java's ServiceLoader finds it, so a framework on the classpath is a framework that initializes.
package com.example.billing;
public class Billing implements ERXPlugin {
@Override
public List<Class<? extends ERXPlugin>> requires() {
return List.of( ERXExtensions.class );
}
@Override
public void finishInitialization( final ERXApplication application ) {
RouteTable.defaultRouteTable().map( "/billing", BillingPage.class );
}
}
# src/main/resources/META-INF/services/er.extensions.ERXPlugin
com.example.billing.Billing
requires() names the plugins this one builds on. Plugins run in that order, a plugin after the plugins it requires, and plugins that don't depend on each other in the order of their class names, never in classpath order. A required plugin that isn't there, or plugins requiring each other in a circle, stop the launch with a message naming them. A plugin's constructor does nothing: when it runs, nothing is configured yet.
There are three points to come in at, each with something guaranteed:
| Method | When, and what's in place |
|---|---|
| beforeApplicationConstruction() | In main(), before the application object exists. The configuration is composed and logging configured. WOApplication.application() is null. |
| finishInitialization( app ) | The application is constructed, your own constructor included, and its adaptors aren't listening yet, so no request has arrived or can. Request handlers registered in constructors are in place. An adaptor added here, with application.adaptorWithName( … ), starts with the others. |
| didFinishLaunching( app ) | The adaptors are listening. Requests may already be arriving, and being handled while this runs: start background work here, or tell something outside that the application is up. |
6. The application's own part
The application takes part at the same points, under the same names, and always after every plugin. Override them in your Application class:
@Override
public void finishInitialization() {
// Every framework has initialized; no request has arrived yet
}
@Override
public void didFinishLaunching() {
// Accepting requests
}
The constructor is the other place for setup, and now a reliable one: it runs with the whole configuration in place and logging configured.
7. Frameworks and their properties
Every framework's Properties is a source, beneath the application's. Where two frameworks set the same key, a framework with a plugin comes after the frameworks its plugin requires, so a framework overrides the frameworks it builds on. Frameworks without a plugin come first, in reverse classpath order, beneath all of those.
In practice this rarely matters, since the application's own Properties comes after every framework and can settle any conflict. The Configuration page shows each value with the framework it came from, and what it overrides.
Moving from ERXFrameworkPrincipal. wonder-slim's frameworks used to initialize through Project Wonder's ERXFrameworkPrincipal; it's gone, and a framework that still subclasses it fails at launch. Converting takes a few minutes:
- The class implements
ERXPlugininstead, and its name goes inMETA-INF/services/er.extensions.ERXPlugin. - The static initializer calling
setUpFrameworkPrincipalClass()goes, and so does the framework's principal class inbuild.properties(principalClass=). REQUIRESbecomesrequires().initialize()becomesbeforeApplicationConstruction(), which now has the configuration and logging in place.finishInitialization()becomesfinishInitialization( application ). It runs a little later than before: after your application's constructor rather than at the end ofERXApplication's, which is what code registering routes or handlers wants anyway.
WebObjects' own principal classes (NSPrincipalClass in a framework's Info.plist) still work, for frameworks that aren't built on wonder-slim: WebObjects initializes them while it loads its bundles, which happens while the configuration is being composed, before it's complete.
8. Things to know
- Static initializers run before any configuration. The JVM initializes the application class and its superclasses before it calls
main(), so a static field initialized from a property sees only the JVM's-Doptions. Read properties in methods, or in the constructor. WebObjects' ownEOEventCenteris caught by this:EOEventLoggingEnabled,EOEventLoggingOverflowDisplay,EOEventLoggingLimitandEOEventLoggingPasswordonly take effect as-Doptions. - The command line wins. A JVM
-Doption overrides every file, and an application argument overrides a-Doption. Only a property set on the running instance goes above them. - A property set with
System.setProperty()isn't known to the configuration: nothing records where it came from, listeners don't hear it, and a reload keeps it unless a source sets the same key. UseERXConfigurationManager.setProperty(). - A setting that did nothing before may now. Project Wonder applied
Properties.dev,Properties.<user>, optional files and/etc/WebObjectsonly after the application was constructed, so settings the constructor reads (the port, caching and others) were silently ignored there. Now they apply.
The classes are ERXConfigurationManager for the configuration, ERXPlugin and ERXPlugins for plugins, and ERXProperties for reading, all in ERExtensions. The wonder-slim repository has them, and its docs/CONFIGURATION.md goes into the details.