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:
-
Build a model. SAX events are recorded, then replayed through Joran rules. Actions (usually subclasses of
BaseModelAction) push and popModelnodes. The result is a tree whose root is typically aConfigurationModel. -
Process the model. A
DefaultProcessorwalks the tree. For each model type it knows about, it invokes a correspondingModelHandler. 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:
a tag (the XML element name),
optional body text,
a line number (for diagnostics),
a list of child models (
subModels),flags such as handled and skipped used during processing.
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:
-
On
begin, it callsbuildCurrentModel(...)(implemented by each subclass) to create a typedModel, records the tag and line number, and pushes the model onto a stack held by the SAX interpretation context. -
On
body, it appends character data to the current model. -
On
end, it pops the model and attaches it as a sub-model of its parent.
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:
read fields from the typed model,
instantiate the corresponding runtime object (for example an
Appender),set the context and name,
push the object on the interpretation stack so nested handlers (properties, encoders, nested components) can attach to it.
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:
an object stack (components under construction),
a model stack (during related operations),
object and property maps,
import aliases (
<import>),variable substitution helpers,
dependency definitions used in the second phase,
the default nested-component registry (which Java type to instantiate for a nested element when the class is not spelled out).
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:
-
which appender names are declared and which are referenced (
AppenderDeclarationAnalyser,AppenderRefDependencyAnalyser), -
whether two appenders would collide on the same file (
FileCollisionAnalyser), -
whether caller-data inclusion instructions contradict each other (
CallerContradictionAnalyser/CallerContradictionWarnAnalyser).
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:
-
Properties and imports first. Variable substitution and short class names must be available before components are instantiated.
-
Analyse before create. Skipping unreferenced appenders, detecting file collisions, and warning about contradictory caller-data settings require a full picture of the configuration, not a single local element.
-
Dependent components last. Loggers and
appender-refentries need appenders that may be defined elsewhere in the file (or only after nested structure is complete).
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):
-
If the parameter type can be built from a simple string, the property is basic (
AggregationType.AS_BASIC_PROPERTY, orAS_BASIC_PROPERTY_COLLECTIONwhen an adder is used). -
Otherwise the property is complex (
AS_COMPLEX_PROPERTYorAS_COMPLEX_PROPERTY_COLLECTION). -
If no matching setter or adder exists, the nested element is unknown: a warning is issued (“Ignoring unknown property …”) and further processing of that branch is skipped.
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:
-
a primitive type (
int,boolean,double, and so on); -
a type in the
java.langpackage (for exampleString,Integer,Boolean,Long); -
an enum type (converted with
Enum.valueOf); -
a type that follows the
valueOfconvention: a public static methodvalueOf(String)that returns an instance of that type; -
java.nio.charset.Charset(viaCharset.forName; the special string"null"meansCharset.defaultCharset()).
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:
-
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). -
Instantiates that class (checking that it is compatible with the setter/adder parameter type).
-
Sets the logback
Contextif the object isContextAware, and pushes the object on the interpretation stack so nested elements apply to it. -
After children are processed (
postHandle), optionally injects the parent via aparentproperty, starts the object if it implementsLifeCycleand is not marked@NoAutoStart, then attaches it to the parent withsetPropertyoraddProperty.
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):
-
an entry in the default nested-component registry maps the parent class and property name to a default implementation (for example,
encoderunderAppenderBase→PatternLayoutEncoderin logback-classic); -
the setter or adder method carries a
@DefaultClassannotation naming an implementation; -
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.AppenderBasech.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:
XML (or a serialized model) →
ModeltreeModeltree →ModelHandlers in phase order → liveLoggerContext
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.