Monday, 28 September 2009

Stand-alone Java

This week I had to do something with Java I haven't done in a while. I had to build a small stand-alone application. It is to be deployed on a live site to manage some changes to the database there. I was going to just slip it into the web application so I would have all the usual stuff you get with that, ie libraries all there, database connections defined etc. But they want to run this with the app server turned off. So it has to be stand alone.

You can do these things quick and dirty but our support people are stressed out enough without giving them grief over a hard-to-install application. But I didn't want to go the whole way and use a real installer 'cos this is a one-off.

The first problem is that all those jar files are not in the classpath. Previously I have just wrapped the thing in a shell script (a bat script in this case because it is Windows). One of the differences between bat and sh is that the * doesn't do the same thing. In shell scripts it normally makes a list of all the files and that is easy for building class paths. Bat files don't do that.

When I can't use a shell script I fall back on ant which does a good job of putting together a classpath of everything in a directory and running the program. But they would have to install ant just for that. Seems like I could make their lives easier.

I read up on putting stuff the manifest file in the jar I'm buiding and that is the answer. This is what my manifest file ends up looking like:


Manifest-Version: 1.0
Ant-Version: Apache Ant 1.7.0
Created-By: 1.6.0_0-b11 (Sun Microsystems Inc.)
Build-Date: September 29 2009
Version: 02.05.07
Company: someone
Main-Class: nz.co.senanque.migration.Main
Class-Path: lib/backport-util-concurrent.jar lib/cglib.jar lib/cl
asses12.jar lib/commons-beanutils.jar lib/commons-collections.jar lib
/commons-dbcp.jar lib/commons-digester.jar lib/commons-discovery.jar
lib/commons-pool.jar lib/dom4j.jar lib/ehcache.jar lib/ejb.jar lib/ej
b3-persistence.jar lib/habanero-core.jar lib/habanero-listener-select
ica.jar lib/habanero-test.jar lib/habanero-wip-hibernate.jar lib/hibe
rnate-annotations.jar lib/hibernate-commons-annotations.jar lib/hiber
nate.jar lib/jaxrpc.jar lib/jcl-over-slf4j.jar lib/jdom.jar lib/jta.j
ar lib/junit.jar lib/logback-classic.jar lib/logback-core.jar lib/oro
.jar lib/quartz.jar lib/saaj.jar lib/slf4j-api.jar lib/spring-aop.jar
lib/spring-beans.jar lib/spring-context.jar lib/spring-core.jar lib/
spring-dao.jar lib/spring-hibernate3.jar lib/spring-jdbc.jar lib/spri
ng-remoting.jar lib/spring-support.jar lib/spring-web.jar lib/tsom_co
re.jar lib/wsdl4j.jar lib/xercesImpl.jar lib/xml-apis.jar


The Main-Class is what gets launched by default. If you double-click the jar file (under Windows anyway) it will run this main class. The Class-Path contains all my jar file references. You can see they are all in the lib file and that is a relative reference. Java will look for the lib dir in the current directory and find the files in there.

To get this I use the following ant script in my build.


<path id="class.path">
<fileset dir="${build.release.dir}/lib" >
<include name="*.jar"/>
</fileset>
</path>

<pathconvert property="class-path" pathsep=" " dirsep="/">
<path refid="class.path"></path>
<map from="${build.release.dir}${file.separator}lib" to="lib" />
</pathconvert>
<echo>${class-path}</echo>

<jar destfile="${build.release.dir}/${jar.name}" compress="true" basedir="${build.classes.dir}">
<manifest>
<attribute name="Build-Date" value="${TODAY}"/>
<attribute name="Version" value="${build.version}"/>
<attribute name="Company" value="${app.company}"/>
<attribute name="Class-Path" value="${class-path}" />
<attribute name="Main-Class" value="nz.co.geni.gsom.migration.Main"/>
</manifest>
</jar>

I already copied all the jar files into the lib dir (actually ${build.release.dir}/lib) and this is just building the classpath and the manifest.

Once the jar is built I zip everything up using this:

<zip destfile="${build.release.dir}/${app.name}.zip"><zip destfile="${build.release.dir}/${app.name}.zip">
<zipfileset excludes="*.zip" dir="${build.release.dir}"/>
</zip>

Actually I have slipped some other stuff into my build.release.dir such as a pdf containing release notes and some config files. So the install is just an unzip, preserving the directories.

How about those database connections? I use system properties for that. I set up all the db stuff using SpringFrameworks and my beas file contains this:

<bean id="propertyPlaceHolder" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE">
<property name="location" value="classpath:migration.properties">
</property>

That tells Spring to look for the properties file specified but if there is a system property that will override anything in the file. Then I can add this:

<bean id="Product2DataSource" class="org.apache.commons.dbcp.BasicDataSource" method="close">
<property name="driverClassName"><value>oracle.jdbc.driver.OracleDriver</value></property>
<property name="url"><value>jdbc:oracle:thin:@${host}:${port}:${database}</value></property>
<property name="username"><value>${product.user}</value></property>
<property name="password"><value>${product.password}</value></property>
</bean>

This is standard Spring, of course. I expect the target environment will have different values, but probably the port will be the same. They can specify system properties in the command line that launches the program, something like:

java -Dhost=xyz -jar migrator.jar

I could have added some code to self-unzip the file but then I might as well put in a proper installer. This will keep it simple enough for the support people to use (they're clever guys, but they have a lot to do).
Post a Comment