Spring Boot 中 @Value 加载资源文件

一、原理概述

  • @Value 注解可以注入来自配置文件(application.properties / application.yml)的属性值,也能通过 classpath:file:http: 等前缀直接加载外部资源。
  • 当注入类型为 org.springframework.core.io.Resource 时,Spring 会根据前缀自动创建相应的 Resource 实例,方便后续通过 getInputStream() 等方法读取资源内容。

二、常见资源前缀

前缀说明
classpath:从 classpath 加载(通常放在 resources 目录下)
file:从文件系统路径加载
http:从远程 HTTP URL 加载
无前缀默认等同于 classpath:

三、注入 Resource 示例

import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

@RestController
@RequestMapping
public class IndexController {

    // 注入位于 classpath:static/logo.svg 的资源
    @Value("classpath:static/logo.svg")
    private Resource logoPath;
    
	// 注入基于http的远程资源
    private Resource remoteText;

    @RequestMapping(value = "/favicon.ico")
    public void favicon(HttpServletResponse response) throws IOException {
        // 1. 设置正确的 MIME 类型
        response.setContentType("image/svg+xml");
        // 2. 可选:浏览器缓存一年
        response.setHeader("Cache-Control", "max-age=31536000");
        // 3. 读取资源并输出到响应流
        try (InputStream in = logoPath.getInputStream()) {
            FileCopyUtils.copy(in, response.getOutputStream());
        }
    }
    
    
     /**
     * 访问 /remote/json 时,读取远程文本资源并以json返回
     */
    @GetMapping(value = "/remote/json", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> fetchRemoteText() throws IOException {
        String content = StreamUtils.copyToString(
                remoteText.getInputStream(),
                StandardCharsets.UTF_8
        );
        return ResponseEntity.ok(content);
    }
}
  • 用途:将自定义的 SVG Logo 作为 Favicon 提供给浏览器,避免默认的 /favicon.ico 404。
  • 关键点
    • Resource#getInputStream() 获取资源输入流。
    • FileCopyUtils.copy(...) 简化流复制操作。
    • @SaIgnore(来自 Sa-Token)用于跳过权限验证。

四、加载证书文件并构建 Bean

import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.RSAPublicKeyConfig;
import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RSAPublicKeyNotificationConfig;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 微信支付配置类
 */
@Data
@Configuration
public class WechatPayConfigProperties {

    @Value("${wechat.pay.app-id}")
    private String appId;

    @Value("${wechat.pay.mch-id}")
    private String mchId;

    @Value("${wechat.pay.mch-serial-number}")
    private String mchSerialNumber;

    @Value("${wechat.pay.api-v3-key}")
    private String apiV3Key;

    @Value("classpath:cert/apiclient_key.pem")
    private Resource privateKeyPath;

    @Value("classpath:cert/pub_key.pem")
    private Resource publicKeyPath;

    @Value("${wechat.pay.public-key-id}")
    private String publicKeyId;

    @Value("${wechat.pay.notify-url}")
    private String notifyUrl;

    @Bean
    public Config wechatPayConfig() throws IOException {
        // 使用微信支付公钥的RSA配置
        String privateKeyContent = StreamUtils.copyToString(
                privateKeyPath.getInputStream(),
                StandardCharsets.UTF_8
        );
        String publicKeyContent = StreamUtils.copyToString(
                publicKeyPath.getInputStream(),
                StandardCharsets.UTF_8
        );
        return new RSAPublicKeyConfig.Builder()
                .merchantId(mchId) //微信支付的商户号
                .privateKey(privateKeyContent) // 商户API证书私钥的存放路径
                .publicKey(publicKeyContent) //微信支付公钥的存放路径
                .publicKeyId(publicKeyId) //微信支付公钥ID
                .merchantSerialNumber(mchSerialNumber) //商户API证书序列号
                .apiV3Key(apiV3Key) //APIv3密钥
                .build();
    }

    @Bean
    public NotificationParser notificationParser() throws IOException {

        String publicKeyContent = StreamUtils.copyToString(
                publicKeyPath.getInputStream(),
                StandardCharsets.UTF_8
        );
        RSAPublicKeyNotificationConfig config = new RSAPublicKeyNotificationConfig.Builder()
                .publicKey(publicKeyContent)
                .publicKeyId(publicKeyId)
                .apiV3Key(apiV3Key)
                .build();

        return new NotificationParser(config);
    }
}
  • 说明
    1. 将 PEM 文件以 Resource 方式注入,便于随项目一起打包。
    2. 使用 StreamUtils.copyToString(...) 将流内容转为字符串。
    3. 配置中的商户号、序列号等敏感信息仍通过 ${} 从配置文件注入,保持灵活与安全。

五、整合 @Value("classpath:static/privacyPolicy.html") 的示例

@RestController
@RequestMapping("/docs")
public class DocController {

    @Value("classpath:static/privacyPolicy.html")
    private Resource privacyPolicyHtml;

    @GetMapping("/privacy")
    public ResponseEntity<String> privacyPolicy() throws IOException {
        String html = StreamUtils.copyToString(
                privacyPolicyHtml.getInputStream(),
                StandardCharsets.UTF_8
        );
        // 返回 HTML 内容,并设置正确的 Content-Type
        return ResponseEntity.ok()
                .contentType(MediaType.TEXT_HTML)
                .body(html);
    }
}
  • 效果:访问 /docs/privacy 时,返回 static/privacyPolicy.html 的原始 HTML 内容,方便展示隐私策略页面。

六、注意事项与最佳实践

  1. 资源路径
    • 放在 src/main/resources 下,对应运行后 classpath:
    • 避免使用相对路径或硬编码文件系统路径,增强可移植性。
  2. 字符编码
    • 读取文本类资源时,务必显式指定编码(如 StandardCharsets.UTF_8),防止乱码。
  3. 大文件处理
    • 若资源体积较大,可考虑使用 InputStreamResource 或分块读取,避免一次性加载至内存。
  4. 热更新
    • 打包后 classpath: 资源随 Jar 打包,不支持运行时更改。若需动态更新,可使用文件系统路径或外部化配置。
  5. 异常处理
    • 建议统一捕获 IOException 并返回友好错误信息,或在全局异常处理器中处理。

人生不作安期生,醉入东海骑长鲸