1. Reading Resource as part of application reliability
Resources are already arranged in folders and wired in as normal dependencies. Now only the most down-to-earth part remains: read them so that the application either gets valid text or fails immediately and clearly.
For mandatory templates in ContextFlow, there is one good scenario: the resource is found, read, and produces valid text. The option “did not work — let’s send an empty string” is worse than any loud error here, because the breakage moves deep into the business scenario.
To feel the contrast, let’s remember the most fragile variant, which often “just happens to work” in the IDE:
import java.nio.file.Files;
import java.nio.file.Path;
// WARNING: the relative path is tied to the current working directory.
// "Works" in the IDE — and breaks after packaging into a jar or when launched from somewhere else.
String text = Files.readString(Path.of(
"src/main/resources/templates/notifications/order-created.txt"
)); // "works" while launched from the project root
The problem is not Files.readString() itself. The problem is that this code is tied to the source tree and the working directory. So from here on, we build a different contract: a mandatory template is either loaded successfully at startup, or the application honestly stops with a clear reason.
2. Fail-fast when loading templates
Since resources have already become dependencies, it also makes sense to decide when to read them at the container level. In infrastructure code, the “fail-fast” approach is almost always better: if a critical resource is missing, we do not pretend everything is fine; we honestly stop context creation. That sounds strict, but in practice it saves hours of time. And yes, it is very Spring-like: the container is allowed to fail at startup precisely because that is more useful than hunting problems later in a random place.
In our project, this naturally connects to the lifecycle topic we already covered. @PostConstruct is the ideal point to check that templates exist and load them into memory. Then the error appears at ApplicationContext startup, not when the user has already “created an order” and is waiting for a notification.
A small diagram of what we are aiming for:
flowchart TD
A[Spring creates bean NotificationTemplateCatalog] --> B["@PostConstruct loadTemplates()"]
B --> C{"Resource exists()?"}
C -- no --> D["IllegalStateException with getDescription()"]
C -- yes --> E["getInputStream + readAllBytes"]
E --> F[Template saved in field/cache]
F --> G[Service uses ready-made string without I/O]
And here is the minimal “skeleton” of the idea in code (for now, without reading details):
import jakarta.annotation.PostConstruct;
@PostConstruct
void loadTemplates() {
// Called once at context startup — a convenient place for fail-fast checks.
// 1) check that resources exist
// 2) read into strings
// 3) save into fields (cache)
}
An important methodological point: we do not read the template on every notification send. A template is part of the application configuration, not “user data”, so it makes sense to load it once and use it many times. That is both faster and more predictable: you do not depend on I/O at the moment the business scenario executes.
3. Resource methods for diagnostics
When you are just getting started, it is easy to drown in the API: Resource has many methods, and each implementation has even more nuances. What we need now is a small “diagnostic set” that helps make decisions and write useful error messages. In a learning project, it is better to master 4-5 methods and use them consistently than to know 20 methods and choose at random every time.
Below is a compact table of what is most often useful for our task specifically (templates and short text snippets):
| Resource method | Why it is needed | How we use it in ContextFlow |
|---|---|---|
| exists() | Check whether the resource was found | Before reading a required template |
| isReadable() | Sometimes the resource exists, but cannot be read | Additional protection, especially for file: |
| getInputStream() | A universal way to read content | Teaching default for reading text |
| getDescription() | Human-readable resource description | Always add it to the error message |
| getFilename() | File name, if there is one | Sometimes useful for logs/messages, but not as a logic anchor |
Let’s immediately “feel” the benefit of getDescription(). This is the case where Spring directly helps you write sensible errors:
import org.springframework.core.io.Resource;
String describe(Resource r) {
// Great for exceptions and logs: Spring itself forms a clear description.
return r.getDescription(); // for example: "class path resource [templates/.../order-created.txt]"
}
If you later see an exception with this string, you almost instantly understand exactly what was being looked for and where. That is much better than “File not found” without context, where you then spend 20 minutes remembering: “which file was it again?”
4. getInputStream instead of getFile
At this point, many beginners (and, frankly, some adult developers after a hard week) make the same mistake: they try to turn a Resource into a File and then keep living as before. In their mind, it goes like this: “well, if it is a resource, then it must have a file.” But a classpath resource is not necessarily a file, especially when the application is packaged into a jar.
Here is an example of code that looks plausible, but may break unexpectedly:
import java.nio.file.Files;
import org.springframework.core.io.Resource;
String readViaFile(Resource resource) throws Exception {
// Dangerous: for a classpath resource inside a jar, there may be no physical file.
return Files.readString(resource.getFile().toPath()); // may fail for classpath inside a jar
}
Why? Because for a resource “inside a jar”, there may be no physical file in the filesystem. There is a “piece of archive”, not a path on disk. So for universal reading, Spring gives us a much more honest API: getInputStream().
Here is a short and correct way to read text (for small templates, this is an excellent teaching default):
import java.nio.charset.StandardCharsets;
import org.springframework.core.io.Resource;
String readUtf8(Resource resource) throws Exception {
// Universal: works for classpath, file:, and URL resources.
try (var in = resource.getInputStream()) { // try-with-resources guarantees the stream is closed
// For small templates, readAllBytes() is simple and transparent.
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
There are two important bonuses here. First, we are not tied to the filesystem: the stream can be opened from classpath, URL, or anywhere else. Second, try-with-resources guarantees that the stream is closed. A stream is like a water tap: if you open it and do not close it, “leaks” may not be visible right away, but over time they start to bite (especially when there are many such reads).
5. Helper ResourceTextReader
If in two or three classes you start manually writing exists(), try-with-resources, new String(...), catch(IOException e), and error messages — copy-paste will appear very quickly. And copy-paste in infrastructure is unpleasant because it multiplies “different behavior standards”: one class crashes, another is silent, a third returns an empty string, a fourth logs to System.out.println (yes, that is also a sin, just a milder one).
So the sensible step is to move reading into a small helper that becomes the single way to work with text resources in ContextFlow. It should not be a “universal resource engine”; we are not building a spaceship. We need a neat, understandable, and reusable building block.
Let’s start with a fail-fast check:
import org.springframework.core.io.Resource;
public void requireExists(Resource resource) {
// Fail-fast: if a required resource is missing, it is better to fail at startup with a clear reason.
if (!resource.exists()) {
throw new IllegalStateException(
"Resource not found: " + resource.getDescription()
);
}
}
Now let’s add reading and a proper IOException wrapper into a runtime exception so we do not drag checked exceptions through all services:
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.springframework.core.io.Resource;
public String readRequiredUtf8(Resource resource) {
requireExists(resource);
try (var in = resource.getInputStream()) {
// Explicitly fix the charset so behavior does not depend on the platform.
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
// Important point: do not swallow the error; add context and rethrow as a runtime exception.
throw new IllegalStateException(
"Cannot read resource: " + resource.getDescription(), e
);
}
}
Notice that we do not “swallow” the error. We rethrow it, adding context. That is what proper diagnostics look like: the error is not hidden, but becomes clearer.
In a real project, this helper is convenient to make into a Spring bean so it can be injected into template catalogs and other infrastructure components:
import org.springframework.stereotype.Component;
@Component
public class ResourceTextReader {
// methods requireExists(...) and readRequiredUtf8(...)
}
6. Notification templates in ContextFlow
Now let’s assemble a working ContextFlow example where the template catalog does three things at once: keeps references to resources, reads them fail-fast at startup, and gives business services ready-made render...() methods.
Suppose the following files live in src/main/resources:
src/main/resources/templates/notifications
├─ order-created.txt
└─ order-cancelled.txt
To avoid introducing a separate template language, we keep templates as simple as possible: plain text plus %s, which we later substitute through String.formatted(...).
# order-created.txt
ORDER CREATED: id=%s, customer=%s
# order-cancelled.txt
ORDER CANCELLED: id=%s, reason=%s
Now let’s make a catalog that injects two Resources and reads them in @PostConstruct. To keep wiring simple, you can inject resources via @Value directly with classpath:. That is not “bad hardcoding” if you treat them as part of the application delivery.
Constructor fragment:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
@Component
public class NotificationTemplateCatalog {
public NotificationTemplateCatalog(
// Explicitly tell Spring that this is a classpath resource (not a "file on disk").
@Value("classpath:templates/notifications/order-created.txt") Resource created,
@Value("classpath:templates/notifications/order-cancelled.txt") Resource cancelled) {
// ...
}
}
Now the key part: @PostConstruct and reading through our helper. Let’s split it into small pieces so the code does not look like a wall of text.
Fields:
import org.springframework.core.io.Resource;
private final Resource createdTemplate;
private final Resource cancelledTemplate;
private final ResourceTextReader reader;
private String createdText;
private String cancelledText;
@PostConstruct load:
import jakarta.annotation.PostConstruct;
@PostConstruct
void load() {
createdText = reader.readRequiredUtf8(createdTemplate);
cancelledText = reader.readRequiredUtf8(cancelledTemplate);
}
And methods that return the prepared text. We can build the simplest “template engine” on top of String.formatted() — without libraries and without magic:
public String renderCreated(String orderId, String customerName) {
return createdText.formatted(orderId, customerName);
}
public String renderCancelled(String orderId, String reason) {
return cancelledText.formatted(orderId, reason);
}
This form of catalog is useful here: the class honestly depends on resources, reads them once, and then gives services a simple API: “produce the notification text.” Business services no longer need to know where the files are or how they are read. This is a great example of healthy isolation.
Error diagnostics and the “role” of a resource
An error message is not the place for creativity, but it is the place for being considerate to your future self. “Future you” is usually tired, under deadline pressure, and does not want to solve the quest “what exactly broke?” So we aim for errors that answer three questions: what we looked for, where we looked, and why we failed.
Let’s compare two messages:
Bad (formally true, but useless):
IllegalStateException: Cannot read template
Good (it already contains specifics):
IllegalStateException: Cannot read resource: class path resource [templates/notifications/order-created.txt]
To improve diagnostics one more step, it is sometimes useful to add the “role” of the resource. Not just “did not read the resource”, but “did not read the order creation template”. Then the problem is immediately connected in your mind to the business scenario.
Here is a minimal method that accepts a role and adds it to the exception:
import org.springframework.core.io.Resource;
public String readRequiredUtf8(Resource resource, String role) {
if (!resource.exists()) {
throw new IllegalStateException(
"Missing resource (" + role + "): " + resource.getDescription()
);
}
return readRequiredUtf8(resource); // use the base method
}
The call then becomes self-documenting:
createdText = reader.readRequiredUtf8(createdTemplate, "order-created template");
This technique is very “educationally useful”: it shows that diagnostics are part of design. We are not just reading a file; we are building application support. And yes, support starts with proper exceptions, not heroic debugging on a Friday evening.
NotificationDispatchService uses the catalog
To reinforce that we have really improved the architecture, let’s see how this looks in the notification dispatch service. Before, the temptation to “just read the file right here” could easily appear. Now it simply asks the catalog to generate the text.
Service mini-version:
import org.springframework.stereotype.Service;
@Service
public class NotificationDispatchService {
private final NotificationTemplateCatalog templates;
private final NotificationSender sender;
public NotificationDispatchService(NotificationTemplateCatalog templates,
NotificationSender sender) {
this.templates = templates;
this.sender = sender;
}
}
And the method that sends an order-created notification:
public void notifyOrderCreated(String orderId, String customerName) {
String text = templates.renderCreated(orderId, customerName);
sender.send(text); // sending details are in the NotificationSender implementation
}
If somewhere in the scenario (for example, in ScenarioRunner) you call this, you will get a clear result, and resource reading will happen exactly once at startup. In a good version, scenario code should not have any I/O related to templates at all: it handles business actions, while infrastructure is prepared in advance.
If you want to see it with your own eyes, you can temporarily use ConsoleNotificationSender and see something like:
System.out.println(text); // ORDER CREATED: id=ORD-1, customer=Alice
And a pleasant fact: if you accidentally renamed order-created.txt or placed it in the wrong folder, you will learn about it when the context starts, not “inside” notifyOrderCreated().
Here we work with text as a concrete physical resource: we found the file, read it, and substituted values. When there are more and more texts and they need to be looked up not by path, but by key, locale, and fallback rules, MessageSource appears on top of the same resource foundation.
7. Typical mistakes when reading Resource
Mistake #1: immediately calling getFile on a classpath resource.
This mistake comes from an old habit: “if something looks like a file, then it is a file.” But a classpath resource can live inside a jar, and there may be no physical file. That is why getFile sometimes works in the IDE and suddenly fails after packaging. The teaching default for reading is getInputStream().
Mistake #2: forgetting to close the stream.
resource.getInputStream() returns an InputStream, and a stream is a resource in the literal sense. It must be closed. The simplest and correct way is try-with-resources. If you read resources “manually” and do not close them, you can get strange problems in a long-running application and very unpleasant symptoms that are hard to connect to the root cause.
Mistake #3: swallowing IOException and continuing with an empty template.
Sometimes you want to do it “like in the UI”: if something failed to load, show emptiness. But a notification template or a report title is part of the application configuration, not an optional element. If it is missing, it is better to fail immediately and explain why. Otherwise, you get a “silent breakage”: scenarios keep running, but notifications become meaningless, and debugging turns into ghost hunting.
Mistake #4: writing an error message without getDescription().
The message “Resource not found” without indicating which resource was being looked for helps almost not at all. In Spring, getDescription() usually already contains information about the resource type and its “address” (classpath/file/url). That is free diagnostics — it would be a sin not to use it.
Mistake #5: spreading resource reading across business services.
If OrderPlacementService or NotificationDispatchService start assembling path strings and doing I/O, you end up with “hidden infrastructure” inside business code. Such code is hard to test, hard to change, and hard to read. It is much better to keep resource logic in one infrastructure/helper bean (for example, NotificationTemplateCatalog + ResourceTextReader) and give the business layer a ready-made API: “give me the text”.
GO TO FULL VERSION