Loading Resources from Classpath
Java applications frequently bundle non-code files (configuration, SQL scripts, templates, JSON schemas, test fixtures) inside the JAR. These files live in src/main/resources (or src/test/resources for tests) and are accessed at runtime through the classpath using getResourceAsStream(). This article explains the two approaches (Class vs ClassLoader), what the leading / means, how to read the stream as a String, Properties, or line-by-line, and how resource loading differs between tests and production code.
Contents
getClass().getResourceAsStream()
getClass().getResourceAsStream(name)com.example.MyClasscom/example//null
// Class.getResourceAsStream(name) resolves the name RELATIVE to the class's package
// unless the name starts with /, in which case it is treated as absolute from classpath root
// Project layout example:
// src/main/java/com/example/app/MyService.java
// src/main/resources/com/example/app/template.html ← package-relative
// src/main/resources/config/settings.properties ← root-relative
public class MyService {
// Package-relative — no leading slash — looks in com/example/app/
void loadPackageResource() throws IOException {
try (InputStream is = getClass().getResourceAsStream("template.html")) {
if (is == null) throw new IllegalStateException(
"template.html not found in com/example/app/");
String html = new String(is.readAllBytes(), StandardCharsets.UTF_8);
System.out.println(html);
}
}
// Absolute — leading / — looks from classpath root
void loadRootResource() throws IOException {
try (InputStream is = getClass().getResourceAsStream("/config/settings.properties")) {
if (is == null) throw new IllegalStateException(
"config/settings.properties not found on classpath");
Properties props = new Properties();
props.load(new InputStreamReader(is, StandardCharsets.UTF_8));
System.out.println(props.getProperty("app.name"));
}
}
// Static context — Class reference required (no getClass() in static methods)
static void loadFromStatic() throws IOException {
try (InputStream is = MyService.class.getResourceAsStream("/data/seed.sql")) {
if (is == null) throw new IllegalStateException("seed.sql not found");
String sql = new String(is.readAllBytes(), StandardCharsets.UTF_8);
System.out.println(sql);
}
}
}
// getResource(name) — returns a URL instead of InputStream
// Useful when you need to get the path (e.g., for passing to libraries)
java.net.URL url = MyService.class.getResource("/config/settings.properties");
System.out.println(url); // jar:file:/app.jar!/config/settings.properties
getResourceAsStream()nullnullnullProperties.load()InputStreamReaderNullPointerExceptionClassLoader.getResourceAsStream()
ClassLoader.getResourceAsStream(name)Class.getResourceAsStream()ClassLoaderThread.currentThread().getContextClassLoader()
// ClassLoader.getResourceAsStream(name) always treats the name as ABSOLUTE
// from the classpath root — no leading / needed (or allowed)
// Three ways to get a ClassLoader:
// 1. Thread context ClassLoader — usually best in frameworks and web apps
ClassLoader ctxCL = Thread.currentThread().getContextClassLoader();
// 2. Class's own ClassLoader — works reliably in standalone and modular apps
ClassLoader classCL = MyService.class.getClassLoader();
// 3. System ClassLoader — loads from the system classpath
ClassLoader sysCL = ClassLoader.getSystemClassLoader();
// Load a resource — name is ALWAYS root-relative (no leading /)
try (InputStream is = ctxCL.getResourceAsStream("config/settings.properties")) {
if (is == null) throw new IllegalStateException("settings.properties not found");
Properties props = new Properties();
props.load(new InputStreamReader(is, StandardCharsets.UTF_8));
}
// getResources() — find ALL resources with a given name across all jars
// Useful for implementing plugin mechanisms (e.g., META-INF/services)
Enumeration<java.net.URL> urls = ctxCL.getResources("META-INF/services/com.example.Plugin");
while (urls.hasMoreElements()) {
java.net.URL url = urls.nextElement();
System.out.println("Found provider config: " + url);
}
// Module-aware loading (Java 9+)
// In a module, Class.getResourceAsStream() respects module boundaries.
// If the resource is in an encapsulated package, it may return null
// unless the package is opened or the resource is in the module root.
// For cross-module resource access, use Module.getResourceAsStream():
Module mod = MyService.class.getModule();
try (InputStream is = mod.getResourceAsStream("config/settings.properties")) {
// ...
}
Note: Use Thread.currentThread().getContextClassLoader()MyClass.class.getClassLoader()
Reading a resource as String
The standard pattern for converting a classpath resource into a StringInputStreamReaderBufferedReaderInputStream.readAllBytes()new String(bytes, charset)StandardCharsets.UTF_8
import java.io.*;
import java.nio.charset.StandardCharsets;
// Java 9+: InputStream.readAllBytes() — simplest for small resources
static String readResourceAsString(String path) throws IOException {
try (InputStream is = MyService.class.getResourceAsStream(path)) {
if (is == null) throw new IllegalStateException("Resource not found: " + path);
return new String(is.readAllBytes(), StandardCharsets.UTF_8);
}
}
String template = readResourceAsString("/templates/email.html");
// Java 11+: readAllBytes() + String constructor — same result
try (InputStream is = MyService.class.getResourceAsStream("/data/query.sql")) {
if (is == null) throw new IOException("query.sql not found on classpath");
byte[] bytes = is.readAllBytes();
String sql = new String(bytes, StandardCharsets.UTF_8);
System.out.println(sql);
}
// Using InputStreamReader + BufferedReader + stream (pre-Java 9 compatible)
static String readStream(InputStream is) throws IOException {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n');
}
}
return sb.toString();
}
// Using Scanner — concise one-liner
static String readWithScanner(String path) throws IOException {
try (InputStream is = MyService.class.getResourceAsStream(path)) {
if (is == null) throw new IOException("Not found: " + path);
// \A is a regex for "start of input" — makes Scanner read the whole stream
return new java.util.Scanner(is, StandardCharsets.UTF_8).useDelimiter("\\A").next();
}
}
// Reading a URL (from getResource) via NIO
java.net.URL url = MyService.class.getResource("/schema/api.json");
if (url != null) {
Path path = Path.of(url.toURI()); // only works when NOT inside a JAR
String content = Files.readString(path, StandardCharsets.UTF_8);
}
// Note: Path.of(url.toURI()) fails for resources inside a JAR — use InputStream instead
Reading a resource as Properties
Properties.load()InputStreamReadersrc/main/resourcesload(Reader)load(InputStream)
import java.util.Properties;
import java.io.*;
import java.nio.charset.StandardCharsets;
// Load a .properties file from the classpath root
static Properties loadProperties(String classpathPath) throws IOException {
Properties props = new Properties();
try (InputStream is = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream(classpathPath)) {
if (is == null) throw new IOException("Not found on classpath: " + classpathPath);
props.load(new InputStreamReader(is, StandardCharsets.UTF_8));
}
return props;
}
// Usage
Properties config = loadProperties("config/app.properties");
String host = config.getProperty("db.host", "localhost");
int port = Integer.parseInt(config.getProperty("db.port", "5432"));
// Load defaults + environment-specific overlay
Properties defaults = loadProperties("config/defaults.properties");
String env = System.getProperty("app.env", "dev"); // java -Dapp.env=prod
String envFile = "config/" + env + ".properties";
Properties envProps = new Properties(defaults); // fallback to defaults
try (InputStream is = MyConfig.class.getClassLoader().getResourceAsStream(envFile)) {
if (is != null) {
envProps.load(new InputStreamReader(is, StandardCharsets.UTF_8));
System.out.println("Loaded environment config: " + envFile);
} else {
System.out.println("No environment-specific config found for: " + env);
}
}
// Reading XML properties from classpath
Properties xmlProps = new Properties();
try (InputStream is = MyConfig.class.getResourceAsStream("/config/datasource.xml")) {
if (is == null) throw new IOException("datasource.xml not found");
xmlProps.loadFromXML(is);
}
System.out.println(xmlProps.getProperty("datasource.url"));
// Print all loaded properties (useful for startup logging)
System.out.println("=== Effective Configuration ===");
envProps.stringPropertyNames().stream()
.sorted()
.forEach(k -> {
String v = k.toLowerCase().contains("password") ? "***" : envProps.getProperty(k);
System.out.printf(" %-40s = %s%n", k, v);
});
Reading line-by-line with BufferedReader
For text resources that need to be processed one line at a time (SQL scripts, word lists, CSV files) wrap the InputStreamInputStreamReaderBufferedReaderlines()BufferedReader
import java.io.*;
import java.nio.charset.StandardCharsets;
// BufferedReader approach — good for text files processed line by line
// Read and process each line
try (InputStream is = MyApp.class.getResourceAsStream("/data/cities.txt");
InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr)) {
if (is == null) throw new IOException("cities.txt not found");
String line;
int lineNumber = 0;
while ((line = br.readLine()) != null) {
lineNumber++;
if (line.isBlank() || line.startsWith("#")) continue; // skip comments
System.out.printf("%4d: %s%n", lineNumber, line);
}
}
// Java 8+ — lines() stream from BufferedReader
try (InputStream is = MyApp.class.getResourceAsStream("/data/countries.csv");
BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
List<String[]> records = reader.lines()
.skip(1) // skip header row
.filter(l -> !l.isBlank())
.map(l -> l.split(",", -1))
.collect(Collectors.toList());
for (String[] record : records) {
System.out.printf("Country: %-20s Code: %s%n", record[0], record[1]);
}
}
// Collect all non-blank, non-comment lines as a List
static List<String> readLines(String classpathPath) throws IOException {
try (InputStream is = ResourceLoader.class.getResourceAsStream(classpathPath)) {
if (is == null) throw new IOException("Not found: " + classpathPath);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
return reader.lines()
.map(String::strip)
.filter(l -> !l.isEmpty() && !l.startsWith("#"))
.collect(Collectors.toList());
}
}
}
// Use: List<String> stopWords = readLines("/nlp/stop-words.txt");
The / prefix difference explained
With getClass().getResourceAsStream()"file.txt"com.example.MyClasscom/example/file.txt"/config/file.txt"config/file.txtClassLoader.getResourceAsStream()nullnull
// This is the most common source of confusion with classpath resource loading.
// Class.getResourceAsStream(name)
// WITHOUT leading / → resolved RELATIVE to the class's package
// WITH leading / → resolved from the CLASSPATH ROOT (absolute)
// Example: class is com.example.service.MyService
// Resource file is at: src/main/resources/config/settings.properties
// i.e., on classpath root: config/settings.properties
// Absolute (leading /) — correct regardless of which package the class is in
InputStream is1 = MyService.class.getResourceAsStream("/config/settings.properties");
// Looks for: config/settings.properties from classpath root ✓
// Relative (no /) — resolved from com/example/service/
InputStream is2 = MyService.class.getResourceAsStream("config/settings.properties");
// Looks for: com/example/service/config/settings.properties ✗ (probably not there)
// If the resource is IN THE SAME PACKAGE as the class, relative works naturally:
// Resource: src/main/resources/com/example/service/template.html
InputStream is3 = MyService.class.getResourceAsStream("template.html");
// Looks for: com/example/service/template.html ✓
// ClassLoader.getResourceAsStream(name) — ALWAYS root-relative, NEVER with /
ClassLoader cl = MyService.class.getClassLoader();
InputStream is4 = cl.getResourceAsStream("config/settings.properties"); // ✓ correct
// ✗ null! (leading / is wrong)
InputStream is5 = cl.getResourceAsStream("/config/settings.properties");
// Summary table:
// ┌─────────────────────────────────────┬──────────────────────────────────────────┐
// │ Call │ Resolves to │
// ├─────────────────────────────────────┼──────────────────────────────────────────┤
// │ MyClass.class.getResource("a.txt") │ <package-path>/a.txt (relative) │
// │ MyClass.class.getResource("/a.txt") │ a.txt from classpath root (absolute) │
// │ cl.getResource("a.txt") │ a.txt from classpath root (absolute) │
// │ cl.getResource("/a.txt") │ null — leading / not supported │
// └─────────────────────────────────────┴──────────────────────────────────────────┘
/Class.getResourceAsStream("/path")ClassLoader.getResourceAsStream("path")nullLoading in tests vs production
Both src/main/resourcessrc/test/resourcesInputStreamgetResource().toURI()Pathjar:Path
// Maven project layout:
// src/main/java/ → production code
// src/main/resources/ → production classpath resources
// src/test/java/ → test code
// src/test/resources/ → test classpath resources (added to classpath during tests only)
// In tests: src/test/resources/config/test-db.properties
// In prod: src/main/resources/config/db.properties
// Both are accessed the same way — JVM doesn't know which root they came from
// Test-specific resource — available only in test classpath
@Test
void testLoadConfig() throws IOException {
Properties props = new Properties();
try (InputStream is = getClass().getResourceAsStream("/config/test-db.properties")) {
assertNotNull(is, "test-db.properties must exist in src/test/resources/config/");
props.load(new InputStreamReader(is, StandardCharsets.UTF_8));
}
assertEquals("jdbc:h2:mem:testdb", props.getProperty("db.url"));
}
// Overriding production config in tests — test resource shadows the production resource
// If both src/main/resources/app.properties and src/test/resources/app.properties exist,
// the TEST version takes precedence during test execution (test classpath comes first).
// Resource in test code (JUnit 5 example)
class ResourceTest {
@Test
void parsesCsvCorrectly() throws IOException {
List<String> lines;
try (InputStream is = ResourceTest.class.getResourceAsStream("/fixtures/data.csv");
BufferedReader reader = new BufferedReader(
new InputStreamReader(is, StandardCharsets.UTF_8))) {
lines = reader.lines().collect(Collectors.toList());
}
assertEquals(5, lines.size());
}
}
// Useful utility for tests — get the resource URL as a File path
// (only works when NOT running from inside a JAR, i.e., in normal test execution)
static Path resourceToPath(String classpathPath) throws Exception {
java.net.URL url = ResourceTest.class.getResource(classpathPath);
if (url == null) throw new IllegalArgumentException("Not found: " + classpathPath);
return Path.of(url.toURI());
}
// Usage in a test:
Path csvFile = resourceToPath("/fixtures/data.csv");
List<String> rows = Files.readAllLines(csvFile, StandardCharsets.UTF_8);
// Production code — never use toURI() + Path, always use InputStream
// The Path-based approach FAILS when the app runs from a JAR because
// jar: URLs cannot be converted to filesystem Paths.
// Always prefer: is.readAllBytes() or BufferedReader(InputStreamReader(is))
InputStreamjava.nio.file.PathgetResource().toURI()jar:PathFileSystem