SpringBoot-启动原理(一)

1、开发任何一个Spring Boot项目,都会用到以下的启动类java

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

从上面代码能够看出,Annotation定义(@SpringBootApplication)和类定义(SpringApplication.run)最为耀眼,因此要揭开SpringBoot的神秘面纱,咱们要从这两位开始就能够了。web

一、SpringBootApplicationspring

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

(1)@Target:数组

        说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。app

        做用:用于描述注解的使用范围(即:被描述的注解能够用在什么地方)框架

        取值(ElementType)有:ide

    1.CONSTRUCTOR:用于描述构造器
    2.FIELD:用于描述域
    3.LOCAL_VARIABLE:用于描述局部变量
    4.METHOD:用于描述方法
    5.PACKAGE:用于描述包
    6.PARAMETER:用于描述参数
    7.TYPE:用于描述类、接口(包括注解类型) 或enum声明工具

        使用实例:学习

@Target(ElementType.TYPE)
public @interface Table {
    /**
     * 数据表名称注解,默认值为类名称
     * @return
     */
    public String tableName() default "className";
}

@Target(ElementType.FIELD)
public @interface NoDBColumn {

}

       注解Table 能够用于注解类、接口(包括注解类型) 或enum声明,而注解NoDBColumn仅可用于注解类的成员变量。ui

(2)@Retention:

       定义了该Annotation被保留的时间长短:某些Annotation仅出如今源代码中,而被编译器丢弃;而另外一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另外一些在class被装载时将被读取(请注意并不影响class的执行,由于Annotation与class在使用上是被分离的)。使用这个meta-Annotation能够对 Annotation的“生命周期”限制。

      做用:表示须要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)

      取值(RetentionPoicy)有:

    1.SOURCE:在源文件中有效(即源文件保留)
    2.CLASS:在class文件中有效(即class保留)
    3.RUNTIME:在运行时有效(即运行时保留)

      Retention meta-annotation类型有惟一的value做为成员,它的取值来自java.lang.annotation.RetentionPolicy的枚举类型值。具体实例以下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    public String name() default "fieldName";
    public String setFuncName() default "setField";
    public String getFuncName() default "getField"; 
    public boolean defaultDBValue() default false;
}

      Column注解的的RetentionPolicy的属性值是RUTIME,这样注解处理器能够经过反射,获取到该注解的属性值,从而去作一些运行时的逻辑处理。

(3)@Documented:

      @Documented用于描述其它类型的annotation应该被做为被标注的程序成员的公共API,所以能够被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。    

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
    public String name() default "fieldName";
    public String setFuncName() default "setField";
    public String getFuncName() default "getField"; 
    public boolean defaultDBValue() default false;
}

(4)@Inherited:

       @Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。若是一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

       注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。

  当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API加强了这种继承性。若是咱们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工做:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。

/**
 * 
 * @author peida
 *
 */
@Inherited
public @interface Greeting {
    public enum FontColor{ BULE,RED,GREEN};
    String name();
    FontColor fontColor() default FontColor.GREEN;
}

(5)自定义注解:

        使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其余细节。在定义注解时,不能继承其余的注解或接口。@interface用来声明一个注解,其中的每个方法其实是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。能够经过default来声明参数的默认值。

        定义注解格式:
   public @interface 注解名 {定义体}

        注解参数的可支持数据类型:

    1.全部基本数据类型(int,float,boolean,byte,double,char,long,short)
    2.String类型
    3.Class类型
    4.enum类型
    5.Annotation类型
    6.以上全部类型的数组

       Annotation类型里面的参数该怎么设定: 
        第一,只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型;   
        第二,参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数成员就为String;  
        第三,若是只有一个参数成员,最好把参数名称设为"value",后加小括号.例:下面的例子FruitName注解就只有一个参数成员。

       简单的自定义注解和使用注解实例:

package annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 水果名称注解
 * @author peida
 *
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
    String value() default "";
}
package annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 水果颜色注解
 * @author peida
 *
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
    /**
     * 颜色枚举
     * @author peida
     *
     */
    public enum Color{ BULE,RED,GREEN};
    
    /**
     * 颜色属性
     * @return
     */
    Color fruitColor() default Color.GREEN;

}
package annotation;

import annotation.FruitColor.Color;

public class Apple {
    
    @FruitName("Apple")
    private String appleName;
    
    @FruitColor(fruitColor=Color.RED)
    private String appleColor;
    
    
    
    
    public void setAppleColor(String appleColor) {
        this.appleColor = appleColor;
    }
    public String getAppleColor() {
        return appleColor;
    }
    
    
    public void setAppleName(String appleName) {
        this.appleName = appleName;
    }
    public String getAppleName() {
        return appleName;
    }
    
    public void displayName(){
        System.out.println("水果的名字是:苹果");
    }
}

       注解元素的默认值:

  注解元素必须有肯定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。所以, 使用空字符串或0做为默认值是一种经常使用的作法。这个约束使得处理器很难表现一个元素的存在或缺失的状态,由于每一个注解的声明中,全部元素都存在,而且都具备相应的值,为了绕开这个约束,咱们只能定义一些特殊的值,例如空字符串或者负数,一次表示某个元素不存在,在定义注解时,这已经成为一个习惯用法。例如:

package annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 水果供应者注解
 * @author peida
 *
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
    /**
     * 供应商编号
     * @return
     */
    public int id() default -1;
    
    /**
     * 供应商名称
     * @return
     */
    public String name() default "";
    
    /**
     * 供应商地址
     * @return
     */
    public String address() default "";
}

      定义了注解,并在须要的时候给相关类,类属性加上注解信息,若是没有响应的注解信息处理流程,注解能够说是没有实用价值。如何让注解真真的发挥做用,主要就在于注解处理方法,下一步咱们将学习注解信息的获取和处理!

      注解的使用:

      第一步:新建一个annotation,名字为:MyAnnotation.java。

package com.dragon.test.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by gmq on 2015/9/10.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation
{

    String hello () default "hello";
    String world();
}

     第二步:创建一个MyTest.java 来使用上面的annotation。

package com.dragon.test.annotation;

/**
 * Created by gmq on 2015/9/10.
 */
public class MyTest
{

    @MyAnnotation(hello = "Hello,Beijing",world = "Hello,world")
    public void output() {
        System.out.println("method output is running ");
    }
}

    第三步:用反射机制来调用注解中的内容

package com.dragon.test.annotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

/**
 * 用反射机制来调用注解中的内容
 * Created by gmq on 2015/9/10.
 */
public class MyReflection
{
    public static void main(String[] args) throws Exception
    {
        // 得到要调用的类
        Class<MyTest> myTestClass = MyTest.class;
        // 得到要调用的方法,output是要调用的方法名字,new Class[]{}为所须要的参数。空则不是这种
        Method method = myTestClass.getMethod("output", new Class[]{});
        // 是否有类型为MyAnnotation的注解
        if (method.isAnnotationPresent(MyAnnotation.class))
        {
            // 得到注解
            MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
            // 调用注解的内容
            System.out.println(annotation.hello());
            System.out.println(annotation.world());
        }
        System.out.println("----------------------------------");
        // 得到全部注解。必须是runtime类型的
        Annotation[] annotations = method.getAnnotations();
        for (Annotation annotation : annotations)
        {
            // 遍历全部注解的名字
            System.out.println(annotation.annotationType().getName());
        }
    }
}

      输出:

      Hello,Beijing
      Hello,world
      ----------------------------------
      com.dragon.test.annotation.MyAnnotation

===================================================================================

      虽然定义使用了多个Annotation进行了原信息标注,但实际上重要的只有三个Annotation:

  • @Configuration(@SpringBootConfiguration点开查看发现里面仍是应用了@Configuration)
  • @EnableAutoConfiguration
  • @ComponentScan

     因此,若是咱们使用以下的SpringBoot启动类,整个SpringBoot应用依然能够与以前的启动类功能对等:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

     每次写这3个比较累,因此写一个@SpringBootApplication方便点。接下来分别介绍这3个Annotation。

二、@Configuration

     这里的@Configuration对咱们来讲不陌生,它就是JavaConfig形式的Spring Ioc容器的配置类使用的那个@Configuration,SpringBoot社区推荐使用基于JavaConfig的配置形式,因此,这里的启动类标注了@Configuration以后,自己其实也是一个IoC容器的配置类。
举几个简单例子回顾下,XML跟config配置方式的区别:

   (1)表达形式层面

    基于XML配置的方式是这样:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
       default-lazy-init="true">
    <!--bean定义-->
</beans>

    而基于JavaConfig的配置方式是这样:

@Configuration
public class MockConfiguration{
    //bean定义
}

    任何一个标注了@Configuration的Java类定义都是一个JavaConfig配置类。

  (2)注册bean定义层面

   基于XML的配置形式是这样:

<bean id="mockService" class="..MockServiceImpl">
    ...
</bean>

  而基于JavaConfig的配置形式是这样的:

@Configuration
public class MockConfiguration{
    @Bean
    public MockService mockService(){
        return new MockServiceImpl();
    }
}

  任何一个标注了@Bean的方法,其返回值将做为一个bean定义注册到Spring的IoC容器,方法名将默认成该bean定义的id。

(3)表达依赖注入关系层面

  为了表达bean与bean之间的依赖关系,在XML形式中通常是这样:

<bean id="mockService" class="..MockServiceImpl">
    <propery name ="dependencyService" ref="dependencyService" />
</bean>

<bean id="dependencyService" class="DependencyServiceImpl"></bean>

  而基于JavaConfig的配置形式是这样的:

@Configuration
public class MockConfiguration{
    @Bean
    public MockService mockService(){
        return new MockServiceImpl(dependencyService());
    }
    
    @Bean
    public DependencyService dependencyService(){
        return new DependencyServiceImpl();
    }
}

 若是一个bean的定义依赖其余bean,则直接调用对应的JavaConfig类中依赖bean的建立方法就能够了。

三、@ComponentScan

      @ComponentScan这个注解在Spring中很重要,它对应XML配置中的元素,@ComponentScan的功能其实就是自动扫描并加载符合条件的组件(好比@Component和@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中。

      咱们能够经过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,若是不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。

      注:因此SpringBoot的启动类最好是放在root package下,由于默认不指定basePackages。

四、@EnableAutoConfiguration

     我的感受@EnableAutoConfiguration这个Annotation最为重要,因此放在最后来解读,你们是否还记得Spring框架提供的各类名字为@Enable开头的Annotation定义?好比@EnableScheduling、@EnableCaching、@EnableMBeanExport等,@EnableAutoConfiguration的理念和作事方式其实一脉相承,简单归纳一下就是,借助@Import的支持,收集和注册特定场景相关的bean定义

  • @EnableScheduling是经过@Import将Spring调度框架相关的bean定义都加载到IoC容器。
  • @EnableMBeanExport是经过@Import将JMX相关的bean定义加载到IoC容器。

    而@EnableAutoConfiguration也是借助@Import的帮助,将全部符合自动配置条件的bean定义加载到IoC容器,仅此而已!

    @EnableAutoConfiguration做为一个复合Annotation,其自身定义关键信息以下:

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

     其中,最关键的要属@Import(EnableAutoConfigurationImportSelector.class),借助EnableAutoConfigurationImportSelector,@EnableAutoConfiguration能够帮助SpringBoot应用将全部符合条件的@Configuration配置都加载到当前SpringBoot建立并使用的IoC容器。就像一只“八爪鱼”同样

    借助于Spring框架原有的一个工具类:SpringFactoriesLoader的支持,@EnableAutoConfiguration能够智能的自动配置功效才得以大功告成!

   (1)自动配置幕后英雄:SpringFactoriesLoader详解

    SpringFactoriesLoader属于Spring框架私有的一种扩展方案,其主要功能就是从指定的配置文件META-INF/spring.factories加载配置。

public abstract class SpringFactoriesLoader {
    //...
    public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
        ...
    }


    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        ....
    }
}

   配合@EnableAutoConfiguration使用的话,它更可能是提供一种配置查找的功能支持,即根据@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration做为查找的Key,获取对应的一组@Configuration类。

    上图就是从SpringBoot的autoconfigure依赖包中的META-INF/spring.factories配置文件中摘录的一段内容,能够很好地说明问题。

    因此,@EnableAutoConfiguration自动配置的魔法骑士就变成了:从classpath中搜寻全部的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项经过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,而后汇总为一个并加载到IoC容器。

2、SpringApplication执行流程

   SpringApplication的run方法的实现是咱们本次旅程的主要线路,该方法的主要流程大致能够概括以下:

   1) 若是咱们使用的是SpringApplication的静态run方法,那么,这个方法里面首先要建立一个SpringApplication对象实例,而后调用这个建立好的SpringApplication的实例方法。在SpringApplication实例初始化的时候,它会提早作几件事情:

  • 根据classpath里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该建立一个为Web应用使用的ApplicationContext类型。
  • 使用SpringFactoriesLoader在应用的classpath中查找并加载全部可用的ApplicationContextInitializer。
  • 使用SpringFactoriesLoader在应用的classpath中查找并加载全部可用的ApplicationListener。
  • 推断并设置main方法的定义类。

  2) SpringApplication实例初始化完成而且完成设置后,就开始执行run方法的逻辑了,方法执行伊始,首先遍历执行全部经过SpringFactoriesLoader能够查找到并加载的SpringApplicationRunListener。调用它们的started()方法,告诉这些SpringApplicationRunListener,“嘿,SpringBoot应用要开始执行咯!”。

  3) 建立并配置当前Spring Boot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile)。

  4) 遍历调用全部SpringApplicationRunListener的environmentPrepared()的方法,告诉他们:“当前SpringBoot应用使用的Environment准备好了咯!”。

  5) 若是SpringApplication的showBanner属性被设置为true,则打印banner。

  6) 根据用户是否明确设置了applicationContextClass类型以及初始化阶段的推断结果,决定该为当前SpringBoot应用建立什么类型的ApplicationContext并建立完成,而后根据条件决定是否添加ShutdownHook,决定是否使用自定义的BeanNameGenerator,决定是否使用自定义的ResourceLoader,固然,最重要的,将以前准备好的Environment设置给建立好的ApplicationContext使用。

  7) ApplicationContext建立好以后,SpringApplication会再次借助Spring-FactoriesLoader,查找并加载classpath中全部可用的ApplicationContext-Initializer,而后遍历调用这些ApplicationContextInitializer的initialize(applicationContext)方法来对已经建立好的ApplicationContext进行进一步的处理。

 8) 遍历调用全部SpringApplicationRunListener的contextPrepared()方法。

 9) 最核心的一步,将以前经过@EnableAutoConfiguration获取的全部配置以及其余形式的IoC容器配置加载到已经准备完毕的ApplicationContext。

 10) 遍历调用全部SpringApplicationRunListener的contextLoaded()方法。

 11) 调用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序。

 12) 查找当前ApplicationContext中是否注册有CommandLineRunner,若是有,则遍历执行它们。

 13) 正常状况下,遍历执行SpringApplicationRunListener的finished()方法、(若是整个过程出现异常,则依然调用全部SpringApplicationRunListener的finished()方法,只不过这种状况下会将异常信息一并传入处理)
去除事件通知点后,整个流程以下:

总结

      到此,SpringBoot的核心组件完成了基本的解析,综合来看,大部分都是Spring框架背后的一些概念和实践方式,SpringBoot只是在这些概念和实践上对特定的场景事先进行了固化和升华,而也偏偏是这些固化让咱们开发基于Sping框架的应用更加方便高效。

相关文章
相关标签/搜索