SoFunction
Updated on 2025-04-13

How to inject dynamic proxy beans in Spring

Injecting dynamic proxy beans in Spring

In Springboot we can use built-in annotations such as@Service@Component@RepositoryTo register a bean, you can also pass it in the configuration class@BeanCome to register bean. These are all built-in notes from Spring.

In addition, you can also use it@WebFilter@WebServlet@WebListenerCombination of annotations@ServletComponentScanAutomatically register beans. But here@WebFilter@WebServlet@WebListenerIt is not an annotation of Spring, but an annotation of Servlet 3+. Why can these annotated classes be automatically registered as Spring's beans, and what is their implementation principle?

If entering@ServletComponentScanYou can find that there is another annotation on this annotation:@Import(), further check:class ServletComponentScanRegistrar implements ImportBeanDefinitionRegistrar. The key here isImportBeanDefinitionRegistrar interface.

ImportBeanDefinitionRegistrar

The most classic design in Spring is AOP and IOC, which makes the Spring framework have good scalability, andImportBeanDefinitionRegistrar It is one of the hooks used to expand.

Generally speaking, beans in Spring are registered through XML configuration files, annotations or configuration classes in Spring. But sometimes, it may be necessary to dynamically register some beans according to certain conditions at runtime, and you can use itImporterBeanDefinitionRegistrarInterface to implement this function.

Specifically, it has been implementedImporterBeanDefinitionRegistrarThe interface class can be@ImporterThe annotation is introduced. Spring will call this implementation class when initializing the container.regisgterBeanDefinitionsMethods to register some extra beans as needed at runtime.

This interface is usually used in some advanced scenarios, such as dynamically registering different beans according to the runtime environment, or determining whether to register certain beans based on certain external configurations. This way makes the configuration of Spring applications more flexible and dynamic.

Dynamic registration bean

Below is passedImportBeanDefinitionRegistrar Come and register the bean dynamically.

First,@ServletComponentScan Copy it and change the name:

@Target({})
@Retention()
@Documented
@Import({})
public @interface MetaComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
}

Then implement the custom registrar:

public class MetaComponentScanRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

    private ResourceLoader resourceLoader;

    private Environment environment;

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

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

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metaData, BeanDefinitionRegistry registry) {
        MetaBeanDefinitionScanner scanner = new MetaBeanDefinitionScanner(registry, ,
                );
        Set<String> packagesToScan = (metaData);
        ((new String[]{}));
    }

    private static class MetaBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
        public MetaBeanDefinitionScanner(BeanDefinitionRegistry registry, Environment environment,
                                         ResourceLoader resourceLoader) {
            super(registry, false, environment, resourceLoader);
            registerFilters();
        }

        protected void registerFilters() {
            addIncludeFilter(new AnnotationTypeFilter());
        }
    }

    private Set<String> getBasePackages(AnnotationMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
                .fromMap((()));
        String[] basePackages = ("basePackages");
        Class<?>[] basePackageClasses = ("basePackageClasses");
        Set<String> packagesToScan = new LinkedHashSet<>((basePackages));
        for (Class<?> basePackageClass : basePackageClasses) {
            ((basePackageClass));
        }
        if (()) {
            ((()));
        }
        return packagesToScan;
    }
}

Custom registrants must be implementedImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAwareThese three interfaces are then overriddenregisterBeanDefinitions Method, this method is called when the Spring container is initialized.

In this method, a scanner is required, which has a filter for filtering custom annotation classes. Therefore, a custom annotation is needed:

@Target({})
@Retention()
@Documented
public @interface Meta {
}

All classes that use this annotation will be scanned by the scanner and registered as beans. When scanning, you need to know the path to scan, throughgetBasePackages Method to obtain. Last callClassPathBeanDefinitionScanner The scan method to scan and register beans, which is an inherent implementation in Spring.

Now create a@MetaFor annotated classes, see if they are automatically registered as beans:

@Meta
public class DemoBean {
    public  DemoBean() {
        ("DemoBean register!");
    }
}

Start SpringBootApplication and you will find that the following output is found in the console log:

DemoBean register!

It shows that it has indeed been calledDemoBean The constructor method automatically registers a bean.

Inject dynamic proxy beans

If it is not in a third-party framework, under normal circumstances, there is no need to customize registration for ordinary classes. Just use the built-in annotations in Spring such as@ComponentJust do it.

So what are the other usage scenarios for dynamically registering beans in Spring using custom annotations?

Mapper injection principle

If you understandFeignOr mybatisMapperIt should be known that when calling a remote interface through feign or accessing a database through mapper, there is no need to implement the class, but it is called directly through the interface.

Below isMapperFor example (mapper-spring: 4.3.0), see how it is implemented.

Similarly, first you need to add annotations to the Springboot startup class@MapperScan, this annotation passes@ImporterIntroducedMapperScannerRegistrar, and this registrar implementsImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAwareInterface and overrideregisterBeanDefinitionsmethod. In this method, theClassPathBeanDefinitionScannerSubclasses ofClassPathMapperScannerofdoScanMethods to scan and register beans for matching packages, the code is as follows:

@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = (basePackages);

    if (()) {
        ("No MyBatis mapper was found in '" + (basePackages) + "' package. Please check your configuration.");
    } else {
        processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
}

You can see that the method first calls the parent classdoScanMethod, that is, Spring classClassPathBeanDefinitionScanner doScan method, throughBeanDefinitionReaderUtilsTo register the bean, the code is as follows:

public static void registerBeanDefinition(
		BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
		throws BeanDefinitionStoreException {

	// Register bean definition under primary name.
	String beanName = ();
	(beanName, ());

	// Register aliases for bean name, if any.
	String[] aliases = ();
	if (aliases != null) {
		for (String alias : aliases) {
			(beanName, alias);
		}
	}
}

reigstryThere are three implementations, here mainlyDefaultListableBeanFactory, in this categoryregisterBeanDefinitionIn the method, frombeanDefinitionMapGet it according to beanNamebeanDefinition, if it does not exist, it will be customizedbeanDefinitionPut it inbeanDefinitionMap middle.

Calling the parent classdoScanAfter the method, call it nextprocessBeanDefinitionsMethodsbeanDefinitionsPerform processing. In this method,beanClassDepend onmapperThe interface class has becomeMapperFactoryBean,andMapperFactoryBean ImplementedFactoryBeaninterface. This will make the final generated bean a proxy object.

When the Spring container starts, it scans all bean definitions in the application and instantiates those that need to be instantiated. If you encounter a bean definition that implements the FactoryBean interface, Spring will create a special proxy object for the bean, so that the FactoryBean method is called when needed to create the actual bean instance.

When you need to use a bean created by FactoryBean, Spring will call the getObject() method of the proxy object to get the actual bean instance. If necessary, Spring will also call the getObjectType() method of the proxy object to determine the type of the actual Bean instance.

If the Bean created by FactoryBean is singleton pattern, Spring will create an instance when the getObject() method is called the first time. Every time the getObject() method is called in the future, the same instance will be returned. If the Bean created by FactoryBean is not a singleton pattern, a new instance is created every time the getObject() method is called.

At this point,MapperThe process of injecting interface into Spring is clearer.

Custom Injection

The following is to customize the annotation and proxy factory according to the implementation principle of Mapper to implement custom injection of dynamic proxy beans.

Similarly, first define the basic annotation, and introduce Registrar through this annotation:

@Target({})
@Retention()
@Documented
@Import({})
public @interface MapperScan {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
}

@Target({})
@Retention()
@Documented
public @interface Mapper {
}

public class MapperScanRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

    private ResourceLoader resourceLoader;

    private Environment environment;

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

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

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false, environment) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                return ().isIndependent() && !().isAnnotation();
            }
        };
        ();
        (new AnnotationTypeFilter());

        Set<String> basePackages = getBasePackages(metadata);
        for (String pkg : basePackages) {
            Set<BeanDefinition> beanDefinitions = (pkg);
            for (BeanDefinition candidate : beanDefinitions) {
                if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
                    AnnotationMetadata annotationMetadata = ();
                    String className = ();
                    Class<?> beanClass = (className, ());
                    String beanName = (className);
                    BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
                            .genericBeanDefinition()
                            .addPropertyValue("type", beanClass);
                    (beanName, ());
                }
            }
        }

    }

    private Set<String> getBasePackages(AnnotationMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
                .fromMap((()));
        String[] basePackages = ("basePackages");
        Class<?>[] basePackageClasses = ("basePackageClasses");
        Set<String> packagesToScan = new LinkedHashSet<>((basePackages));
        for (Class<?> basePackageClass : basePackageClasses) {
            ((basePackageClass));
        }
        if (()) {
            ((()));
        }
        return packagesToScan;
    }
}

The registration logic here is the focus.

inScannerNot inherited fromClassPathBeanDefinitionScanner but it is the same level as it needs to be overriddenisCandidateComponent method.
ClassPathBeanDefinitionScannerIt is a class that is directly used to scan beans and register, and it inheritsClassPathScanningCandidateComponentProvider, and added the function of registering bean definition.
andClassPathScanningCandidateComponentProvideris a provider that scans candidate components, which is responsible for identifying the classes that meet the criteria, but is not responsible for registering these classes. In other words, the function that registers the bean definition needs to be implemented by yourself.

The code for registering a bean definition is as follows:

if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
    AnnotationMetadata annotationMetadata = ();
    String className = ();
    Class<?> beanClass = (className, ());
    String beanName = (className);
    BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition()
            .addPropertyValue("type", beanClass);
    (beanName, ());
}

First get the metadata of the bean definition, which contains the class name of the bean, and you can use this to obtain the class object through reflection.

Then update the bean definition, mainly to update the beanClass, and change it from the original interface class toMapperBeanFactory. At the same time, atypeField, value is the original interface class. In this way, a proxy object can be generated when instantiating a bean, and the type of the proxy object is an interface class.

Finally, let’s take a look at the implementation of MapperBeanFactory:

public class MapperBeanFactory<T> implements FactoryBean<T> {

    private Class<T> type;

    public MapperBeanFactory() {
    }

    public MapperBeanFactory(Class<T> type) {
         = type;
    }

    @Override
    public Class<T> getObjectType() {
        return type;
    }

    @Override
    public T getObject() {
        return (T) ((), new Class[]{type}, (proxy, method, args) -> {
            ("Class %s, execute %s method, parameters=%s%n",
                    ().getName(), (), args[0]);
            return switch (()) {
                case "sayHello" -> "hello, " + args[0];
                case "sayHi" -> "hi, " + args[0];
                default -> "hello, world!";
            };
        });
    }

    public void setType(Class<T> type) {
         = type;
    }
}

The setType method here is required, and the added "type" attribute is set through this set method.getObjectThe method is used to generate the actual proxy object, specifically theto generate. This method requires three parameters, namely: the loader of the proxy class, the list of interfaces to be implemented by the proxy class, and the proxy class handler (the implementation class of the InvocationHandler interface). Among them, the third parameter is an anonymous class object (the lambda expression is simplified here), and the anonymous class implementsInvocationHandler Interface and overrideinvokeAgent method. In the proxy method, different values ​​are returned according to the original calling method.

Next, let’s take a look at the interface and interface controller of the Mapper annotation:

@Mapper
public interface UserMapper {

    String sayHello(String userName);

    String sayHi(String userName);
}

@RestController
@RequestMapping("/sample")
public class HelloController {

    @Resource
    private UserMapper userMapper;

    @RequestMapping("/hello")
    public String sayHello(@RequestParam String userName) {
        return (userName);
    }

    @RequestMapping("/hi")
    public String sayHi(@RequestParam String userName) {
        return (userName);
    }
}

When the system starts, accesshttp://localhost:8080/sample/hello?userName=testandhttp://localhost:8080/sample/hi?userName=testDifferent results will be returned.

Here, the methods in the UserMapper interface are not implemented, and the real implementation logic is done in the proxy method according to the method name.

You can make a reasonable guess that in addition to Mapper, the specific logic of the interface accessing the database in Spring Data JPA is also implemented in the proxy method.

Summarize

The above is personal experience. I hope you can give you a reference and I hope you can support me more.