Feign source code deep analysis - component scanner: ClassPathScanningCandidateComponentProvider

"No step, no even a thousand miles."

In this article, let's take a look at the core mechanism of feign and how to scan the @ FeignClient annotated interfaces under all packages

The entry is the registerFeignClients(metadata, registry) method

public void registerFeignClients(AnnotationMetadata metadata,
                                 BeanDefinitionRegistry registry) {
    ClassPathScanningCandidateComponentProvider scanner = getScanner();
    scanner.setResourceLoader(this.resourceLoader);

    Set<String> basePackages;

    Map<String, Object> attrs = metadata
        .getAnnotationAttributes(EnableFeignClients.class.getName());
    AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
        FeignClient.class);
    final Class<?>[] clients = attrs == null ? null
        : (Class<?>[]) attrs.get("clients");
    if (clients == null || clients.length == 0) {
        scanner.addIncludeFilter(annotationTypeFilter);
        basePackages = getBasePackages(metadata);
    }
    else {
        final Set<String> clientClasses = new HashSet<>();
        basePackages = new HashSet<>();
        for (Class<?> clazz : clients) {
            basePackages.add(ClassUtils.getPackageName(clazz));
            clientClasses.add(clazz.getCanonicalName());
        }
        AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
            @Override
            protected boolean match(ClassMetadata metadata) {
                String cleaned = metadata.getClassName().replaceAll("\\$", ".");
                return clientClasses.contains(cleaned);
            }
        };
        scanner.addIncludeFilter(
            new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
    }

    for (String basePackage : basePackages) {
        Set<BeanDefinition> candidateComponents = scanner
            .findCandidateComponents(basePackage);
        for (BeanDefinition candidateComponent : candidateComponents) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                // verify annotated class is an interface
                AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(),
                              "@FeignClient can only be specified on an interface");

                Map<String, Object> attributes = annotationMetadata
                    .getAnnotationAttributes(
                    FeignClient.class.getCanonicalName());

                String name = getClientName(attributes);
                registerClientConfiguration(registry, name,
                                            attributes.get("configuration"));

                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
}

ClassPathScanningCandidateComponentProvider scanner = getScanner();

First, get a Scanner here. Pay attention to the name of the Scanner. ClassPathScanningCandidateComponentProvider, as the name suggests, is a Provider that scans candidate components from the classpath. You can simply understand it as a Scanner, and then scans the components in a path under the classpath

clients == null || clients.length == 0

Here is a judgment. This judgment must be true because we did not assign a value to the clients property of the EnableFeignClients annotation

scanner.addIncludeFilter(annotationTypeFilter);

Add an annotation filter to the scanner, with @ FeignClient as the filtering condition. It seems to be used to filter out the interface marked with @ FeignClient annotation under the specified path. You can see it later through the breakpoint

basePackages = getBasePackages(metadata);

protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {
    Map<String, Object> attributes = importingClassMetadata
        .getAnnotationAttributes(EnableFeignClients.class.getCanonicalName());

    Set<String> basePackages = new HashSet<>();
    for (String pkg : (String[]) attributes.get("value")) {
        if (StringUtils.hasText(pkg)) {
            basePackages.add(pkg);
        }
    }
    for (String pkg : (String[]) attributes.get("basePackages")) {
        if (StringUtils.hasText(pkg)) {
            basePackages.add(pkg);
        }
    }
    for (Class<?> clazz : (Class[]) attributes.get("basePackageClasses")) {
        basePackages.add(ClassUtils.getPackageName(clazz));
    }

    if (basePackages.isEmpty()) {
        basePackages.add(
            ClassUtils.getPackageName(importingClassMetadata.getClassName()));
    }
    return basePackages;
}

This method is to parse the final package to be scanned, basePackages

Because in @ EnableFeignClients, we do not configure value, basePackages and basepackagecalasses

So I'll go to basepackages In the logic of isempty()

ClassUtils.getPackageName(importingClassMetadata.getClassName())

Here, the package path where the startup class is located will be obtained through a Utils tool class and returned

After you get the base packages, you start the logic in the for loop

Set candidateComponents = scanner.findCandidateComponents(basePackage);

A key line of code. What does this code do?

The components marked with @ FeignClient annotation are scanned from the basePackages path and placed in a set set

A method of this core must go in and have a look

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
        return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    }
    else {
        return scanCandidateComponents(basePackage);
    }
}

If you continue, you will come to the logic of scancandidate components (base package)

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
            resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        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);
                    if (isCandidateComponent(metadataReader)) {
                        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;
}

The value of this packageSearchPath is classpath *: COM / WC / resttemplate / * * / * Class, specify the scanning range. Under classpath, start all of the package and its sub packages where the class is located Class file

isCandidateComponent(metadataReader)

Note that the parameter type of this method is MetadataReader!!!

Why should I emphasize this, because an overloaded method with the same name will be called later, but the implementation covered in getScanner() is used!!!

I was a little confused when I saw it. In fact, it didn't matter, because I was a little confused when I saw it for the first time. I didn't understand it until I read it several times later. First remember the conclusion, and then turn it back

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    for (TypeFilter tf : this.excludeFilters) {
        if (tf.match(metadataReader, getMetadataReaderFactory())) {
            return false;
        }
    }
    for (TypeFilter tf : this.includeFilters) {
        if (tf.match(metadataReader, getMetadataReaderFactory())) {
            return isConditionMatch(metadataReader);
        }
    }
    return false;
}

Here, excludeFilters is empty, because we didn't add it, so the above code directly

includeFilters have values. We added an annotation type filter to filter @ FeignClient annotations

Remember that code? scanner.addIncludeFilter(annotationTypeFilter);

Keep going, TF match(metadataReader, getMetadataReaderFactory())

Because my XXXController is currently scanned, naturally there is no @ FeignClient annotation, so false is returned

That is, iscandidatecomponent (metadata reader) returns false, so no operation is performed, and the next cycle is directly performed to scan other classes

Keep going, TF match(metadataReader, getMetadataReaderFactory())

Because this time I scanned the FeignClient annotated class, this match succeeded

The isConditionMatch(metadataReader) method is called and finally returns true. This method does not go in and take a closer look. It belongs to the lower level operation of spring context. It is mainly to see whether the component meets some conditions of condition annotation, such as missingBean. This is the set of automatic configuration. Our business components generally return true

That is, iscandidatecomponent (metadata reader) returns true here

Going down, isCandidateComponent(sbd) enters an overloaded method with the type AnnotatedBeanDefinition

protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    AnnotationMetadata metadata = beanDefinition.getMetadata();
    return (metadata.isIndependent() && (metadata.isConcrete() ||
                                         (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
}

Do you think you'll get into the above method?

No!

Remember the first line of the registerFeignClients method?

ClassPathScanningCandidateComponentProvider scanner = getScanner();

Let's look at the logic of this getScanner()

protected ClassPathScanningCandidateComponentProvider getScanner() {
    return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
        @Override
        protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
            boolean isCandidate = false;
            if (beanDefinition.getMetadata().isIndependent()) {
                if (!beanDefinition.getMetadata().isAnnotation()) {
                    isCandidate = true;
                }
            }
            return isCandidate;
        }
    };
}

Think about what I said before, and look at the parameters of this overloaded method, AnnotatedBeanDefinition!!!

You see, the second method actually enters the method overridden in getScanner()

Let's look at the breakpoint to verify it

beanDefinition.getMetadata().isAnnotation()

In this method, we will judge whether the scanned component is an annotation (how to play with annotations ~). If it is not an annotation, we will return a ture. Of course, our feignclient client is not an annotation, but an interface, so we return a ture

candidates.add(sbd);

Then put the @ FeignClient into the set set, and finally return the set

Set<BeanDefinition> candidateComponents = scanner
    .findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
    if (candidateComponent instanceof AnnotatedBeanDefinition) {
        // verify annotated class is an interface
        AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
        AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
        Assert.isTrue(annotationMetadata.isInterface(),
                      "@FeignClient can only be specified on an interface");

        Map<String, Object> attributes = annotationMetadata
            .getAnnotationAttributes(
            FeignClient.class.getCanonicalName());

        String name = getClientName(attributes);
        registerClientConfiguration(registry, name,
                                    attributes.get("configuration"));

        registerFeignClient(registry, annotationMetadata, attributes);
    }
}

if (candidateComponent instanceof AnnotatedBeanDefinition)

The judgment of if means that your filtered bean must be annotated. This must be true, true

Assert.isTrue(annotationMetaCdata.isInterface(), "@FeignClient can only be specified on an interface");

This assertion says that the @ FeignClient annotation must be marked on the interface!!!

Then get the @ FeignClient annotation. The most important attribute we added is to get the name of the service we want to access specified by our @ FeignClient. This name gets the name of the service we want to access

registerClientConfiguration(registry, name, attributes.get("configuration"));

Register the configuration properties of name (service name) and @ FeignClient in BeanDefinitionRegistry. This is not the core thing. Go directly

registerFeignClient(registry, annotationMetadata, attributes);

Finally, register the scanned @ FeignClient interface, which is the key. Let's focus on the analysis in the next article

ok, this article mainly analyzes the core process of feign, creates a scanner, scans out all the interfaces marked with @ FeignClient annotation, and then puts them into a set for subsequent registration. This article may be a little windy, but I must have made it clear. Friends who are not very clear can read it several times. If they don't understand it, they can also leave a message at the bottom of the article. I will answer it in time when I see it.

Keywords: microservice

Added by spaggle on Wed, 22 Dec 2021 11:10:35 +0200