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