The answer, my friend, is blowin' in the wind, The answer is blowin' in the wind.

—BOB DYLAN, The Freewheelin' Bob Dylan

Joran

Joran stands for a cold north-west wind which, every now and then, blows forcefully on Lake Geneva. Located right in the middle of Western-Europe, the surface of Lake Geneva is smaller than many other European lakes. However, with its average depth of 153 meters, it is unusually deep, and happens to be, by volume, the largest sweet water reserve in Western-Europe.

As apparent in previous chapters, logback relies on Joran, a mature, flexible and powerful configuration framework. Many of the capabilities offered by logback modules are only possible on account of Joran. This chapter focuses on Joran as it exists since logback 1.3: how an XML (or serialized) configuration is turned into a model, and how that model is applied by model handlers in distinct processing phases.

Joran is a generic configuration system that can be used independently of logging. The logback-core module has no notion of loggers; classic-specific models and handlers live in logback-classic.

For developers interested in writing logback components, e.g. appenders, the most relevant material is the section on implicit actions and property handling.

For developers simply interested in writing logback components (appenders, encoders, filters, and similar), the most relevant material is the section on implicit actions and property handling, including basic and complex property conversion. That is what determines how nested elements in a configuration file map onto JavaBeans properties of your components. Developers who want to understand the inner workings of Joran may elect to read this chapter in full.

Historical perspective

Reflection makes it possible to configure software systems declaratively. Logback's settings are typically expressed in XML. In log4j, the DOMConfigurator class hard-coded how each element was interpreted. That style did not scale when the configuration vocabulary grew. The commons-digester project showed that pattern-matching rules could drive parsing: small rule classes associated with path patterns.

Joran was inspired by digester but uses slightly different terminology. In Joran, a rule associates a pattern with an action. When a matching SAX element is seen, the action runs. That design still underpins the first stage of configuration. What changed in logback 1.3 is that actions no longer configure appenders and loggers while the XML is being read. Instead, they build a serializable Model tree. A later stage walks that tree with ModelHandler instances and performs the real work.

SAX, not free-form DOM jumps

Joran is built on the SAX API. Elements are presented in sequential, depth-first order. That fits pattern/action interpretation and preserves line/column location information for error messages. Free-form jumps across a DOM tree while matching patterns turned out to be a poor fit; Joran only needs events in document order.

Non goals

Given its dynamic nature, Joran is not intended to parse very large XML documents with many thousands of elements.

The configuration pipeline since 1.3

since 1.3 Configuring logback-classic with JoranConfigurator (or any GenericXMLConfigurator subclass) proceeds in two major stages:

  1. Build a model. SAX events are recorded, then replayed through Joran rules. Actions (usually subclasses of BaseModelAction) push and pop Model nodes. The result is a tree whose root is typically a ConfigurationModel.

  2. Process the model. A DefaultProcessor walks the tree. For each model type it knows about, it invokes a corresponding ModelHandler. Handlers instantiate components, set properties, attach appenders, and so on. Processing is split into phases so that dependencies can be resolved in a sensible order.

In code, GenericXMLConfigurator.doConfigure roughly does the following:

SaxEventRecorder recorder = populateSaxEventRecorder(inputSource);
Model topModel = buildModelFromSaxEventList(recorder.getSaxEventList());
sanityCheck(topModel);
processModel(topModel);  // DefaultProcessor + model handlers

Because the model is an ordinary object graph, it can be inspected, duplicated, or even serialized and re-applied later (see SerializedModelConfigurator and tools such as logback-tyler). Reconfiguration after a file change reuses the same idea: obtain a model, then process it.

Model

A Model is an abstract representation of a configuration element. It is not the live logger or appender; it is data describing what should be created.

Every model has at least:

Concrete subclasses carry element-specific fields. For example AppenderModel and LoggerModel extend named component models; PropertyModel holds property names and values; conditional configuration uses IfModel, ThenModel and ElseModel. Classic adds ConfigurationModel, RootLoggerModel, LevelModel, and others.

Models form a tree that mirrors the nesting of the XML document. That tree is the single intermediate form between “what was written in the file” and “what runs in the LoggerContext”.

How models are built from XML

During the model-building stage, Joran still matches patterns to actions (see below). For logback configuration, almost every action extends BaseModelAction rather than performing side effects immediately.

BaseModelAction implements the usual begin / body / end callbacks as follows:

Thus actions are still essential, but their role is narrower than in pre-1.3 logback: they translate XML into models. The heavy lifting—creating appenders, starting life-cycle objects, wiring references—belongs to model handlers.

ModelHandler

A ModelHandler (concrete types extend ModelHandlerBase) consumes one kind of model and applies it to the context. Handlers implement:

public abstract void handle(ModelInterpretationContext mic, Model model)
    throws ModelHandlerException;

public void postHandle(ModelInterpretationContext mic, Model model)
    throws ModelHandlerException {
  // optional; default is empty
}

Typical responsibilities of handle:

postHandle runs after child models have been processed. Handlers use it to finish wiring: pop the object from the stack, call start() on life-cycle components, or attach a completed appender to a logger. Dependency analysers also use postHandle when they need a global view after the whole subtree has been visited (for example CallerContradictionWarnAnalyser).

Examples of handlers include AppenderModelHandler, LoggerModelHandler, RootLoggerModelHandler, PropertyModelHandler, ImplicitModelHandler (nested beans and complex properties), and conditional handlers for IfModel/ThenModel/ElseModel.

ModelInterpretationContext

Handlers collaborate through a ModelInterpretationContext (MIC). The MIC holds:

This is the model-era counterpart of the older interpretation context that pure actions used for ad hoc collaboration.

Linking models to handlers

Before processing, the configurator registers associations from model class to handler factory. In logback-classic, ModelClassToModelHandlerLinker (and its core base class) performs that registration on a DefaultProcessor, for example:

defaultProcessor.addHandler(AppenderModel.class, AppenderModelHandler::makeInstance);
defaultProcessor.addHandler(LoggerModel.class, LoggerModelHandler::makeInstance);
defaultProcessor.addHandler(RootLoggerModel.class, RootLoggerModelHandler::makeInstance);
// ... and many more

It also registers analysers for the dependency-analysis phase (see below), such as AppenderRefDependencyAnalyser, AppenderDeclarationAnalyser, FileCollisionAnalyser, and caller-data contradiction analysers.

If a model type has no registered handler, DefaultProcessor reports an error naming the model class, tag, and line number (“Can't handle model of type …”).

Phases of model processing

Applying a model is not a single depth-first walk that creates every component immediately. DefaultProcessor.process runs three ordered steps, corresponding to the ProcessingPhase enumeration:

public enum ProcessingPhase {
    FIRST,
    DEPENDENCY_ANALYSIS,
    SECOND;
}

In outline:

mainTraverse(topModel, phaseOneFilter);   // FIRST
analyseDependencies(topModel);            // DEPENDENCY_ANALYSIS
secondPhaseTraverse(topModel, phaseTwoFilter); // SECOND

FIRST phase

The first phase handles models that should run early and that do not depend on appenders or loggers already existing. By default, a model class without a @PhaseIndicator annotation is treated as FIRST.

Typical first-phase work includes imports, properties, timestamps, status listeners, conversion rules, defines, includes, shutdown hooks, and conditional scaffolding—configuration that sets up the environment for later instantiation.

During this phase DefaultProcessor traverses the tree with a filter that allows first-phase model types and, after registration is sealed, denies everything else. For each allowed, still-unhandled model it creates the handler, calls handle, recurses into children, then calls postHandle.

DEPENDENCY_ANALYSIS phase

Between the two instantiation phases, the processor runs dependency analysis. Analysers are registered with DefaultProcessor.addAnalyser(modelClass, supplier) and are themselves ModelHandlerBase subclasses annotated with @PhaseIndicator(phase = DEPENDENCY_ANALYSIS).

Analysis walks the entire model tree. For each node, registered analysers for that type receive handle; after children are visited, they receive postHandle. They do not normally create appenders. Instead they record facts needed later, for example:

Dependency definitions collected here feed the second phase: an appender that nobody references can be skipped; an appender reference can wait until the named appender has been started.

SECOND phase

The second phase creates and starts the components that form the logging runtime: appenders, loggers, root logger, levels, and appender references. Model classes such as AppenderModel, AppenderRefModel, LoggerModel and RootLoggerModel are annotated with @PhaseIndicator(phase = ProcessingPhase.SECOND).

Second-phase traversal is dependency-aware. A model is handled only when it is still unhandled and all of its named dependencies that were declared as appenders have already been started. That is why the order of elements in the XML file is less rigid than in older logback versions: an <appender-ref> may appear before the corresponding <appender> in the document; the second phase postpones attachment until the appender exists.

After handlers are registered, filter chains are sealed so that first-phase filters deny unknown types and second-phase filters allow remaining unhandled models as a fallback. The second phase may loop a small number of times so that deferred nodes become runnable once their dependencies start.

Why phases matter

Phased processing solves practical ordering problems:

When you write a custom model and handler, choose the phase deliberately: put independent setup in the first phase; use an analyser if you only need to gather information; put components that depend on appenders or similar named objects in the second phase and register the appropriate dependency analysers.

Patterns and rules (model building)

The model-building stage still uses Joran patterns and a rule store. A pattern is a string. Exact patterns such as "configuration/appender" match a specific path. Wildcards match suffixes or prefixes: "*/appender" matches any <appender> element; "appender/*" matches elements nested under <appender>.

Rules associate a pattern with an action. Rules live in a RuleStore (commonly SimpleRuleStore). When several rules match, exact matches override suffix matches, and suffix matches override prefix matches.

As SAX events stream in, Joran maintains the current path and looks up an action. For configuration files that action almost always builds or completes a Model node, as described earlier.

Implicit actions

Highly extensible systems cannot register an explicit rule for every possible nested element. Joran keeps a list of implicit actions, tried when no explicit pattern matches. In logback configuration, ImplicitModelAction is registered as the implicit action: it builds an ImplicitModel for any nested element that is not already covered by an explicit rule.

Later, during model processing, ImplicitModelHandler applies that model to the object currently on the interpretation stack. Using JavaBeans introspection (via PropertySetter / AggregationAssessor), it decides whether the nested element is a basic property, a complex property, or a collection of either, and then converts and attaches the value accordingly.

The decision hinges on the parameter type of the matching setter (setFoo) or adder (addFoo) method on the parent object. That type is classified with StringToObjectConverter.canBeBuiltFromSimpleString(Class):

Basic properties

A property is basic (also called simple) when its value can be obtained by converting element body text (or an attribute-style string) into a Java value without instantiating a nested configuration component. Conversion is performed by StringToObjectConverter.

canBeBuiltFromSimpleString returns true when the parameter class is any of the following:

Among logback’s own types, several follow the valueOf convention and are therefore basic properties, including Level, Duration and FileSize. Application types can use the same convention so that configuration files may set them with plain text.

In XML, a basic property looks like a nested element whose body is the string to convert:

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
  <file>myApp.log</file>              <!-- String -->
  <append>true</append>               <!-- boolean -->
  <prudent>false</prudent>
  ...
</appender>

<root level="DEBUG">                  <!-- Level via valueOf -->
  ...
</root>

At processing time, ImplicitModelHandler substitutes variables in the body text, then either calls PropertySetter.setProperty (single basic property) or addBasicProperty (collection of basic values). Those methods invoke StringToObjectConverter.convertArg and pass the resulting object to the parent’s setter or adder.

Complex properties

A property is complex when its type cannot be built from a simple string according to canBeBuiltFromSimpleString. Typical examples are encoders, layouts, filters, policies, and other user-defined objects nested under an appender or similar parent. Complex properties can themselves contain further nested basic or complex properties.

When ImplicitModelHandler sees a complex property it:

  1. Determines the class to instantiate: the class attribute on the nested element if present (after variable substitution and <import> resolution); otherwise a class deduced by implicit rules (see below).

  2. Instantiates that class (checking that it is compatible with the setter/adder parameter type).

  3. Sets the logback Context if the object is ContextAware, and pushes the object on the interpretation stack so nested elements apply to it.

  4. After children are processed (postHandle), optionally injects the parent via a parent property, starts the object if it implements LifeCycle and is not marked @NoAutoStart, then attaches it to the parent with setProperty or addProperty.

Example—an encoder is a complex property of an appender; its nested pattern is a basic property of the encoder:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  <encoder>   <!-- complex: default PatternLayoutEncoder -->
    <pattern>%d %-5level %logger - %msg%n</pattern>  <!-- basic: String -->
  </encoder>
</appender>

If the class attribute is omitted, the class of the nested component is deduced when any of the following succeeds (in order):

  1. an entry in the default nested-component registry maps the parent class and property name to a default implementation (for example, encoder under AppenderBasePatternLayoutEncoder in logback-classic);

  2. the setter or adder method carries a @DefaultClass annotation naming an implementation;

  3. the parameter type of the setter/adder is a concrete class with a public constructor, which is then used directly.

Default class mapping

In logback-classic, a small set of internal rules map parent class / property name pairs to a default nested class. Common entries include:

Parent class property name default nested class
ch.qos.logback.core.AppenderBase
ch.qos.logback.core.UnsynchronizedAppenderBase
encoder ch.qos.logback.classic.encoder.PatternLayoutEncoder
AppenderBase / UnsynchronizedAppenderBase layout ch.qos.logback.classic.PatternLayout

This list may change between releases. See the addDefaultNestedComponentRegistryRules method of logback-classic’s JoranConfigurator (and the corresponding method in logback-access) for the rules in effect in your version.

Collections of properties

In addition to a single basic or complex property, implicit handling supports collections. If the parent defines an adder method (addFoo(…)) rather than only a setter, the aggregation type becomes AS_BASIC_PROPERTY_COLLECTION or AS_COMPLEX_PROPERTY_COLLECTION. Each nested element with that tag name contributes one element to the collection (for example multiple <filter> elements under an appender, if the appender exposes addFilter).

Note on action-only examples

Older editions of this chapter spent many pages on collaborative actions that pushed numbers on a stack (the “calculator” examples). Those samples remain under logback-examples/src/main/java/chapters/onJoran/ and are still a valid way to learn raw Joran rule dispatch. They are not how logback-classic configuration works today.

For logging, prefer this mental model:

  1. XML (or a serialized model) → Model tree

  2. Model tree → ModelHandlers in phase order → live LoggerContext

When extending logback, you typically add a Model subclass, a BaseModelAction (or reuse an existing generic action) to parse XML into that model, a ModelHandler to apply it, and a registration line in the appropriate ModelClassToModelHandlerLinker. Annotate the model with @PhaseIndicator if it must run in the second phase.

New rules on the fly

Joran can learn additional pattern/action rules while reading a document, via NewRuleAction and a <newRule> element. That facility is useful for specialized dialects and teaching examples. Logback's stock configuration does not require application authors to declare rules in XML; the fixed rule set and model handlers already cover the supported vocabulary.

Example shape:

<newRule pattern="*/computation/literal"
          actionClass="chapters.onJoran.calculator.LiteralAction"/>

See the logback-examples/src/main/java/chapters/onJoran/newRule/ directory for a complete illustration.