|
What appears below are my personal notes I wish were part of my long-term memory but don't always seem to fit. I strive for accuracy and clarity and appreciate feedback. If applying any of this information anywhere, confirm for youself the correctness of your work as what you see below might very well be, albeit unintentionally, incorrect or misleading. These notes are here as an easy reference for myself.
Information worthy of a more formal presentation will appear elsewhere than this "Scratch" area. - ksb
Here's my Maven 2 notes, using a "Hello, World" example to explain Maven use and concepts.
Table of Contents | References |
On FreeBSD use the port. On RedHat, I get the latest .jar file, un-jar it in /usr/local which creates a /usr/local/maven-2.0.5/ and symlink /usr/local/bin/mvn to /usr/local/maven-2.0.5/bin/mvn
# cd /tmp # wget http://www.apache.org/dyn/closer.cgi/maven/binaries/maven-2.0.5-bin.tar.bz2 # cd /usr/local # tar -xvjf /tmp/maven-2.0.5-bin.tar.bz2 # cd /bin # ln -s /usr/local/maven-2.0.5/bin/mvn .Then check that it is working as a regular user
$ mvn -v Maven version: 2.0.5
Maven operates by executing plugins or mojos. These plugins implement 'goals' (tasks/targets in ant/make parlance). Running a specific goal of a given plugin is done using the following syntax:
$ mvn <options> [<plugin-alias>:<goal> ...]
Maven organizes the execution of these plugins via lifecycles. A lifecycle in Maven is a predefined sequence of phases through which Maven will march during a given invocation. There are 3 default lifecycles: build, clean and site (for creating a web site).
Certain lifecycle phases are by default mapped directly to a specific plugin goal. So invoking that lifecycle phase invokes the corresponding plugin, you can though still invoke the plugin:goal pair. For example, in the clean lifecycle, the clean plugin's clean goal is bound to the clean lifecycle phase, so rather than running
$ mvn clean:cleanyou could more simply run
$ mvn clean
$ mvn -hwhich will show you the list of command-line args the mvn command accepts.
Especially helpful is
$ mvn help:effective-pomwhich shows you the fully expanded pom.xml which would be used were a mvn command run at that point.
For debugging the build itself try the -X option:
$ mvn -X <plugin-alias>:<goal>
To disable unit tests set the maven.test.skip property to true:
$ mvn -Dmaven.test.skip=true clean compileor specify a test to run, which if is a test that doesn't exist, effectively turns the running of unit tests (but still compiles all the unit test code):
$ mvn -Dtest=xxx clean compile
To see information on a given plugin use:
$ mvn -Dplugin=clean help:describe [INFO] Scanning for projects... ... [INFO] Plugin: 'org.apache.maven.plugins:maven-clean-plugin:2.1' ----------------------------------------------- Group Id: org.apache.maven.plugins Artifact Id: maven-clean-plugin Version: 2.1 Goal Prefix: clean Description: ...
(See Maven in 5 Minutes for a better coverage of this.)
To get started right away, the first thing to try is the archetype plugin's create goal (in an empty dir):
$ mvn archetype:create -DgroupId=com.example -DartifactId=Hello
This will do quite a bit. It will download all the plugins it needs, populating a ~/.m2 tree, known as your local repository so these won't be downloaded again unless there are updates. It also creates a Hello dir tree in the current dir with a "quickstart" pom.xml and src tree under it. Under src/ are two trees: main/ for the application itself and test/ for the unit tests. The pom.xml file is Maven's analog to ant's build.xml or make's Makefile, where you define the behavior of Maven for your project.
Now cd into the Hello dir and try the following command:
$ mvn clean packageThis executes the clean and package lifecycle phases which will fist clean and then, because package is specified, runs the compile, test and package phases. During each phase maven will display the goals being executed (in [brackets]) and will download or update the needed plugins to your local repo.
Just to prove that this actually built something:
$ java -cp ./target/Hello-1.0-SNAPSHOT.jar com.example.App Hello World!
To simplify the invocation of our new app, and demonstrate the use of a Maven plug in, let's change the pom.xml file so that an "executable" jar is built (meaning one that can be run with the java -jar command). To do this, the Maven Jar Plugin is customized so that it creates an executable jar file. This by adding the following into the pom.xml file:
<project> ... <url>http://maven.apache.org</url> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>com.example.App</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build> <dependencies> ... <project>
Since that jar:jar goal is already bound to the package phase, this just changes how the jar plugin works (so that it builds the jar file with a manifest identifying the Main-Class).
$ mvn clean package [INFO] Scanning for projects... ... [INFO] [jar:jar] [INFO] Building jar: /home/ksb/Scratch/Maven/Hello/target/Hello-1.0-SNAPSHOT.jar ... $ java -jar ./target/Hello-1.0-SNAPSHOT.jar Hello World!
Looking inside the jar file that is built:
$ jar -tf ./target/Hello-1.0-SNAPSHOT.jar META-INF/ META-INF/MANIFEST.MF com/ com/example/ com/example/App.class META-INF/maven/ META-INF/maven/com.example/ META-INF/maven/com.example/Hello/ META-INF/maven/com.example/Hello/pom.xml META-INF/maven/com.example/Hello/pom.propertiesyou can see the manifest as well as pom.xml and pom.properties files. The pom.xml file is a copy of the one used to build the project and the .properties file sets a few java properties related to the project (which are probably customizable, but I've yet to find detailed docs on the jar plugin configuration options).
To demonstrate how Maven handles dependencies and how Maven enables you to add resources (like log4j config files) into your project, let's add the use of a logging package into this example.
Start by adding the logging code to App.java:
package com.example; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Hello world! * */ public class App { private static Log log = LogFactory.getLog(App.class); public static void main( String[] args ) { log.info("main: in"); System.out.println( "Hello World!" ); log.info("main: out"); } }
If you rebuild at this point, it will fail because the commons.logging package can't be found. We need to add that as a dependency in the pom.xml:
<project> ... <dependencies> ... <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1</version> </dependency> </dependencies> </project>The exact values for the groupId and artifactId fields are usually best found by stealing from another pom.xml file or googling the name of the lib and "pom.xml".
If you rebuild at this point, it should complete and you'll see that mvn is pulling down the commons-logging lib as well as all the other dependencies of commons-logging into your local ~/.m2 repository.
If you try running now (as before using java -jar) it won't work as the app can't find the the logging lib:
$ java -jar ./target/Hello-1.0-SNAPSHOT.jar Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory at com.example.App.<clinit>(App.java:12)
But if you run it as a non-executable jar file, specifying the classpath, it will work:
$ java -cp ./target/Hello-1.0-SNAPSHOT.jar:${HOME}/.m2/repository/commons-logging/commons-logging/1.1/commons-logging-1.1.jar com.example.App Jun 20, 2007 2:30:12 PM com.example.App main INFO: main: in Hello World! Jun 20, 2007 2:30:12 PM com.example.App main INFO: main: out
This is a pain which is solved by assembling the executable jar together with all it's dependencies. See below for details about that...
The Maven Assembly Plugin is used to collect all the files which you might want as part of a releasable packaging of your project: your code (as class files, src or both), scripts, property files, required libs, etc. It can create this packaging in several different formats: jar, zip, tar, gzipped tar, a directory, etc.
The assembly plugin can also be used to create an executable jar file like we are using the jar plugin above. This is intriguing as it would be nice to have a single "executable" jar file with everything it needs (app code and required jars).
Trying this, unfortunately, leads to two somewhat undesirable situations. Since Java can't load required jar files from within the same jar file the main class is in, you either have to use a specialized class-loader to load your dependencies, or unzip all the jar files you need into your jar file. The latter solution won't work if you've got signed jar files and the former solution seems rather heavy weight (and an admission of failure on the part of Java, IMO).
So, I'm opting for creating a bzipped tar file with all the .jar files needed: the app in an executable jar and the required libs next to it. The concession is that this .tar.bz2 file will need to be exploded before the app can be run. That will have then have to be part of the deployment. During development though, and here, to avoid that last step, I'll use the directory assembly format.
To start all this, add the assembly plugin to the pom.xml:
<project> ... <build> <plugins> ... <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptors> <descriptor>src/assembly/dist.xml</descriptor> </descriptors> </configuration> </plugin> </plugins> </build> ... </project>
The maven-assembly-plugin can create several assemblies, with each one described in a separate file. The one used here is src/assembly/dist.xml which is listed below. (You've have to create the src/assembly dir first, to create this file.)
<assembly> <id>dist</id> <formats> <format>dir</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <fileSets> <fileSet> <directory>target</directory> <outputDirectory></outputDirectory> <includes> <include>*.jar</include> </includes> </fileSet> </fileSets> <dependencySets> <dependencySet> <outputDirectory>/lib</outputDirectory> <unpack>false</unpack> <scope>runtime</scope> </dependencySet> </dependencySets> </assembly>
This basically says to create a distribution into a directory (rather than a zip or tar file), with all the dependent .jar files, including the one created by the jar plugin. To see the results try the following:
$ mvn clean package assembly:assemblyand then look in the ./target/Hello-1.0-SNAPSHOT-dist.dir dir to see the results. You need to include assembly:assembly in the mvn command since the assembly plugin is not, by default, bound to any phase of the build lifecycle. We'll fix that later...
Since we are now assembling the dependent libs to live along side the executable jar file, the manifest needs to have a classpath which points to these libs. This is done by added the following to the jar plugin configuration in the pom.xml file:
<project> ... <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>com.example.App</mainClass> <addClasspath>true</addClasspath> <classpathPrefix>lib</classpathPrefix> </manifest> </archive> </configuration> </plugin> ... </project>
Now, after another mvn clean package assembly:assembly you'll see that the executable jar has the proper setting in its manifest to find its libs:
Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Created-By: Apache Maven Built-By: ksb Build-Jdk: 1.5.0 Main-Class: com.example.App Class-Path: lib/avalon-framework-4.1.3.jar lib/commons-logging-1.1.jar lib/servlet-api-2.3.jar lib/log4j-1.2.12.jar lib/logkit-1.0.1.jarThe impressive thing here is that it picks up the proper versioned jar files ultimately declared in the pom.xml file - and not just the one we declared we need (commons-logging-1.1) but all of that libs dependencies as well! Maven calls these transitive dependencies. (BTW, the line-wrapping in the Class-Path declaration - with a single-space at the front of the line, is correct syntax in a manifest file!)
When running that jar file:
$ java -jar ./target/Hello-1.0-SNAPSHOT-dist.dir/Hello-1.0-SNAPSHOT.jar log4j:WARN No appenders could be found for logger (com.example.App). log4j:WARN Please initialize the log4j system properly. Hello World!
Things are working but log4j is complaining about not getting initialized properly. To solve this we need to create and put into place a log4j.xml file. This is done by creating the dir src/main/resources/ and dropping into that dir a log4j configuration file like the following:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'> <appender name="CA" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.SimpleLayout"/> </appender> <root> <priority value ="debug" /> <appender-ref ref="CA"/> </root> </log4j:configuration>
Now when building again, this file (and any other files in or under the src/main/resources/ dir) in the resultant .jar files.
To see this in action:
$ java -jar ./target/Hello-1.0-SNAPSHOT-dist.dir/Hello-1.0-SNAPSHOT.jar INFO - main: in Hello World! INFO - main: out
An interesting question is "Why did we not need the log4j.xml file before, right after we first added in the dependency on commons-logging?". The answer is that the commons-logging package is a logging "wrapper" which does not do the logging itself but appears to search for an actual logging lib using the first one it finds. Before there was no log4j lib on the classpath, so it defaulted to the java.util.logging package (notice the different format of the log messages and run with java -verbose to see which class it loads). Since now, we have the log4j lib on the classpath (even before we had a log4j.xml file around) commons-logging used log4j for logging.
To make this assembly part of the package phase of the build lifecycle, add the following bolded bits into the pom.xml file:
<project> ... <build> <plugins> ... <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> ... </configuration> <executions> <execution> <phase>package</phase> <!-- exec as part of the package phase. --> <goals> <goal>attached</goal> <!-- use assembly's 'attached' goal --> </goals> </execution> </executions> </plugin> </plugins> </build> ... </project>
This adds an execution element into the assembly plugin declaration, attaching the execution of this plugin with the package phase of the build lifecycle. So now the assembly will be created as part of the package target and we don't need to have assembly:assembly on the mvn command line any more.
$ mvn clean package [...] [INFO] [jar:jar] [INFO] Building jar: /home/ksb/Scratch/Maven/Hello/target/Hello-1.0-SNAPSHOT.jar [INFO] [assembly:attached {execution: default}] [INFO] Reading assembly descriptor: /home/ksb/Scratch/Maven/Hello/src/assembly/dist.xml [INFO] Processing DependencySet (output=/lib) [INFO] Copying 6 files to /home/ksb/Scratch/Maven/Hello/target/Hello-1.0-SNAPSHOT-dist.dir [...]
Maven has some very powerful web site building tools as well. To build a generic informational web site for this project use:
$ mvn siteThen point your browser at ./target/site/index.html.
Change/Add a few tags to the pom.xml file to fill out this web site with information about the project. There are many, many more elements to add described in the Maven POM Reference and Guide to creating a site to make this more complete. As a simple example, add the bolded:
<project> [...] <name>Hello</name> <description>Software demonstrating a simple greeting.</description> <url>http://dsd.lbl.gov/~ksb/Scratch/Maven_notes.html</url> <organization> <name>KSB</name> </organization> [...] <project>Then regenerate the site to see that the "Project Summary" section has these details.