在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;@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;}@Overridepublic void setResourceLoader(ResourceLoader resourceLoader) {this.resourceLoader = resourceLoader;}@Overridepublic 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的子类ClassPathMapperScannerdoScan方法来对符合条件的包进行扫描并注册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进行处理。在该方法中,将beanClassmapper接口类变成了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;@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;}@Overridepublic void setResourceLoader(ResourceLoader resourceLoader) {this.resourceLoader = resourceLoader;}@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false, environment) {@Overrideprotected 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;}@Overridepublic Class<T> getObjectType() {return type;}@Overridepublic 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 {@Resourceprivate 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=testhttp://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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/5596.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

二分图--判定以及最大匹配

水了个圈钱杯省一&#xff0c;不过估计国赛也拿不了奖&#xff0c;但还是小小挣扎一下。 什么是二分图&#xff1a;G(V,E)是一个无向图&#xff0c;若顶点V可以分为两个互不相交的子集A,B&#xff0c;并图中的每一条边&#xff08;i,j)所关联的ij属于不同的顶点集&#xff0c;…

QT登录界面,(页面的切换)

以登陆界面为例&#xff0c;&#xff08;QDialog&#xff09; 1.主界面先构造login 的对话框类 int main(int argc, char *argv[]) {QApplication a(argc, argv);//先显示Login的界面Study_Login_Dialog login;............ }2.Login的类&#xff0c;可以用自定义的信号&#…

Java image-processing 包依赖错误

错误的信息为&#xff1a; [ERROR] Failed to execute goal on project image-processing: Could not resolve dependencies for project com.ossez:image-processing:jar:0.0.2-SNAPSHOT: Failed to collect dependencies at org.openimaj:core-image:jar:1.3.10 -> org.op…

spring-boot示例

spring-boot版本&#xff1a;2.0.3.RELEASE 数据库: H2数据库 &#xff08;嵌入式内存性数据库&#xff0c;安装简单&#xff0c;方便用于开发、测试&#xff0c;不适合用于生产&#xff09; mybatis-plus框架&#xff0c;非常迅速开发CRUD

SpringMVC整体工作流程

. 用户发起一个请求&#xff0c;请求首先到达前端控制器前端控制器接收到请求后会调用处理器映射器&#xff0c;由此得知&#xff0c;这个请求该由哪一个Controller来进行处理(并未调用Controller)&#xff1b;前端控制器调用处理器适配器&#xff0c;告诉处理器适配器应该要…

Macos安装OrbStack

什么是OrbStack OrbStack 是一种在 macOS 上运行容器和 Linux 机器的快速、轻便和简单方法。它是 Docker Desktop 和 WSL 的超强替代品&#xff0c;所有这些都在一个易于使用的应用程序中。 在Macos M系列芯片上&#xff0c;经常遇到docker镜像不兼容的问题&#xff0c;此时使…

ubuntu的镜像源+bionic版本

首先第一步 查找和你自己ubuntu版本匹配的版本号 匹配代号如下 在终端输入lsb_release -a查看自己系统上的版本号 可以看到我这个版本号的代号是bionic。 每个版本的镜像文件都是有规律的。 bionic版本的源如下 # 阿里源 deb http://mirrors.aliyun.com/ubuntu/ bionic ma…

Linux内核之页面映射到虚拟地址:insert_page用法实例(六十五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

vscode连接阿里云 无法连接

如果是首次连接&#xff0c;需要在阿里云控制台下 点击重置密码 同时注意在重置密码页面最下方&#xff0c;有开启密码登录选项

Nginx实现端口转发与负载均衡配置

前言&#xff1a;当我们的软件体系结构较为庞大的时候&#xff0c;访问量往往是巨大的&#xff0c;所以我们这里可以使用nginx的均衡负载 一、配置nginx实现端口转发 本地tomcat服务端口为8082 本地nginx端口为8080 目的&#xff1a;将nginx的8080转发到tomcat的8082端口上…

SOLIDWORKS DRAFTSIGHT 2024新功能Top10

SOLIDWORKS 2024 以更加强大的姿态亮相&#xff0c;帮助您重塑设计。为了助力您简化和加快由概念到成品的产品开发流程&#xff0c;SOLIDWORKS 2024 涵盖全新以用户为中心的增强功能&#xff0c;致力帮您实现更智能、更快速地与您的团队和外部合作伙伴协同工作&#xff0c;下面…

C语言 循环语句 (1) 讲述循环概念演示while语句

接下来 我们来说 循环控制结构 循环的基本原理及循环语句 再说原理之前 我们 先来看几个案例 要求是 让用户在键盘中输入三个整数 然后将这些整数求和 这个用我们之前的知识就能轻松搞定 #define _CRT_SECURE_NO_WARNINGS//禁用安全函数警告 #pragma warning(disable:6031)…

Tomcat的请求连接配置

从默认配置看&#xff0c;SpringBootd的最大可以处理8292个请求&#xff0c;方便记忆是&#xff1a;不到8300 server:port: 8081tomcat:threads:max: 20 #最大工作线程数min-spare: 10 #最小工作线程数max-connections: 8192 #最大请求连接数ac…

MyBatis(XML映射器操作)

文章目录 XML映射器操作&#xff08;XxxMapper.xml&#xff09;文件目录1.基本介绍1.优点2.常用顶级元素 2.环境配置1.在原来的父模块下创建一个子模块2.删除没用的两个文件夹3.创建基本目录4.父模块的pom.xml5.jdbc.properties6.mybatis-config.xml7.测试使用MonsterMapperTes…

FSNotes for Mac v6.7.1中文激活:轻量级笔记管理工具

FSNotes for Mac&#xff0c;一款专为Mac用户打造的轻量级笔记管理工具&#xff0c;让您的笔记管理变得简单而高效。 FSNotes for Mac v6.7.1中文激活版下载 它采用Markdown文件格式&#xff0c;让您轻松创建和编辑富文本笔记&#xff0c;无需担心格式问题。同时&#xff0c;FS…

C++ | Leetcode C++题解之第59题螺旋矩阵II

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<vector<int>> generateMatrix(int n) {int num 1;vector<vector<int>> matrix(n, vector<int>(n));int left 0, right n - 1, top 0, bottom n - 1;while (left < r…

十一、大模型-Semantic Kernel与 LangChain 的对比

Semantic Kernel 与 LangChain 的对比 Semantic Kernel 和 LangChain 都是用于开发基于大型语言模型&#xff08;LLM&#xff09;的应用程序的框架&#xff0c;但它们各有特点和优势。 基本概念和目标 Semantic Kernel 是一个由微软开发的轻量级 SDK&#xff0c;旨在帮助开发…

STM32标准库控制一盏LED闪烁

实物连接&#xff1a; ## 软件编程&#xff1a;默认已经有一个工程模板&#xff0c;代码实现逻辑&#xff1a; 1、使用RCC开启GPIO的时钟&#xff1b; 2、使用GPIO初始化函数实现初始化GPIO 3、使用输入或输出的函数控制GPIO口 #include "stm32f10x.h" …

AForge.NET是啥

AForge.NET是一个专门为开发者和研究者基于C#框架设计的开源项目。这个框架提供了不同的类库和关于类库的资源&#xff0c;还有很多应用程序例子&#xff0c;涵盖了计算机视觉与人工智能、图像处理、神经网络、遗传算法、机器学习、模糊系统、机器人控制等多个领域。 AForge.NE…

Android(Java)项目支持Kotlin语言开发

Android&#xff08;Java&#xff09;项目通过相关Kotlin设置后&#xff0c;允许同时使用Java语言和Kotlin语言进行开发代码的。 示例环境&#xff1a; Android Studio Giraffe | 2022.3.1 Patch 3 Java 8 Kotlin 1.9.20 设置Kotlin选项&#xff1a; 第一步&#xff1a;在项…