Zademy

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

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

Spring は ResourceResourceLoader インターフェースで強力で柔軟なリソース処理抽象システムを提供します。このシステムは異なるタイプのリソース(システムファイル、クラスパス、URL)を物理的位置に関わらず統一的にアクセスできます。

Resource インターフェース:抽象の基礎

org.springframework.core.io.Resource インターフェースは Spring リソース抽象システムの中心的支柱です。標準 java.net.URL クラスのより強力な代替として設計され、クラスパスリソースや ServletContext 相対リソースへのアクセスの制限を克服します。

Resource の基本メソッド

メソッド説明
exists()リソースが物理的に存在する場合 true を返す
getInputStream()リソースを開き、各呼び出しで新しい InputStream を返す
isOpen()リソースが既に開かれたストリームを持つハンドルを表すかを示す
getDescription()エラーメッセージで使用するリソース説明を返す
getURL()利用可能な場合リソース URL を返す
getFile()リソースがシステムファイルの場合 File オブジェクトを返す

Resource の主な実装

Spring は異なるリソースタイプ向けにいくつかの専門実装を提供:

実装説明プレフィックス
UrlResource標準 URL でリソースにアクセスhttp:https:file:https://api.example.com/config.json
ClassPathResourceアプリケーションのクラスパスリソースclasspath:classpath:application.properties
FileSystemResourceシステムファイルリソースfile: またはプレフィックスなし/data/config/app.xml
ServletContextResourceWeb アプリケーション内のリソースプレフィックスなし/WEB-INF/views/home.jsp
ByteArrayResourceバイト配列ベースのリソースN/Aメモリ内リソース向け

ResourceLoader:統一ロード戦略

ResourceLoader インターフェースは場所(String)からのリソースロード戦略を定義します。すべての ApplicationContext は ResourceLoader を実装し、直接注入が可能です。

解決ルール

  1. プレフィックスなし: リソースタイプは ApplicationContext に依存
  2. プレフィックスあり: 特定 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");
    }
}

ワイルドカードを使用した複数リソースロード

パターンに一致するリソースを検索するには、Spring は ResourcePatternResolver を提供:

@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. モジュラー設定ロード

@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. キャッシュリソースロード

@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 で動作
@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. 安全ストリーム処理

public void processResource(Resource resource) {
    try (InputStream is = resource.getInputStream()) {
        // リソースを処理
        // ストリームは自動クローズ
    } catch (IOException e) {
        throw new RuntimeException("リソース処理エラー:" + resource.getDescription(), e);
    }
}

4. リソース検証

@PostConstruct
public void validateResources() {
    if (!requiredResource.exists()) {
        throw new IllegalStateException("必須リソースが見つかりません:" + requiredResource.getDescription());
    }
}

Spring Boot との統合

Spring Boot はさらにリソース処理を簡素化:

@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);
    }
}

パフォーマンス考慮

  1. ClassPathResource: JAR 内リソースで高速
  2. FileSystemResource: 大ファイルとランダムアクセスで高速
  3. キャッシュ: 頻繁にアクセスするリソースでキャッシュ実装
  4. 遅延ロード: 必要時のみリソースロード

結論

Spring のリソース抽象は異なるタイプのリソースを扱うための優雅で統一された方法を提供します。ResourceResourceLoader を使用することで:

  • 場所に依存しないリソースアクセス
  • よりポータブルでテスト可能なコード作成
  • Spring の依存注入の恩恵
  • ワイルドカードでの柔軟なロードパターン

この抽象は設定、テンプレート、静的リソースを一貫してアクセスする必要がある堅牢で保守しやすい Spring アプリケーション構築に不可欠です。


推奨: 歧義を避けコードのポータビリティを向上させるために、可能な限り常に明示的プレフィックス(classpath:file:)付き @Value を使用してください。_