Introduction to Shell script packaging of Java programs

  • 2020-04-01 03:57:44
  • OfStack

In many Java projects, it is common to see wrapper shell scripts that invoke Java commands with program custom arguments. For example,
 


$ANT_HOME/bin/ant, $GROOVY_HOME/bin/groovy

Even in our TimeMachine Scheduler program
 


$TIMEMACHINE_HOME/bin/scheduler.sh

Writing these wrapping scripts is tedious and error-prone. Most of the problems come from setting the classpath right for your application. If you are working on an internal project for a company, you may be able to stay away from tangled paths and environment variables. But for open source projects, people need to make the packaging more flexible and universal. Most even provide a.bat version. Windows DOS is really a brutal and restricted terminal that doesn't work well for your project scripting needs. Therefore, I always encourage others to use Cygwi as much as possible. At least it has a real bash shell. Another common problem is that the packaging can quickly get out of control and there are many redundant scripts throughout your project.

Run-introduction to Java wrapper scripts

If you look at the $timemachine_hom/bin/scheduler.sh code, you'll see that it actually loops through the run-java script in the same directory.
 


DIR=$(dirname $0)
SCHEDULER_HOME=$DIR/..
$DIR/run-java -Dscheduler.home="$SCHEDULER_HOME" timemachine.scheduler.tool.SchedulerServer "$@"

As you can see, our run-java can use the -d option, and not only that, it can also use the -cp option! Also, you can specify these options after the main class! This allows run-java to be wrapped by other scripts and still add additional system properties and classpath.

For example, TimeMachine comes with the Groovy library, so you can simply call it like this
 


groovy : $TIMEMACHINE_HOME/bin/run-java groovy.ui.GroovyMain test.groovy

Without having to download the entire branch again.

You can easily use it in any directory, it confirms its directory and then automatically loads any jars in the lib directory. Now if you want to attach more jars to run Groovy, you can use the -cp option as follows:
 


$TIMEMACHINE_HOME/bin/run-java -cp "$HOME/apps/my-app/lib/*" groovy.ui.GroovyMain test.groovy
Usually if you don't set the Java classpath carefully it will cause errors, but with run-java you can run it up front once:
 

RUN_JAVA_DRY=1 $TIMEMACHINE_HOME/bin/run-java -cp "$HOME/apps/my-app/lib/*" groovy.ui.GroovyMain test.groovy

All you need to do is run the entire line in the command prompt. It outputs the full Java command with all the options and parameters.

Run-script also contains many other options that you can see by looking at its comments. The current script can be run on any Linux bash and Windows Cygwin.


Use run-java through Maven in development

Based on the example mentioned above, assume that the project release structure is as follows:


$TIMEMACHINE_HOME
 +- bin/run-java
 +- lib/*.jar

But what does the directory look like during development? A common use case is that you want to be able to run the newly compiled code under target/classes instead of packaging or publishing the entire project. You can also use run-java in this case. First, simply add bin/run-java to your project and run
 


mvn compile dependency:copy-dependencies

All jar files will be generated under target/dependency. That's all you have to do. Run-java will automatically detect these directories and create the correct classpath for your main class.

If you use Eclipse for development, your target/classes directory will always be up to date, and run-java will be the gem of your project.

Gets the run-java wrapper script
 


#!/usr/bin/env bash
#
# Copyright 2012 Zemian Deng
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
 
# A wrapper script that run any Java6 application in unix/cygwin env.
#
# This script is assumed to be located in an application's "bin" directory. It will
# auto resolve any symbolic link and always run in relative to this application
# directory (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 this 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_PARSE=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 "C:appslibjunit4.8.2*"
#
# 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:=/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 $CYGWIN; then
    # Provide Windows directory conversion
    JAVA_HOME_WIN=$(cygpath -aw "$JAVA_HOME")
    APP_DIR_WIN=$(cygpath -aw "$APP_DIR")
 
    if [ -d "$APP_DIR_WINconfig" ]; then RUN_JAVA_CP="$RUN_JAVA_CP;$APP_DIR_WINconfig" ; fi
    if [ -d "$APP_DIR_WINtargettest-classes" ]; then RUN_JAVA_CP="$RUN_JAVA_CP;$APP_DIR_WINtargettest-classes" ; fi
    if [ -d "$APP_DIR_WINtargetclasses" ]; then RUN_JAVA_CP="$RUN_JAVA_CP;$APP_DIR_WINtargetclasses" ; fi
    if [ -d "$APP_DIR_WINtargetdependency" ]; then RUN_JAVA_CP="$RUN_JAVA_CP;$APP_DIR_WINtargetdependency*" ; fi
    if [ -d "$APP_DIR_WINlib" ]; then RUN_JAVA_CP="$RUN_JAVA_CP;$APP_DIR_WINlib*" ; fi
  else
    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
fi
 
# 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=
  declare -a NEW_ARGS
  IDX=0
   
  # Parse all arguments and look for "-cp" and "-D"
  for ARG in "$@"; do
    if [[ -n $FOUND_CP ]]; then 
      if [ "$OS" = "Windows_NT" ]; then
        # Can't use cygpath here, because cygpath will auto expand "*", which we do not
        # want. User will just have to use OS path when specifying "-cp" option.  
        #ARG=$(cygpath -w -a $ARG)
        RUN_JAVA_CP="$RUN_JAVA_CP;$ARG"
      else
        RUN_JAVA_CP="$RUN_JAVA_CP:$ARG"
      fi
      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
     
  # Display full Java command.
  if [ -n "$RUN_JAVA_DEBUG" ] || [ -n "$RUN_JAVA_DRY" ]; then
    echo "$JAVA_CMD" $RUN_JAVA_OPTS -cp "$RUN_JAVA_CP" "${NEW_ARGS[@]}"
  fi
   
  # Run Java Main class using parsed variables
  if [ -z "$RUN_JAVA_DRY" ]; then
    "$JAVA_CMD" $RUN_JAVA_OPTS -cp "$RUN_JAVA_CP" "${NEW_ARGS[@]}"
  fi
else
  # Display full Java command.
  if [ -n "$RUN_JAVA_DEBUG" ] || [ -n "$RUN_JAVA_DRY" ]; then
    echo "$JAVA_CMD" $RUN_JAVA_OPTS -cp "$RUN_JAVA_CP" "$@"
  fi
   
  # Run Java Main class
  if [ -z "$RUN_JAVA_DRY" ]; then
    "$JAVA_CMD" $RUN_JAVA_OPTS -cp "$RUN_JAVA_CP" "$@"
  fi
fi


Related articles: