Dynamically inject beans into Spring containers based on ImportBeanDefinitionRegistrar and FactoryBean

Posted by Tonic-_- on Mon, 07 Mar 2022 21:17:55 +0100

1, Background

We have developed a third-party jar package to integrate with Spring and inject it into the Spring container. In their own jar packages, a custom annotation @ customimport (beanname = "") is added to the classes that need to be added to the Spring container. The value of the beanname attribute represents the name that needs to be injected into the Spring container.

2, Implementation scheme

1. Implementation based on @ ComponentScan annotation

Using this scheme is relatively simple. You can directly add our customized annotations to the Spring container by using @ ComponentScan(includeFilters = {@ComponentScan.Filter(value = CustomImport.class)}). This scheme is slightly.

2. Implementation based on ImportBeanDefinitionRegistrar and FactoryBean

1. By implementing this interface (ImportBeanDefinitionRegistrar), we can add our own BeanDefinition object to BeanDefinitionRegistry and wait for subsequent Spring initialization objects.

Note:

​ 1. We can get some properties from the custom annotation, and then personalize what kind of Bean we want to initialize.

​ 2. The class implementing ImportBeanDefinitionRegistrar is annotated with @ Configuration and @ Import annotations, which can be loaded when the program starts.

2. Implementing this interface (FactoryBean) allows us to customize and instantiate the Bean we want to build.

3. Attention

Some people may say, why should I choose to implement based on the second method when I can handle things with @ ComponentScan? Isn't this unnecessary? In fact, I mainly record such an idea. For example, how can I integrate @ scanner and Mapper into Spring management? It should be similar to scheme 2 above.

3, Implementation steps

1. Customize an annotation CustomImport. The class marked by this annotation indicates that it needs to be added to the Spring container.

  1. The CustomImport annotation can add some additional attributes, such as beanName, which indicates the name of the bean when injected into the Spring container.

2. Write a class to implement CustomImportFactoryBean, indicating how to instantiate the class marked by CustomImport annotation.

  1. Construct the object.
  2. The constructed object needs to complete the initialization operation by itself. If you need to use the object in Spring, you can get the ApplicationContext and then get the injection.

3. Write a class to implement ImportBeanDefinitionRegistrar, scan out all CustomImport classes, then construct BeanDefinition and add it to the Spring container.

  1. When constructing BeanDefinition, the BeanClass attribute needs to specify the class of the previous CustomImportFactoryBean.
  2. ClassPathBeanDefinitionScanner can scan packages.

4. Customize an annotation enablecummimport. After adding this annotation to the startup class, implement the call of ImportBeanDefinitionRegistrar.

  1. The @ Import annotation is required on this annotation

1. Custom annotation CustomImport

/**
 * The classes marked by this annotation will also be automatically added to Spring management.
 *
 * @author huan.fu 2021/4/14 - 9:23 am
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomImport {
    /**
     * The name of the bean injected into the Spring container
     */
    String beanName();
}

2. Implement the CustomImportFactoryBean build object

package com.huan.study.framewrok;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * Factory Bean, which is used to build the class annotated by CustomImport annotation. How to instantiate it
 *
 * @author huan.fu 2021/4/14 - 9:44 am
 */
public class CustomImportFactoryBean implements FactoryBean<object>, ApplicationContextAware {

    private Class<!--?--> type;
    private String beanName;
    private ApplicationContext applicationContext;

    /**
     * If the objects built here need to rely on spring beans, they need to be built by themselves. By default, they will not be injected automatically, that is, the @ Autowired annotation will not take effect by default
     */
    @Override
    public Object getObject() throws Exception {
        Object instance = this.type.getDeclaredConstructor().newInstance();
        applicationContext.getAutowireCapableBeanFactory().autowireBean(instance);
        return instance;
    }

    @Override
    public Class<!--?--> getObjectType() {
        return type;
    }

    public Class<!--?--> getType() {
        return type;
    }

    public void setType(Class<!--?--> type) {
        this.type = type;
    }

    public String getBeanName() {
        return beanName;
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

3. Write ImportBeanDefinitionRegistrar

package com.huan.study.framewrok;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;

import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * @author huan.fu 2021/4/14 - 9:25 am
 */
public class CustomImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

    private static final Logger log = LoggerFactory.getLogger(CustomImportBeanDefinitionRegistrar.class);

    private Environment environment;
    private ResourceLoader resourceLoader;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
        if (!annotationMetadata.hasAnnotation(EnableCustomImport.class.getName())) {
            return;
        }
        Map<string, object> annotationAttributesMap = annotationMetadata.getAnnotationAttributes(EnableCustomImport.class.getName());
        AnnotationAttributes annotationAttributes = Optional.ofNullable(AnnotationAttributes.fromMap(annotationAttributesMap)).orElseGet(AnnotationAttributes::new);
        // Get the package to be scanned
        String[] packages = retrievePackagesName(annotationMetadata, annotationAttributes);
        // useDefaultFilters = false, that is, the second parameter means that the classes marked with @ Component, @ ManagedBean and @ Named annotations are not scanned
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false, environment, resourceLoader);
        // Add our custom annotation scan
        scanner.addIncludeFilter(new AnnotationTypeFilter(CustomImport.class));
        // Scan package
        for (String needScanPackage : packages) {
            Set<beandefinition> candidateComponents = scanner.findCandidateComponents(needScanPackage);
            try {
                registerCandidateComponents(registry, candidateComponents);
            } catch (ClassNotFoundException e) {
                log.error(e.getMessage(), e);
            }
        }
    }

    /**
     * Get the package to be scanned
     */
    private String[] retrievePackagesName(AnnotationMetadata annotationMetadata, AnnotationAttributes annotationAttributes) {
        String[] packages = annotationAttributes.getStringArray("packages");
        if (packages.length &gt; 0) {
            return packages;
        }
        String className = annotationMetadata.getClassName();
        int lastDot = className.lastIndexOf('.');
        return new String[]{className.substring(0, lastDot)};
    }

    /**
     * Register BeanDefinition
     */
    private void registerCandidateComponents(BeanDefinitionRegistry registry, Set<beandefinition> candidateComponents) throws ClassNotFoundException {
        for (BeanDefinition candidateComponent : candidateComponents) {
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
                Map<string, object> customImportAnnotationAttributesMap = annotationMetadata.getAnnotationAttributes(CustomImport.class.getName());
                AnnotationAttributes customImportAnnotationAttributes = Optional.ofNullable(AnnotationAttributes.fromMap(customImportAnnotationAttributesMap)).orElseGet(AnnotationAttributes::new);
                String beanName = customImportAnnotationAttributes.getString("beanName");
                String className = annotationMetadata.getClassName();
                Class<!--?--> clazzName = Class.forName(className);
                AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(CustomImportFactoryBean.class)
                        .addPropertyValue("type", clazzName)
                        .addPropertyValue("beanName", beanName)
                        .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)
                        .setRole(BeanDefinition.ROLE_INFRASTRUCTURE)
                        .getBeanDefinition();
                registry.registerBeanDefinition(beanName, beanDefinition);

            }
        }
    }


    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
}

4. Write @ EnableCustomImport

/**
 * Enable automatic import
 *
 * @author huan.fu 2021/4/14 - 9:29 am
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CustomImportBeanDefinitionRegistrar.class)
public @interface EnableCustomImport {

    String[] packages() default {};
}

5. Run a small case to get the test results

4, Completion code

1,https://gitee.com/huan1993/spring-cloud-parent/tree/master/springboot/bean-definition-registrar

5, Reference link

1,https://stackoverflow.com/questions/4970297/how-to-get-beans-created-by-factorybean-spring-managed

Topics: Java Spring Spring Boot Spring Cloud gitee