The Java Command Line Workflow

by: Ethan McCue

A while ago, I released a draft of the jresolve command line tool. Its function is to take a set of root dependency declarations and resolve the full set of transitive dependencies.

I'm happy with the API, but there are some things to fix up.

But I think jresolve's existence, and why I bothered to make it, only makes sense as a part of a larger story. This is an attempt to tell it.

The Problem

We all use Maven or Gradle. There are other up-and-coming build tools like bld and some Ant holdovers from the 2000s, but if you threw darts at Java codebases that is what you would hit.

This is a good state of affairs in many ways, but there are downsides.

The specific downside I want to focus on is how it affects the way people learn Java. What follows are my own opinions and perception.

Step 1.

When people learn how to code, typically they start with a "Hello, world" program.

In the past, this part involved hand-waving away public static void main(String[] args). In some future release of Java it will be simpler. That's great. I'll talk about how can affect curriculums at some point.

But from a tooling perspective, this step is a choice between having them run java Main.java on the command line and having them click the "Big Green Run Button" in whatever text editor they installed or online platform they signed up for.

Step 2.

You can actually go pretty far into the language without leaving a single file, but at some point a student needs to have more than one file in their projects.

To do this, you again have a (non-exclusive) choice of how to approach it. Either the command line or the Big Green Run Button.

If you take the command line route, it will be still java Main.java, followed by java src/Main.java after you guide them to keep their code in a folder.

Green button is the green button.

Step 3.

Because it will be relevant to things to come, you at some point want to explain that Java code can be compiled ahead of time to .class files. You could point to the directory where the B.G.R.B. put the class files, or you could explain what javac is and how to use it. For that you would land on something like this.

javac --source-path ./src -d classes src/Main.java
java --class-path classes Main

So you would have been able to introduce javac, the concept of ahead of time compilation, .class files, and the --class-path.

Step 4.

Once they've made an app the next thing they'll want is to package it up into a jar.

In the 🟢 world, you show them a menu in their editor and what buttons to click.

In the CLI, it's a chance to show them how to use the jar tool.

jar --create --file app.jar --main-class Main -C classes .

Step 5.

This is where things get tricky, because once they know how to build an app it won't be long before they want to make something that requires a dependency.

And it is this step where things fall apart.

If you are lucky, the dependency they need has no transitive dependencies. You show them how to download a .jar file, how to add it to the IDE or where to put it on the --class-path, and warn them that they won't be able to get way with that forever.

If you aren't, you need Maven or Gradle. That is by far the easiest way to make sure they get their dependencies.

It is also easy to justify. Chances are any Java job would use one of those.

One problem is that because Maven and Gradle also take over compiling the code, you invalidate their investment in learning how to use javac and jar. They won't be using either of those from now on.

Another is that both are going to throw a lot in their face. Either an entirely new programming language with Gradle or a relatively beefy pom.xml with Maven.

This is what a "blank" Maven gives you in IntelliJ. It's not horrible, but it does have some public static void main(String[] args)-like properties.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>untitled92</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

You could also use something like jbang, which automatically downloads dependencies declared as comments in the code. But this stops being viable if you want those dependencies for things other than running the code as a script, unfortunately.

So the point of jresolve specifically is to let you stick with the command-line flow and avoid pivoting to build tools until later.

You show them how to use it to get dependencies from the internet as well as how to use those dependencies.

jresolve \
    --output-directory libraries \
    pkg:maven/de.gurkenlabs/litiengine@0.8.0

javac \
    --module-path libraries \
    --add-modules ALL-MODULE-PATH \
    --source-path ./src \
    -d classes \
    src/Main.java

java \
    --module-path libraries \
    --add-modules ALL-MODULE-PATH \
    --class-path classes \
    Main

As an aside, while it requires the unpleasant appearance of ALL-MODULE-PATH I would argue that showing early that you should put your external dependencies on the --module-path is a good thing.

Step 6.

If you took the path enabled by a tool like jresolve, the commands you are asking folks to run likely are getting pretty hard to remember.

It is a good time to introduce some "command runner" mechanism. Some way so they only have to say compile instead of a long javac incantation.

For this purpose, I have a liking for just, but shell scripts, makefiles, etc. are all valid.

help:
    just --list
    
clean:
    rm -rf classes
    rm -rf libraries

install:
    rm -rf libraries
    jresolve \
        --output-directory libraries \
        pkg:maven/de.gurkenlabs/litiengine@0.8.0
   
compile:
    rm -rf classes
    javac \
        --module-path libraries \
        --add-modules ALL-MODULE-PATH \
        --source-path ./src \
        -d classes \
        src/Main.java

run:
    java \
        --module-path libraries \
        --add-modules ALL-MODULE-PATH \
        --class-path classes \
        Main  

Now that they can do something spiritually like just compile, commands no longer need to be produced from memory every time they want to do things with their code.

It also instills the notion that software projects are generally built by a set of named and repeatable processes.

And if you haven't or its just time for a refresher, you can use this as an opportunity to go a little deeper into the command line and explain tools like cd and rm.

Step 7.

Now that they know how to use dependencies and can run somewhat involved processes in the CLI, you can show them how to package their code to share with someone who doesn't have Java.

If they've made games and other such gui things, then you can show them jpackage. Have them make a jar with their classes and show them the flags to include their dependencies and make an installer.

If you've kept more of a server-y focus, maybe you'd just show them how to copy files to a remote machine, maybe you'd go through something like docker and show them the commands to build images.

But at this point they have some idea of how to "ship" their code.

Step 8.

Now that they have all the mechanisms to deliver code from concept to product, they are going to start making big projects.

At least some students will have an idea for a game or a website or similar they're going to invest a lot of time in, but also the assignments you are giving will probably require more structure. A thing I've seen in a lot of curriculums is to have everyone do the M part of an MVC type assignment and swap Ms with another group and write the V and C using that.

As such, it is as good a time as any to introduce modules.

The path of least resistance would be to introduce the multi-module format that javac understands. I.E you have a top level directory for each module that has the module's name.

some.mod/
    src/
        module-info.java
        some/
            mod/
                ...
other.mod/
    src/
        module-info.java
        other/
            mod/
                ...
javac \
    -d compiled \
    --module-path libraries \
    --module-source-path  "./*/src" \
    --module some.mod,other.mod

That will require talking about visibility and packages, so it's a good point to also start talking about higher level concepts like encapsulation and library contracts.

Step 9.

Maybe this can be done a bit sooner, but you definitely need to show those goobers how to write unit tests now.

The best way to do this is to show them how to use junit.

java --module-path libraries:compiled \
     --add-modules ALL-MODULE-PATH \
     org.junit.platform.console.ConsoleLauncher execute --scan-modules

Maybe there can be a junit executable ready via some mechanism, but either way all the mechanics of even this relatively verbose incantation have been shown.

And this is a great point to introduce the practice of having a separate test folder. Also potentially resources since tests can use those as a source of test data.

Step 10.

At some point now that they know how to write code, write tests, design modules, etc. It would be a good point to get into library writing. Not everyone will, but some will try their hand.

It's at this point that learning Maven or Gradle probably becomes needed, though I think with a smidgen more tooling that can be delayed. Maybe just something to generate a POM + jreleaser would be enough.

Step N.

Then, at some point, they have need for a real build tool. I won't opine on this, but I think some people wouldn't ever reach this step.

They will have already gotten a relatively deep understanding of the underlying tools, naturally come across the concept of a build task, know what a library is and what Maven coordinates are and do, and they will have enough context to know why Maven would choose src/main/java as the place to put code.

I think this is a healthier level to engage with build-tools at. Understanding what tasks they automate because you've done those tasks manually.

It also gives a firmer foundation for the more exotic parts of tooling like agents, AppCDS, annotation processors, etc. Build tools aren't always the most intuitive with those.

Conclusion

This might not be appropriate for all curriculums. Sometimes you are in a boot-camp and you just gotta be employable with Spring in 6 months.

But when the goal of an education isn't optimizing time to employment, I think teaching with the command-line first has value. I just think that in order for it to be actually practical, a few more pieces need to be in place.

So that aspiration is what jresolve is for. It is what whatever CLI tool I make next will probably be for. That's the vision. Have the JDK be enough, by itself, to get bootstrapped into modern software development. Lower the barrier of entry to be around that of JavaScript and Python.


Tell me what I got wrong in the comments below.


<- Index