SpringCloud的核心之SpringBoot

Scroll Down

Spring Ioc\DI

Ioc(Inversion of Control)和DI(Dependency Injection)的全称分别是控制反转和依赖注入。

  • Ioc
    Ioc(控制反转)实际上就是把对象的生命周期托管到Spring容器中,而反转是指对象的获取方式被反转了。当使用Spring Ioc容器之后,客户端不需要再通过new来创建这些对象。在早期的Spring中,主要通过XML的方式来定义Bean、Spring会解析XML文件,把定义的Bean装载到IOC容器中。

  • DI
    DI(Dependency Inject),也就是依赖注入,简单理解就是Ioc容器在运行期间,动态地把某种依赖关系注入组件中。实现依赖注入的方法有三种,分别是接口注入、构造方法注入和setter方法注入。不过现在基本上都基于注解的方式来描述Bean之间的依赖关系,比如@Autowired、@Inject、@Resource但是不管形式怎么变化,本质上还是一样的。

Bean装配方式的升级

基于XML配置的方式很好地完成了对象生命周期的描述和管理,但是随着项目规模的不断扩大,XML的配置也逐渐增多,使得配置文件难以管理。另一方面,项目中依赖关系越来越复杂,配置文件变得难以理解,这个时候迫切需要一种方式来解决这类问题。
随着Jdk1.5带来的注解支持,Spring从2.X开始,可以使用注解的方式来对Bean进行声明和注入,大大减少了XML的配置量。
Spring升级到3.x后提供了JavaConfig的能力,它可以完全取代XML,通过Java代码的方式来完成Bean的注入。

虽然通过注解的方式来装配Bean,可以在一定程度上减少XML配置带来的问题,但是本质问题仍然没有解决。比如:

  • 依赖过多。Spring可以整合几乎所有常用的技术框架,比如JSON、Mybatis、Redis、Log等,不同依赖包的版本很容易导致版本兼容的问题。
  • 配置太多。以Spring使用JavaConfig方式整合Mybatis为例,需要配置注解驱动、配置数据源、配置Mybatis、配置事务管理器等,这些只是集成一个技术组件需要的基础配置,在一个项目中这类配置很多,开发者需要做很多类似的重复工作。
  • 运行和部署很繁琐,需要先把项目打包,再部署到容器上。

@Configuration注解的使用方式

使用@Configuration来注解类,在类里面包含多个@Bean注解的方法。这些使用@Bean注解的方法,会被加载为BeanFactory里面的BeanDefinition,其中beanName默认为方法名,并且默认会创建对应的bean对象实例,其中bean默认的热加载和单例的。其实@Configuration注解的类,就相当于一个xml配置文件的beans标签。
使@Configuration注解的类生效方式,即被spring容器ApplicationContext感知并加载。

  • 使用AnnotationConfigApplicationContext,在refresh之前,通过AnnotationConfigApplicationContext的register方法注册这个类,如下:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
 
MyBean myBean = ctx.getBean(MyBean.class);

在web项目中也可以在web.xml中的contextClass中指定AnnotationConfigWebApplicationContext:

<web-app>
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.
            support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>
    
    ...
    
    <servlet>
        <servlet-name>sampleServlet</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.
                support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
    </servlet>
    
    ...
    
</web-app>
  • 基于Spring的context:annotation-config
<beans>
   <context:annotation-config/>
   <bean class="com.acme.AppConfig"/>
</beans>
  • 基于Spring的componentScan,因为@Configuration注解自身也是一个@Component。可以是使用xml或者使用@ComponentScan注解,但是@ComponentScan注解要么不指定basePackages或者basePackageClasses,因为@ComponentScan注解默认是扫描当前类所在的包及其子包;要么需要包含当前类。
<context:component-scan/>

spring配置注解context:annotation-config和context:component-scan区别

  • context:annotation-config
    context:annotation-config 是用于激活那些已经在spring容器里注册过的bean上面的注解,也就是显示的向Spring注册
AutowiredAnnotationBeanPostProcessor
CommonAnnotationBeanPostProcessor
PersistenceAnnotationBeanPostProcessor
RequiredAnnotationBeanPostProcessor

这四个Processor,注册这4个BeanPostProcessor的作用,就是为了你的系统能够识别相应的注解。BeanPostProcessor就是处理注解的处理器。

比如我们要使用@Autowired注解,那么就必须事先在 Spring 容器中声明 AutowiredAnnotationBeanPostProcessor Bean。传统声明方式如下

<bean class="org.springframework.beans.factory.annotation. AutowiredAnnotationBeanPostProcessor "/>

如果想使用@ Resource 、@ PostConstruct、@ PreDestroy等注解就必须声明CommonAnnotationBeanPostProcessor。传统声明方式如下

<bean class="org.springframework.beans.factory.annotation. CommonAnnotationBeanPostProcessor"/> 

如果想使用@PersistenceContext注解,就必须声明PersistenceAnnotationBeanPostProcessor的Bean。

<bean class="org.springframework.beans.factory.annotation.PersistenceAnnotationBeanPostProcessor"/> 

如果想使用 @Required的注解,就必须声明RequiredAnnotationBeanPostProcessor的Bean。

<bean class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor"/> 

一般来说,像@ Resource 、@ PostConstruct、@Antowired这些注解在自动注入还是比较常用,所以如果总是需要按照传统的方式一条一条配置显得有些繁琐和没有必要,于是spring给我们提供< context:annotation-config/>的简化配置方式,自动帮你完成声明。

假如我们要使用如@Component、@Controller、@Service等这些注解,使用能否激活这些注解呢?

单纯使用< context:annotation-config/>对上面这些注解无效,不能激活!

  • context:component-scan
    Spring 给我提供了context:component-scan配置,如下
<context:component-scan base-package=”XX.XX”/> 

该配置项其实也包含了自动注入上述 四个processor 的功能,因此当使用 < context:component-scan/> 后,就可以将 < context:annotation-config/> 移除了。
通过对base-package配置,就可以把controller包下 service包下 dao包下的注解全部扫描到了!

总结

(1)< context:annotation-config />:仅能够在已经在已经注册过的bean上面起作用。对于没有在spring容器中注册的bean,它并不能执行任何操作。
(2)< context:component-scan base-package="XX.XX"/> :除了具有上面的功能之外,还具有自动将带有@component,@service,@Repository等注解的对象注册到spring容器中的功能。
< context:annotation-config />和 < context:component-scan>同时存在的时候,前者会被忽略。如@autowire,@resource等注入注解只会被注入一次!

Springboot的价值

Spring Boot并不是一个新的技术框架,其主要作用就是简化Spring应用的开发,开发者只需要通过少量的代码就可以创建一个产品级的Spring应用,而达到这一目的最核心的思想就是约定优于配置(Convention over Configuration)。
如何理解约定优于匹配
约定优于配置(Convention Over Configuration)是一种软件设计范式,目的在于减少配置的数量或者降低理解难度,从而提升开发效率。需要注意的是,它并不是一种新的思想,实际上从我们开始接触Java依赖,就会发现很多地方都有这种思想的体现。比如,数据库中的表名的设计对应到Java中实体类的名字,就是一种约定,我们可以从这个实体类的名字知道它对应数据库中哪张表。
在Spring Boot中,约定优于配置的思想主要体现在以下方面(包括但不限于):

  • Maven目录结构的约定。
  • Spring Boot默认的配置文件及配置文件中配置属性的约定。
  • 对于Spring MVC的依赖,自动依赖内置的Tomcat容器。
  • 对于Starter组件自动完成装配。

Spring Boot的核心
Spring Boot是基于Spring Framework体系来构建的,所以它并没有什么新的东西,但是要想学好Spring Boot,必须直到它的核心:

  • Start组件,提供开箱即用的组件。
  • 自动装配,自动根据上下文完成Bean的装配。
  • Actuator,Spring Boot 应用的监控。
  • Spring Boot CLI,基于命令行工具快速构建SpringBoot应用。

其中,最核心的部分应该是自动装配,Starter组件的核心部分也是基于自动装配来实现的。

自动装配的实现

自动装配在Spring Boot中是通过@EnableAutoConfiguration注解来开启的,这个注解的声明在启动类注解@SpringBootApplication内。

@MapperScan({"com.example.matrixweb.benchmark.dao", "com.example.matrixweb.tmp.dao"})
@SpringBootApplication
public class MatrixWebApplication {

    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

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

进入@SpringBootApplication注解,可以看到@EnableAutoConfiguration注解的声明。

@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 {

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * @return the class names to exclude
	 * @since 1.3.0
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	/**
	 * Base packages to scan for annotated components. Use {@link #scanBasePackageClasses}
	 * for a type-safe alternative to String-based package names.
	 * <p>
	 * <strong>Note:</strong> this setting is an alias for
	 * {@link ComponentScan @ComponentScan} only. It has no effect on {@code @Entity}
	 * scanning or Spring Data {@link Repository} scanning. For those you should add
	 * {@link org.springframework.boot.autoconfigure.domain.EntityScan @EntityScan} and
	 * {@code @Enable...Repositories} annotations.
	 * @return base packages to scan
	 * @since 1.3.0
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	/**
	 * Type-safe alternative to {@link #scanBasePackages} for specifying the packages to
	 * scan for annotated components. The package of each class specified will be scanned.
	 * <p>
	 * Consider creating a special no-op marker class or interface in each package that
	 * serves no purpose other than being referenced by this attribute.
	 * <p>
	 * <strong>Note:</strong> this setting is an alias for
	 * {@link ComponentScan @ComponentScan} only. It has no effect on {@code @Entity}
	 * scanning or Spring Data {@link Repository} scanning. For those you should add
	 * {@link org.springframework.boot.autoconfigure.domain.EntityScan @EntityScan} and
	 * {@code @Enable...Repositories} annotations.
	 * @return base packages to scan
	 * @since 1.3.0
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

	/**
	 * Specify whether {@link Bean @Bean} methods should get proxied in order to enforce
	 * bean lifecycle behavior, e.g. to return shared singleton bean instances even in
	 * case of direct {@code @Bean} method calls in user code. This feature requires
	 * method interception, implemented through a runtime-generated CGLIB subclass which
	 * comes with limitations such as the configuration class and its methods not being
	 * allowed to declare {@code final}.
	 * <p>
	 * The default is {@code true}, allowing for 'inter-bean references' within the
	 * configuration class as well as for external calls to this configuration's
	 * {@code @Bean} methods, e.g. from another configuration class. If this is not needed
	 * since each of this particular configuration's {@code @Bean} methods is
	 * self-contained and designed as a plain factory method for container use, switch
	 * this flag to {@code false} in order to avoid CGLIB subclass processing.
	 * <p>
	 * Turning off bean method interception effectively processes {@code @Bean} methods
	 * individually like when declared on non-{@code @Configuration} classes, a.k.a.
	 * "@Bean Lite Mode" (see {@link Bean @Bean's javadoc}). It is therefore behaviorally
	 * equivalent to removing the {@code @Configuration} stereotype.
	 * @since 2.2
	 * @return whether to proxy {@code @Bean} methods
	 */
	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;

}

进入到@EnableAutoConfiguration注解里,可以看到除@Import注解之外,还多了一个@AutoConfigurationPackage注解(它的作用是把该注解的类所在的包及子包下所有组件扫描到Spring Ioc容器中)。并且,@Import注解中导入的并不是一个Configuration的配置类,而是一个AutoConfigurationSelector类。从这一点来看,它就和其他的@Enable注解有很大的不同。

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

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	Class<?>[] exclude() default {};

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * @return the class names to exclude
	 * @since 1.3.0
	 */
	String[] excludeName() default {};

}

AutoConfigurationSelector实现了ImportSelector,它只有一个selectImports抽象方法,并且返回一个String数组,在这个数组中可以指定需要装配到Ioc容器的类,当在@Import中导入一个ImportSelector的实现类之后,会把该实现类中返回的Class名称都装载到Ioc容器中。

public interface ImportSelector {

	/**
	 * Select and return the names of which class(es) should be imported based on
	 * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);

}

和@Configuration不同的是,ImportSelector可以实现批量装配,并且还可以通过逻辑处理来实现Bean的选择性装配,也就是可以根据上下文来决定哪些类能够被Ioc容器初始化。

自动装配原理分析

自动装配的核心是扫描约定目录下的文件进行解析,解析完成之后把得到的Configuration配置类通过ImportSelector进行导入,从而完成Bean的自动装配过程。那么接下来我们通过分析AutoConfigurationImortSelector的实现。
定位到AutoConfigurationImportSelector中的selectImports方法,它是ImportSelector接口的实现,这个方法中主要有两个功能:

  • AutoConfigurationMetadataLoader.loadMetadata从META-INF/spring-autoconfigure-metadata.properties中加载自动装配的条件元数据,简单来说就是只有满足条件的Bean才能够进行装配。
  • 收集所有符合条件的配置类autoConfigurationEntry.getConfiguration(),完成自动装配。

需要注意的是,在AutoConfigurationImportSelector中不执行selectImports方法,而是通过ConfigurationClassPostProcessor中的processConfigBeanDefinitions方法来扫描和注册所有配置类的Bean,最终还是会调用getAutoConfigurationEntry方法来获取所有需要自动装配的配置类。

总的来说,它先获得所有的配置类,通过去重、exclude排除等操作,得到最终需要实现自动装配的配置类。
这里用到了SpringFactoriesLoader,它是Spring内部提供的一种约定俗称的加载方式,类似于Java中的SPI。简单来说,它会扫描classpath下的META-INF/spring.factories文件,spring.factories文件中的数据以key-value形式存储,而SpringFactoriesLoader.loadFactoryNames会根据key得到对应的value值。
至此,自动装配的原理基本上就分析完了,简单来总结下核心过程:

  • 通过@Import(AutoConfigurationImportSelector)实现配置类的导入,但是这里并不是传统意义上的单个配置类装配。
  • AutoConfigurationImportSelector类实现了ImportSelector接口,重写了方法selectImports,它用于实现选择性批量配置类的装配。
  • 通过Spring提供的SpringFactoriesLoader机制,扫描classpath路径下的META-INF/spring.factories,读取需要实现自动装配的配置类。
  • 通过条件筛选的方式,把不符合条件的配置类移除,最终完成自动装配。