Showing posts with label Maven. Show all posts
Showing posts with label Maven. Show all posts

Tuesday, April 21, 2009

Using antlib to work with Maven

If you are using ant to build your project, you may want to start looking at moving away from ant and leverage Maven instead. I've been using Maven for about a year now and after the initial pains in understanding and building a complex project with Maven, I have understood and started liking what it has to offer. However, for those still stuck in the Ant world, you can still leverage the Antlib library to leverage the dependency management capabilities of Maven. The best thing is that if you use Eclipse, you can also leverage the m2eclipse plugin for maven for projects using antlib. This helps get rid of all your dependent 3rd party jars embedded in your project.

So if you have a JEE project - typically one that builds a separate WAR and EAR file, you will probably need 3 Maven POM files.

---MyProject
|
---web
| |_
| pom.xml
|
---ear
| |_
| pom.xml
|
---pom.xml <--(toplevel pom)
|
---build.xml

All that's required in the top level pom is the following:

<project xmlns="http://maven.apache.org/POM/4.0.0" xsi="http://www.w3.org/2001/XMLSchema-instance" schemalocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelversion>4.0.0</modelversion>
<groupid>MyProject</groupid>
<artifactid>MyProject</artifactid>
<packaging>pom</packaging>
<version>1.0.0</version>
<modules>
<module>web</module>
<module>ear</module>
</modules>
</project>

The web and ear directories will have a POM file depicting the dependencies for these respective artifacts. So for example, the WAR pom file could look like:

<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>com.mycompany.myproject</groupId>
<artifactId>myproject-war</artifactId>
<version>1.0.0</version>
<packaging>war</packaging>
<dependencies>
<!-- Application specific -->
<dependency>
<groupId>struts</groupId>
<artifactId>struts</artifactId>
<version>1.2.8</version>
</dependency>

<!-- provided -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.3</version>
<scope>provided</scope>
</dependency>
</dependencies>

</project>

Once you have these POM files setup, you can enable Maven support for the project using m2Eclipse. The "update configuration" and "update dependencies" options available through m2eclipse should create a CLASSPATH variable for your project with jars included based on those depicted in the WAR and EAR POM files.

Now for your ant scripts to work with Maven and these POM files, here's what the ant script could look like:

<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="buildAll" name="MyProject" xmlns:artifact="urn:maven-artifact-ant">
<!-- Antlib -->
<path id="maven-ant-tasks.classpath" path="lib/maven-ant-tasks-ext-1.0.0.jar" />
<typedef resource="org/apache/maven/artifact/ant/antlib.xml"
uri="urn:maven-artifact-ant"
classpathref="maven-ant-tasks.classpath" />
<artifact:pom id="maven.war.project" file="${web.dir}/pom.xml"/>
<artifact:pom id="maven.ear.project" file="${ear.dir}/pom.xml"/>
<artifact:dependencies pathId="war.dependency.pathset">
<artifact:pom refid="maven.war.project" />
</artifact:dependencies>
<artifact:dependencies pathId="ear.dependency.pathset">
<artifact:pom refid="maven.ear.project" />
</artifact:dependencies>


<target name="compile" depends="prepare">
<condition property="debug.flag" value="on">
<istrue value="${localbuild}" />
</condition>
<condition property="debug.flag" value="off">
<isfalse value="${localbuild}" />
</condition>
<echo>Compiling source files, debug turned on</echo>

<!-- Load dependency set -->
<javac debug="on" deprecation="${deprecation.flag}" destdir="${classes.dir}" optimize="${optimize.flag}" source="1.6" target="1.6">
<src path="${gensrc.dir}" />
<src path="${src.dir}" />
<exclude name="${cvs.dirs}" />
<classpath refid="war.dependency.pathset" />
<classpath refid="ear.dependency.pathset" />
</javac>
</target>

<target name="buildWebModule" description="Create the Project Web WAR" depends="compile">
<!-- Copy dependencies to temporary web lib -->
<artifact:dependencies filesetId="war.dependency.fileset" useScope="runtime">
<artifact:pom refid="maven.war.project" />
</artifact:dependencies>
<delete dir="${web.build.dir}/lib"/>
<copy todir="${web.build.dir}/lib" flatten="true">
<fileset refid="war.dependency.fileset" />
</copy>

<!-- Create WAR -->
<war destfile="${war.path}" webxml="${web-inf.dir}/web.xml" manifest="${web.dir}/META-INF/MANIFEST.MF">
<fileset dir="${web.dir}">
<include name="**" />
</fileset>
<classes dir="${classes.dir}"/>
<lib dir="${web.build.dir}/lib"/>
<webinf dir="${web.build.dir}"/>
</war>
</target>
</project>

And you can expand the build.xml for creating the EAR file similarly. Some caveats here: notice the usescope for the WAR dependencies (useScope="runtime"). This is critical to get dependencies that are only needed to be included in your WAR versus ones which are already in the App server classpath. However, the problem here is that Antlib doesn't work exactly like to the tune of the configuration specified for a jar in the Maven POM. I had to actually make a code change in the antlib source for it to behave exactly as Maven does for dependencies marked with a scope of provided as well as the optional flag in the POM file. The change is a simple one and is listed below if you want to have this behavior. Check out the source code using the information here.

The code needs to be added to the DependenciesTask.java file:
 (line 198)
for ( Iterator i = result.getArtifacts().iterator(); i.hasNext(); )
{
Artifact artifact = (Artifact) i.next();

// Added condition to recognize optional flag and filter artifacts based on useScope
if (!(artifact.isOptional() && (artifact.getScope()
.equals(Artifact.SCOPE_RUNTIME) || (useScope != null && useScope
.equals(Artifact.SCOPE_RUNTIME)))))
addArtifactToResult(localRepo, artifact, fileSet, fileList);

versions.add( artifact.getVersion() );

The changes I've listed above should help you migrate your application over to Maven easily when you or your organization decides to adopt Maven completely.

Saturday, September 27, 2008

Building enterprise applications with Maven to use optional packages


The Java 1.3 feature of optional packages introduced support for an expanded set of Jar-file manifest attributes that enable an application jar to specify its list of dependencies on other jars. Maven can generate a jar and build a manifest automatically for you so that you can leverage optional packages deployed in your environment (whether it be the classpath or jars deployed in an application container). If you are looking to build and deploy an application EAR that has dependencies on a slew of 3rd party libraries, you can deploy the 3rd party libraries as optional packages and cut down on the size of your EAR file across many applications. The reuse of these 3rd party jars across multiple Web or enterprise application deployments becomes really handy if you want all these applications to do a common upgrade to a different version for their dependent library. So how do you tell Maven to do this? Here's an example of a Web application POM file. Within the build section, specify the following snippet -

<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addExtensions>true</addExtensions>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>


However, while building this WAR file, the dependencies will still get pulled in unless you specify another attribute on your dependencies. Example here - if you have a dependency on aspectj and aspectj is deployed to your container, say Weblogic10, as an optional package, then in your POM for your WAR file, you would specify the following -

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.0</version>
<!-- <scope>provided</scope> -->
<optional>true</optional>
</dependency>

The attribute that's important in this snippet is the "optional" attribute, the presence of which will omit the file from being included in the WAR file. The omission of the file is not the only thing that is important here. The "scope" element is also important. There are some specific rules that Maven follows when generating the manifest based on the "scope" and "optional" tags that you need to understand. Here's their docs that shows a table that helps clearly explain the relationship between these 2 attributes. In the above example, while the optional element will will exclude the jar from being added to the WAR file, the scope element will also cause the jar's extension name to be omitted from the MANIFEST - something you probably do not want if you are looking to leverage optional packages deployed.

A jar file deployed as an optional package essentially needs to have 3 properties in its manifest file -

Extension-Name: asjpectj
Specification-Version: 1.6.0
Implementation-Version: 1.6.0

The manifest file built by Maven will put the extension name of the dependent jar in the "Extension-List" property of the manifest of your WAR file. It will also put the implementation version. You have to make sure that both these attributes match with those of the deployed optional package (i.e. in this example the aspectj jar). Otherwise, you'll probably get a ClassNotFoundException. As long as you get the extension name and implementation version of the optional package the same as what Maven generates in the manifest of the dependent WAR, everything will work and deploy without a problem. Take a look at this article for Weblogic to understand the constraints on naming for optional packages.

While the above is tedious at first, it becomes a very efficient way of managing application dependencies in enterprise production systems once you get everything working and lined up. I actually created a Swing tool that takes an EAR file that has all the dependencies in it and spits out the jars converted into optional packages that Maven understands. All you really have to do is take the version information from the name of the jar and put it into the Specification(as long as this contains only digits eg. "1.1.2" ) and implementation properties of the jar's manifest.

Though your initial effort to get everything setup will take a little bit of time, it will be well worth it. So give this operational efficiency a try.....

Thursday, July 17, 2008

Eclipse and Maven integration

If you are looking to use Maven on your next Project and currently use Eclipse as your IDE, then I would suggest you also take a look at MyEclipse 6.5. This new version of MyEclipse has some nifty features implemented that aren't just a whole bunch of open source plugins bundled together (like prior versions of Myeclipse) - especially the integration built in for Maven.


Myeclipse 6.5 is built on Eclipse 3.3 and the Web Tools Project(WTP). The Maven support I am referring to isn't available in the regular Maven IDE plugin. What has been provided with MyEclipse is the ability to create EJB and WEB projects structured around Maven. These EJB/WEB projects are enabled to be deployable to your application server of choice from within the IDE. While this makes life easy for a developer, one still needs to understand a little bit more about how to go about structuring these EJB and WEB projects to create the necessary enterprise artifact or EAR file.
Using the MyEclipse project options, I went ahead and created two projects - FrmkConsoleServices, an EJB project and FrmkWebConsole, a WEB project. Due to the lact of support for creating an Enterprise project (essentially one that creates the EAR file), I created the project - FrmkConsole, that brings together the other two projects or creates the EAR file containing a JAR and WAR. The POM for this EAR project looks like this, which I had to hand-code since MyEclipse does not have a capability to create such an Enterprise project (a feature they need to have in their next version asap ):

<dependencies>
<dependency>
<groupId>com.mycompany.myappfrmk.console</groupId>
<artifactId>console-svcs</artifactId>
<type>ejb</type>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.mycompany.myappfrmk.console</groupId>
<artifactId>web-console</artifactId>
<type>war</type>
<version>1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-ear-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<generateApplicationXml>
true
</generateApplicationXml>
<generatedDescriptorLocation>
${basedir}/target
</generatedDescriptorLocation>
<finalName>FrmkConsole</finalName>
</configuration>
</plugin>

You will also need to control the creation of the various project artifacts, or get the WAR,JAR built before the EAR, and that's the reason for creating FrmkConsoleRoot. It essentially specifies the modules that make up the project and the POM really only contains the following, besides declaring the packaging for it to be of type "pom":

<modules>
<module>../FrmkConsoleServices</module>
<module>../FrmkWebConsole</module>
<module>../FrmkConsole</module>
</modules>

Overall though, I was not particularly happy that I had to create 4 top-level projects for me to create an EAR file. This approach may work for groups of developers working separately on the WEB and Service layer, however the ideal structure here should be one top-level project that has multiple modules under it - in this case 3 (the ear, the war, the jar). If you don't want to use MyEclipse, then I would go with the m2eclipse plugin. This is pretty good and allows you to create projects based on archetypes available. For example an archetype that helps in building a EAR file is "maven-archetype-j2ee-simple". Using m2eclipse, you can select this archetype by building a new Maven project and selecting the "internal" catalog that has these predefined template projects. You essentially want your project structured according to this archetype.At command line you can retrieve it using the following command:
mvn archetype:create -DgroupId=com.mycompany.myapp -DartifactId=my-webapp 
-DarchetypeArtifactId=maven-archetype-j2ee-simple -DarchetypeVersion=1.0

Well, one thing I can say after all that - its not easy to find this information, but at-least its getting there. Once you have your first project setup, subsequent ones should be easy with Maven. Those of you who are used to ant may find all this a little cumbersome initially, but Maven's archetype is exactly what convention over configuration is all about. Give Maven a try, its not all bad....:).