在Spring中注入动态代理Bean

在Springboot中我们可以通过内置的注解如@Service,@Component,@Repository来注册bean,也可以在配置类中通过@Bean来注册bean。这些都是Spring内置的注解。

除此之外,还可以用@WebFilter,@WebServlet,@WebListener注解结合@ServletComponentScan自动注册Bean。但这里的@WebFilter,@WebServlet,@WebListener并不是Spring的注解,而是Servlet 3+ 的注解。为什么这些注解的类能自动注册为Spring的Bean,其实现原理是什么呢?

如果进入@ServletComponentScan中查看可以发现,该注解上有另外一个注解:@Import(ServletComponentScanRegistrar.class),进一步查看可知:class ServletComponentScanRegistrar implements ImportBeanDefinitionRegistrar。这里的关键就是ImportBeanDefinitionRegistrar 接口。


ImportBeanDefinitionRegistrar

Spring中最经典的设计就是AOP和IOC,使得Spring框架具有良好的扩展性,而ImportBeanDefinitionRegistrar 就是其中用来扩展的hook之一。

通常情况下,Spring中的bean就是通过XML配置文件,Spring中的注解或配置类来注册的。但有时候,可能需要在运行时根据某些条件动态地注册一些bean,这时就可以使用ImporterBeanDefinitionRegistrar接口来实现此功能。

具体来说,实现了ImporterBeanDefinitionRegistrar 接口的类可以在@Importer注解中被引入,Spring在初始化容器时会调用这个实现类的regisgterBeanDefinitions方法,以便在运行时根据需要需要注册一些额外的bean。

这个接口通常用于一些高级的场景,比如根据运行时环境来动态的注册不同的bean,或者根据某些外部配置来决定是否注册某些bean等。通过这种方式使得Spring应用程序的配置更加灵活和动态化。

动态注册Bean

下面通过ImportBeanDefinitionRegistrar 来动态注册Bean。

首先将@ServletComponentScan 抄过来改一下名字:

复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({MetaAutoConfigureRegistrar.class})
public @interface MetaComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
}

然后实现自定义注册器:

复制代码
public class MetaComponentScanRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

    private ResourceLoader resourceLoader;

    private Environment environment;

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

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

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metaData, BeanDefinitionRegistry registry) {
        MetaBeanDefinitionScanner scanner = new MetaBeanDefinitionScanner(registry, this.environment,
                this.resourceLoader);
        Set<String> packagesToScan = this.getBasePackages(metaData);
        scanner.scan(packagesToScan.toArray(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(Meta.class));
        }
    }

    private Set<String> getBasePackages(AnnotationMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
                .fromMap(metadata.getAnnotationAttributes(MetaComponentScan.class.getName()));
        String[] basePackages = attributes.getStringArray("basePackages");
        Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
        Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
        for (Class<?> basePackageClass : basePackageClasses) {
            packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
        }
        if (packagesToScan.isEmpty()) {
            packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName()));
        }
        return packagesToScan;
    }
}

自定义注册器必须实现ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware这三个接口,然后覆写registerBeanDefinitions 方法,该方法在Spring容器初始化的时候被调用。

在该方法中,需要一个扫描器,该扫描器中有一个过滤器,用于过滤自定义的注解类。因此,需要一个自定义注解:

复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Meta {
}

所有使用该注解的类都将被扫描器扫到并注册为bean。扫描时需要知道要扫描的路径,通过getBasePackages 方法获取。最后调用ClassPathBeanDefinitionScanner 的scan方法来扫描和注册bean,这部分是Spring中的固有实现。

现在来创建一个通过@Meta注解的类,看一下是否被自动注册为bean:

复制代码
@Meta
public class DemoBean {
    public  DemoBean() {
        System.out.println("DemoBean register!");
    }
}

启动SpringBootApplication,会发现控制台日志中有如下输出:

DemoBean register!

表明确实调用了DemoBean 的构造方法,自动注册了一个bean。

注入动态代理bean

如果不是在第三方框架中,正常情况下,普通的类完全没必要自定义注册,直接用Spring内置的注解如@Component即可。

那使用自定义注解来动态注册Spring中的bean还有什么使用场景呢?

Mapper注入原理

如果了解Feign或者mybatis的Mapper应该知道,在通过feign调用远程接口或者通过mapper访问数据库时,是不需要实现类的,而是直接通过接口进行调用的。

下面以Mapper为例(mapper-spring:4.3.0)看下是如何实现的。

同样的,首先需要在Springboot的启动类上加上注解@MapperScan,该注解中通过@Importer引入了MapperScannerRegistrar,而这个注册器实现了ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware接口,并覆写了registerBeanDefinitions方法。在该方法中,调用了ClassPathBeanDefinitionScanner的子类ClassPathMapperScanner的doScan方法来对符合条件的包进行扫描并注册bean,其代码如下:

复制代码
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

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

    return beanDefinitions;
}

可以看到,该方法首先调用了父类的doScan方法,也就是Spring类ClassPathBeanDefinitionScanner 中的doScan方法,通过BeanDefinitionReaderUtils来注册bean,代码如下:

复制代码
public static void registerBeanDefinition(
		BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
		throws BeanDefinitionStoreException {

	// Register bean definition under primary name.
	String beanName = definitionHolder.getBeanName();
	registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

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

reigstry有三个实现,这里主要看DefaultListableBeanFactory,在该类的registerBeanDefinition方法里,从beanDefinitionMap中根据beanName来获取beanDefinition,如果不存在,就将自定义的beanDefinition放到beanDefinitionMap 中。

调用完父类的doScan方法之后,接下来调用processBeanDefinitions方法对beanDefinitions进行处理。在该方法中,将beanClass由mapper接口类变成了MapperFactoryBean,而MapperFactoryBean 实现了FactoryBean接口。这将使得最终生成的bean为代理对象。

当Spring容器启动时,它会扫描应用程序中的所有Bean定义,并实例化那些需要实例化的Bean。如果遇到实现了FactoryBean接口的Bean定义,Spring将会为该Bean创建一个特殊的代理对象,以便在需要时调用FactoryBean的方法来创建实际的Bean实例。
当需要使用由FactoryBean创建的Bean时,Spring将会调用代理对象的getObject()方法来获取实际的Bean实例。有需要的话,Spring还会调用代理对象的getObjectType()方法来确定实际Bean实例的类型。
如果FactoryBean创建的Bean是单例模式,那么Spring将在第一次调用getObject()方法时创建实例,并将其缓存起来。以后每次调用getObject()方法时,都会返回同一个实例。如果FactoryBean创建的Bean不是单例模式,则每次调用getObject()方法时都会创建一个新的实例。

至此,Mapper接口注入到Spring中的过程就比较清晰了。

自定义注入

下面仿照Mapper的实现原理来自定义注解和代理工厂,实现自定义注入动态代理Bean。

同样地,先定义基础注解,通过该注解引入Registrar:

复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({MapperScanRegistrar.class})
public @interface MapperScan {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Mapper {
}

public class MapperScanRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

    private ResourceLoader resourceLoader;

    private Environment environment;

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

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

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

        Set<String> basePackages = getBasePackages(metadata);
        for (String pkg : basePackages) {
            Set<BeanDefinition> beanDefinitions = scanner.findCandidateComponents(pkg);
            for (BeanDefinition candidate : beanDefinitions) {
                if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
                    AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
                    String className = annotationMetadata.getClassName();
                    Class<?> beanClass = ClassUtils.resolveClassName(className, ClassUtils.getDefaultClassLoader());
                    String beanName = ClassUtils.getShortName(className);
                    BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
                            .genericBeanDefinition(MapperBeanFactory.class)
                            .addPropertyValue("type", beanClass);
                    registry.registerBeanDefinition(beanName, definitionBuilder.getBeanDefinition());
                }
            }
        }

    }

    private Set<String> getBasePackages(AnnotationMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
                .fromMap(metadata.getAnnotationAttributes(MapperScan.class.getName()));
        String[] basePackages = attributes.getStringArray("basePackages");
        Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
        Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
        for (Class<?> basePackageClass : basePackageClasses) {
            packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
        }
        if (packagesToScan.isEmpty()) {
            packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName()));
        }
        return packagesToScan;
    }
}

这里的注册逻辑是重点。

其中Scanner不是继承自ClassPathBeanDefinitionScanner 的,而是与其同级的,需要覆写isCandidateComponent 方法。
ClassPathBeanDefinitionScanner是直接用于扫描Bean并注册的类,它继承了ClassPathScanningCandidateComponentProvider,并添加了注册Bean定义的功能。

而ClassPathScanningCandidateComponentProvider是扫描候选组件的provider,它负责识别符合条件的类,但不负责注册这些类。换句话说,注册Bean定义的功能需要自己实现。

注册Bean定义的代码如下:

复制代码
if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
    AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
    String className = annotationMetadata.getClassName();
    Class<?> beanClass = ClassUtils.resolveClassName(className, ClassUtils.getDefaultClassLoader());
    String beanName = ClassUtils.getShortName(className);
    BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition(MapperBeanFactory.class)
            .addPropertyValue("type", beanClass);
    registry.registerBeanDefinition(beanName, definitionBuilder.getBeanDefinition());
}

先获取bean定义的元数据,这其中包含bean的类名,可以借此通过反射来获取类对象。

然后更新bean定义,主要是更新beanClass,将其由原始的接口类更改为MapperBeanFactory。同时,还添加了一个type字段,值为原始的接口类。这样实例化bean时就能生成代理对象了,且代理对象的类型为接口类。

最终看下MapperBeanFactory的实现:

复制代码
public class MapperBeanFactory<T> implements FactoryBean<T> {

    private Class<T> type;

    public MapperBeanFactory() {
    }

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

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

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

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

这里的setType方法是必须的,添加的"type"属性就是通过此set方法设置进来的。getObject方法用于生成实际的代理对象,具体是由Proxy.newProxyInstance来生成的。该方法需要三个参数,分别是: 代理类的加载器,代理类要实现的接口列表,代理类handler(InvocationHandler接口的实现类)。其中,第三个参数是一个匿名类对象(这里用lambda表达式进行了简化),该匿名类实现了InvocationHandler 接口,并覆写了invoke代理方法。在代理方法中,根据原始调用方法的不同返回不同的值。

接下来看一下Mapper注解的接口和接口controller:

复制代码
@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 userMapper.sayHello(userName);
    }

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

当系统启动后,访问http://localhost:8080/sample/hello?userName=test和http://localhost:8080/sample/hi?userName=test会返回不同的结果。

这里UserMapper接口中的方法并没有实现,真正的实现逻辑是在代理方法中根据方法名做的。

可以做一下合理的推测,除了Mapper之外,Spring Data JPA中的接口访问数据库的具体逻辑,也是在代理方法中实现的。

参考资料

1. https://blog.csdn.net/u011972171/article/details/80295778

2. https://blog.csdn.net/zxd1435513775/article/details/121104087

3. https://www.cnblogs.com/kukuxjx/p/17505609.html

4. https://www.cnblogs.com/zuxp/p/15504619.html

相关推荐
福建佰胜张工4 分钟前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
念何架构之路4 分钟前
go-grpc设计思想
开发语言·后端·golang
catoop7 分钟前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x35 分钟前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事1 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客1 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1882 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn2 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su3 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
码事漫谈3 小时前
AI 读你的提示词,其实很没耐心
后端