13 February 2015

Tags: java | maven |

blog_post_image

I provided my reflections about “Java for Everything” in the previous post.
Here we will review other implementation of this concept.

I have the next concerns about any Java application (big or small):

Use Case

We have to create simple and small REST application based on Spark Java framework (A Sinatra inspired framework for Java).

Here is the source:

import static spark.Spark.get;
import static spark.SparkBase.port;

public class App {
    public static void main( String[] args ) throws NumberFormatException {
        
        for(String arg: args) System.out.printf("> %s", arg);
        
        get("/hello", (request, response) -> {
            return "Hello World!";
        });
    }
}   

So, here is the list of issues:

Traditional Maven Way

mvn archetype:generate \
-DgroupId=com.halyph \ 
-DartifactId=sparkblog \ 
-Dpackage=com.halyph.blog \ 
-Dversion=1.0-SNAPSHOT \
-DarchetypeGroupId=org.apache.maven.archetypes \ 
-DarchetypeArtifactId=maven-archetype-quickstart \
-DinteractiveMode=false
<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/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.halyph</groupId>
  <artifactId>sparkblog</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>sparkblog</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.sparkjava</groupId>
      <artifactId>spark-core</artifactId>
      <version>2.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <target>1.8</target>
          <source>1.8</source>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
package com.halyph.blog;

import static spark.Spark.get;

public class App {
    public static void main(String[] args) throws NumberFormatException {

        String myArgs = "";
        for (String arg : args) {
            System.out.printf("> %s", arg);
            myArgs += arg + " : ";
        }
        System.out.println();
        final String finalMyArgs = myArgs;
        get("/hello", (request, response) -> {
            return "Hello World!\n args = " + finalMyArgs;
        });
    }
}

This application can be easily run via IDE, but lets run it via Maven

$ mvn clean compile exec:java -Dexec.mainClass="com.halyph.blog.App"  -Dexec.args="9090 one 1 2"
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building sparkblog 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.4.1:clean (default-clean) @ sparkblog ---
[INFO] Deleting d:\MyProjects\jwrapper\bloggg\sparkblog\target
[INFO]
[INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ sparkblog ---
[debug] execute contextualize
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory d:\MyProjects\sparkblog\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ sparkblog ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to d:\MyProjects\jwrapper\bloggg\sparkblog\target\classes
[INFO]
[INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) @ sparkblog >>>
[INFO]
[INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) @ sparkblog <<<
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ sparkblog ---
> 9090> one> 1> 2
[Thread-1] INFO spark.webserver.SparkServer - == Spark has ignited ...
[Thread-1] INFO spark.webserver.SparkServer - >> Listening on 0.0.0.0:4567
[Thread-1] INFO org.eclipse.jetty.server.Server - jetty-9.0.2.v20130417
[Thread-1] INFO org.eclipse.jetty.server.ServerConnector - Started ServerConnector@4afe75c9{HTTP/1.1}{0.0.0.0:4567}

In case the application run configurations is persistent (“main” class and CLI arguments are changing rarely) we can configure it in pom.xml:

 <build>
    <plugins>
      ...
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.3.2</version>
        <executions>
          <execution>
            <goals>
              <goal>java</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <mainClass>com.halyph.blog.App</mainClass>
          <arguments>
            <argument>9090</argument>
            <argument>one</argument>
            <argument>1</argument>
            <argument>2</argument>
          </arguments>
        </configuration>
      </plugin>
    </plugins>
  </build>

Well, I borrowed the ideas from “A better java shell script wrapper” script. Here it is:

#!/usr/bin/env bash
#
# Copyright 2012 Zemian Deng
#
# A wrapper script that run any Java application in unix/cygwin bash env.
#
# This script is assumed to be located in an application's "bin" directory. It will
# auto resolve its directory location relative to the application path (which is one
# parent up from the script.) Therefore, this script can be run any where in the file
# system and it will still reference the same application directory.
#
# This script will by default auto setup a Java classpath that picks up any "config"
# and "lib" directories under the application directory. It also will also add a
# any typical Maven project output directories such as "target/test-classes",
# "target/classes", and "target/dependency" into classpath. This can be disable by
# setting RUN_JAVA_NO_AUTOCP=1.
#
# If the "Default parameters" section bellow doesn't match to user's env, then user
# may override these variables in their terminal session or preset them in shell's
# profile startup script. The values of all path should be in cygwin/unix path,
# and this script will auto convert them into Windows path where is needed.
#
# User may customize the Java classpath by setting RUN_JAVA_CP, which will prefix to existing
# classpath, or use the "-cp" option, which will postfix to existing classpath.
#
# Usage:
#   run-java [java_opts] <java_main_class> [-cp /more/classpath] [-Dsysprop=value]
#
# Example:
#   run-java example.Hello
#   run-java example.Hello -Dname=World
#   run-java org.junit.runner.JUnitCore example.HelloTest -cp "$HOME/apps/junit/lib/*"
#
# Created by: Zemian Deng 03/09/2012

# This run script dir (resolve to absolute path)
SCRIPT_DIR=$(cd $(dirname $0) && pwd)    # This dir is where this script live.
APP_DIR=$(cd $SCRIPT_DIR/.. && pwd)      # Assume the application dir is one level up from script dir.

# Default parameters
JAVA_HOME=${JAVA_HOME:=$HOME/apps/jdk}     # This is the home directory of Java development kit.
RUN_JAVA_CP=${RUN_JAVA_CP:=$CLASSPATH}     # A classpath prefix before -classpath option, default to $CLASSPATH
RUN_JAVA_OPTS=${RUN_JAVA_OPTS:=}           # Java options (-Xmx512m -XX:MaxPermSize=128m etc)
RUN_JAVA_DEBUG=${RUN_JAVA_DEBUG:=}         # If not empty, print the full java command line before executing it.
RUN_JAVA_NO_PARSE=${RUN_JAVA_NO_PARSE:=}   # If not empty, skip the auto parsing of -D and -cp options from script arguments.
RUN_JAVA_NO_AUTOCP=${RUN_JAVA_NO_AUTOCP:=} # If not empty, do not auto setup Java classpath
RUN_JAVA_DRY=${RUN_JAVA_DRY:=}             # If not empty, do not exec Java command, but just print

# OS specific support.  $var _must_ be set to either true or false.
CYGWIN=false;
case "`uname`" in
  CYGWIN*) CYGWIN=true ;;
esac

# Define where is the java executable is
JAVA_CMD=java
if [ -d "$JAVA_HOME" ]; then
	JAVA_CMD="$JAVA_HOME/bin/java"
fi

# Auto setup applciation's Java Classpath (only if they exists)
if [ -z "$RUN_JAVA_NO_AUTOCP" ]; then
	if [ -d "$APP_DIR/config" ]; then RUN_JAVA_CP="$RUN_JAVA_CP:$APP_DIR/config" ; fi
	if [ -d "$APP_DIR/target/test-classes" ]; then RUN_JAVA_CP="$RUN_JAVA_CP:$APP_DIR/target/test-classes" ; fi
	if [ -d "$APP_DIR/target/classes" ]; then RUN_JAVA_CP="$RUN_JAVA_CP:$APP_DIR/target/classes" ; fi
	if [ -d "$APP_DIR/target/dependency" ]; then RUN_JAVA_CP="$RUN_JAVA_CP:$APP_DIR/target/dependency/*" ; fi
	if [ -d "$APP_DIR/lib" ]; then RUN_JAVA_CP="$RUN_JAVA_CP:$APP_DIR/lib/*" ; fi
fi

ARGS="$@"
# Parse addition "-cp" and "-D" after the Java main class from script arguments
#   This is done for convenient sake so users do not have to export RUN_JAVA_CP and RUN_JAVA_OPTS
#   saparately, but now they can pass into end of this run-java script instead.
#   This can be disable by setting RUN_JAVA_NO_PARSE=1.
if [ -z "$RUN_JAVA_NO_PARSE" ]; then
	# Prepare variables for parsing
	FOUND_CP=
	NEW_ARGS[0]=''
	IDX=0

	# Parse all arguments and look for "-cp" and "-D"
	for ARG in "$@"; do
		if [[ -n $FOUND_CP ]]; then
			RUN_JAVA_CP="$RUN_JAVA_CP:$ARG"
			FOUND_CP=
		else
			case $ARG in
			'-cp')
				FOUND_CP=1
				;;
			'-D'*)
				RUN_JAVA_OPTS="$RUN_JAVA_OPTS $ARG"
				;;
			*)
				NEW_ARGS[$IDX]="$ARG"
				let IDX=$IDX+1
				;;
			esac
		fi
	done
	ARGS="${NEW_ARGS[@]}"
fi

# Convert Windows Java Classpath
if $CYGWIN; then
	RUN_JAVA_CP=$(cygpath -mp $RUN_JAVA_CP)
fi

# Display full Java command.
if [ -n "$RUN_JAVA_DEBUG" ] || [ -n "$RUN_JAVA_DRY" ]; then
	echo "$JAVA_CMD" $RUN_JAVA_OPTS -cp "$RUN_JAVA_CP" $ARGS
fi

# Run Java Main class
if [ -z "$RUN_JAVA_DRY" ]; then
	"$JAVA_CMD" $RUN_JAVA_OPTS -cp "$RUN_JAVA_CP" $ARGS
fi

The main idea is to run mvn dependency:copy-dependencies, this will generate all the jar files into target/dependency folder

$ mvn dependency:copy-dependencies
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building sparkblog 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.1:copy-dependencies (default-cli) @ sparkblog ---
[INFO] Copying spark-core-2.1.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\spark-core-2.1.jar
[INFO] Copying jetty-http-9.0.2.v20130417.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\jetty-http-9.0.2.v20130417.jar
[INFO] Copying jetty-io-9.0.2.v20130417.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\jetty-io-9.0.2.v20130417.jar
[INFO] Copying jetty-security-9.0.2.v20130417.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\jetty-security-9.0.2.v20130417.jar
[INFO] Copying jetty-server-9.0.2.v20130417.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\jetty-server-9.0.2.v20130417.jar
[INFO] Copying jetty-servlet-9.0.2.v20130417.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\jetty-servlet-9.0.2.v20130417.jar
[INFO] Copying jetty-util-9.0.2.v20130417.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\jetty-util-9.0.2.v20130417.jar
[INFO] Copying jetty-webapp-9.0.2.v20130417.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\jetty-webapp-9.0.2.v20130417.jar
[INFO] Copying jetty-xml-9.0.2.v20130417.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\jetty-xml-9.0.2.v20130417.jar
[INFO] Copying javax.servlet-3.0.0.v201112011016.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\javax.servlet-3.0.0.v201112011016.jar
[INFO] Copying slf4j-api-1.7.7.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\slf4j-api-1.7.7.jar
[INFO] Copying slf4j-simple-1.7.7.jar to d:\MyProjects\jwrapper\bloggg\sparkblog\target\dependency\slf4j-simple-1.7.7.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.978s
[INFO] Finished at: Thu Feb 12 18:22:54 EET 2015
[INFO] Final Memory: 9M/243M
[INFO] ------------------------------------------------------------------------

Now, we can reuse the provided above script or use the provided below one-liner:

java -cp target\classes;target\dependency\* com.halyph.blog.App 9090 one 1 2

This one-liner is very simple and can be transformed to shell/batch scripts depending on the level of re-use you’d like to implement.

 <build>
    <plugins>
     ...
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>appassembler-maven-plugin</artifactId>
        <version>1.9</version>
        <!--
        This (executions) section can be omitted.
        In case it's omitted we should call the next command to generate wrapper:
        mvn package appassembler:assemble
        -->
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>assemble</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <programs>
            <program>
              <mainClass>com.halyph.blog.App</mainClass>
              <id>app</id>
            </program>
          </programs>
        </configuration>
      </plugin>
    </plugins>
  </build>

The Application Assembler Plugin is a Maven plugin for generating scripts for starting java applications. All dependencies and the artifact of the project itself are placed in a generated Maven repository in a defined assemble directory. All artifacts (dependencies + the artifact from the project) are added to the classpath in the generated bin scripts.

Maven Application Assembler Plugin usage:

$ mvn package
$ target/appassembler/bin/app
  • All dependencies and the artifact itself are placed in the defined assemble directory (defaults to $project.build.directory/appassembler).

  • A bin/ directory is created in the assemble directory and the generated bin scripts are placed in that directory (defaults to both unix shell scripts and Windows bat files).

Note: Maven Application Assembler Plugin have a lot of customization options, just check the documentation.

Summary

Here was shown that using such tool as Maven you can be productive and “agile” (use Gradle if you’d like to be in trend):

Yes, it’s not a simple write-one Java “script”, but it’s flexible enough to feel like it is.

References


comments powered by Disqus