春季:寻找背景

几个月前,我的个人资料中发布了有关在JVM上加载类的详细信息演讲之后,我的同事们提出了一个很好的问题:Spring使用什么机制来解析配置,以及如何从上下文中加载类?




在经过数小时的Spring源代码调试之后,我的同事实验性地得出了非常简单和可以理解的事实。

一点理论


立即确定ApplicationContext是提供应用程序配置信息的Spring应用程序中的主要接口。

在直接进行演示之前, 我们将看一下创建ApplicationContext所涉及的步骤



在本文中,我们将分析第一步,因为我们对读取配置和创建BeanDefinition感兴趣。

BeanDefinition是一个接口,用于描述bean,其属性,构造函数参数和其他元信息。

关于bean本身的配置,Spring有4种配置方法:

  1. Xml配置 -ClassPathXmlApplicationContext(“ context.xml”);
  2. Groovy配置 -GenericGroovyApplicationContext(“ context.groovy”);
  3. 通过注释进行配置,该注释指示要扫描的软件包 -AnnotationConfigApplicationContext(“ package.name”);
  4. JavaConfig-通过注释进行配置,该注释指示用@Configuration标记的类(或类数组)-AnnotationConfigApplicationContext(JavaConfig.class)。

XML配置


我们以一个简单的项目为基础:

public class SpringContextTest{
   private static String classFilter = "film.";
   
   public static void main(String[] args){
        
         printLoadedClasses(classFilter);
         /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
            All - 5 : 0 - Filtered      /*
        doSomething(MainCharacter.num); doSomething(FilmMaker.class);
        printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
            All - 7 : 2 - Filtered     /*

在这里,您应该解释一些方法和使用的方法:

  • printLoadedClasses(字符串...过滤器) -该方法将作为参数传递的包中的加载器和加载的JVM类的名称打印到控制台。此外,还有关于所有已加载类的数量的信息。
  • doSomething(对象o)是一种完成原始工作的方法,但不允许在编译期间的优化过程中排除提到的类。

我们连接到Spring项目(在下文中,Spring 4充当测试主题):

11 public class SpringContextTest{
12    private static String calssFilter = "film.";
13    
14    public static void main(String[] args){
15        
16        printLoadedClasses(classFilter);
17       /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
18           All - 5 : 0 - Filtered      /*
19        doSomething(MainCharacter.num); doSomething(FilmMaker.class);
20        printLoadedClasses(classFilter);
21        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
22               class film.MainCharacter
23               class film.FilmMaker
24               All - 7 : 2 - Filtered   /*
25        ApplicationContext context = new ClassPathXmlApplicationContext(
26                  configLocation: "applicationContext.xml");
27        printLoadedClasses(classFilter);

第25行是通过Xml配置声明和初始化ApplicationContext

配置xml文件如下:

<beans xmlns = "http://www.spingframework.org/schema/beans" xmlns:xsi = "http..."
        <bean id = "villain" class = "film.Villain" lazy-init= "true">
                <property name = "name" value = "Vasily"/>
        </bean>
</beans> 

在配置Bean时,我们指定一个确实存在的类。请注意指定的属性lazy-init =“ true”:在这种情况下,仅在从上下文中请求bin后才创建bin。

我们看一下Spring在引发上下文时如何使用配置文件中声明的类来清除这种情况:

public class SpringContextTest {
    private static String classFilter = "film.";
    
    public static void main(String[] args) {
        
           printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
           All - 5 : 0 - Filtered      /*
        doSomething(MainCharacther.num); doSomething(FilmMaker.class);
        printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
            All - 7 : 2 - Filtered     /*
        ApplicationContext context = new ClassPathXmlApplicationContext(
                  configLocation: "applicationContext.xml");
        printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
               class film.Villain

            All - 343 : 3- Filtered     /*

让我们检查Xml配置的详细信息:

-读取配置文件的类是XmlBeanDefinitionReader,它实现了BeanDefinitionReader接口

- 输入处的XmlBeanDefinitionReader接收InputStream并通过DefaultDocumentLoader加载Document

Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);

-之后,将处理此文档的每个元素,如果它是bin,则将基于填充的数据(id,名称,类,别名,初始化方法,destroy方法等)创建BeanDefinition:

} else if (delegate.nodeNameEquals(ele, "bean")) {
    this.processBeanDefinition(ele, delegate);

-每个BeanDefinition都放置在一个Map中,该Map存储在DefaultListableBeanFactory类中:

this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);

在代码中,Map如下所示:

/** Map of bean definition objects, keyed by bean name */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(64);

现在,在同一配置文件中,用film.BadVillain类添加另一个bean声明

<beans xmlns = "http://www.spingframework.org/schema/beans" xmlns:xsi = "http..."
        <bean id = "goodVillain" class = "film.Villain" lazy-init= "true">
                <property name = "name" value = "Good Vasily"/>
        </bean>
        <bean id = "badVillain" class = "film.BadVillain" lazy-init= "true">
                <property name = "name" value = "Bad Vasily"/>
        </bean>

我们将看到如果您打印已创建的BeanDefenitionNames和已加载的类的列表,会发生什么情况

ApplicationContext context = new ClassPathXmlApplicationContext(
        configLocation: "applicationContext.xml");
System.out.println(Arrays.asList(context.getBeanDefinitionNames()));
        
printLoadedClasses(calssFilter);

尽管配置文件中指定film.BadVillain不存在,但Spring可以正常工作:

ApplicationContext context = new ClassPathXmlApplicationContext(
        configLocation: "applicationContext.xml");
System.out.println(Arrays.asList(context.getBeanDefinitionNames()));
//  [goodVillain, badVillain]
printLoadedClasses(calssFilter);
/* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
               class film.Villain
    All - 343 : 3- Filtered   /*

所述BeanDefenitionNames列表包含2个元素;也就是说,
在我们的文件中配置的那两个BeanDefinition已创建。

两个垃圾箱的配置基本相同。但是,在加载现有类的同时,没有出现问题。从中我们可以得出结论,也有人尝试加载不存在的类,但是失败的尝试并没有破坏任何东西。

让我们尝试通过它们的名称获取bean本身:

ApplicationContext context = new ClassPathXmlApplicationContext(
        configLocation: "applicationContext.xml");
System.out.println(Arrays.asList(context.getBeanDefinitionNames()));
//  [goodVillain, badVillain]
System.out.println(context.getBean( name: "goodVillain"));

System.out.println(context.getBean( name: "badVillain"));

我们得到以下信息:



如果在第一种情况下收到有效的bean,则在第二种情况下到达异常。

注意堆栈跟踪:延迟加载类。所有类装入器都经过爬网,以尝试在先前装入的装入器中找到它们正在寻找的类。在找不到所需的类之后,通过调用Utils.forName方法,尝试按名称查找不存在的类,这导致自然错误。

引发上下文时,仅加载一个类,而尝试加载不存在的文件并不会导致错误。为什么会发生?

那是因为我们注册了lazy-init:true并禁止Spring创建bean的实例,在该实例中生成先前接收到的异常。如果从配置中删除此属性或更改其值lazy-init:false,则上述错误也将崩溃,但不会忽略该应用程序。在我们的例子中,上下文已初始化,但是我们无法创建bean的实例,因为 找不到指定的类。

Groovy配置


使用Groovy文件配置上下文时,您需要生成GenericGroovyApplicationContext,它接收带有上下文配置作为输入的字符串。在这种情况下,GroovyBeanDefinitionReader类用于读取context 此配置与Xml基本相同,仅对Groovy文件有效。另外,GroovyApplicationContext与Xml文件配合使用也很好。

一个简单的Groovy配置文件的示例:

beans {
    goodOperator(film.Operator){bean - >
            bean.lazyInit = 'true' >
            name = 'Good Oleg' 
         }
    badOperator(film.BadOperator){bean - >
            bean.lazyInit = 'true' >
            name = 'Bad Oleg' / >
        }
  }

我们尝试做与Xml相同的操作:



错误立即崩溃:Groovy与Xml一样,创建BeanDefenitions,但是在这种情况下,后处理器立即给出错误。

通过注释进行配置,该注释指示要扫描的软件包或JavaConfig


此配置与前两个不同。通过注释进行配置使用2个选项:JavaConfig和基于类的注释。

此处使用相同的上下文:AnnotationConfigApplicationContext(“包” /JavaConfig.class)它的工作取决于传递给构造函数的内容。

AnnotationConfigApplicationContext的上下文中,有2个私有字段:

  • 私有的最终AnnotatedBeanDefinitionReader阅读器(与JavaConfig一起使用);
  • 私有的最终ClassPathBeanDefinitionScanner扫描(扫描数据包)。

AnnotatedBeanDefinitionReader 的独特之处在于它可以在多个阶段中工作:

  1. 注册所有@Configuration文件以进行进一步解析;
  2. 注册一个特殊的BeanFactoryPostProcesso r,即BeanDefinitionRegistryPostProcessor,它使用ConfigurationClassParser解析JavaConfig并创建一个BeanDefinition

考虑一个简单的例子:

@Configuration
public class JavaConfig {
    
    @Bean
    @Lazy
    public MainCharacter mainCharacter(){
        MainCharacter mainCharacter = new MainCharacter();
        mainCharacter.name = "Patric";
        return mainCharacter;        
   }
}

public static void main(String[] args) {

     ApplicationContext javaConfigContext = 
               new AnnotationConfigApplicationContext(JavaConfig.class);
     for (String str : javaConfigContext.getBeanDefinitionNames()){
          System.out.println(str);
     }
     printLoadedClasses(classFilter);

我们使用最简单的bin创建一个配置文件。我们看一下将要加载的内容:



如果在Xml和Groovy的情况下,加载了与声明的数量一样多的BeanDefinition,那么在这种情况下,在引发上下文的过程中,将同时加载声明的BeanDefinition和其他BeanDefinition。在通过JavaConfig实现的情况下,所有类都会立即加载,包括JavaConfig本身的类,因为它本身是bean。

另一个要点:在Xml和Groovy配置的情况下,上传了343个文件,这里出现了631个其他文件,更加“繁重”。ClassPathBeanDefinitionScanner的

工作步骤

  • 指定的软件包确定要扫描的文件列表。所有文件都属于目录;
  • , InputStream org.springframework.asm.ClassReader.class;
  • 3- , org.springframework.core.type.filter.AnnotationTypeFilter. Spring , Component , Component;
  • , BeanDefinition.

Xml和Groovy的使用注释的所有“魔术”都恰好位于springframework.asmClassReader.class类中该阅读器的特殊性在于它可以使用字节码。也就是说,阅读器从字节码中获取一个InputStream,对其进行扫描并在其中查找注释。 考虑一个简单的例子的扫描仪。 创建您自己的注释以搜索相应的类:





import org.springframework.stereotype.Component
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface MyBeanLoader{
       String value() default "";

我们创建2个类:一个带有标准Component注释,第二个带有自定义注释:

@Component 
public class MainCharacter {
      public static int num = 1;
      @Value("Silence")
      public String name;
      public MainCharacter() { }

MyBeanLoader("makerFilm")
@Lazy 
public class FilmMaker {
      public static int staticInt = 1;
      @Value("Silence")
      public String filmName;
      public FilmMaker(){}

结果,我们为这些类和成功加载的类获取了生成的BeanDefinition。

ApplicationContext annotationConfigContext =
       new AnnotationConfigApplicationContext(...basePackages: "film");
for (String str : annotationConfigContext.getBeanDefinitionNames()){
     System.out.println(str);
}
printLoadedClasses(classFilter);



结论


综上所述,所提出的问题可以回答如下:

  1. Spring ?

    , . BeanDefinition, : , BeanDefinition’ . BeanDefinition, ..
  2. Spring ?

    Java: , , , .

PS我希望在这篇文章中我能够“揭开秘密的面纱”,并详细说明如何在第一阶段形成弹性背景。事实证明,并非所有事物都如此“恐怖”。但这只是大型框架的一小部分,这意味着未来还有很多新的有趣的事物。

All Articles