Packaging and logging a Java application
27th Apr 2020 by Aneesh Mistry

Key Takeaways
• Maven-assembly-plugin enables us to package Maven dependencies into an executable JAR file.
• We can use Log4J to debug and understand the runtime context of our application as it is run from a server.
• There are many ways that we can configure the format and layout of our logs to enhance readability.

Running a Java application with a JAR

I have recently been developing applications that perform small and simple tasks to eliminate the repetitive work from my daily activity. The applications are packaged into an executable JAR file and run on a rota through a batch job.
Each time I create an application, I repeatedly deploy a similar project structure that allows me to include dependencies within the JAR and external logging of the runtime. The JAR is designed to be run from a server and will provide debugging and context in the form of logging to the runtime execution.

We will begin by running a simple Java class from the command line:

public class Example{

public static void main(String[] args){

    System.out.println("Hello from your Java application");
    }
}

Once we have navigated to the directory of the class, we can compile and execute the main thread with the following lines:

javac Example.java

java Example
Hello from your Java application.

We can achieve the same result if we package our class into an executable JAR and run it from the command line.
Using IntelliJ, you can package your application with the following steps:

1. Select File > Project Structure
2. Select Artifacts > +
3. Select JAR > From modules with dependencies
4. Select Example Class (or class with the main thread) > OK > OK.
5. Select Build > Build Artifacts... > Build

IntelliJ will create a new Out directory in your project structure that will contain the JAR file.
IntelliJ will also create a new folder and file in the src folder: src/META-INF/MANIFEST.MF
The Manifest file contains information about the project structure; the JAR will reference the Manifest file to locate the class with the main thread, thus making the JAR executable.
If we return to the command line, we can now run our class through the JAR file that has been created in the out/artifacts/{project name}_jar/ directory:

java -jar example.jar
Hello from your Java application.


Injecting dependencies into a JAR file

Our current JAR file will compile the classes and run the main thread as per the Manifest file.
I will now introduce a new Class that uses the jackson.databind dependency to convert a JSON file to and from a POJO.
The purpose of this example is to introduce a dependency into our application; the main thread will create a POJO and return the 'firstName' property as per the JSON file.
The pom.xml will include a new dependency:

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.11.0.rc1</version>
    </dependency>
</dependencies>

The main method is also updated to use the dependency:

    public static void main(String[] args) throws IOException {

        ObjectMapper mapper = new ObjectMapper();

        Student student = mapper.readValue(
                new File("data/student_a.json"), Student.class);

        System.out.println("First name: " + student.getFirstName());

    }

If we attempt to rebuild and run our JAR file with the new dependencies, we will receive the following error message from the command line:

no main manifest attribute

The error arises as the JAR file has not been generated through Maven and does not reference the dependencies.
To resolve the problem, we can use maven-assembly-plugin from within our pom.xml file to include the dependencies and reference the location of the Manifest file:

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <configuration>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifestFile>src/main/java/META-INF/MANIFEST.MF</manifestFile>
        </archive>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Within maven-assembly-plugin, there are a few key values that set the configuration of the JAR file creation:
We define descriptorRef to the value 'jar-with-dependencies'.
The configuration of the descriptorRef will instruct Maven to create a JAR that contains the unpacked dependencies. The descriptorRef will subsequently append 'jar-with-dependencies' to our JAR filename.
The execution block ensures the maven-assembly-plugin is included during the Maven build lifecycle. Therefore, the JAR file will be updated upon each build.
The Manifest file is defined within the manifestFile property of the archive configuration; the JAR can now find and use the Manifest file properties.
Once we have configured our pom.xml file, we can build our new JAR by selecting Maven > Install in the Maven Lifecycle menu on the right side of the IDE.

Maven Install will build a new JAR in the target directory with the dependencies included.
Before we run the new JAR file, it is important to remember that any relative file locations you may specify in the application (such as a properties file) must also be moved relative to where the JAR file is located, otherwise the JAR will not be able to resolve the location.
If we navigate to the target directory at the command line, we can run the JAR file to successfully include the dependencies and print the result:

java -jar example-1.0-SNAPSHOT-jar-with-dependencies.jar
First name: Alice

Introducing logging for our packaged application

Running our packaged application with Maven dependencies is the first step to creating simple and transparent applications, however in a production environment we may find some problems...
• What happens if the application throws an exception?
• How can we verify that the application is working as expected?
• How can we debug our application if any unexpected results occur?
The concerns outlined above can be resolved with logging which will provide us with a view into the runtime activity of our application.
Logging will create a persisted document from the application that can be referenced for debugging or runtime information. The log file can be generated at custom intervals such as each day, each hour, or each execution.

Log4J

Log4J is a popular package that we can implement into our application to provide a detailed logging context.
We will register Log4J to our application and append records to it throughout the life of the application. Each record will include information or debugging context from the application.
We can format the log files to be conveniently stored alongside the JAR file in a separate directory, named with the time and date for ease of access. Log4J will be included in our application with the following Maven dependency in our pom.xml file:

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

The runtime configuration for the log is defined in the properties file log4j.properties. Properties may include details such as where we log our files to and the format of the records:

log4j.rootLogger=DEBUG, consoleAppender, fileAppender

log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.consoleAppender.layout.ConversionPattern=[%t] %-5p %c %x - %m%n

log4j.appender.fileAppender=org.apache.log4j.RollingFileAppender
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.fileAppender.layout.ConversionPattern=[%t] %-5p %c %x - %m%n
log4j.appender.fileAppender.File=log/${current.date.time}_Application.log

The above file makes use of the Log4J interfaces to define and create two different logs.
On line 1, the rootLogger defines variables that we will use to assign different logs.
From line 3 onwards, we use the log4j.appender interface to create and design the layout of our logs:
On lines 3 to 5, we are defining a pattern layout for a log to the console.
On lines 7 to 10, we are creating a File for each log. The format for the log is also defined and the creation of the log will append the current date and time of the machine to the filename.

Once we have the properties configured, we can implement Log4J in our main class with a new static method and two lines to instantiate the logger and reference the properties file:

 static{
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMdd-hhmmss");
        System.setProperty("current.date.time", dateFormat.format(new Date()));
    }

    static Logger logger = Logger.getLogger(Example.class);

    public static void main(String[] args){

        PropertyConfigurator.configure("log4j.properties");
    
    ...

The static block at the top of the main class will set the system property we define as current.date.time.
You may recognise the system property from the properties file as it is used to append the date and time for log filenames.
On line 6, we create an instance of the logger. As we may create many different loggers in our application, we name our logger after the class. Using the class name helps us to avoid name duplication and also provides descriptive detail in the log file for which logger within the application is being used.
On line 10 we use the PropertyConfigurator which allows our logger to be configured by an external file.

Now that the logger is successfully configured to the application, we can begin writing logger records that will provide information and debugging context to our log file:

 
 //inside the main method

logger.info("Starting the application");

 ...

 catch(IOException e){
            logger.fatal("Unable to find the json file");
        }

The output of our log file from a successful application run will appear as:

[main]  INFO    Application     - Starting the application

However, if an exception is thrown, we may see more detail:
[main]  INFO    Application     - Starting the application
[main]  FATAL   Application     - Unable to find the JSON file

Conclusion

Packaging and logging are both frequent utilities to implement in our applications; they are two important foundations to any new application we build to enable transparency and ease of use to the application.
The example we have used has only touched upon the various message types and formatting that is available with Log4J. I would encourage you to explore Log4J functionality here to enhance your logging to be succinct and effective.
If you would like to implement logging across your application with enhanced control and structure, please visit my blog on aspect-orientated-programming here.
Finally, you can find the source code from this blog via GitHub here.


Share this post