Spring のリソース抽象:Resource と ResourceLoader
Spring の Resource と ResourceLoader は、ファイルシステムでもクラスパスでも URL でも、物理的な場所を意識せずにリソースへアクセスできる仕組みです。地味ですが、ちゃんと理解しておくと設定ファイルの読み込みやテンプレート処理が格段に書きやすくなります。
Resource インターフェース:抽象の基礎
org.springframework.core.io.Resource は標準 java.net.URL の強力な代替として作られました。java.net.URL にはクラスパスリソースや ServletContext 相対リソースへのアクセスが足りないのですが、Resource がそれを補います。
exists() でリソースの物理的存在を確認できます。getInputStream() は呼び出しごとに新しい InputStream を返します。isOpen() は開かれたストリームを持つかを示します。getDescription() はエラーメッセージ用の説明を返し、getURL() と getFile() はそれぞれ URL と File オブジェクトを返します(リソースのタイプによっては使えません)。
Resource の主な実装
Spring はいくつか専門実装を用意しています。
UrlResource は http:、https:、file: などの標準 URL でリソースにアクセスします。ClassPathResource は classpath: プレフィックスでアプリケーションのクラスパスからリソースを取得します。FileSystemResource はシステムファイル、ServletContextResource は Web アプリケーション内(/WEB-INF/ など)のリソース向けです。ByteArrayResource はメモリ内のバイト配列をリソースとして扱いたいときに使います。
ResourceLoader:統一ロード戦略
ResourceLoader インターフェースは、文字列で場所を指定してリソースをロードする戦略を定義します。重要なのは、すべての ApplicationContext が ResourceLoader を実装しているので、そのまま注入できる点です。
プレフィックスなしの場合、リソースタイプは ApplicationContext の種類に依存します(ClassPathXmlApplicationContext ならクラスパス、FileSystemXmlApplicationContext ならファイルシステム)。プレフィックスありなら、classpath: や file: で特定の Resource タイプを強制できます。
@Component
public class ResourceExample {
private final ResourceLoader resourceLoader;
public ResourceExample(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public void loadResources() throws IOException {
// コンテキスト依存解決
Resource template1 = resourceLoader.getResource("config/template.txt");
// ClassPathResource 強制
Resource config = resourceLoader.getResource("classpath:app.properties");
// UrlResource 強制(ファイルシステム)
Resource fileLog = resourceLoader.getResource("file:/var/log/app.log");
}
}ワイルドカードを使用した複数リソースロード
パターンに一致するリソースをまとめて取りたい場合は ResourcePatternResolver を使います。classpath*: プレフィックスと組み合わせると、JAR の中も含めてクラスパス全体から検索できます。
@Service
public class ConfigurationService {
private final ResourcePatternResolver resolver;
public ConfigurationService(ResourcePatternResolver resolver) {
this.resolver = resolver;
}
public void loadAllConfigurations() throws IOException {
// クラスパスからすべての XML をロード
Resource[] configs = resolver.getResources("classpath*:META-INF/*.xml");
// ディレクトリからすべてのプロパティファイルをロード
Resource[] properties = resolver.getResources("file:/config/*.properties");
for (Resource config : configs) {
// 各設定を処理
try (InputStream is = config.getInputStream()) {
// ファイルを読み取り処理
}
}
}
}@Value でのリソース注入
リソースを注入する一番シンプルな方法は @Value 注釈です。コンストラクタ引数にもフィールドにも使えます。
@Service
public class TemplateService {
private final Resource emailTemplate;
private final Resource logoImage;
public TemplateService(
@Value("${app.email.template:classpath:templates/default.html}") Resource emailTemplate,
@Value("classpath:static/images/logo.png") Resource logoImage) {
this.emailTemplate = emailTemplate;
this.logoImage = logoImage;
}
public void processTemplate() throws IOException {
if (emailTemplate.exists()) {
try (InputStream is = emailTemplate.getInputStream()) {
// テンプレートを処理
String content = StreamUtils.copyToString(is, StandardCharsets.UTF_8);
System.out.println("テンプレートを以下からロード:" + emailTemplate.getDescription());
}
}
}
}高度なユースケース
1. モジュラー設定ロード
複数モジュールのプロパティファイルをクラスパスからかき集めて、ひとつの Properties に統合するパターンです。プラグイン的な設定の拡張がしたいときに便利です。
@Configuration
public class ModuleConfiguration {
@Bean
public Properties moduleProperties(ResourceLoader resourceLoader) throws IOException {
Properties props = new Properties();
// クラスパスからすべてのモジュールプロパティをロード
Resource[] moduleResources = resourceLoader.getResources("classpath*:modules/*.properties");
for (Resource resource : moduleResources) {
try (InputStream is = resource.getInputStream()) {
Properties moduleProps = new Properties();
moduleProps.load(is);
// メインプロパティと統合
props.putAll(moduleProps);
}
}
return props;
}
}2. リソース検証
ロードする前に存在と読み取り可能性、ファイルサイズをチェックする例です。本番環境では地味に重要です。
@Component
public class ResourceValidator {
public void validateResource(Resource resource) throws IOException {
if (!resource.exists()) {
throw new IllegalArgumentException("リソースが存在しません:" + resource.getDescription());
}
if (!resource.isReadable()) {
throw new IllegalArgumentException("リソースが読み取り不可:" + resource.getDescription());
}
// ファイルサイズ検証
if (resource.isFile()) {
File file = resource.getFile();
if (file.length() > 10 * 1024 * 1024) { // 10MB
throw new IllegalArgumentException("ファイルが大きすぎます:" + file.getName());
}
}
}
}3. キャッシュリソースロード
頻繁にアクセスするリソースはキャッシュすべきです。ConcurrentHashMap で簡単に実装できます。
@Service
public class CachedResourceService {
private final Map<String, Resource> resourceCache = new ConcurrentHashMap<>();
private final ResourceLoader resourceLoader;
public CachedResourceService(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
public Resource getResource(String location) {
return resourceCache.computeIfAbsent(location, loc -> {
Resource resource = resourceLoader.getResource(loc);
// 検証後にキャッシュ
try {
if (resource.exists()) {
return resource;
}
} catch (IOException e) {
throw new RuntimeException("リソース検証エラー:" + loc, e);
}
throw new IllegalArgumentException("リソースが見つかりません:" + loc);
});
}
}ベストプラクティス
1. クラスパスリソースを優先
クラスパスリソースは JAR にパッケージしても動きます。ファイルパスは開発環境でしか使えないので避けるべきです。
// ✅ 良い:ポータブルで JAR で動作
@Value("classpath:config/app.properties")
Resource config;
// ⚠️ 避ける:開発中だけ動作
@Value("file:src/main/resources/config/app.properties")
Resource configFile;2. 外部設定にプレースホルダー使用
@Value("${app.template.location:classpath:templates/default.html}")
Resource template;3. 安全ストリーム処理
InputStream は try-with-resources で必ずクローズします。
public void processResource(Resource resource) {
try (InputStream is = resource.getInputStream()) {
// リソースを処理
// ストリームは自動クローズ
} catch (IOException e) {
throw new RuntimeException("リソース処理エラー:" + resource.getDescription(), e);
}
}4. リソース検証
起動時に必須リソースがあるなら、@PostConstruct で検証しておくと初期障害に早く気付けます。
@PostConstruct
public void validateResources() {
if (!requiredResource.exists()) {
throw new IllegalStateException("必須リソースが見つかりません:" + requiredResource.getDescription());
}
}Spring Boot との統合
Spring Boot では @ConfigurationProperties でリソースのベースパスを外部化しやすくなります。
@ConfigurationProperties(prefix = "app.resources")
@Component
public class ResourceProperties {
private String templatesLocation = "classpath:templates/";
private String staticLocation = "classpath:static/";
private String externalLocation = "file:/var/app/resources/";
// getter と setter
public Resource getTemplate(String name) {
return new PathMatchingResourcePatternResolver()
.getResource(templatesLocation + name);
}
}パフォーマンス考慮
ClassPathResource は JAR 内のリソースアクセスで高速です。FileSystemResource は大ファイルやランダムアクセスに向いています。頻繁にアクセスするリソースはキャッシュを実装し、不要なロードは避けるのが基本です。
結論
Resource と ResourceLoader を使えば、クラスパス、ファイルシステム、URL の違いを意識せずに統一的にリソースを扱えます。コードがポータブルになり、テストもしやすくなります。設定ファイル、テンプレート、静的リソースを扱う Spring アプリケーションでは必須の知識だと思います。
コードのポータビリティを高めるため、@Value でリソースを注入するときは可能な限り明示的なプレフィックス(classpath:、file:)を付けることをお勧めします。