“八股文”到底有没有用——记一次类实例化顺序引发的问题


问题背景及代码示例

一个SDK,想通过java -jar命令动态设置后台服务的地址,开发完成后,再本地测试时一路绿灯,非常丝滑,但是将后端服务部署到服务器后,SDK死活都访问不到服务端,最终一顿调试定位给到问题:启动时传入的参数根本没生效,而代码里面我默认写了本地地址,所以只可以访问到本地。废话不多说,先看当时出现问题的示例代码。

代码示例

public class ConfigHolder {
     private static final Logger log = org.slf4j.LoggerFactory.getLogger(ConfigHolder.class);

    public String BASE_URL = "http://127.0.0.1:8888";
    public String API_VERSION = "/api/v1";
    public final String HEART_BEAT_URL = BASE_URL + API_VERSION + "/heartBeat";
    // 其他接口地址和参数...

    private static class InstanceHolder {
        private final static ConfigHolder INSTANCE = new ConfigHolder();
    }

    private ConfigHolder() {
    }

    public static ConfigHolder getInstance() {
        return InstanceHolder.INSTANCE;
    }

    public void parseArgs(String[] args) {
        for (int i = 0; i + 1 < args.length; i += 2) {
            if ("--api".equals(args[i])) {
                this.BASE_URL = args[i + 1];
                log.info("Api address: {}", this.BASE_URL);
            }
        }
    }
}


// 启动类
public class Main {
    public static void main(String[] args) {
         ConfigHolder.getInstance().parseArgs(args);
        // 其他操作...
    }
}

代码并不复杂,就是一个单例类,然后一个parseArgs方法解析启动类中mian函数传过来的启动参数,然后赋值给相应的成员变量。

看完代码不知各位是否已经发现参数不生效的问题,如果没有发现,说明准备面试“背诵八股文”的时候,单纯是为了背诵。(手动狗头)

问题分析

我们先来回顾下一道著名的“八股”——Java类实例化的顺序。在Java中,如果一个类不涉及继承,那它实例化的时候,顺序时这样的:静态变量 -> 静态代码块 -> 静态方法 -> 普通成员变量 -> 普通代码块 -> 构造方法。

在上面的代码中,变量BASE_URL是在类实例化完成后就已经被赋值http://127.0.0.1:8888,因为JVM对字符串拼接的优化,变量HEART_BEAT_URL在类实例化时就被赋值为http://127.0.0.1:8888/api/v1/heartBeat,而参数解析是在类实例化之后,也就是说,我成功将BASE_URL改成了我预期的地址,而HEART_BEAT_URL在初始化的时候,取的是旧的BASE_URL,那么实例化完成后,无论我怎么修改BASE_URLHEART_BEAT_URL都不会变,永远是http://127.0.0.1:8888/api/v1/heartBeat,所以我怎么可能用访问得到服务端。

解决方法

当然解决方式不止这一种,这里给出一个示例。

import java.util.function.Supplier;

public class ConfigHolder {
    private static final Logger log = org.slf4j.LoggerFactory.getLogger(ConfigHolder.class);
    
    public String BASE_URL = "http://127.0.0.1:8888";
    public String API_VERSION = "/api/v1";
    // 使用 Supplier 做包装,用到 HEART_BEAT_URL 的时候再实例化
    public final Supplier<String> HEART_BEAT_URL = () -> BASE_URL + API_VERSION + "/heartBeat";
    // 其他接口地址和参数...

    private static class InstanceHolder {
        private final static ConfigHolder INSTANCE = new ConfigHolder();
    }

    private ConfigHolder() {
    }

    public static ConfigHolder getInstance() {
        return InstanceHolder.INSTANCE;
    }

    public void parseArgs(String[] args) {
        for (int i = 0; i + 1 < args.length; i += 2) {
            if ("--api".equals(args[i])) {
                this.BASE_URL = args[i + 1];
                AnsiLog.info("Api address: " + this.BASE_URL);
            }
        }
    }
}

Supplier(Java 8 的新特性)对地址做了一层包装,只有在调用HEART_BEAT_URL.get()的时候才会实例化该字符串,在启动的时候肯定是先解析参数,再获取接口地址,获取接口地址的时候BASE_URL已经更新为最新值,所以获取到的接口地址也就是最新的了。

总结

回到文章的题目,“八股文”到底有没有用,如果是面向面试学习,简单的背诵,显然意义不大,但如果看完了可以自己理解,对我们排查、定位及分析问题,还是有用的。而且“八股文”相当于是一份精华笔记,里面很少有废话,比起系统学习,虽然深度欠缺一些,但可以快速掌握一些知识,所以以学习为目的去看“八股文”,我认为还是有意义的。所以,“八股文”并不“八股”(狗头)。

声明:迟於|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - “八股文”到底有没有用——记一次类实例化顺序引发的问题


栖迟於一丘