March 1, 2016

Just Enough Maven Part 2

This is the second blog post in a series of how to learn just enough Maven so that you're comfortable working with the JVM. If you need some extra convincing that this is a good idea, check out the first post here.

Maven can help do a lot of tedious tasks. But before you learn how to do these tasks with maven, I think it's important to first have a good understanding of how to do them without maven.

Even if you've been doing Java Development for some time (and especially if you always use the same IDE like eclipse for all of your work), I think you might be surprised about how much of these basics you've forgotten.

If you'd like to follow along, make sure you have a Java SDK installed, and then go ahead and create a new directory named upgradingdave.

Or, if docker is your cup of tea, here's a docker command that will get you into a console so that you can follow along:

docker run --rm -it isuper/java-oracle:jdk_8 /bin/bash

Compiling and Running

Most everyone knows that javac is the command line program used to compile java source code into jvm bytecode (class) files.

Typing javac -help will show you a list of options available.

Let's start simple. Copy and paste the following commands into your console.

mkdir upgradingdave

cd upgradingdave

curl -s http://www.upgradingdave.com/blog/samples/2016-03-01-maven/1/HelloWorld.java > HelloWorld.java

javac HelloWorld.java

java -cp . HelloWorld

If all goes well, you should see:

Hello from Upgradingdave!

Just FYI, this dead simple little exercise took me 20 minutes to get working! And we haven't even added package names yet!

After learning the basics of Maven this sort of thing will take seconds.

So, let's add some packages:

cd upgradingdave

rm HelloWorld.java HelloWorld.class

mkdir -p com/upgradingdave

curl -s http://www.upgradingdave.com/blog/samples/2016-03-01-maven/2/HelloWorld.java > com/upgradingdave/HelloWorld.java

javac com/upgradingdave/HelloWorld.java

java -cp . HelloWorld

If all goes well, you should see ... wait, what's this about?

Error: Could not find or load main class HelloWorld

Any idea how to fix that? Yep, we forgot to fully qualify the package name. Maven will figure this sort of thing out for you.

java -cp . com.upgradingdave.HelloWorld

If all goes well, you should see:

Packages are handy

This is as simple as it gets, and was still a little tricky to compile by hand. After you know maven, all you'll need to remember is mvn compile no matter the size or complexity of the project.

Classpath Resources

We have our very basic program compiling and running. In the real world, any programs we write will probably need to read some configuration files.

A nice way to do this is to put the configuration file onto the classpath.

Anything that is not a java source file that can be found in the classpath is referred to as a "classpath resource". In a later blog post I'll show how Maven helps with managing these types of files.

This updated version of HelloWorld.java reads a "greeting" property from a config file named "hello.properties".

Let's see this version in action:

cd upgradingdave

curl -s http://www.upgradingdave.com/blog/samples/2016-03-01-maven/3/HelloWorld.java > com/upgradingdave/HelloWorld.java

curl -s http://www.upgradingdave.com/blog/samples/2016-03-01-maven/3/hello.properties > hello.properties

javac com/upgradingdave/HelloWorld.java

java -cp . com.upgradingdave.HelloWorld

If all goes well, you should see:

Hello, classpath resources!

Sweet, that worked pretty nicely. But, inevitably, we'll want load different versions of our config file for different environments. When doing testing, for example, we might want the greeting to be "Hello, Test!". In production, we still want to see "Hello, classpath resources!".

So, now we have some decisions to make: Do we create two separate versions of the file? Maybe we should create a copy named hello-dev.properties? Or do we have two copies of hello.properties, one for production and one for development? And if so, where do we put those files? Are we going to need to edit HelloWorld.java and compile differently for each environment? If so, that would be a pain!

As you can see, just from this very simple, contrived example, it get's complicated quickly. Imagine having to manage database connections, and email configurations, and OAuth secrets, and LDAP connections, etc!

Maven will help you manage all this much more easily.

Adding Dependencies to a Java Project

So far, we've written all the code ourselves. But of course we're going to eventually want to use some third party libraries.

This is where Maven really shines. But first, in order to really appreciate the difficulties, let's try to manage some jar files ourselves.

Let's add some command line paramaters to HelloWorld.java. Instead of writing the code ourselves, let's use the Apache Commons Command Line library.

Here's another version of HelloWorld.java. This time, the program accepts an input parameter of "-greeting". If the greeting is specified as a command line argument, it overrides the value in the conf file.

cd upgradingdave

curl -s http://shinyfeather.com/commons/cli/binaries/commons-cli-1.3.1-bin.tar.gz > commons-cli.tar.gz

tar -xzvf commons-cli.tar.gz && rm commons-cli.tar.gz

mkdir lib

mv commons-cli-1.3.1 lib/

curl -s http://www.upgradingdave.com/blog/samples/2016-03-01-maven/4/HelloWorld.java > com/upgradingdave/HelloWorld.java

javac -cp lib/commons-cli-1.3.1/commons-cli-1.3.1.jar com/upgradingdave/HelloWorld.java

java -cp .:lib/commons-cli-1.3.1/commons-cli-1.3.1.jar com.upgradingdave.HelloWorld

If all goes well, you should see:

From config file: classpath resources!

Now try this

java -cp .:lib/commons-cli-1.3.1/commons-cli-1.3.1.jar com.upgradingdave.HelloWorld -greeting "help us, maven!"

If all goes well, you should see:

From Command Line: help us, maven!

Introducing just a single dependency added several extra steps:

  • We had to find the jar file
  • We had to remember to add the jar file to the classpath in order to compile
  • And, we had to add the jar file to the classpath in order to run the program.

This is only the tip of the complexity iceberg when it comes to dependencies!

When it comes to dependencies, believe me, without any system, it gets convoluted very, very quickly.

Imagine if your coworker handed you this source code.

First of all, how do you figure out what dependencies are needed? Would you be able to figure out on your own that you need to download the commons-cli jar? If so, which version should you use?

What if the project has 15 dependencies?

Transitive Dependencies

We got lucky with commons-cli-1.3.1.jar. It doesn't have any dependencies on other jar files. If it did, we would have had to track down those jar files as well. If any of those jar files depended on other jar files (which is often the case), then we would have had to track down those as well in order to download dependencies of dependencies!

This sort of thing is called "transitive dependencies". On any typical project, managing transitive dependencies opens all kinds of cans of worms.

Maven makes managing transitive dependencies possible.

And I have bad news. I haven't even scraped the surface of ways in which dependencies add more (mostly necessary) complexity.

Multiple Environments

Often you'll use things like junit and selenium for tesing your code. But you probably don't want to have the junit and selenium jars ending up in your production deployment.

A different yet related problem is when the environment you deploy to will supply some libraries for you. This happens commonly in servlet container environments. Usually the servlet api jar will be provided by servlet containers. If you accidentally include a servlet api jar, they you'll see obscure errors when you go and try to deploy. This also happens a lot with xml parsers jars.

Maven has nice ways to keep test dependencies separate from production dependencies and also nice ways to mark dependencies as provided by a deployment environment

Snapshots

Finally, Maven can also help with something called SNAPSHOT dependencies.

As a quick example, say you're developing some code and your friend is using your project as a dependency. You're code is under active development, but your friend wants to get started using it as soon as possible. So you send them version 0.1.0. A few hours later, you'd like to send an updated jar (but you don't really want to change the version number). Your friend now has to make sure they're using the correct 0.1.0 jar file. How many times do you think your friend might accidentally use the old version? Hint: a lot of times! And you and your friend will be troubleshooting stupid bugs that were fixed in the new version of your code!

Maven has a very elegant way to handle so called "SNAPSHOT" dependencies.

Building and Packaging

Say that now we have our code how we like it. Now we want to deliver it to the customer. Or we might want to make it available for others to use.

Here's how we might bundle it up as an executable jar.

cd upgradingdave

$JAVA_HOME/bin/jar cvf HelloWorld.jar com/upgradingdave/HelloWorld.class hello.properties

Success! Now you can send that jar to your customer or share it with your friends. If they download commons-cli-1.3.1 and put everything in the right place, then they can run it like this:

java -cp HelloWorld.jar:lib/commons-cli-1.3.1/commons-cli-1.3.1.jar com.upgradingdave.HelloWorld -greeting "I can't wait for maven to all this for me"

I'm sure you can imagine the chore of keeping a script like this up to date. Every time you add a new dependency, you'd need to change the jar command above.

Web applications add more challenges. You'll need to include a WEB-INF directory and web.xml along with javascript, images, and stylesheet resources.

And what if you need to build your project for multiple operating systems? Also, what if your friend doesn't have a JRE installed?

As you can see, there's even more complexity in the seemingly simple task of building and packaging.

Luckily Maven helps with this as well. The maven command mvn install will do all of this and more.

Maven Makes this all a lot easier

I hope this was a good overview of how to manually accomplish the basic tasks of compiling, resolving dependencies, bulding and running java projects. And I'm sure you'll agree that these tasks are pretty tedious even for this extremely simplified example.

Once you get comfortable with maven, it can manage all of the complexity I described above pretty much right out of the box with very little configuration. The other benefit is that it does this all very consistently. In other words, once you know the basics, you can pick up pretty much any project using maven, no matter how complicated, and immediately know how to compile, run, resolve dependencies, and build.

Next up, I'll give an overview of the pieces that make up Maven and how the pieces work together to complete the tasks we discussed here.

Tags: tools software java maven