jPOS GenericPackager throws java.lang.NullPointerException from JAR but not in IDE (with RCA and Solution)

135 views Asked by At

Steps to recreate the issue

  1. Run mvn clean package (This will create the jar)
  2. Run the app from IsoPackagerExceptionApplication class in an IDE (This works fine)
  3. Run the jar using java -jar target/ISOPackagerException-0.0.1-SNAPSHOT.jar (This throws the error)
  4. Use Postman or cURL curl --location --request POST 'http://localhost:8123/test'

Running JAR Exception Stacktrace


org.jpos.iso.ISOException: java.lang.NullPointerException
        at org.jpos.iso.packager.GenericPackager.readFile(GenericPackager.java:224) ~[jpos-2.1.8.jar!/:2.1.8]
        at org.jpos.iso.packager.GenericPackager.<init>(GenericPackager.java:126) ~[jpos-2.1.8.jar!/:2.1.8]
        at com.example.ISOPackagerException.controller.Controller.lambda$testMethod$0(Controller.java:52) ~[classes!/:0.0.1-SNAPSHOT]
        

The Main Code (You can create any SpringBoot app and add this)

package com.example.ISOPackagerException.controller;

import org.jpos.iso.packager.GenericPackager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.io.InputStream;
import java.util.concurrent.CompletableFuture;

@RestController
@RequestMapping(value = "/test")
public class Controller {

    private static final Logger logger = LoggerFactory.getLogger(Controller.class);

    @PostMapping
    @ResponseStatus(HttpStatus.OK)
    public void test() {
        testMethod();
    }

    public void testMethod() {
        logger.info("Testing GenericPackager1 (This works fine)");
        try {
            InputStream inputStream = getClass().getResourceAsStream("/some.xml");
            if (inputStream == null) {
                logger.error("GenericPackager1 inputStream is null");
            } else {
                logger.info("GenericPackager1 inputStream is NOT null");
                GenericPackager genericPackager = new GenericPackager(inputStream);
                logger.info("Successful GenericPackager1: " + genericPackager);
            }
        } catch (Exception e) {
            logger.error("GenericPackager1 error", e);
        }

        createCompletableFutureRun(() -> {
            logger.info("Testing GenericPackager2 in CompletableFuture (This throws the NPE when running from JAR)");
            try {
                InputStream inputStream = getClass().getResourceAsStream("/some.xml");
                if (inputStream == null) {
                    logger.error("GenericPackager2 inputStream is null");
                } else {
                    logger.info("GenericPackager2 inputStream is NOT null");
                    GenericPackager genericPackager = new GenericPackager(inputStream);
                    logger.info("Successful GenericPackager2 in CompletableFuture: " + genericPackager);
                }
            } catch (Exception e) {
                logger.error("GenericPackager2 error", e);
            }
        });
    }

    private CompletableFuture<Void> createCompletableFutureRun(Runnable runnable) {
        return CompletableFuture.runAsync(runnable);
        // Solution
        // return CompletableFuture.runAsync(runnable, Executors.newFixedThreadPool(1));
    }
}
2

There are 2 answers

0
Alarka Sanyal On

RCA

We are using the jPOS framework, and in the packaging class, it loads the resources from the thread where the request came from here. Now, when we use just

CompletableFuture.runAsync(runnable);

it uses the internal ForkJoinPool, and the ContextClassLoader for that is set to the system class loader (i.e., the class loader that loaded the Java runtime itself) by default. And that is why it is unable to find the resources, causing the org.jpos.iso.ISOException: java.lang.NullPointerException But when we use

CompletableFuture.runAsync(runnable, Executors.newFixedThreadPool(n));

we are creating a new thread pool, and that means these threads will be using the thread context class loader of the thread that creates the thread pool, i.e. the main thread and will use the same class loader as the main one, which helps it finding the resources properly.

IntelliJ however is probably caching the resources or class loader, which is why the issue was not identified when running in our local.

2
apr On

You can use code like this:

GenericPackager p = new GenericPackager("jar:path/to/some.xml");

jPOS understand the prefix jar: and takes care of defining an InputStream within your classpath. We usually place packager definitions in src/main/resources/packager within the classpath. You can try with

GenericPackager p = new GenericPackager("jar:packager/cmf.xml");

(CMF is the jPOS Common Message Format defined in http://jpos.org/doc/jPOS-CMF.pdf).