Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ahead-of-time binding

An ordinary jar binds its models while it runs and needs no build step. A native image, or a library that ships widgets, adds one build step: the weaver.

When you need it

You are buildingAdd the weaver?What it does for you
An application run on the JVM: java -jar, gradle run, an IDENoNothing to add. Models are bound at run time
A GraalVM native imageYes, --modelsBinds the models at build time, so the image needs no reflection
A library with @Markup widgetsYes, --catalogRegisters the widgets, so a document finds them by name

The annotations and the behaviour are the same either way. A model that works in a jar works in an image.

What it does

A model is plain Java. Markup binds to its fields and calls its methods:

@Model
public final class Settings {
    @Bind("app.gain") private int gain = 40;

    @Action("app.louder") private void louder() { gain++; }
}
slider bind="app.gain" min=0 max=100
button "Louder" press="app.louder"

gain++ moves the slider. Building an application covers the annotations. Java cannot observe a field write from outside the class, so the weaver rewrites the compiled classes:

  • each assignment to a @Bind field, in the model or in any class beside it, becomes a call that stores the value and notifies the controls bound to it;
  • the model gets a table of its bindings and actions, built from the annotations, so nothing is looked up reflectively;
  • a class that neither is a model nor writes to one is left untouched.

It runs between compileJava and jar, on the .class files, using the JDK’s class-file API. It needs no agent, no opens and no code generated at run time.

Adding it to a build

The weaver is one jar, dev.goldberry:goldberry-weaver, with no dependencies beyond the JDK. It takes directories of compiled classes and rewrites them in place. --models and --catalog select one half; with neither, it runs both.

Gradle

configurations { goldberryWeaver }
dependencies { goldberryWeaver "dev.goldberry:goldberry-weaver:$goldberryVersion" }

def weave = tasks.register('weaveModels', JavaExec) {
    dependsOn tasks.compileJava
    def classes = tasks.compileJava.flatMap { it.destinationDirectory }
    classpath = files(configurations.goldberryWeaver, classes, sourceSets.main.compileClasspath)
    mainClass = 'dev.goldberry.weaver.WeaverMain'
    argumentProviders.add({ [classes.get().asFile.absolutePath] } as CommandLineArgumentProvider)
    inputs.dir(classes)
    // A stamp file, not outputs.dir(classes): sharing javac's output directory
    // makes Gradle recompile the module from scratch on every build.
    outputs.file(layout.buildDirectory.file('tmp/weaveModels/stamp'))
    outputs.upToDateWhen { false }
    doLast { layout.buildDirectory.file('tmp/weaveModels/stamp').get().asFile.text = 'woven' }
}
tasks.named('classes') { dependsOn weave }

Maven

Run it with exec-maven-plugin in the process-classes phase:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>3.5.0</version>
  <executions>
    <execution>
      <id>weave</id>
      <phase>process-classes</phase>
      <goals><goal>java</goal></goals>
      <configuration>
        <mainClass>dev.goldberry.weaver.WeaverMain</mainClass>
        <arguments>
          <argument>${project.build.outputDirectory}</argument>
        </arguments>
        <classpathScope>compile</classpathScope>
      </configuration>
    </execution>
  </executions>
  <dependencies>
    <dependency>
      <groupId>dev.goldberry</groupId>
      <artifactId>goldberry-weaver</artifactId>
      <version>${goldberry.version}</version>
    </dependency>
  </dependencies>
</plugin>

Any other build

java -cp goldberry-weaver.jar:target/classes:<compile classpath> \
     dev.goldberry.weaver.WeaverMain target/classes

Put the classes and their compile classpath on the classpath. The weaver loads your types to rebuild the methods it rewrites, and without them it fails with Could not resolve class. It prints one line per class it changed and exits with 1, naming the member, when it refuses a model.

Widget catalogues

A module that ships widgets annotates each with @Markup("name"). The --catalog half collects them into the module’s WidgetCatalog and declares it in both module-info.class and META-INF/services, so the module works on the module path and on the class path. Widgets.inflater(...) then finds every catalogue on the path, and an application uses the widgets without naming the module.

There is no run-time equivalent: finding annotated classes while the program runs would mean scanning the class path. A widget library runs --catalog in every build. Writing a widget covers the rest.

Without it: the sweep

A model that is not woven is bound at run time. Reads are exact. Writes are found by comparing each field with its last value, which happens:

  • after every action a document dispatches;
  • at the start of every frame, for the models Application.models() returns;
  • whenever you call Models.refresh(model).

A field written from anywhere else, such as a background callback, needs the explicit call:

job.onFinished(text -> {
    model.status = text;
    Models.refresh(model);   // does nothing once the class is woven
});

In a named module, the model’s package must be open to the toolkit:

opens com.example.app to dev.goldberry.core;

The error message quotes this line if it is missing. A class-path application needs nothing.

What it refuses

The weaver at build time, and the run-time binder on first use, refuse the same models. Each refusal names the member.

RefusedBecause
a static @Bind fieldA binding belongs to one instance
a final @Bind field, unless a PropertyA value that cannot change has nothing to notify
an arrayOnly assignment is observed, so values[0] = x notifies nobody. Assign a new List
a path that is not a.b.cThe binding path grammar
two members with one nameOne name, one value
an @Action with two parametersAn action reports that something happened, or what it should become
an @Action parameter that is not String, double, int, boolean or a boxA valued action arrives as the string the document wrote
a static @ActionAn action changes a model instance
an abstract or empty @ModelNothing to bind
@Actions with a @Bind field, or with no @ActionA class with values is a @Model; one with no actions publishes nothing
@Model and @Actions on one classPick one
a @Model extending a @ModelInherited fields would notify nobody
@Bind(restyle = true) on a PropertyWrites to a Property are not rewritten, so there is nowhere to restyle from
@Markup without public static Widget inflate(KdlNode, List<Widget>, Wiring)The node name has nothing to build
two classes with one @Markup nameA document would get whichever loaded last