Reading from a file within a JAR

Photo by Tom Jur on Unsplash

Reading from a file within a JAR

TL;DR:
When reading resources from a file in Java it is better
to use an `InputStream` rather than a `File`.
Using `InputStream` allows you to read content
regardless of where the resource file is located.

Recently in one of the projects I'm working on, we had the problem that we wanted to use a CSV file to read some mapping content. The content of the file was a large number of lines with 2 entries per line. The first entry contains a value to map from (lookup value) and the second entry the value to map to.

The idea was to read the content of the file at application startup and then hold it in a hashmap. So whenever there would be changes to the file content necessary, we only had to exchange the file and restart the application.

The file was placed within the /src/main/resources directory of the project. Well, maybe not the first choice of directory for a file you might want to exchange later, but that thought was pushed aside for the moment.

Since we develop using TDD principles, we also wrote tests to see, that the content was correctly read into the hashmap. Since the application is a SpringBoot application, we not only had plain Java tests but also tests annotated with @SpringBootTest. These kinds of tests start the whole Spring application context and run the tests within this context.

Here is the sample code, which reads the content of a file at application startup from a file within the classpath.

@SpringBootApplication
public class ReadingFromFileWithinJarApplication {
    private static final Logger LOGGER = LoggerFactory.getLogger(ReadingFromFileWithinJarApplication.class);

    private final ReadMappingsService service;

    public ReadingFromFileWithinJarApplication(ReadMappingsService service) throws IOException {
        this.service = service;
        service.readMappingFile();
    }

    public static void main(String[] args) throws IOException {
        SpringApplication.run(ReadingFromFileWithinJarApplication.class, args);
    }

    @Service
    static class ReadMappingsService {
        public String readMappingFile() throws IOException {
            final var file = ResourceUtils.getFile("classpath:mappings.csv");
            final var fileContent = Files.readAllLines(Path.of(file.getPath()));
            final var fileContentAsString = String.join("\n", fileContent);
            LOGGER.info(fileContentAsString);
            return fileContentAsString;
        }
    }
}

And here is the test code:

@SpringBootTest
class ReadingFromFileWithinJarApplicationTests {

    @Autowired
    ReadingFromFileWithinJarApplication.ReadMappingsService service;

    @Test
    void contextLoads() {
    }

    @Test
    void shouldReadFromMappingsFile() throws IOException {
        final var mappingsFileContent = service.readMappingFile();
        assertThat(mappingsFileContent).isNotBlank();
    }
}

Using IntelliJ:

  • the tests are green

  • a call to mvn clean verify is also successful, also when called from Command Line

  • starting the application with the help of IntelliJ's Run Configuration starts it just fine

But starting the application from Command Line with java -jar target/ReadingFromFileWithinJar-0.0.1-SNAPSHOT.jar --> BOOM, it crashes.

How can that be?

So what does the error message tell me?
FileNotFoundException

Caused by: java.io.FileNotFoundException: class path resource [mappings.csv]
cannot be resolved to absolute file path because it does not reside in the file system: jar:file
<absolutePathToFile>/ReadingFromFileWithinJar/ReadingFromFileWithinJar/target/ReadingFromFileWithinJar-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/mappings.csv

Navigating to the path shown in the error message, shows, that the file is there. It exists! But somehow it cannot be "found"? It's there! When I can find it, so should the program!

After a bit of research (Stackoverflow is your reliable friend) it dawned on me.
https://stackoverflow.com/questions/25869428/classpath-resource-not-found-when-running-as-jar

As you can see in the sample code, we used Java's File class to read the content from the file.

And Andy Wilkinson's answer states it very clearly:
"resource.getFile() expects the resource itself to be available on the file system, i.e. it can't be nested inside a jar file. This is why it works when you run your application in STS (Spring Tool Suite) but doesn't work once you've built your application and run it from the executable jar. Rather than using getFile() to access the resource's contents, I'd recommend using getInputStream() instead. That'll allow you to read the resource's content regardless of where it's located."

Java's File class uses the "physical" space of the underlying operating system.

"Instances of this class may or may not denote an actual file-system object such as a file or a directory." (https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/File.html)

But since the file we wanted to read from was part of the application's JAR file (which is a file itself), it is not possible to use a Java File class to read from a file within a JAR file.
If the file we want to read from was outside of the application's JAR file, it would be no problem.

So what is the solution to this problem? Well, there are at least two options.

  • As I just mentioned - put the file to read from outside the application's JAR file. In this case, Java's File class can be used.

  • If the previous is not an option, then you are left with using Java's InputStream to read the content from the file.

Once we refactored the source code using an InputStream everything worked like a charm. All the tests were still green, even though I had not touched them.

@Service
static class ReadMappingsService {
        public String readMappingFile() throws IOException {
            try (var inputStream = new DefaultResourceLoader().getResource("classpath:mappings.csv").getInputStream();
                 var bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
                var fileContentAsString = bufferedReader.lines().collect(Collectors.joining("\n"));
                LOGGER.info(fileContentAsString);
                return fileContentAsString;
            }
        }
    }

Finally, we ended up using a third option. Since the large file would not change as often as presumed, we put the content into a static HashMap inside the program. But this might not be possible in every case.

You can find the (not working) source, as well as the working one on Github.