Zademy

Spring のリソース抽象:Resource と ResourceLoader

spring-boot resource; resource-loader; classpath
words 単語

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:)を付けることをお勧めします。