SpringBoot自动装配原理及自定义starter开发

starter解决的问题

  当我们定义一个公共的组件时,如果别人要引用它,可以通过导入一个jar包的方式,然后如果要使用其中的类的话,我们就需要使用@Bean,但是这样会造成代码强依赖于该jar包,一旦我们进行组件的更新,甚至更改了类名的时候,就会导致用户的@Bean也需要更改,这样的用户体验不好。

  这就引入了starter,我们就可以直接使用@Autowired,因为在组件内部已经为我们托管到Spring了,我们只要引入依赖到pom.xml文件,然后注入就可以了。

思考的问题

  我们先要思考的问题,我们知道SpringMVC需要配置web.xml文件,Spring也需要配置applicationContext.xml,但是问题来了,为什么基于Spring和SpringMVC的SpringBoot却没有这些配置文件。那SpringBoot是如何做到这些自动配置的呢?我们很容易地知道是@SpringBootApplication的原因,但底层是怎样实现的呢?

解析自动装配

加载yml、properties、yaml文件

首先,我们要知道为什么可以自动加载src/main/application.yml文件?其实是在springboot项目的父模块中定义了的。
<includes>
      <include>**/application*.yml</include>
      <include>**/application*.yaml</include>
      <include>**/application*.properties</include>
</includes>

自动装配原理

先看@SpringBootApplication
在这里插入图片描述
  元注解肯定不可能自动装配,接下来看@SpringBootConfiguration

@SpringBootConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration//如果不改属性值proxyBeanMethods,可以保证绝对单例(这也是@Configuration和@Component的区别)
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;//默认使用CGLIB代理该类
}

  可以知道@SpringBootConfiguration相当于@Configuration

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";

    boolean proxyBeanMethods() default true;//proxyBeanMethods中文是代理Bean的方法,默认参数是true
}

关于@Configuration 可参考https://blog.csdn.net/weixin_46055693/article/details/125656999

下一个看@EnableAutoConfiguration

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})//导入参数类到IOC
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

  接下来,看@AutoConfigurationPackage

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})//因为spring不会自动扫描@Entity等注解,spring自动扫描的是@Controller这些注解,所以需要保存扫描路径,提供给spring-data-jpa扫描@Entity等注解,默认当前类所在的包(@Import的第二种使用情况,可点进去看一下源码)
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};
}

  Springboot自动装配的核心注解@Import

  • 参数如果是普通类,将该类实例化交给IOC容器管理。(除开下面两种都是普通类,4.2之前的版本只能加载有@Configuration的配置类,但后面版本没有该注解也可以加载)
  • 参数如果是ImportBeanDefinitionRegistrar的实现类,支持手工注册bean。
  • 参数如果是ImportSelector的实现类,注册selectImports返回的数组(类全路径)则IOC容器批量注册。

  然后从@Import({AutoConfigurationImportSelector.class})
点进去,找到selectImports方法,再从this.getAutoConfigurationEntry(annotationMetadata)点进去

protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
       if (!this.isEnabled(annotationMetadata)) {
           return EMPTY_ENTRY;
       } else {
           AnnotationAttributes attributes = this.getAttributes(annotationMetadata);//根据注解获取注解属性
           //从META-INF/spring.factories加载EnableAutoConfiguration类
           List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
           configurations = this.removeDuplicates(configurations);//去重
           Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);//获取需要排除的属性
           this.checkExcludedClasses(configurations, exclusions);//检查
           configurations.removeAll(exclusions);//移除掉需要排除的
           configurations = this.getConfigurationClassFilter().filter(configurations);//过滤
           this.fireAutoConfigurationImportEvents(configurations, exclusions);//监听
           return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);//把配置信息返回
       }
   }

  按顺序点击this.getCandidateConfigurations(annotationMetadata, attributes);---->this.getSpringFactoriesLoaderFactoryClass()可以知道最后要加载的是EnableAutoConfiguration类,接着返回到上一个函数,然后点击SpringFactoriesLoader.loadFactoryNames—>(List)loadSpringFactories(classLoaderToUse)—>
Enumeration urls = classLoader.getResources(“META-INF/spring.factories”);

 private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = (Map)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            HashMap result = new HashMap();

            try {
                Enumeration urls = classLoader.getResources("META-INF/spring.factories");

                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryTypeName = ((String)entry.getKey()).trim();
                        String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        String[] var10 = factoryImplementationNames;
                        int var11 = factoryImplementationNames.length;

                        for(int var12 = 0; var12 < var11; ++var12) {
                            String factoryImplementationName = var10[var12];
                            ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                                return new ArrayList();
                            })).add(factoryImplementationName.trim());
                        }
                    }
                }

                result.replaceAll((factoryType, implementations) -> {
                    return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
                });
                cache.put(classLoader, result);
                return result;
            } catch (IOException var14) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
            }
        }
    }

  大概扫描一下上面的代码,我们可以知道,其实是加载了META-INF/spring.factories文件然后存到Map中,我们再看一下调用该方法的代码

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        ClassLoader classLoaderToUse = classLoader;
        if (classLoader == null) {
            classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
        }

        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
    }

  再看上段代码的最后一行,就可以知道原来springboot在获取到Map之后是根据类名从键值对中去取值的。

  然后找到Maven: org.springframework.boot:spring-boot-autoconfigure:2.4.5这个包,再进入 到META-INF/spring.factories。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\

  上述的org.springframework.boot.autoconfigure.EnableAutoConfiguration,其实就是我们根据取值的键名,这里我们可以先猜测springboot就是通过它加载类到IOC容器的。以DataSourceAutoConfiguration为例,

@Configuration(
   proxyBeanMethods = false
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(
   type = {"io.r2dbc.spi.ConnectionFactory"}
)
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
   public DataSourceAutoConfiguration() {
   }

  DataSourceAutoConfiguration类上面有一个@Configuration注解,相当于一个xml配置,它通过使用@Bean或者@Import注解将需要托管的类托管的IOC容器中,而这些与sprinboot无关,你想要什么托管就可以加@bean或者@Import注解

自定义Starter

  1. 创建一个springboot项目
  2. 引入依赖
<!-- 自定义starter时,必须引入的依赖(SpringBoot自动引入了)-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

3.建立一个Properties类

@ConfigurationProperties(prefix = "yc.connection")
public class ConnectionProperties {

   private String driverClassName;
   private String url;
   private String username;
   private String password;
   //省略了set和get方法

  通常情况下,我们使用@Value来从application.yml文件中读取属性到类中。但是@Value只能单个单个注入,并且是根据字符串匹配的,所以这里使用@ConfigurationProperties(prefix = “yc.connection”)来批量注入,通过前缀匹配的方式,换句话说@ConfigurationProperties是批量的@Value,但是它需要配合@EnableConfigurationProperties(ConnectionProperties.class)

4.建立一个功能代码类

public class DBHelper implements IDBHelper{

    @Autowired
    private ConnectionProperties cp;
    @Override
    public Connection getConnection() {
        Connection conn=null;
        try {
            Class.forName(cp.getDriverClassName());
            conn= DriverManager.getConnection(cp.getUrl(),cp.getUsername(),cp.getPassword());
        } catch (ClassNotFoundException |SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }
}
  1. 自动配置类
@Configuration
@EnableConfigurationProperties(ConnectionProperties.class)
@ConditionalOnClass(IDBHelper.class)
public class  ConnectionAutoConfiguration {

    @Bean
    public IDBHelper idbHelper(){
        return new DBHelper();
    }
}

  其中@EnableConfigurationProperties(ConnectionProperties.class)可以将ConnectionProperties.class加载到IOC容器。

  1. 创建src/main/resources/META-INF/spring.factories 类上需要加@Component这种注解
    https://blog.csdn.net/weixin_44923168/article/details/119903295
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yc.ConnectionAutoConfiguration
  1. 把项目打包成jar包
  2. 再创建一个springboot项目,导入自定义starter的jar包
@SpringBootApplication
@RestController
public class TestmystarterMysqlstarterApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestmystarterMysqlstarterApplication.class, args);
    }

    @Autowired
    private IDBHelper idbHelper;

    @GetMapping("/conn")
    public String testConn(){
        Connection conn=idbHelper.getConnection();
        String constr=conn.toString();
        return constr;
    }
}

  不需要@bean,直接使用@Autowired。因为只是说扫描META-INF/spring.factories但没有说扫描哪个包下的,所以都会扫描。

  第二种:使用META-INF/spring.factories加载配置类是一种方法,但是我们也可以在启动类使用@Import加载配置类

@SpringBootApplication
@RestController
@Import(ConnectionAutoConfiguration.class)  
public class TestmystarterMysqlstarterApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestmystarterMysqlstarterApplication.class, args);
    }

    @Autowired
    private IDBHelper idbHelper;

    @GetMapping("/conn")
    public String testConn(){
        Connection conn=idbHelper.getConnection();
        String constr=conn.toString();
        return constr;
    }
}

  第三种:还可以自定义注解
写一个类实现ImportSelector ,而springboot底层会调用selectImports,所以我们只需要实现这个方法,然后将其用spring托管即可

public class MyImport implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{ConnectionAutoConfiguration.class.getName()};//直接传全路径名
    }
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MyImport.class)
public @interface EnableUtil {
}

  最后在启动类上添加@EnableUtil

@SpringBootApplication
@RestController
@EnableUtil
public class TestmystarterMysqlstarterApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestmystarterMysqlstarterApplication.class, args);
    }

    @Autowired
    private IDBHelper idbHelper;


    @GetMapping("/conn")
    public String testConn(){
        Connection conn=idbHelper.getConnection();
        String constr=conn.toString();
        return constr;
    }
}

  其实我们也可以使用@ComponentScan,但是会造成强依赖,以上三种方案,最好的就是第一种,因为后面的两种,如果托管多个配置类,就需要写多个@Import,同时也需要修改类名,这样就失去了starter弱依赖的优势。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值