Environment.getProperty()

  • 问题背景:在 HTTP 过滤器、拦截器等高频调用场景中,直接使用 environment.getProperty("key") 获取配置属性会带来性能损耗:

    • 属性源遍历开销:每次调用需遍历 PropertySources 查找键值
    • 重复计算:若属性值固定(如 spring.application.name),重复调用浪费资源
  • 典型场景:

    • 过滤器或拦截器中设置 HTTP 响应头时频繁读取固定配置。
    • 微服务调用链中传递应用名、版本号等元数据。
  • 性能问题分析

1、原始实现

public class XdrHttpResponseFilter extends OncePerRequestFilter {

    @Autowired
    private Environment environment;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        // 每次请求调用 getProperty,存在性能隐患
        setHeader(response, "X-App-Name", environment.getProperty("spring.application.name"));
        setHeader(response, "X-App-Version", environment.getProperty("xdr.monitor.application.version"));
        filterChain.doFilter(request, response);
    }

    private void setHeader(HttpServletResponse response, String key, String value) {
        if (StringUtils.hasText(value)) {
            response.setHeader(key, value);
        }
    }
}

性能问题:

  • 高频属性查找:每个 HTTP 请求触发 2 次 getProperty 调用。
  • 属性源遍历:若配置源较多(如多配置文件、远程配置中心),查找耗时增加。

2、优化方案

  • 缓存配置值,在初始化阶段一次性读取配置,避免重复调用 getProperty

  • 优化后的实现:

public class XdrHttpResponseFilter extends OncePerRequestFilter {

    private String appName;

    private String appVersion;

    @Autowired
    private Environment environment;

    @Override
    protected void initFilterBean() {
        // 初始化阶段一次性读取配置
        this.appName = environment.getProperty("spring.application.name");
        this.appVersion = environment.getProperty("xdr.monitor.application.version");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        // 直接使用缓存值
        setHeader(response, "X-App-Name", appName);
        setHeader(response, "X-App-Version", appVersion);
        filterChain.doFilter(request, response);
    }

    private void setHeader(HttpServletResponse response, String key, String value) {
        if (StringUtils.hasText(value)) {
            response.setHeader(key, value);
        }
    }
}

优化关键点:

  • 缓存配置值:在 initFilterBean 生命周期阶段读取并缓存配置。

  • 线程安全:配置值通常不变,无需同步。

测试验证:JMH 基准测试

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Fork(2)
@Warmup(iterations = 3, time = 2)
@Measurement(iterations = 5, time = 5)
@Threads(8) // 模拟 8 线程并发
public class EnvironmentGetPropertyBenchmark {

    private Environment environment;

    private String appName;
    private String appVersion;

    @Setup
    public void setup() {
        // 初始化 MockEnvironment
        MockEnvironment env = new MockEnvironment();
        env.setProperty("spring.application.name", "my-service");
        env.setProperty("xdr.monitor.application.version", "1.0.0");
        this.environment = env;

        // 优化版本预读取配置
        this.appName = env.getProperty("spring.application.name");
        this.appVersion = env.getProperty("xdr.monitor.application.version");
    }

    /** 原始实现:每次调用 getProperty */
    @Benchmark
    public void testOriginal(Blackhole bh) {
        bh.consume(environment.getProperty("spring.application.name"));
        bh.consume(environment.getProperty("xdr.monitor.application.version"));
    }

    /** 优化实现:使用缓存值 */
    @Benchmark
    public void testOptimized(Blackhole bh) {
        bh.consume(appName);
        bh.consume(appVersion);
    }
}
  • 测试结果
Benchmark                                          Mode  Cnt        Score        Error   Units
TT.EnvironmentGetPropertyBenchmark.testOptimized  thrpt   10  1335706.702 ± 100309.134  ops/ms
TT.EnvironmentGetPropertyBenchmark.testOriginal   thrpt   10       20.171 ±      0.972  ops/ms
  • 结论:优化后性能是原始版本的 66,200 倍。

  • 上述缓存的形式也可以换成 ConcurrentHashMap 来缓存

建议优化场景

  • HTTP 过滤器/拦截器:每个请求均触发 getProperty
  • 循环体内部:避免在循环中重复调用 getProperty
  • 需要频繁读取固定配置的场景(如服务名、版本号、开关配置)
Logo

更多推荐