Hand-held to read the source code to see the implementation of the IoC container

Posted by dino345 on Thu, 29 Aug 2019 18:07:41 +0200

Here is the 13th article in the Good Interview series.Nowadays there are many articles about IoC on the Internet, but there are not many manual debugging of source code and few descriptions of the implementation steps of the IoC container. Now let's debug the source code under the snow and see what IoC is like. By the way, remember a few key points and have a good interview in the future; brothers who do not know how to read the source code should keep up with themWell, take a closer look at my debugging sayings.

Prerequisite reading

I can hardly remember that many years ago, in the resume skills section, I wrote a few words to familiarize myself with spring, and then the interviewer took a look to familiarize me with spring?Yes, let's talk about the implementation of the IoC container.

I was confused at that time. To tell the truth, I always know what IoC is and what its principle is. I can also use analogy to say this ghost, but if you let me talk about the implementation of the IoC container, TM can't answer it for a while because I haven't looked at the source code.And that's why I wrote this article. To make this process clear, I've looked through the entire SpringBoot startup process and the implementation of the IoC container.

Summary after looking at the source code

After carefully reviewing the source code, a summary is drawn, which is also a "hot spot for interviews". The implementation of the IoC container can be divided into the following steps:

  • 1. Location of Resource

As anyone who has used SpringBoot knows, scans for components start with the package in which the main class resides. By reading the source code, we can see that in the prepareContext() method, SpringBoot first resolves the main class, which is the class using the annotation @SpringBootApplication, into BeanDefinition, then invokeBeanFact.The oryPostProcessors () method parses the BeanDefinition of the main class to get the path to the basePackage, which is what we call positioning, and of course, the use of the comment @Import is not discussed here.

  • 2. Loading BeanDefinition

With Resource positioned, naturally the next step must be to load various BeanDefinitions into memory. Here you can see that the source code has been debugging for a long time, but the process is very simple.In so-called loaded memory, think with your toes that you know it's actually through the location of the Resource, splicing a path into: classpath: org/springframework/boot/demo/.class, and then using a more wonderful class, PathMatchingResourcePatternResolver, all of the classes under that path.The file loads in, and then iterates to see if there is a @Component comment, but don't think you can invert the source code directly through the @Component comment. Younger me do the same, but I find it's not Ouch, just keep looking if you want to.If there are any comments, it is the BeanDefinition that this step will load.

  • 3. Register BeanDefinition

The so-called registration sounds very big, registering classes in the IoC container, but it's really simple, put ting them into a CurrentHashMap. Yes, the IoC container actually uses this Map to store these BeanDefinition data.

[Warm Tip]: If you don't know what BeanDefinition is, Spring knows it's really shallow and didn't read my previous article, you can read this one Dynamic Registration of Spring Bean s

In fact, it is almost OK to see here, after all, if the process is roughly described, it is really the three above, and it is interesting to see the source code parsing can continue.Later, I will describe the process of debugging with the source code, preferably with code debugging, otherwise halo will occur.

Location of resources

Still follow the previous routine, by locating directly where the resource is located, and then by debugging the page to see what the program does, we can jump directly to the parse function in the ComponentScanAnnotationParser class, explaining first what a wave of ComponentScanAnnotationParsers is called ComponentScanAnnotationParserIt's actually just an internal tool for Spring that parses a specified package based on the @ComponentScan annotation property on a class to get its BeanDefiniiton.

Next, let's look at the parse function. Following the debugging classmates, you can add a breakpoint to the last section of the function directly, start the project, and you can see the running process of the project, as shown below.

// By debugging, you can see the data for the componentScan carrier, basePackages, while declaringClass is
// "com.nuofankj.demo.DemoApplication", which is package path + main class
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
                componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

        Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
        boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
        scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
                BeanUtils.instantiateClass(generatorClass));

        ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
        if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
            scanner.setScopedProxyMode(scopedProxyMode);
        }
        else {
            Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
            scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
        }

        scanner.setResourcePattern(componentScan.getString("resourcePattern"));

        for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
            for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                scanner.addIncludeFilter(typeFilter);
            }
        }
        for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
            for (TypeFilter typeFilter : typeFiltersFor(filter)) {
                scanner.addExcludeFilter(typeFilter);
            }
        }

        boolean lazyInit = componentScan.getBoolean("lazyInit");
        if (lazyInit) {
            scanner.getBeanDefinitionDefaults().setLazyInit(true);
        }
        // [Key] The place where resources are actually stored
        Set<String> basePackages = new LinkedHashSet<>();
        String[] basePackagesArray = componentScan.getStringArray("basePackages");
        for (String pkg : basePackagesArray) {
            String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            Collections.addAll(basePackages, tokenized);
        }
        for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
            basePackages.add(ClassUtils.getPackageName(clazz));
        }
        // [Important] This run, since it was not started by default, the final basePackages store the package path of declaringClass, which is straightforward to see
        // ClassUtils.getPackageName(declaringClass)ll
        if (basePackages.isEmpty()) {
            basePackages.add(ClassUtils.getPackageName(declaringClass));
        }

        scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
            @Override
            protected boolean matchClassName(String className) {
                return declaringClass.equals(className);
            }
        });
        // [Important] StringUtils.toStringArray(basePackages) breakpoint You can see that the printed package name is the main class [com.nuofankj.demo]
        return scanner.doScan(StringUtils.toStringArray(basePackages));
    }

Let's look at what we've done in scanner.doScan

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    for (String basePackage : basePackages) {
        // [Important] Scan the specified package for beans to be loaded
        Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
        for (BeanDefinition candidate : candidates) {
            ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
            candidate.setScope(scopeMetadata.getScopeName());
            String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
            if (candidate instanceof AbstractBeanDefinition) {
                postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
            }
            if (candidate instanceof AnnotatedBeanDefinition) {
                AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
            }
            if (checkCandidate(beanName, candidate)) {
                BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                definitionHolder =
                        AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                beanDefinitions.add(definitionHolder);
                // [Important] Register the Bean in the IoC container (beanDefinitionMap)
                registerBeanDefinition(definitionHolder, this.registry);
            }
        }
    }
    return beanDefinitions;
}

Two more important methods are noted above
Set <BeanDefinition> candidates = findCandidate Components (basePackage); function is to scan classes from basePackage and parse them into BeanDefinition. After Dun gets all eligible classes, he registers them in the registerBeanDefinition(definitionHolder, this.registry); and register them in the IoC container.That is, the second and third steps of the IoC container initialization process, the loading of BeanDefinition, and the registration of BeanDefinition are completed in this method.

At this point, the positioning of resources is over.

Load of BeanDefinition

The function scanCandidateComponents that we then mentioned above

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
        // [Important] Splice scan paths, resulting in classpath*:com/nuofankj/demo/**/*.class
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        // Scan all classes in the packageSearchPath path stitched from above
        Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
        boolean traceEnabled = logger.isTraceEnabled();
        boolean debugEnabled = logger.isDebugEnabled();
        for (Resource resource : resources) {
            if (traceEnabled) {
                logger.trace("Scanning " + resource);
            }
            if (resource.isReadable()) {
                try {
                    MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
                    // [Important] This determines if the class is labeled with the @Component annotation
                    if (isCandidateComponent(metadataReader)) {
                        // Encapsulate this class as ScannedGenericBeanDefinition, which is the implementation class of the BeanDefinition interface
                        ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                        sbd.setResource(resource);
                        sbd.setSource(resource);
                        if (isCandidateComponent(sbd)) {
                            if (debugEnabled) {
                                logger.debug("Identified candidate component class: " + resource);
                            }
                            candidates.add(sbd);
                        } else {
                            if (debugEnabled) {
                                logger.debug("Ignored because not a concrete top-level class: " + resource);
                            }
                        }
                    } else {
                        if (traceEnabled) {
                            logger.trace("Ignored because not matching any filter: " + resource);
                        }
                    }
                } catch (Throwable ex) {
                    throw new BeanDefinitionStoreException(
                            "Failed to read candidate component class: " + resource, ex);
                }
            } else {
                if (traceEnabled) {
                    logger.trace("Ignored because not readable: " + resource);
                }
            }
        }
    } catch (IOException ex) {
        throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
    }
    return candidates;
}

Looking at the source code and the comments, we can see that the packageSearchPath is stitched together and all the classes in the path are scanned in the getResources(packageSearchPath) method, then traversed through the Resources to see if they are classes labeled with the @Component annotation and not classes that need to be excluded.The scanned class is then resolved to a ScannedGenericBeanDefinition, which is the implementation class of the BeanDefinition interface.
Here the BeanDefinition load of the IoC container ends.

Registration of BeanDefinition

Loading is over, so let's see how to register the BeanDefinition, go back to the registerBeanDefinition(definitionHolder, this.registry), and as you can see from debugging, eventually enter the following functions

    public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException {
        String beanName = definitionHolder.getBeanName();
        // [Key]
        registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
        String[] aliases = definitionHolder.getAliases();
        if (aliases != null) {
            String[] var4 = aliases;
            int var5 = aliases.length;

            for(int var6 = 0; var6 < var5; ++var6) {
                String alias = var4[var6];
                registry.registerAlias(beanName, alias);
            }
        }

    }

Let's first look at what BeanDefinitionRegistry is. This class is primarily used to register BeanDefinition instances in the registry to complete the registration process.Continue to look at the key function registerBeanDefinition

@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
        throws BeanDefinitionStoreException {

    Assert.hasText(beanName, "Bean name must not be empty");
    Assert.notNull(beanDefinition, "BeanDefinition must not be null");

    if (beanDefinition instanceof AbstractBeanDefinition) {
        try {
            ((AbstractBeanDefinition) beanDefinition).validate();
        } catch (BeanDefinitionValidationException ex) {
            throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                    "Validation of bean definition failed", ex);
        }
    }
    // [Important] You can see here that the ConcurrentHashMap is used to store bean s, so you can see that the bottom level of the IoC container is a ConcurrentHashMap, but it is placed in an object, you can know by looking at the source that this object is a DefaultListableBeanFactory
    BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
    if (existingDefinition != null) {
        // [Important] If this class does not allow Overriding to throw an exception directly
        if (!isAllowBeanDefinitionOverriding()) {
            throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                    "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
                            "': There is already [" + existingDefinition + "] bound.");
        } else if (existingDefinition.getRole() < beanDefinition.getRole()) {
            // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
            if (logger.isWarnEnabled()) {
                logger.warn("Overriding user-defined bean definition for bean '" + beanName +
                        "' with a framework-generated bean definition: replacing [" +
                        existingDefinition + "] with [" + beanDefinition + "]");
            }
        } else if (!beanDefinition.equals(existingDefinition)) {
            if (logger.isInfoEnabled()) {
                logger.info("Overriding bean definition for bean '" + beanName +
                        "' with a different definition: replacing [" + existingDefinition +
                        "] with [" + beanDefinition + "]");
            }
        } else {
            if (logger.isDebugEnabled()) {
                logger.debug("Overriding bean definition for bean '" + beanName +
                        "' with an equivalent definition: replacing [" + existingDefinition +
                        "] with [" + beanDefinition + "]");
            }
        }
        // [Key] Register in beanDefinitionMap
        this.beanDefinitionMap.put(beanName, beanDefinition);
    } else {
        if (hasBeanCreationStarted()) {
            // Cannot modify startup-time collection elements anymore (for stable iteration)
            synchronized (this.beanDefinitionMap) {
                this.beanDefinitionMap.put(beanName, beanDefinition);
                List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
                updatedDefinitions.addAll(this.beanDefinitionNames);
                updatedDefinitions.add(beanName);
                this.beanDefinitionNames = updatedDefinitions;
                if (this.manualSingletonNames.contains(beanName)) {
                    Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);
                    updatedSingletons.remove(beanName);
                    this.manualSingletonNames = updatedSingletons;
                }
            }
        } else {
            // [Important] Register in beanDefinitionMap if you are still in the startup registration stage
            this.beanDefinitionMap.put(beanName, beanDefinition);
            this.beanDefinitionNames.add(beanName);
            this.manualSingletonNames.remove(beanName);
        }
        this.frozenBeanDefinitionNames = null;
    }

    if (existingDefinition != null || containsSingleton(beanName)) {
        resetBeanDefinition(beanName);
    }
}

Through step-by-step debugging, we can see that we have finally reached the DefaultListableBeanFactory. First, we will introduce what the DefaultListableBeanFactory is. This class can be referred to as the IoC container native. The debugging source can see that at the end, the IoC container is actually a ConcurrentHashMap.
So when was DefaultListableBeanFactory built?We can see that

Built by obtainFreshBeanFactory, the internal function after debugging looks nothing, does not affect the overall process, does not go into depth, is interested in you can follow the debugging.

Blow water for a few minutes

The article summarizes me to the front, so let's not summarize it. The main reason for the slowing down of the latest writing speed is not good, busy has been busy. After all, in the game industry, I work at 9 or 10 o'clock every day and leave work at around 23 o'clock. However, the main reason for the bad state is emotional problems.Unmotivated learning leads to inadvertent writing of articles.Secondly, I found that my neck is sore recently. Occupational disease. Most of the programmers should pay attention to me. If you have good suggestions, you can put it down. I said occupational disease prevention. Of course, if you want to introduce my girlfriend to me, I don't mind Hiahiahia...

Topics: Java Spring SpringBoot