Steps to recreate the issue
- Run
mvn clean package (This will create the jar)
- Run the app from IsoPackagerExceptionApplication class in an IDE (This works fine)
- Run the jar using
java -jar target/ISOPackagerException-0.0.1-SNAPSHOT.jar (This throws the error)
- 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));
}
}
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
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.NullPointerExceptionBut when we usewe 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.