跳至主要內容

Spring Boot 的基本认识

soulballad微服务SpringBootSpringBoot约 2931 字大约 10 分钟

SpringBoot 的基本认识

不管是 spring cloud alibaba 还是 spring cloud netflix,都是基于 springboot 这个微框架来构建的,所以我希望花一点时间来讲一下 springboot

什么是 springboot

对于 spring 框架,我们接触得比较多的应该是 spring mvc、和 spring。而 spring 的核心在于 IOC(控制反转)和 DI(依赖注入)。而这些框架在使用的过程中会需要配置大量的 xml,或者需要做很多繁琐的配置。

springboot 框架是为了能够帮助使用 spring 框架的开发者快速高效的构建一个基于 spirng 框架以及 spring 生态体系的应用解决方案。它是对 “约定优于配置” 这个理念下的一个最佳实践。因此它是一个服务于框架的框架,服务的范围是简化配置文件。

约定优于配置的体现

约定优于配置的体现主要是

  1. maven 的目录结构
    a) 默认有 resources 文件夹存放配置文件
    b) 默认打包方式为 jar

  2. spring-boot-starter-web 中默认包含 spring mvc 相关依赖以及内置的 tomcat 容器,使得构建一个 web 应用更加简单

  3. 默认提供 application.properties/yml 文件

  4. 默认通过 spring.profiles.active 属性来决定运行环境时读取的配置文件

  5. EnableAutoConfiguration 默认对于依赖的 starter 进行自动装载

SpringBootApplication 注解

为了揭开 springboot 的奥秘,我们直接从 Annotation 入手,看看 @SpringBootApplication 里面,做了什么?
打开 SpringBootApplication 这个注解,可以看到它实际上是一个复合注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
    @AliasFor(annotation = EnableAutoConfiguration.class)
    Class<?>[] exclude() default {};

    @AliasFor(annotation = EnableAutoConfiguration.class)
    String[] excludeName() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
    Class<?>[] scanBasePackageClasses() default {};
}

SpringBootApplication 本质上是由 3 个注解组成,分别是

  1. @Configuration
  2. @EnableAutoConfiguration
  3. @ComponentScan

我们可以直接用这三个注解也可以启动 springboot 应用,只是每次配置三个注解比较繁琐,所以直接用一个复合注解更方便些。

然后仔细观察者三个注解,除了 EnableAutoConfiguration 可能稍微陌生一点,其他两个注解使用得都很多

@Configuration

Configuration 这个注解大家应该有用过,它是 JavaConfig 形式的基于 Spring IOC 容器的配置类使用的一种注解。因为 SpringBoot 本质上就是一个 spring 应用,所以通过这个注解来加载 IOC 容器的配置是很正常的。所以在启动类里面标注了@Configuration,意味着它其实也是一个 IoC 容器的配置类。

传统意义上的 spring 应用都是基于 xml 形式来配置 bean 的依赖关系。然后通过 spring 容器在启动的时候,把 bean 进行初始化并且,如果 bean 之间存在依赖关系,则分析这些已经在 IoC 容器中的 bean 根据依赖关系进行组装。

直到 Java5 中,引入了 Annotations 这个特性,Spring 框架也紧随大流并且推出了基于 Java 代码和 Annotation 元信息的依赖关系绑定描述的方式。也就是 JavaConfig。

从 spring3 开始,spring 就支持了两种 bean 的配置方式,一种是基于 xml 文件方式、另一种就是 JavaConfig 任何一个标注了 @Configuration 的 Java 类定义都是一个 JavaConfig 配置类。而在这个配置类中,任何标注了@Bean 的方法,它的返回值都会作为 Bean 定义注册到Spring 的 IOC 容器,方法名默认成为这个 bean 的 id

//自定义Bean
public class DemoClass {
    public void say(){
        System.out.println("Say: Hello Mic");
    }
}
//Configuration类
@Configuration
public class ConfigurationDemo {
    @Bean
    public DemoClass demoClass() {
        return new DemoClass();
    }
}

public class DemoMain {
    public static void main(String[] args) {
        ApplicationContext applicationContext = 
            		new AnnotationConfigApplicationContext(ConfigurationDemo.class);
        String[] definitionNames = applicationContext.getBeanDefinitionNames();
        for (String definitionName : definitionNames) {
            System.out.println(definitionName);
        }
    }
}
...
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
configurationDemo
demoClass

@ComponentScan

ComponentScan 这个注解是大家接触得最多的了,相当于 xml 配置文件中的 context:component-scan。 它的主要作用就是扫描指定路径下的标识了需要装配的类,自动装配到 spring 的 Ioc 容器中。

标识需要装配的类的形式主要是: @Component 、@Repository、@Service、@Controller 这类的注解标识的类。

ComponentScan 默认会扫描当前 package 下的的所有加了相关注解标识的类到 IoC 容器中;

@Component
public class DemoClass {

}
//默认扫描当前包下所有类,可以通过basePackages进行配置
@ComponentScan
public class DemoMain {
    public static void main(String[] args) {
        ApplicationContext applicationContext = 
            		new AnnotationConfigApplicationContext(DemoMain.class);
        String[] definitionNames = applicationContext.getBeanDefinitionNames();
        for (String definitionName : definitionNames) {
            System.out.println(definitionName);
        }
    }
}
...
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
demoMain
demoClass

@EnableAutoConfiguration

我们把 EnableAutoConfiguration 放在最后讲的目的并不是说它是一个新的东西,只是他对于 springboot 来说意义重大。

Enable 并不是新鲜玩意

仍然是在 spring3.1 版本中,提供了一系列的 @Enable 开头的注解,Enable 注解应该是在 JavaConfig 框架上更进一步的完善,使得用户在使用 spring 相关的框架是,避免配置大量的代码从而降低使用的难度 。

比如常见的一些 Enable 注解:@EnableWebMvc,(这个注解引入了 MVC 框架在 Spring 应用中需要用到的所有
bean); 比如说 @EnableScheduling,开启计划任务的支持; 找到 @EnableAutoConfiguration,我们可以看到每一个涉及到 Enable 开头的注解,都会带有一个@Import 的注解。

@Import(AutoConfigurationImportSelector.class) 
public @interface EnableAutoConfiguration { 
}

@Import 注解

import 注解是什么意思呢? 联想到 xml 形式下有一个 形式的配置,就明白它的作用了。

import 就是把多个分来的容器配置合并在一个配置中。在JavaConfig 中所表达的意义是一样的。

// package com.gupaoedu.springboot.springbootfirst.secondDemo.other;
public class OtherBean {

}
@Configuration
public class OtherConfig {
    @Bean
    public OtherBean otherBean(){
        return new OtherBean();
    }
}

//package com.gupaoedu.springboot.springbootfirst.secondDemo.current;
public class DefaultBean {
}
@Import(OtherConfig.class)
@Configuration
public class SpringConfig {
    @Bean
    public DefaultBean defaultBean(){
        return new DefaultBean();
    }
}

public class SecondMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext=
            new AnnotationConfigApplicationContext(SpringConfig.class);
        String[] defNames=applicationContext.getBeanDefinitionNames();
        for(int i=0;i<defNames.length;i++){
            System.out.println(defNames[i]);
        }
    }
}
...
org.springframework.context.event.internalEventListenerFactory
springConfig
com.gupaoedu.springboot.springbootfirst.secondDemo.other.OtherConfig
otherBean
defaultBean

@Import 注解可以配置三种不同的 class

  1. 第一种就是前面演示过的,基于普通 bean 或者带有 @Configuration 的 bean 进行注入
  2. 实现 ImportSelector 接口进行动态注入
  3. 实现 ImportBeanDefinitionRegistrar 接口进行动态注入

深入分析 EnableAutoConfiguration

EnableAutoConfiguration 的主要作用其实就是帮助 springboot 应用把所有符合条件的 @Configuration 配置都加载到当前 SpringBoot 创建并使用的 IoC 容器中。

再回到 EnableAutoConfiguration 这个注解中,我们发现它的 import 是这样

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

我们可以看到 @EnableAutoConfiguration 也是一个复合注解,它上面配置了 @Import(AutoConfigurationImportSelector.class) 和 @AutoConfigurationPackage。

AutoConfigurationImportSelector 是什么?

Enable注解不仅仅可以像前面演示的案例一样很简单的实现多个 Configuration 的整合,还可以实现一些复杂的场景,比如可以根据上下文来激活不同类型的 bean。

来看一下 AutoConfigurationImportSelector 的定义:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
}

AutoConfigurationImportSelector 的类图如下:

1563011667531

可以看到 AutoConfigurationImportSelector 实现了一个 ImportSelector 接口,重写 selectImports 方法。

public interface ImportSelector {
    String[] selectImports(AnnotationMetadata var1);
}

我们可以自定义类实现 ImportSelector 最终实现注入:

public class CacheService {
}

public class CacheImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        Map<String,Object> attributes= 		                 annotationMetadata.getAnnotationAttributes(EnableDefineService.class.getName());
        //动态注入bean :自己去实现判断逻辑实现动态配置
        return new String[]{CacheService.class.getName()}; //返回的是一个固定的CacheService
    }
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 通过import进行导入
@Import({CacheImportSelector.class}) 
public @interface EnableDefineService {
    //配置一些方法,可以配置排除
    Class<?>[] exclude() default {};
}

@SpringBootApplication
@EnableDefineService
public class EnableDemoMain {
    public static void main(String[] args) {
        ConfigurableApplicationContext  ca=
            	SpringApplication.run(EnableDemoMain.class,args);
        System.out.println(ca.getBean(CacheService.class));
    }
}
...
com.gupaoedu.springboot.springbootfirst.thirdDemo.CacheService@42deb43a

AutoConfigurationPackage 是什么

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}

可以看到 AutoConfigurationPackage 中 导入了 AutoConfigurationPackages.Registrar这个类,来看下这个类

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        register(registry, new PackageImport(metadata).getPackageName());
    }

    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
        return Collections.singleton(new PackageImport(metadata));
    }
}

可以发现 Registrar 实现了 ImportBeanDefinitionRegistrar 接口,重写了 registerBeanDefinitions 方法。

public interface ImportBeanDefinitionRegistrar {
    void registerBeanDefinitions(AnnotationMetadata var1, BeanDefinitionRegistry var2);
}

下面我们自定义类实现 ImportBeanDefinitionRegistrar 接口,实现注入:

public class LoggerService {
}

public class LoggerDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        Class beanClass=LoggerService.class;
        RootBeanDefinition beanDefinition=new RootBeanDefinition(beanClass);
        String beanName=StringUtils.uncapitalize(beanClass.getSimpleName());
        beanDefinitionRegistry.registerBeanDefinition(beanName,beanDefinition);
    }
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({CacheImportSelector.class,LoggerDefinitionRegistrar.class})
public @interface EnableDefineService {
    //配置一些方法
    Class<?>[] exclude() default {};
}

@SpringBootApplication
@EnableDefineService
public class EnableDemoMain {
    public static void main(String[] args) {
        ConfigurableApplicationContext ca=
            			SpringApplication.run(EnableDemoMain.class,args);
        System.out.println(ca.getBean(CacheService.class));
        System.out.println(ca.getBean(LoggerService.class));
    }
}
...
com.gupaoedu.springboot.springbootfirst.thirdDemo.CacheService@3153ddfc
com.gupaoedu.springboot.springbootfirst.thirdDemo.LoggerService@60afd40d

@EnableAutoConfiguration 注解的实现原理

了解了 ImportSelector 和 ImportBeanDefinitionRegistrar 后,对于 EnableAutoConfiguration 的理解就容易一些了 它会通过 import 导入第三方提供的 bean 的配置类:

AutoConfigurationImportSelector

 @Import(AutoConfigurationImportSelector.class) 

从名字来看,可以猜到它是基于 ImportSelector 来实现基于动态 bean 的加载功能。之前我们讲过 Springboot @Enable* 注解的工作原理 ImportSelector 接口,selectImports 方法 返回的数组(类的全类名)都会被纳入到spring 容器中。

那么可以猜想到这里的实现原理也一定是一样的,定位到 AutoConfigurationImportSelector 这个类中的 selectImports 方法

本质上来说,其实 EnableAutoConfiguration 会帮助 springboot 应用把所有符合 @Configuration 配置都加载到当前 SpringBoot 创建的 IoC 容器,而这里面借助了Spring 框架提供的一个工具类 SpringFactoriesLoader 的支持。以及用到了 Spring 提供的条件注解 @Conditional,选择性的针对需要加载的 bean 进行条件过滤

SpringFactoriesLoader

为了给大家补一下基础,我在这里简单分析一下SpringFactoriesLoader 这个工具类的使用。它其实和java 中的 SPI 机制的原理是一样的,不过它比 SPI 更好的点在于不会一次性加载所有的类,而是根据 key 进行加载。

首先 , SpringFactoriesLoader 的 作 用 是 从classpath/META-INF/spring.factories 文件中,根据 key 来加载对应的类到 spring IoC 容器中。

新建 maven 项目 gupao-core,结构图如下

1563013059121

public class GupaoCore {
   public String study(){
       System.out.println("good good study, day day up");
       return "GupaoeEdu.com";
   }
}

@Configuration
public class GupaoConfig {
   @Bean
   public GupaoCore gupaoCore(){
       return new GupaoCore();
   }
}

spring.factories 文件,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.gupaoedu.core.GupaoConfig

然后在另一项目中引入 gupao-core.jar 包,并使用 SpringBoot 启动

@SpringBootApplication
public class FouthMain {
    public static void main(String[] args) {
        ConfigurableApplicationContext ca= 
            					SpringApplication.run(EnableDemoMain.class,args);
        System.out.println(ca.getBean(GupaoCore.class).study());
    }
}

如果没有上面的 spring.factories 文件,则会出现下面错误

1563013250893

添加文件后,则可以执行成功

1563013297639

深入理解条件过滤

在分析 AutoConfigurationImportSelector 的源码时,会先扫描 spring-autoconfiguration-metadata.properties文件,最后在扫描 spring.factories 对应的类时,会结合前面的元数据进行过滤,为什么要过滤呢? 原因是很多的 @Configuration 其实是依托于其他的框架来加载的,如果当前的 classpath 环境下没有相关联的依赖,则意味着这些类没必要进行加载,所以,通过这种条件过滤可以有效的减少 @configuration 类的数量从而降低SpringBoot 的启动时间。

上面的例子,如果添加了文件 spring-autoconfigure-metadata.properties 并配置内容

com.gupaoedu.core.GupaoConfig.ConditionalOnClass=com.gupaoedu.TestClass

就表示 GupaoConfig 这个类的加载需要 classpath 路径有 TestClass 这个类,否则就会出现上面相同的错误,这个配置也可以同过注解来实现。

@ConditionalOnClass(value = {TestClass.class})
@SpringBootApplication
public class FouthMain {
    public static void main(String[] args) {
        ConfigurableApplicationContext ca= 
            		SpringApplication.run(EnableDemoMain.class,args);
        System.out.println(ca.getBean(GupaoCore.class).study());
    }
}

Conditional 中的其他注解

Conditions描述
@ConditionalOnBean在存在某个 bean 的时候
@ConditionalOnMissingBean不存在某个 bean 的时候
@ConditionalOnClass当前 classpath 可以找到某个类型的类时
@ConditionalOnMissingClass当前classpath 不可以找到某个类型的类时
@ConditionalOnResource当前classpath 是否存在某个资源文件
@ConditionalOnProperty当前jvm 是否包含某个系统属性为某个值
@ConditionalOnWebApplication当前 spring context 是否是 web 应用程序
上次编辑于:
贡献者: soulballad