在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中的接口访问数据库的具体逻辑,也是在代理方法中实现的。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论