由@Value注解引发的一次对bean的思考,值得一看

2,904 阅读13分钟

读取配置文件属性

前不久刚优化了一个关于文件上传这块的代码,这里面就涉及到图片的路径问题,我将某些属性配置到了配置文件,但是在优化过程中,让我对bean有了新的认识,既然牵扯到配置文件属性的读取,那么我们先来看看怎么获取配置文件的属性。

@Value注解

在配置文件中编写一些属性(application.yml)

server:
  port: 9199

file:
  server-name: oss.file.com/    #服务器的域名
  mapper: file/    #映射的名字
  images: images/   #存储图片的文件夹
  video: video/        #存放视频的文件夹

FileConfig类

package com.ymy.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
public class FileConfig {

    @Value("${file.server-name}")
    private  String serverName;

    @Value("${file.mapper}")
    private String mapper;

    @Value("${file.images}")
    private String images;

    @Value("${file.video}")
    private String video;

}

@Data:lombok依赖中的注解,他会生成对应的get、Set、toString方法,简便了我们的代码量。 @Configuration 装配,被它装配的bean,回被spring的上下文扫描,这个有点类似springmvc的xml配中的bean,这个注解一定要加,否者读取不到配置文件,当然也可以替换成@Component。 @Value:用于读取配置文件的属性,格式 ${对应配置文件中的属性}。

单元测试

package com.ymy;

import com.ymy.config.FileConfig;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Slf4j
class StaticConfigVarApplicationTests {

    @Autowired
    private FileConfig fileConfig;

    @Test
    void valueTest() {

        log.info("服务名:{},映射:{},存储图片的文件夹:{},存放视频的文件夹:{}",fileConfig.getServerName(),fileConfig.getMapper(),fileConfig.getImages(),fileConfig.getVideo());
        log.info("图片目录:{}",fileConfig.getServerName()+fileConfig.getMapper()+fileConfig.getImages()+"123.jpg");
        log.info("视频目录:{}",fileConfig.getServerName()+fileConfig.getMapper()+fileConfig.getVideo()+"123.mp4");
    }

}

测试结果如下:

2020-03-19 15:56:42.227  INFO 19884 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 服务名:oss.file.com/,映射:file/,存储图片的文件夹:images/,存放视频的文件夹:video/
2020-03-19 15:56:42.229  INFO 19884 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 图片目录:oss.file.com/file/images/123.jpg
2020-03-19 15:56:42.230  INFO 19884 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 视频目录:oss.file.com/file/video/123.mp4

看到这样的结果,说明读取配之文件已经成功了,但是这个还有一点让我很不爽,那就是FileConfig对象还需要注入,能不能不通过注入就能拿到配置文件的属性呢?这个我等会再说,我们来看看还有没有其他读取配置文件的方式呢?答案肯定是有的,接着往下看。

@ConfigurationProperties

它的实现方式很简单,只需要改动一个地方即可,请看代码:

package com.ymy.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "file")
public class FileConfig {

    private  String serverName;

    private String mapper;

    private String images;

    private String video;
}

我们在属性上去掉了@Value注解,在bean中加入了:@ConfigurationProperties(prefix = "file")。 @ConfigurationProperties():这个注解可以让bean的属性与配置文件的属性一一对应,prefix:配置文件的前缀,格式要求:bean属性要与配置文件的属性名相同。

其他代码都不需要改动,我们直接运行单元测试:

2020-03-19 16:21:00.271  INFO 1456 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 服务名:oss.file.com/,映射:file/,存储图片的文件夹:images/,存放视频的文件夹:video/
2020-03-19 16:21:00.273  INFO 1456 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 图片目录:oss.file.com/file/images/123.jpg
2020-03-19 16:21:00.273  INFO 1456 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 视频目录:oss.file.com/file/video/123.mp4

我们发现同样读取到了配置文件的信息,这种方式要求bean的属性名与配置文件的属性名相同,否者将读取不到,不知道你们有没有发现一个问题,那就是FileConfig定义的服务器名称是:serverName,配置文件配置的是:server-name,很神奇的是他居然找到了,为什么呢? @ConfigurationProperties会默认去掉属性名中间的特殊符号,并且不区分大小写

Environment对象

上面的两种读取方式可能很多人都知道,但是有人了解过Environment这个对象吗?

Environment是集成在容器中的抽象,它为 application 环境的两个 key 方面建模:profiles和properties。

profile 是 bean 定义的命名逻辑 group,仅当给定的 profile 为 active 时才向容器注册。 Beans 可以分配给 profile,无论是用 XML 定义还是通过 annotations 定义。与 profiles 相关的Environment object 的作用是确定哪些 profiles(如果有)当前是 active,以及哪些 profiles(如果有)默认情况下应该 active。

Properties 在几乎所有 applications 中都发挥着重要作用,可能源自各种来源:properties files,JVM 系统 properties,系统环境变量,JNDI,servlet context 参数,ad-hoc Properties objects,Maps 等。与 properties 相关的Environment object 的作用是为用户提供方便的服务接口,用于配置 property 源和从中解析 properties。

这是官网的介绍,看着是否很晕?我们只需要抓中重点即可,那就是Environment管理着所有的配置文件。

我们一起来看看Environment是如何获取配置文件属性的,上代码:

package com.ymy;

import com.ymy.config.FileConfig;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;

@SpringBootTest
@Slf4j
class StaticConfigVarApplicationTests {

    @Autowired
    private Environment environment;

    @Test
    void environmentTest() {
        log.info("服务名:{}",environment.getProperty("file.server-name"));
        log.info("映射:{}",environment.getProperty("file.mapper"));
        log.info("存放图片的文件夹:{}",environment.getProperty("file.imagese"));
        log.info("存放视频的文件夹:{}",environment.getProperty("file.video"));
    }
}

我们运行单元测试查看结果

2020-03-19 21:20:19.855  INFO 1680 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 服务名:oss.file.com/
2020-03-19 21:20:19.857  INFO 1680 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 映射:file/
2020-03-19 21:20:19.857  INFO 1680 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 存放图片的文件夹:images/
2020-03-19 21:20:19.857  INFO 1680 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 存放视频的文件夹:video/

没错,就是这么简单,是不是感觉到上面的三种获取配置文件的方式很丝滑?其实还有几种实现方式,我这里做一下代码的展示,具体的测试已经结果就不做展示了,为了开篇说的问题能尽快和我们见面,下面我直接进行重点的代码展示。

Properties读取

package com.ymy.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;

import java.io.IOException;
import java.util.Properties;

@Slf4j
public class MyPropertiesConfig {

    public static Properties get(String relpath){
        Properties p = null;
        try {
             p = PropertiesLoaderUtils.loadProperties(new EncodedResource(new ClassPathResource(relpath),"utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return p;
    }
}

@Test
    void PeopertiesTest() {
        Properties properties = MyPropertiesConfig.get("application.yml");
        System.out.println(properties.getProperty("server-name"));
    }

Properties读取的配置文件属性和之前的几种有点不一样,它不需要前缀,因为Properties是将配置文件的每一行做处理,比如file下面的server-name被拆分成了两个键值对,file="" servername=oss.file.com/,所以使用这种方式读取配置文件的时候需要注意。

@PropertySource

这种读取方式会有一种弊端,那就是会将注释一起读取出来,所以再使用这种方式读取配置文件信息的时候一定要注意,话不多说,直接上代码

package com.ymy.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Data
@Slf4j
@Configuration
@ConfigurationProperties
@PropertySource(value = "classpath:application.yml",encoding = "utf-8")
public class FileConfigByPropertisSource {

    private  String servername;

    private String mapper;

    private String images;

    private String video;
}

@Test
    void fileConfigByPropertisSourceTest() {
        log.info("服务名:{}",fileConfigByPropertisSource.getServername());
    }

这种读取方式还是借助了@ConfigurationProperties,比较好的一点是它可以加载指定的配置文件,这里我没有指定前缀,所以这里肯定是读取不到的,要想得到我们在配置文件中配置的那几个属性,我们还是需要@ConfigurationProperties(prefix = "file")这个玩意,@ConfigurationProperties配合@PropertySource的优势可以读取指定的配置文件,如果没有@PropertySource,那么只能读取到application/yml/application.properties文件。

读取配置文件的方式就先介绍到这里,下面我们一起来看看我遇到的是什么问题?是一个很诡异的问题。

静态变量读取配置文件属性

没错,我是在使用静态变量读取配置文件属性的时候发生的诡异问题,我先说说是怎么发生的,然后在用代码走一遍。

我在优化文件上传所以来的配置文件的时候发现使用@Value注解获取的配置文件属性,需要在使用的地方注入管理配置文件的Bean,这样我看着很不爽,所以我想到使用静态变量去读取这些配置属性,请看代码。

package com.ymy.config;

import com.ymy.enums.FileTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class FileConfig {

    private static   String servername;

    private static String mapper;

    private static String images;

    private static String video;

    private static String apk;

    @Value("${file.server-name}")
    public void setServername(String servername) {
        this.servername = servername;
    }
    @Value("${file.mapper}")
    public void setMapper(String mapper) {
        this.mapper = mapper;
    }
    @Value("${file.images}")
    public void setImages(String images) {
        this.images = images;
    }
    @Value("${file.video}")
    public void setVideo(String video) {
        this.video = video;
    }
    @Value("${file.apk}")
    public  void setApk(String apk) {
        FileConfig.apk = apk;
    }

    public static String getImages() {
        return images;
    }

    public static String getVideo() {
        return video;
    }

    public static String getApk() {
        return apk;
    }

    public static String getImagePath(String relPath){

        return getlocation(relPath,FileTypeEnum.IMG.value());
    }

    public static String getVideoPath(String relPath){

        return getlocation(relPath,FileTypeEnum.VIDEO.value());
    }

    public static String getApkPath(String relPath){

        return getlocation(relPath,FileTypeEnum.APK.value());
    }

    private static String getlocation(String relPath, Integer type) {

        return servername+mapper+FileTypeFactory.map.get(type)+relPath;
    }
}

解释一下上面的代码 1.定义静态变量接收配置文件属性 2.分别给上set方法(set方法必须是静态的)。 3.在set方法上使用@Value注解。 4.装配bean,在类中加上@Configuration注解

这样,就能让静态变量获取到配置文件的属性了,而我有继续做了一个骚操作,那就是使用一个枚举类维护文件的类型,比如:1=图片;2=视频;3=apk,然后又创建了一个工厂,用于管理这三种类型,代码如下:

枚举类

package com.ymy.enums;

public enum FileTypeEnum {

    IMG(1),

    VIDEO(2),

    APK(3);

    private  Integer index;

    private FileTypeEnum(Integer value) {
        this.index = value;
    }

    public Integer value() {
        return this.index;
    }
}

工厂类

package com.ymy.config;

import com.ymy.enums.FileTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
@Slf4j
public class FileTypeFactory {
    public static final Map<Integer,String> map = new HashMap<Integer, String>();
    static {
        map.put(FileTypeEnum.IMG.value(), FileConfig.getImages());
        map.put(FileTypeEnum.VIDEO.value(),FileConfig.getVideo());
        map.put(FileTypeEnum.APK.value(),FileConfig.getApk());
    }
}

然而问题也就随着到来了,请看单元测试

 @Test
    void pathTest() {
        log.info("图片文件夹:{},视频文件夹:{},apk文件夹:{}",FileConfig.getImages(),FileConfig.getVideo(),FileConfig.getApk());
        String relImgPath = "123.png";
        String relVideoPath = "123.mp4";
        String relApkPath = "123.apk";
        log.info("图片的绝对路径:{}",FileConfig.getImagePath(relImgPath));
        log.info("视频的绝对路径:{}",FileConfig.getVideoPath(relVideoPath));
        log.info("apk的绝对路径:{}",FileConfig.getApkPath(relApkPath));
    }

这个时候我们会发现一个神奇的问题,那就是获取文件绝对路径中有null值,但是分别获取他们的文件夹名称却有值,请看结果:

2020-03-20 15:19:44.545  INFO 16956 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 图片文件夹:images/,视频文件夹:video/,apk文件夹:apk/
2020-03-20 15:19:44.547  INFO 16956 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 图片的绝对路径:oss.file.com/file/null123.png
2020-03-20 15:19:44.547  INFO 16956 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 视频的绝对路径:oss.file.com/file/null123.mp4
2020-03-20 15:19:44.547  INFO 16956 --- [           main] com.ymy.StaticConfigVarApplicationTests  : apk的绝对路径:oss.file.com/file/null123.apk

我就是用工厂代理了一下,就找不到目录了?

FileConfig.getImages()能拿到值说明静态变量images已经获取到了配置文件的属性,但为什么被工厂代理一下这个值就没了呢?

这心态崩了啊,这能忍?我决定要深挖到底,我要搞明白是什么原因让我拿不到这三个值,如何分析问题?那我们就需要从bean的执行顺序讲起了。

Bean内部代码加载顺序

bean内部有静态代码块、代码块、构造函数,还有一些方法,但是只有前三个在实例化Bean的时候都会被执行,那么他们的执行顺序是什么样的?

我们直接用代码接证明它们的执行顺序,我们就改造一下FileTypeFactory这个工厂

package com.ymy.config;

import com.ymy.enums.FileTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Slf4j
public class FileTypeFactory {

    public FileTypeFactory(){
        log.info("我是文件类型工厂的构造函数,我被加载了");
    }

    public static final Map<Integer,String> map = new HashMap<Integer, String>();
    static {
        log.info("我是文件类型工厂的静态代码块,我被加载了");
        map.put(FileTypeEnum.IMG.value(), FileConfig.getImages());
        map.put(FileTypeEnum.VIDEO.value(),FileConfig.getVideo());
        map.put(FileTypeEnum.APK.value(),FileConfig.getApk());
    }

    {
        log.info("我是文件类型工厂的代码块,我被加载了");
    }
}

单元测试

@Test
    void loadTest() {

        FileTypeFactory f1 = new FileTypeFactory();

        FileTypeFactory f2 = new FileTypeFactory();

    }

FileTypeFactory对象被我实例化了两次,至于为什么要实例化两次,一次不就能看到它们的执行顺序了吗?我们先来看执行结果

2020-03-20 15:52:02.700  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件类型工厂的静态代码块,我被加载了
2020-03-20 15:52:02.701  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件类型工厂的代码块,我被加载了
2020-03-20 15:52:02.701  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件类型工厂的构造函数,我被加载了
2020-03-20 15:52:02.701  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件类型工厂的代码块,我被加载了
2020-03-20 15:52:02.701  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件类型工厂的构造函数,我被加载了

初始化第一个实例:静态代码块最先加载 -->代码块被加载 —>构造函数被加载

所以我们基本上可以得出一个结论:静态代码块 >代码块>构造函数

现在请看初始化第二个实例,我们发现居然只执行了代码块与构造函数,所以在这里我们又可以得到一个结论,那就是:静态代码块只会在第一次实例化bean的时候被加载。

Bean与Bean之前的执行顺序

我现在想知道bean与bean之前的执行顺序,我新建了三个bean:user1、user2、user3,并且分别在它们内部写上了静态代码块、代码块以及构造函数,然后我们运行主程序看看这三个bean的加载顺序。

package com.ymy.test;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class User1 {

    public User1(){
        log.info("我是User1的构造函数");
    }

    {
        log.info("我是User1的代码块");
    }
    static {
        log.info("我是User1的静态代码块");
    }
}

package com.ymy.test;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class User2 {

    public User2(){
        log.info("我是User2的构造函数");
    }

    {
        log.info("我是User2的代码块");
    }
    static {
        log.info("我是User2的静态代码块");
    }
}

package com.ymy.test;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class User3 {

    public User3(){
        log.info("我是User3的构造函数");
    }

    {
        log.info("我是User3的代码块");
    }
    static {
        log.info("我是User3的静态代码块");
    }
}

请注意,我这里使用了@Configuration注解,由于我这里新建的是springboot项目,所以我直接启动mian方法,我们一起来看执行结果:

2020-03-20 16:11:40.885  INFO 8932 --- [           main] com.ymy.StaticConfigVarApplication       : Starting StaticConfigVarApplication on LAPTOP-3GLHJRE9 with PID 8932 (D:\springboot\static-config-var\target\classes started by admin in D:\springboot)
2020-03-20 16:11:40.887  INFO 8932 --- [           main] com.ymy.StaticConfigVarApplication       : No active profile set, falling back to default profiles: default
2020-03-20 16:11:41.447  INFO 8932 --- [           main] com.ymy.test.User1                       : 我是User1的静态代码块
2020-03-20 16:11:41.448  INFO 8932 --- [           main] com.ymy.test.User2                       : 我是User2的静态代码块
2020-03-20 16:11:41.451  INFO 8932 --- [           main] com.ymy.test.User3                       : 我是User3的静态代码块
2020-03-20 16:11:41.692  INFO 8932 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 9199 (http)
2020-03-20 16:11:41.699  INFO 8932 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-03-20 16:11:41.699  INFO 8932 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.31]
2020-03-20 16:11:41.778  INFO 8932 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-03-20 16:11:41.779  INFO 8932 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 858 ms
2020-03-20 16:11:41.831  INFO 8932 --- [           main] com.ymy.test.User1                       : 我是User1的代码块
2020-03-20 16:11:41.831  INFO 8932 --- [           main] com.ymy.test.User1                       : 我是User1的构造函数
2020-03-20 16:11:41.831  INFO 8932 --- [           main] com.ymy.test.User2                       : 我是User2的代码块
2020-03-20 16:11:41.831  INFO 8932 --- [           main] com.ymy.test.User2                       : 我是User2的构造函数
2020-03-20 16:11:41.832  INFO 8932 --- [           main] com.ymy.test.User3                       : 我是User3的代码块
2020-03-20 16:11:41.832  INFO 8932 --- [           main] com.ymy.test.User3                       : 我是User3的构造函数
2020-03-20 16:11:41.958  INFO 8932 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-03-20 16:11:42.172  INFO 8932 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9199 (http) with context path ''
2020-03-20 16:11:42.176  INFO 8932 --- [           main] com.ymy.StaticConfigVarApplication       : Started StaticConfigVarApplication in 1.764 seconds (JVM running for 2.908)

程序加载完成之后,我们发现user1、user2、user3的静态代码块被最先执行,在分别执行了他们各自的代码块以及构造函数,我们还发现这三个bean的执行顺序是从上往下,所以在多个bean之间使用@Configuration装配的bean之间的执行顺序:

user1静态代码块 > user2静态代码块 > user3静态代码块 > user1代码块 > user1构造函数 > user2代码块 > user2构造函数 > user3代码块 > user3构造函数

看到这种执行顺序我好像想通了为什么工厂拿不到FileConfig的属性,我们一起来看

我在FileConfig bean中添加了构造函数,因为只有被实例化了属性才会被赋值

 FileConfig(){
        log.info("这是文件配置类的构造函数");
    }

FileTypeFactory工厂没有做修改,我们再此启动项目查看结果

2020-03-20 16:30:43.965  INFO 11920 --- [           main] com.ymy.StaticConfigVarApplicationTests  : Starting StaticConfigVarApplicationTests on LAPTOP-3GLHJRE9 with PID 11920 (started by admin in D:\springboot\static-config-var)
2020-03-20 16:30:43.966  INFO 11920 --- [           main] com.ymy.StaticConfigVarApplicationTests  : No active profile set, falling back to default profiles: default
2020-03-20 16:30:44.731  INFO 11920 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件类型工厂的静态代码块,我被加载了
2020-03-20 16:30:44.909  INFO 11920 --- [           main] com.ymy.config.FileConfig                : 这是文件配置类的构造函数
2020-03-20 16:30:44.939  INFO 11920 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件类型工厂的代码块,我被加载了
2020-03-20 16:30:44.940  INFO 11920 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件类型工厂的构造函数,我被加载了

看到这个证实了我的想法,工厂类的静态代码块被先加载,配置类的构造被后加载,所以导致了读取属性为空的情况,由于静态代码块是对象被初始化的时候执行,那我只需要将他放到配置类后执行就可以了啊,最简单有效的方式就是去掉FileTypeFactory工厂类的@Configuration注解,这样,程序启动的时候就不会加载工厂,而是在被调用的时候加载,这个时候配置类早就加载完成了,之前为啥要手贱去加一个注解呢?可能是脑子进水了,不过幸亏加了这个注解让我学习了一波@Configuration注解与@Component注解的区别。

@Configuration注解与@Component注解的区别

刚刚分析的代码中我们并没有牵扯到@Component注解,为什么会学习了一波他们的区别呢?我之前一直认为他们的作用是一致的,知道我测试的时候使用了他们两个之后,我发现我错了。

从表面看@Configuration属于@Component的派生类,所以他们两个@Component能干的事情@Configuration应该也能干,这样的认知虽然没错,但是不严谨,我们还是使用刚刚新建的user1、2、3来说明他们的区别,我们将他们的装配注解替换成@Component

其他都不需要修改,我们直接来看他们的执行顺序

2020-03-20 16:42:40.274  INFO 21048 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 725 ms
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User1                       : 我是User1的静态代码块
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User1                       : 我是User1的代码块
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User1                       : 我是User1的构造函数
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User2                       : 我是User的静态代码块
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User2                       : 我是User的代码块
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User2                       : 我是User的构造函数
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User3                       : 我是User3的静态代码块
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User3                       : 我是User3的代码块
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User3                       : 我是User3的构造函数

还记得被@Configuration装配的bean的执行顺序吗?

user1静态代码块 > user2静态代码块 > user3静态代码块 > user1代码块 > user1构造函数 > user2代码块 > user2构造函数 > user3代码块 > user3构造函数

发现什么了吗?被@Component装配的bean执行顺序居然发生改变了,执行顺序如下:

user1静态代码块 > user1代码块 > user1构造函数 > user2静态代码块 > user2代码块 user2构造函数 > user3静态代码块 > user3代码块 > user3构造函数

可以看出被这两个装配的bean执行顺序发生了很大的改变,这是为什么呢?

为什么@Configuration注解与@Component注解装配的bean执行顺序不一样?

那是因为@Configuration注解是被动态代理的(CGLIB),很容易理解,执行完静态代码块之后,真正的实例代码块这些都会在代理中执行,而不是它本身,所以我们会看到所有被@Configuration装配的bean都是先执行了静态代码块,而后面猜分别执行他们的构造函数,而@Component注解却没有,所以它严格的按照这bean的执行顺序执行,这也就是我们看到的为什么@Configuration注解与@Component注解装配的bean执行顺序不一样。如果你们对spring的注解感兴趣的话,我这里有一个spring的中文官方文档:spring中文官方文档

总结

写到这里就结束了,spring还是那么的强大,我们对它的了解还是那么的细微,本人最近在努力的加深对spring的理解,希望以后会写出更漂亮的文章,如果不是这次的小优化,我可能一直还是以为@Configuration注解与@Component注解就是一个东西,真的是活到老,学到老啊。