Wednesday, November 28, 2018

A Few Additions to Your Bag of Maven-Fu

Apache Maven

Apache Maven is simple, yet quite powerful; with a few tricks here and there, you can greatly simplify and optimize your dev experience.

Working on multiple, non-colocated modules

Say you have two utility modules foo and bar from one master project A, and another project B which pulls in foo and bar.

While working on B, you realize that you need some occasional tweaks to be done on foo and bar as well; however, since they are on a different project, you would usually need to

  • switch to A
  • make the change
  • mvn install
  • switch back to B
  • and "refresh" dependencies (if you're on an IDE).

Every time there's a need to make an upstream change.

With Maven, instead you can temporarily "merge" the three pieces with a mock master POM that defines foo, bar and B as child modules:

<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>
    <!-- blah blah blah -->

    <modules>
        <module>foo</module>
        <module>bar</module>
        <module>B</module>
    </modules>

What's in it for me?

IDEs (like IntelliJ IDEA) will identify the root as a multi-module project; which means you'll be able to:

  • browse seamlessly from one module to other. No more decompilation, bytecode incompatibilities or source maps; handy when you're searching for usages of some class or method - or refactoring one - across your "composite" project's scope.
  • modify sources and resources of each module on demand, within the same project window. The IDE will automatically recompile the changes and add everything to the runtime classpath; handy for in-IDE testing and debugging with hot reload.
  • version seamlessly. If the three modules are under different VCS roots, IDEs like IDEA will track each repo individually; if you commit one set of changes, each repo will have a new commit reflecting its own part of the change; all with the same message!

Meanwhile, plain Maven will build foo/bar (as required) and B in proper sequence, if the root module is built - exactly what we would have wanted.

Relative paths, FTW!

Even if the modules are scattered all over the filesystem, Maven can resolve them easily via relative paths:

    <modules>
        <module>../foo</module>
        <module>grandpa/papa/bar</module>
        <module>../../../../../media/disk2/repos/top-secret/B</module>
    </modules>

Drop that clean

Perhaps the most used (hence the most misused) Maven command is:

mvn clean install

The de-facto that gets run, right after you make some change to your project.

And, for most scenarios, it's grossly overkill.

From scratch?

The command combines two lifecycle phases - "stages" of a Maven build process. Phases have a definite sequence; so if you request some phase to run, all previous phases in its lifecycle will run before it. But Maven plugins are smart enough to skip their work if they detect that they don't have anything to do; e.g. no compilation will happen when compiled classes are up-to-date.

Now, clean is not part of the default lifecycle; rather it is used to start from scratch by removing the entire target directory. On the other hand, install is almost the end of the line (just before deploy in the default lifecycle).

mvn clean install will run both these phases; and, thanks to clean, everything in between as well.

It's handy when you want to clean up everything, and end up with the latest artifacts installed into your local Maven repo. But in most cases, you don't need all of that.

Besides, install will eventually clutter your local Maven cache; especially if you do frequent snapshots/releases with MB- or GB-sized bundles.

Be lazy; do only what's necessary!

Yawn!

If you updated one source file, and want to propagate it to the target/classes dir:

mvn compile

where Maven will auto-detect any changes - and skip compilation entirely if there are none.

If the change was in a test class or resource:

mvn test-compile

will get it into target/test-classes.

Just to run the tests (which will automatically compile any dirty source/test classes):

mvn test

To get a copy of the final bundle in target:

mvn package

As you might often want to start with a clean slate before doing the packaging:

mvn clean package

Likewise, just specify the end phase; or both start and end goals, if you want to go clean to some extent. You will save a whole lot of time, processing power, and temper.

Meanwhile in production...

If your current build would go into production, just forget most of the above ;)

mvn clean package

While any of the "sub-commands" should theoretically do the same thing, you don't want to take chances ;)

While I use package above, in a sense install could be better as well; because then you'll have a copy of the production artifact in your .m2/repository - could be a lifesaver if you lose the delivered/deployed copy.

More skips...

--no-snapshot-updates

If you have watched closely, a build that involves snapshot dependencies, you'd have noticed it taking several seconds to search for Maven metadata files for the snapshots (and failing in the end with warnings; unless you have a habit of publishing snapshot artifacts to remote).

This is usually useless if you're also builidng the snapshot dependencies locally, so you can disable the metadata check (and snapshot sync attempt) via the --no-snapshot-updates or -nsu parameter.

Of course -o would prevent all remote syncs; but you can't use it if you actually want to pull some of the dependencies, in which case -nsu would help.

You can skip compile!

Like the (in)famous -Dmaven.test.skip - or -DskipTests, you can skip the compilation step (even if there are code changes) via -Dmaven.main.skip. Handy when you just want to run tests, without going through the compilation overhead; if you know the stuff is already compiled, of course. Just like -DskipTests - but the other way around!

(Kudos to this SO post)

Skip, skip, skip.

Continuation: -rf

You might already know that, if a module fails in the middle of a build, you can resume the build from that point via the -rf :module-name parameter.

This parameter also works out of the blue; it's not limited to failure scenarios. If you have 30 modules but you just want to build the last 5, just run with -rf :name-of-26th-module.

Tasty testing tricks

Inheriting tests

Generally Maven artifacts don't include test classes/resources. But there are cases where you want to inherit some base test classes into child modules.

With the test-jar specifier, you can inherit an artifact that only contains test classes and resources:

        <dependency>
            <groupId>com.acme</groupId>
            <artifactId>foo</artifactId>
            <version>3.2.1</version>
            <type>test-jar</type>
            <scope>test</scope>
        </dependency>

The corresponding build configuration on the "depended" module would be like:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>test-jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

One caveat is that transitive test dependencies are not inherited in this process, and have to be manually specified again at each usage of the test JAR. (At least I have't come across a better alternative.)

If you're working on one test case, don't run the whole bunch.

-Dtest=com.acme.my.pkg.Test can single-out your WIP test, so you can save plenty of time.

Depending on your test runner, -Dtest may support wildcard selectors as well.

Of course you can temporarily modify the or array of your test plugin config (e.g. SureFire) to limit the set of runnable tests.

Debuggin' it

Beautiful, but still a bug!

Debug a Maven test?

If your test runner (e.g. SureFire) allows you to customize the command line or JVM args used for the test, you can easily configure the forked JVM to wait for a debugger before the test starts executing:

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <!-- ... -->
                    <configuration>
                        <argLine>-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,address=8000</argLine>

Debug Maven itself?!

If you're writing or troubleshooting a Maven plugin or extension it would be handy to run Maven itself in debug mode.

Maven is ultimately Java, so you can simply grab the ultimate command that gets run when you invoke, and re-run it with the -Xdebug... params.

But Maven already has a way cooler mvnDebug command that does this automatically for you. It has the same syntax as mvn so is pretty easy to get used to.

Once invoked, it will by default listen on port 8000 for a debugger, and start executing as soon as one gets attached; and stop at breakpoints, reveal internal states, allow expression evaluations, etc.

Look at the logs!!!

This deserves its own section, because we are very good at ignoring things - right in front of our eyes.

Right at the start

I bet there's 95% chance that Maven will be spewing off at least one [WARNING] at the start of your build. While we almost always ignore or "postpone" them, they will bite back at some point in the future.

Right before the end

If there's a compile, test or other failure, Maven will try to help by dumping the context (stacktraces, [ERROR]s etc.). Sometimes you'd need to scroll back a page or two to find the actual content, so don't give up and smack your computer in the face, at the first attempt itself.

Recently I spent almost an hour trying to figure out why a -rf :'d build was failing; while the same thing was succeeding when starting from scratch. In the end it boiled down to two little [WARNING] lines about a systemPath dependency resolution error. Right in front of my eyes, yet so invisible.

Stupid me.

Desperate times, -X measures

In some cases, where the standard Maven output is incapable of pinpointing the issue, running Maven in trace mode (-X) is the best course of action. While its output can be daunting, it includes everything Maven (and you) needs to know during the build; plugin versions, compile/test dependency trees, external command invocations (e.g. tests); so you can dig deep and find the culprit.

As always, patience is a virtue.

Final words

As with anything else, when using Maven,

  • know what you're doing.
  • know what you really want to do.
  • believe Maven can do it, until proven otherwise ;)

No comments: