Need to compile Java sources and ship a JAR? You’ve got options — Gradle, Maven, Ant — and they’re all great at what they do. But for a CFML developer, firing up a whole build toolchain to package a bit of Java is overkill, and none of them feel familiar. CommandBox can now compile your Java sources, run your JUnit tests, package them into JARs, generate Javadocs, and even merge everything into a single runnable “fat” JAR — all from one command, or from a fluent DSL inside your task runners.
Under the hood, CommandBox locates your JDK, globs your .java files, drives javac, and then runs the JAR tool to package the result. No JDK installed? CommandBox will download Java 21 for you and get out of your way.
Note: This feature is currently available only in the bx-cli beta builds of CommandBox, not the current stable releases. Give it a spin and let us know what you think!
Sensible Defaults
The Compile DSL follows Gradle’s standard conventions out of the box — your sources, resources, classes, JARs, and Javadocs land exactly where a Gradle developer would expect them. And every single path is configurable.
| Setting | Default |
|---|---|
| Java sources | src/main/java |
| Test sources | src/test/java |
| Resources | src/main/resources |
| Test resources | src/test/resources |
| Compiled classes | build/classes/java/main |
| Compiled test classes | build/classes/java/test |
| JAR output | build/libs |
| Javadocs | build/docs/javadoc |
| Runtime dependencies | dist/dependencies |
| Dev/test dependencies | dist/test-dependencies |
Test sources and test output default to their main counterparts with main swapped for test, the same swap Gradle uses. A flat src layout has no main segment, so you set those two explicitly.
1. Just Compile
Feed it a source folder, get back classes. That’s the whole example.
File structure you feed in:
src/
└── main/
└── java/
└── example/
└── Hello.java
Task runner DSL:
compile().run();
The same thing at the shell:
java compile
Output — your compiled classes land in the default output directory:
build/
└── classes/
└── java/
└── main/
└── example/
└── Hello.class
Run it
And now we can execute the class file right from our CLI
java run example.Hello
Hello World!
2. Build a JAR
One extra method call (or one extra flag) and you get a JAR in build/libs.
Task runner DSL:
var result = compile()
.toJar( "hello.jar" )
.run();
print.line( result.jarPath ); // build/libs/hello.jar
Shell equivalent:
java compile --jar jarFile=hello.jar
You get a JAR at build/libs/hello.jar (and your compiled classes in build/classes/java/main).
3. Custom Layout & Compiler Options
Non-standard project? Point the DSL anywhere and pass real javac options as a struct.
File structure you feed in:
src/
└── custom/
└── java/
└── com/
└── myapp/
└── Main.java
Task runner DSL:
compile()
.fromSource( "src/custom/java" )
.toClasses( "target/classes" )
.libsDir( "dist" )
.compileOptions( {
"release" : 8,
"debug" : [ "lines", "source" ],
"encoding" : "UTF-8",
"parameters" : true
} )
.toJar( "myapp.jar" )
.run();
Shell equivalent:
java compile sourcePath=src/custom/java classesDirectory=target/classes libsDirectory=dist --jar jarFile=myapp.jar compileOptions:release=8 compileOptions:debug=lines,source compileOptions:encoding=UTF-8 compileOptions:parameters=true
Output:
target/
└── classes/
└── com/
└── myapp/
└── Main.class
dist/
└── myapp.jar
4. Manifest Entries & Resources
Bundle a Main-Class, version metadata, and resource files straight into the archive. Resources from src/main/resources are included automatically with their directory structure preserved.
File structure you feed in:
src/
└── main/
├── java/
│ └── example/
│ └── App.java
└── resources/
└── config.properties
Task runner DSL:
compile()
.manifest( {
"Implementation-Title" : "Example Application",
"Implementation-Version" : "1.2.3",
"Main-Class" : "example.App"
} )
.toJar( "example.jar" )
.run();
Shell equivalent:
java compile --jar jarFile=example.jar manifest:Main-Class=example.App manifest:Implementation-Version=1.2.3 manifest:Implementation-Title="Example Application"
Output — what ends up inside the JAR:
example.jar
├── example/
│ └── App.class
├── config.properties
└── META-INF/
└── MANIFEST.MF
Bonus: if your project has a box.json, its metadata (name, version, author, homepage, license, and more) is used to populate the manifest automatically. An explicit manifest: call always wins over it.
5. Classpaths & Javadocs
Need dependency JARs on the compile classpath, or want documentation generated? Both are one-liners.
Task runner DSL:
compile()
.withClassPath( "libs" )
.withJavaDocs()
.toJar( "example.jar" )
.run();
Shell equivalent:
java compile classPath=libs --javaDocs --jar jarFile=example.jar
You get your JAR at build/libs/example.jar and Javadocs in build/docs/javadoc.
6. The Showstopper: Fat JARs
This is the one that gets people excited. Hand the DSL your dependency JARs and it merges them into a single runnable uber-JAR — combining META-INF/services provider files and stripping dependency signature files so the result actually runs.
File structure you feed in:
src/
└── main/
└── java/
└── example/
└── App.java
libs/
└── dependencies/
├── gson-2.10.1.jar
└── slf4j-api-2.0.9.jar
Task runner DSL:
compile()
.withClassPath( "libs/dependencies" )
.toFatJar(
jarName = "app-all.jar",
includeJars = [ "libs/dependencies" ]
)
.run();
Shell equivalent:
java compile classPath=libs/dependencies --fatJar fatJarFile=app-all.jar fatJarPaths=libs/dependencies
Output — the project JAR plus the merged fat JAR, side by side:
build/
└── libs/
├── app.jar ← your project JAR
└── app-all.jar ← everything merged in
Ship the fat JAR anywhere with a JRE:
java -jar build/libs/app-all.jar
Hello from your fat JAR!
Tune the merge behavior — service-descriptor merging, signature exclusion, and the duplicate-entry policy (last, first, or error) — with configureFatJar() in the DSL or fatJarOptions: flags on the command line.
7. Compile and Run Tests
Tests get the same treatment as sources. java compile --tests builds the test source set after the main one — main output plus your runtime dependencies are on its classpath, so tests can see everything they need.
java compile --tests
Running them needs no test-runner plumbing: java test compiles both source sets and hands the classpath to JUnit’s own console launcher.
java test
java test testClasses=com.foo.MyTest
java test testClasses=com.foo.MyTest#myMethod,com.foo.*
java test reportsDirectory=build/test-results --JSON
Behind the scenes that’s just one dependency, junit-platform-console-standalone, which bundles the platform, the launcher and the Jupiter engine. Add it to devDependencies and CommandBox installs it into dist/test-dependencies — separate from your runtime dependencies, so a test-only JAR can never end up in a jar or fatJar.
{
"devDependencies" : {
"junit" : "maven:org.junit.platform:junit-platform-console-standalone:1.12.2"
}
}
Exit codes are CI-friendly: 0 all passed, 1 a test or the build failed, 3 no tests found, 4 no runner on the classpath. --JSON returns totals, per-test status and durations.
Need a clean slate? java clean removes the class output and the JAR for the current version, and leaves dependency directories alone unless you pass --dependencies. java compile --clean and java test --clean run the clean step first, like gradle clean build.
8. Wire Up Your Editor
java ide-init writes the metadata VS Code, Eclipse and IntelliJ need, from the same box.json block:
java ide-init
java ide-init ide=vscode
It points the editor at both source roots and at the dependency globs, so the gutter test runner resolves JUnit without any project files. Generated classes are kept separate from your build output, since the editor’s compiler owns its own output folder.
Everything You Need, Returned To You
run() hands back an object with every path and option used, which makes the DSL ideal for scripting and CI pipelines:
var result = compile()
.toJar( "example.jar" )
.run();
result.projectRoot // where you ran the command/task
result.jdkBinDirectory // path to the JDK bin folder
result.sourcePaths // [ "src/main/java" ]
result.classOutputDirectory // build/classes/java/main
result.testClassOutputDirectory // build/classes/java/test
result.testsCompiled // true when --tests was used
result.libsDirectory // build/libs
result.jarPath // build/libs/example.jar
result.resourcePath // src/main/resources
result.testResourcePaths // [ "src/test/resources" ]
result.manifest // the manifest entries that were applied
Prefer that from the CLI? Add --JSON and the command prints and returns the same object without the human-readable summary.
The Whole Lifecycle: init → compile → test → run
Put it all together and a brand-new project goes from nothing to tested and running in four commands:
java init // scaffold folders, java config, a class and its test
install // fetch the dev dependencies
java test // compile both source sets and run the tests
java run // execute it
java init stubs a Hello, World! class and its test, sets the class as the main class in box.json, and adds JUnit to devDependencies, so java test works immediately and java run works with zero arguments. And java run understands box.json completely — override the entry point, the JAR, the classpath, or the Java version on the fly:
java run mainClass=com.foo.App
java run javaVersion=17
java run :1=arg1 :2=arg2
box.json is the whole config
The java object in box.json controls everything the compile DSL does. Point it at custom source folders, pin compiler options, declare manifest entries, turn JARs or Javadocs on — and a bare java compile (or java run) picks it all up with zero flags:
{
"java" : {
"sourcePath" : "src/custom/java",
"classesDirectory" : "build/classes",
"libsDirectory" : "dist",
"jar" : true,
"jarFile" : "my-app.jar",
"compileOptions" : { "release" : 8 },
"manifest" : { "Implementation-Title" : "My App" }
}
}
That’s the whole point: a project can be fully customized and still run java compile with no arguments — the conventions live in box.json, not in your muscle memory.
Next Steps
- Run
java compile --helpandjava test --helpinside CommandBox for the full list of options - Point the DSL at another project with
compile( "path/to/project" ).toJar( "thing.jar" ).run() - Curious how deep this goes? CommandBox dogfoods it — the CLI’s own launcher JAR is built with this very DSL in the build script
Give it a spin — your next JAR is a single command away.