martedì 30 ottobre 2012

Maven, the better quickstart

http://slopjong.de/2011/03/04/maven-the-better-quickstart/


Posted on March 4, 2011



I read a lot of Maven documentation and at the end the most interesting question “How do I run an applicationwith its dependencies?” wasn’t answered. Instead I was flooded with many xml configuration sections without showing a simple and complete configuration file. That’s why I wrote this howto but first a little story.
I came across Ivy and Maven a couple of days ago, both of which make it very easy to add a list of libraries to your Java project and add them to the classpath.
Basically both look for a project configuration file including your dependency list and they download the libraries from the internet and install them in the local repository ~/.ivy2 for Ivy and ~/.m2/repository for Maven which are then shared with your Ivy/Maven projects.
While Ivy is only a dependency manager which is integrated with the most popular build management system for Java projects (Ant), Maven is more than just such a manager. With Maven you have a build system and a dependency manager in one tool.
I focused on Maven and it took me a while to find out that a feature I was looking for wasn’t there natively. I assumed it would have a similar behaviour than Ivy but it hasn’t and works completely different.
I was missing a clear Maven howto which shows in 5 minutes how your project is created, compiled, packaged and especially how your standard java application is run afterwards.
I assume that Maven is already installed and that you are familiar with using a shell as I won’t use m2eclipse, the Maven integration for Eclipse. Maybe I’ll update this post later and give you a little introduction but for now let’s concentrate on using it in a shell.
I don’t guarantee that everything is 100% correct because I don’t use Maven that long. Post a comment if something is wrong.

Creating a new project

First you should know that every single action in Maven is done by a plugin. Maven is shipped with the core plugin (1) by default but I didn’t figure out yet if this downloads the actual jar files the first time the actions are triggered. Maven actions are either goals or phases which groups multiple goals but I’m not going into details here just keep in mind that when you trigger a Maven action it has the following scheme:
mvn plugin:goal options
mvn phase
By entering the following line you create your project:
mvn archetype:generate -DgroupId=de.slopjong.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
Remember the scheme plugin:goalarchetype is the plugin here and generate the goal. What the options mean is listed next.
  1. groupId – this is your package name where your source is located into. In general this is a unique ID of your department or simply use your domain in reverse order.
  2. artifactId – this is the folder name of your project. This name will be used for your package name. In this case my-app would appear in the package name if you created a bundle of your compiled project.
  3. archetypeArtifactId – the archetype is a template of the directory structure. maven-archetype-quickstart is the standard template for simple Maven projects. If you were creating a webapp the archetype would have been maven-archetype-webapp.
  4. interactiveMode – by setting the interactive mode to true you can define a source package named other than your groupId and the version number can be specified by yourself instead of using the standard 1.0-SNAPSHOT
For the archetype maven-archetype-quickstart the resulting folder structure will be …
my-app
|-- pom.xml
`-- src
    |-- main
    |   `-- java
    |       `-- de
    |           `-- slopjong
    |               `-- app
    |                   `-- App.java
    `-- test
        `-- java
            `-- de
                `-- slopjong
                    `-- app
                        `-- AppTest.java
… and for maven-archetype-webapp:
my-app
|-- pom.xml
`-- src
    `-- main
        |-- resources
        `-- webapp
            |-- WEB-INF
            |   `-- web.xml
            `-- index.jsp
You can see that the project object model file pom.xmlsrc/main/java and src/test/java were created for the standard java application. Web applications need another structure, that’s why for the template maven-archetype-webapp something slightly different was created. Essentially the directory layout is …
my-app/
|-- pom.xml
|-- src
|   |-- main
|   |   |-- assembly
|   |   |-- config
|   |   |-- filters
|   |   |-- java
|   |   |-- resources
|   |   `-- webapp
|   |-- site
|   `-- test
|       |-- filters
|       |-- java
|       `-- resources
`-- target
    |-- classes
    |-- maven-archiver
    `-- test-classes
… for every Maven project. Of course not all these trees are also created by every archetype. Only those are created that are really needed. Having the same directory structure in every Maven project allows you to download projects from other developers while you already know its structure. This is called convention over configuration. In Ant you need to explicity configure it.
When you run the generate goal Maven outputs something like this (I removed all the download messages):
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom >>>
[INFO] 
[INFO] <<< maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom <<<
[INFO] 
[INFO] --- maven-archetype-plugin:2.0:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Batch mode
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Old (1.x) Archetype: maven-archetype-quickstart:1.0
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: de.slopjong.app
[INFO] Parameter: packageName, Value: de.slopjong.app
[INFO] Parameter: basedir, Value: /home/slopjong
[INFO] Parameter: package, Value: de.slopjong.app
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: artifactId, Value: my-app
[INFO] ********************* End of debug info from resources from generated POM ***********************
[INFO] project created from Old (1.x) Archetype in dir: /home/slopjong/my-app
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.958s
[INFO] Finished at: Wed Mar 02 18:01:44 CET 2011
[INFO] Final Memory: 7M/77M
[INFO] ------------------------------------------------------------------------

Compiling

The previous step created a hello world code example …
package de.slopjong.app;

public class App 
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
    }
}
… which is compiled by executing the compile phase
mvn compile
Now Maven compiled your source to target/classes in your project folder. To run the main method change intotarget and execute
java -cp classes de.slopjong.app.App
Because the package with your class file is located in classes and not in the current directory you have to add it to the classpath by setting the cp option

Adding dependencies

Now the project object model must be edited. Open pom.xml which contains information about the project and configuration details used to build the project. Some of those information were given to Maven at the beginning when you created the project. It also created a jUnit dependency by default.
<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>de.slopjong.app</groupId>
  <artifactId>my-app</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>my-app</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>
If we’d like to use log4j – a framework of the Apache Logging Services – we have to add it as a dependency which Maven will try to resolve next time the project is built. To find out the groupID, the artifactId and theversion of your dependencies I recommend to use a Maven browser like mvnrepository.com.
...
  <dependencies>
    ...
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.16</version>
    </dependency>
  </dependencies>
...
Change the java source …
package de.slopjong.app;

import org.apache.log4j.Logger;

public class App 
{
    static Logger logger = Logger.getLogger(App.class);

    public static void main( String[] args )
    {
        logger.info( "Hello World!" );
    }
}
… and compile again. If you try now to run the main method again at this stage, you’ll get an exception.
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/log4j/Logger
This is because the logger library is not on your classpath. We could add the library from our local repository to the classpath but while this works for a very little amount of dependencies it shouldn’t be done like this. The reason is that the libraries are not flat organized in the repository. Each library itself is contained in a directory like groupId/artifactId/1.2.13/ so you would have to specify the full path to every single library. The disadvantage in adding directly libraries from the repository is that you can’t share your application with somebody else. You can’t just send the class file to a friend and hope he has a Maven environment or the required libraries installed. So now we are at the famous question.

How do I run an application with its dependencies?

When I started looking for an answer I had not yet understood the nitty-gritty of how the plugins integrate with Maven. The quickstarts Maven in 5 minutes and Maven Getting Started Guide don’t talk about this. Instead they have the section How do I build more than one project at once? but is this so important when you’re trying to compile a standard java application, seriously?
The keyword here is again plugin. What you are looking for is done by the assembly plugin which packs all your dependencies into a jar file by executing …
mvn assembly:single
… but please continue reading before execution.
Some older documentation refer to assembly goals other than single but these have been deprecated so don’t use them anymore. Before the product is assembled the first time the plugin must be configured first. Copy the build section so that your pom.xml looks like mine.
<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>de.slopjong.app</groupId>
  <artifactId>my-app</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>my-app</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.16</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>2.2.1</version>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
Now be sure that you are in the project root directory and assemble. The jar file is located in the target folder afterwards and it only contains the dependencies. Check the plugin site if you wish another plugin configuration .
Change again into target and execute this:
java -cp classes:my-app-1.0-SNAPSHOT-jar-with-dependencies.jar de.slopjong.app.App
The only difference to the previous run is only the newly created jar file on your classpath. And don’t wonder about the warnings …
log4j:WARN No appenders could be found for logger (de.slopjong.app.App).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
… you’ve got. There’s no appender configured but log4j actually is working.
You could also package your class files into a jar which makes it easier to distribute your application. This is done with the package phase.
mvn package
Also this jar file appears in the target folder which must be added to the classpath in place of classes.
java -cp my-app-1.0-SNAPSHOT.jar:my-app-1.0-SNAPSHOT-jar-with-dependencies.jar de.slopjong.app.App
You’ve reached the end of this howto but have a look at the ebooks Sonatype wrote.

Books

Sonatype.com offers Maven ebooks and if you want to avoid filling out a form with your name and email you can append the following lines one after the other to the domain:
  • /books/nexus-book/pdf/nxbook-pdf.pdf
  • /books/m2eclipse-book/pdf/m2ebook-pdf.pdf
  • /books/mvnex-book/pdf/mvnex-pdf.pdf
  • /books/mvnref-book/pdf/mvnref-pdf.pdf

Nessun commento: