偶发性无法读取配置内容——Bean 加载顺序问题

733 阅读2分钟

案例分析

@Configuration
public class EnvironmentConfig implements EnvironmentAware {

    private static Environment env;

    @Override
    public void setEnvironment(Environment environment) {
        env=environment;
    }

    public static Environment getEnvironment(){
        return env;
    }
}

public class EnvUtils {
    public static final String ENV_PARAM = EnvironmentConfig.getEnvironment().getProperty("env.param");
}

@DependsOn("environmentConfig")
@RestController
public class TestController {
    @RequestMapping("/env")
    public String getEnv() {
        System.out.println("输出结果:" + EnvUtils.ENV_PARAM);
        return EnvUtils.ENV_PARAM;
    }
}

项目代码是通过以上方式获取配置信息,但存在间歇性的无法读取配置的问题,检查后发现和 Bean 实例化顺序有关。EnvUtils 中的静态变量在编译时初始化,但赋值是在程序运行时进行的。这就导致了一个问题,当系统启动的时候存在先实例化 Controller 再实例化 Config 的情况,而在这种情况下就会报错。

为了验证猜测,在 EnvironmentConfig 类上增加了 @Lazy 注解,随后启动应用,发起请求发现系统报错,顺利重现了线上错误。

image-20210118113857705

解决思路

  1. 通过 @Value 直接注入配置
  2. 依赖于 EnvironmentConfig 对象 3. @DependsOn("environmentConfig") 2. 注入 EnvironmentConfig

知识点拓展

懒加载

@Lazy 创建 Bean 对象延迟到第一次使用 Bean 的时候

@Lazy
@Configuration
public class LazyInitConfig {

    public LazyInitConfig() {
        System.out.println("LazyInitConfig init!");
    }

    public void printInfo() {
                 int mb = 1024 * 1024;
        //Getting the runtime reference from system
        Runtime runtime = Runtime.getRuntime();
        System.out.println("##### Heap utilization statistics [MB] #####")
        //Print used memory
        System.out.println("Used Memory:"
                + (runtime.totalMemory() - runtime.freeMemory()) / mb);
        //Print free memory
        System.out.println("Free Memory:"
                + runtime.freeMemory() / mb);
        //Print total available memory
        System.out.println("Total Memory:" + runtime.totalMemory() / mb)
        //Print Maximum available memory
        System.out.println("Max Memory:" + runtime.maxMemory() / mb);
    }
}
// 测试类
@SpringBootTest	// 见 Tips 1
public class TestLazy {

    @Autowired
    LazyInitConfig lazyInitConfig;
    @Test
    public void testLazy() {
        lazyInitConfig.printInfo();
    }
}

image-20210117232625791

去掉 @Lazy 注解后在应用启动时就会直接调用构造函数创建对象

image-20210118103035952

@Lazy 标记的 Bean 只要使用到就会被实例化。 如果在 BeanA 中注入了 LazyBean 那么在启动的时候仍然会直接实例化,因为在 BeanA 加载的时候,依赖于 LazyBean 所以 LazyBean 就会被实例化。

@RestController
public class TestController {
    @Autowired
    LazyInitConfig lazyInitConfig;

    @RequestMapping("/lazy")
    public void getLazy() {
        lazyInitConfig.printInfo();
    }
}

image-20210118110259268

当我们在依赖代码上加上 @Lazy 注解启动的时候就不会去初始化 Bean 而是创建一个代理对象

@RestController
public class TestController {
		@Lazy
    @Autowired
    LazyInitConfig lazyInitConfig;

    @RequestMapping("/lazy")
    public void getLazy() {
        lazyInitConfig.printInfo();
    }
}

image-20210118111109317

Tips

1. @SpringBootTest 注解在 Java 目录下无法使用

实际测试的时候我是在 Java 目录下创建的 Test 类。

image-20210117232958985

结果发现还需要额外引入依赖,但是平时创建测试类都是不需要引入包的,原因在于默认的 pom.xml 文件中引入的 test 启动器默认的 scope 是 test 所以只能在 test 中使用。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
<!--            <scope>test</scope>-->
        </dependency>

注释代码后就可以直接在 Java 目录下使用。