Skip to content

Repository files navigation

auto-valhalla

Automatically turn your plain classes/records into value classes/records! Codes like a class on older JDKs, works like an int on Valhalla.

auto-valhalla is a Java agent that rewrites eligible identity classes into value classes at class-load time, so existing code (compiled on older JDKs or even JDK28) transparently gets benefits value objects when you run on Valhalla-enabled JVM.

Background: Project Valhalla JEP 401 (https://openjdk.org/jeps/401).

Quick start

1. Opt in with the annotation

Add auto-valhalla-annotation dependency and annotate your plain identity class or record:

import io.github.thunkware.auto.valhalla.AutoValhalla;

@AutoValhalla
public final class Point {
    public final int x;
    public final int y;
    public Point(int x, int y) { 
        this.x = x; 
        this.y = y; 
    }
}

@AutoValhalla
public record Pair<T>(T first, T second) { }

Then download the agent and launch:

java --enable-preview \
     -javaagent:auto-valhalla.jar \
     -jar myapp.jar

After transformation, your class or record behaves like a value object:

  • Objects.hasIdentity(new Point(1, 2)) == false
  • Objects.hasIdentity(new Pair(3, 4)) == false

Because @AutoValhalla annotation was compiled with Java 5, it is compatible JDK 1.5 and later. You can apply the annotation in older codebases without raising their JDK compile version.

2. Select by package or class with includes

Use -Dauto-valhalla.includes to convert classes if you cannot or do not want to edit the source code. A setting ending in . matches a package prefix; otherwise it is an exact class name:

java --enable-preview \
     -Dauto-valhalla.includes=com.example. \
     -javaagent:auto-valhalla.jar \
     -jar myapp.jar

3. Discover classes that can be converted:

To discover classes that can or cannot be converted, run this a few times:

java --enable-preview \
    -Dauto-valhalla.includes-mode=safe \
    -Dauto-valhalla.includes='*' \
    -Dauto-valhalla.includes.on-success-append-to=success.txt \
    -Dauto-valhalla.includes.on-fail-append-to=failures.txt \
    -Dauto-valhalla.excludes-file=failures.txt \
    -javaagent:auto-valhalla.jar \
    -jar myapp.jar

success.txt will contain all classes that can be converted, and failures.txt that cannot.

Be warned that a converted class becomes final and loses identity — == becomes value equality and System.identityHashCode, synchronized, WeakReference, and IdentityHashMap behave differently. Just because a class can be converted does not mean your application will behave correctly on all execution paths.

Options

Flags are supplied as, in that order of precedence:

  • agent arguments -javaagent:auto-valhalla.jar=option1=value1,option2=value2, or
  • system properties -Doption1=value1, or
  • environment variables.

Within the agent-argument list, later options override earlier ones, and a .config file is expanded in place (see below).

Option Env var Description
auto-valhalla.includes AUTO_VALHALLA_INCLUDES Comma-separated classes/packages to convert. * matches everything; foo. is a package prefix; foo.Bar an exact class name
auto-valhalla.excludes AUTO_VALHALLA_EXCLUDES Same matching rules, but for exclusion (overrides includes and the annotation).
auto-valhalla.includes-file AUTO_VALHALLA_INCLUDES_FILE Path to a file with one pattern per line. Blank lines and # comments are ignored.
auto-valhalla.excludes-file AUTO_VALHALLA_EXCLUDES_FILE As above, for excludes.
auto-valhalla.annotation-mode AUTO_VALHALLA_ANNOTATION_MODE Modes narrowing annotation-selected classes. Defaults to safe. See the mode table below.
auto-valhalla.includes-mode AUTO_VALHALLA_INCLUDES_MODE Modes narrowing includes-selected classes. Defaults to yolo. See the mode table below.
auto-valhalla.debug AUTO_VALHALLA_DEBUG true for verbose logging of selection decisions.
auto-valhalla.annotation.on-success-append-to AUTO_VALHALLA_ANNOTATION_ON_SUCCESS_APPEND_TO Path to a file; Appends class name of each annotation-selected class that is successfully converted.
auto-valhalla.includes.on-success-append-to AUTO_VALHALLA_INCLUDES_ON_SUCCESS_APPEND_TO Same, for includes-selected classes.
auto-valhalla.annotation.on-fail-throw AUTO_VALHALLA_ANNOTATION_ON_FAIL_THROW true (default) to surface a loud LinkageError (a ClassFormatError at load) when an annotation-selected class cannot be safely transformed, instead of silently keeping it an identity class.
auto-valhalla.includes.on-fail-throw AUTO_VALHALLA_INCLUDES_ON_FAIL_THROW Same, for includes-selected classes. Defaults to false so a broad includes sweep cannot crash the application.
auto-valhalla.annotation.on-fail-append-to AUTO_VALHALLA_ANNOTATION_ON_FAIL_APPEND_TO Path to a file; Appends class name of each annotation-selected class that fails to transform.
auto-valhalla.includes.on-fail-append-to AUTO_VALHALLA_INCLUDES_ON_FAIL_APPEND_TO Same, for includes-selected classes.
auto-valhalla.config AUTO_VALHALLA_CONFIG Path to a Java properties file supplying the options above (keys may omit the auto-valhalla. prefix).

For every *-append-to option the target file is read once at start-up so names already present are not appended again, and a missing file is simply treated as empty (no error).

Canonical form uses the auto-valhalla. prefix; agent arguments may also use the unprefixed name (e.g. includes-mode).

mode values

The @AutoValhalla annotation and includes/excludes decide which classes should be selected. Mode further decides which or how those selected classes should be converted. If a class is selected but not converted, that's considered a failure; see on-fail-* flags for failure handling.

Mode Effect
safe Keep only candidates classes that can be safely converted.
ignore-synchronized Allow candidates with synchronized instance methods; their ACC_SYNCHRONIZED is stripped so they can become value classes.
mark-class-final Also convert non-final candidates. They are made final, so only opt in when nothing subclasses them (otherwise subclasses fail with IncompatibleClassChangeError).
mark-fields-final If instance fields are non-final yet written only once in a constructor, mark them final. Candidates with a non-final field written elsewhere (or more than once) are rejected, since a value class cannot have a mutable field.
yolo Shorthand for ignore-synchronized,mark-class-final,mark-fields-final (the default).

Mode names are case-insensitive and may use -, _ or camelCase. e.g. mark-class-final, mark_class_final, and markClassFinal are all the same.

.config precedence

When .config agent argument is expanded, its entries are placed at that position in the agent argument stream. Therefore:

  • if .config appears first, later agent arguments override it;
  • if agent argument options appear first and .config later, the file overrides them.

Examples

# convert a whole package by prefix
-Dauto-valhalla.includes=com.example.

# convert only specific classes
-Dauto-valhalla.includes=com.example.Foo,com.example.Bar

# read options from a properties file (keys may omit auto-valhalla.)
-Dauto-valhalla.config=/etc/auto-valhalla.properties

/etc/auto-valhalla.properties:

includes=com.example.
excludes=com.example.dto.
annotation.on-fail-throw=true

Feedback loop: .includes.on-fail-append-to + .excludes-file

includes.on-fail-append-to and .excludes-file are designed to work together. Run once with includes.on-fail-throw disabled (the default) and includes.on-fail-append-to pointing at a file; every class that could not be safely transformed is recorded there. Feed that file back as .excludes-file on subsequent runs so those classes are skipped instead of surfacing errors:

# first pass: record anything that fails
-Dauto-valhalla.includes=com.example. \
-Dauto-valhalla.includes.on-fail-append-to=/var/tmp/auto-valhalla-failures.txt

# later passes: skip the classes that failed before
-Dauto-valhalla.includes=com.example. \
-Dauto-valhalla.excludes-file=/var/tmp/auto-valhalla-failures.txt

The companion includes.on-success-append-to records the classes that were converted, which is handy for turning a broad includes sweep into an explicit includes-file list:

# record what a broad sweep actually converted
-Dauto-valhalla.includes='*' \
-Dauto-valhalla.includes-mode=safe \
-Dauto-valhalla.includes.on-success-append-to=/var/tmp/auto-valhalla-converted.txt

Notes & limitations

  • If annotation-selected classes fail conversion, an exception is thrown by default. Use annotation.on-fail-throw to change behavior.
  • If includes-selected classes fail conversion, the classes are left as identity classes. Use includes.on-fail-throw to change behavior.
  • A converted class is final. If anything subclasses it, that subclass will fail class loading.
  • The agent rewrites identity records and final classes only. It never transforms JDK/system classes or its own support classes. Non-final classes are converted (as final) when the mode includes mark-class-final; any existing subclass then fails to load.
  • Semantics change. For a converted class, == becomes value equality (two instances with equal fields compare ==), equals/hashCode of the two fields, synchronized methods no longer take a monitor, and System.identityHashCode, WeakReference, and IdentityHashMap no longer see per-instance identity. A value class has no identity, so identity-keyed caches and ==-based deduplication silently change behavior. This is especially dangerous with includes-mode, which converts classes without annotating them.
  • Safe to use with any JDK. The agent's entry point is a JDK 5 class file, so the agent jar loads on any JVM from JDK 5 up. On a JVM older than JDK 28 (or on JDK 28 without --enable-preview) the agent prints a single warning and does nothing — your classes keep their original (identity) behavior and the application runs unchanged. There is no need to guard its use behind a JVM-version check.
  • An already-loaded identity class cannot be retroactively made a value class at runtime; classes loaded after the agent attaches (or from the start, when attached via -javaagent) are the ones rewritten.

Note on AI assistance

This project was vibe-coded with an AI coding agent (opencode). Humans designed, directed, reviewed, edited the work. The agent authored the bulk of the implementation, build configuration, and documentation.

About

Automatically turn your plain classes/records into value classes/records.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages