在Spring中如何注入動態(tài)代理Bean
在Spring中注入動態(tài)代理Bean
在Springboot中我們可以通過內(nèi)置的注解如@Service,@Component,@Repository來注冊bean,也可以在配置類中通過@Bean來注冊bean。這些都是Spring內(nèi)置的注解。
除此之外,還可以用@WebFilter,@WebServlet,@WebListener注解結(jié)合@ServletComponentScan自動注冊Bean。但這里的@WebFilter,@WebServlet,@WebListener并不是Spring的注解,而是Servlet 3+ 的注解。為什么這些注解的類能自動注冊為Spring的Bean,其實現(xiàn)原理是什么呢?
如果進入@ServletComponentScan中查看可以發(fā)現(xiàn),該注解上有另外一個注解:@Import(ServletComponentScanRegistrar.class),進一步查看可知:class ServletComponentScanRegistrar implements ImportBeanDefinitionRegistrar。這里的關(guān)鍵就是ImportBeanDefinitionRegistrar 接口。
ImportBeanDefinitionRegistrar
Spring中最經(jīng)典的設(shè)計就是AOP和IOC,使得Spring框架具有良好的擴展性,而ImportBeanDefinitionRegistrar 就是其中用來擴展的hook之一。
通常情況下,Spring中的bean就是通過XML配置文件,Spring中的注解或配置類來注冊的。但有時候,可能需要在運行時根據(jù)某些條件動態(tài)地注冊一些bean,這時就可以使用ImporterBeanDefinitionRegistrar接口來實現(xiàn)此功能。
具體來說,實現(xiàn)了ImporterBeanDefinitionRegistrar 接口的類可以在@Importer注解中被引入,Spring在初始化容器時會調(diào)用這個實現(xiàn)類的regisgterBeanDefinitions方法,以便在運行時根據(jù)需要需要注冊一些額外的bean。
這個接口通常用于一些高級的場景,比如根據(jù)運行時環(huán)境來動態(tài)的注冊不同的bean,或者根據(jù)某些外部配置來決定是否注冊某些bean等。通過這種方式使得Spring應(yīng)用程序的配置更加靈活和動態(tài)化。
動態(tài)注冊Bean
下面通過ImportBeanDefinitionRegistrar 來動態(tài)注冊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 {};
}然后實現(xiàn)自定義注冊器:
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;
}
}自定義注冊器必須實現(xiàn)ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware這三個接口,然后覆寫registerBeanDefinitions 方法,該方法在Spring容器初始化的時候被調(diào)用。
在該方法中,需要一個掃描器,該掃描器中有一個過濾器,用于過濾自定義的注解類。因此,需要一個自定義注解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Meta {
}所有使用該注解的類都將被掃描器掃到并注冊為bean。掃描時需要知道要掃描的路徑,通過getBasePackages 方法獲取。最后調(diào)用ClassPathBeanDefinitionScanner 的scan方法來掃描和注冊bean,這部分是Spring中的固有實現(xiàn)。
現(xiàn)在來創(chuàng)建一個通過@Meta注解的類,看一下是否被自動注冊為bean:
@Meta
public class DemoBean {
public DemoBean() {
System.out.println("DemoBean register!");
}
}啟動SpringBootApplication,會發(fā)現(xiàn)控制臺日志中有如下輸出:
DemoBean register!
表明確實調(diào)用了DemoBean 的構(gòu)造方法,自動注冊了一個bean。
注入動態(tài)代理bean
如果不是在第三方框架中,正常情況下,普通的類完全沒必要自定義注冊,直接用Spring內(nèi)置的注解如@Component即可。
那使用自定義注解來動態(tài)注冊Spring中的bean還有什么使用場景呢?
Mapper注入原理
如果了解Feign或者mybatis的Mapper應(yīng)該知道,在通過feign調(diào)用遠程接口或者通過mapper訪問數(shù)據(jù)庫時,是不需要實現(xiàn)類的,而是直接通過接口進行調(diào)用的。
下面以Mapper為例(mapper-spring:4.3.0)看下是如何實現(xiàn)的。
同樣的,首先需要在Springboot的啟動類上加上注解@MapperScan,該注解中通過@Importer引入了MapperScannerRegistrar,而這個注冊器實現(xiàn)了ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware接口,并覆寫了registerBeanDefinitions方法。在該方法中,調(diào)用了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;
}可以看到,該方法首先調(diào)用了父類的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有三個實現(xiàn),這里主要看DefaultListableBeanFactory,在該類的registerBeanDefinition方法里,從beanDefinitionMap中根據(jù)beanName來獲取beanDefinition,如果不存在,就將自定義的beanDefinition放到beanDefinitionMap 中。
調(diào)用完父類的doScan方法之后,接下來調(diào)用processBeanDefinitions方法對beanDefinitions進行處理。在該方法中,將beanClass由mapper接口類變成了MapperFactoryBean,而MapperFactoryBean 實現(xiàn)了FactoryBean接口。這將使得最終生成的bean為代理對象。
當(dāng)Spring容器啟動時,它會掃描應(yīng)用程序中的所有Bean定義,并實例化那些需要實例化的Bean。如果遇到實現(xiàn)了FactoryBean接口的Bean定義,Spring將會為該Bean創(chuàng)建一個特殊的代理對象,以便在需要時調(diào)用FactoryBean的方法來創(chuàng)建實際的Bean實例。
當(dāng)需要使用由FactoryBean創(chuàng)建的Bean時,Spring將會調(diào)用代理對象的getObject()方法來獲取實際的Bean實例。有需要的話,Spring還會調(diào)用代理對象的getObjectType()方法來確定實際Bean實例的類型。
如果FactoryBean創(chuàng)建的Bean是單例模式,那么Spring將在第一次調(diào)用getObject()方法時創(chuàng)建實例,并將其緩存起來。以后每次調(diào)用getObject()方法時,都會返回同一個實例。如果FactoryBean創(chuàng)建的Bean不是單例模式,則每次調(diào)用getObject()方法時都會創(chuàng)建一個新的實例。
至此,Mapper接口注入到Spring中的過程就比較清晰了。
自定義注入
下面仿照Mapper的實現(xiàn)原理來自定義注解和代理工廠,實現(xiàn)自定義注入動態(tài)代理Bean。
同樣地,先定義基礎(chǔ)注解,通過該注解引入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,它負責(zé)識別符合條件的類,但不負責(zé)注冊這些類。換句話說,注冊Bean定義的功能需要自己實現(xiàn)。
注冊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定義的元數(shù)據(jù),這其中包含bean的類名,可以借此通過反射來獲取類對象。
然后更新bean定義,主要是更新beanClass,將其由原始的接口類更改為MapperBeanFactory。同時,還添加了一個type字段,值為原始的接口類。這樣實例化bean時就能生成代理對象了,且代理對象的類型為接口類。
最終看下MapperBeanFactory的實現(xiàn):
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方法設(shè)置進來的。getObject方法用于生成實際的代理對象,具體是由Proxy.newProxyInstance來生成的。該方法需要三個參數(shù),分別是: 代理類的加載器,代理類要實現(xiàn)的接口列表,代理類handler(InvocationHandler接口的實現(xiàn)類)。其中,第三個參數(shù)是一個匿名類對象(這里用lambda表達式進行了簡化),該匿名類實現(xiàn)了InvocationHandler 接口,并覆寫了invoke代理方法。在代理方法中,根據(jù)原始調(diào)用方法的不同返回不同的值。
接下來看一下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);
}
}當(dāng)系統(tǒng)啟動后,訪問http://localhost:8080/sample/hello?userName=test和http://localhost:8080/sample/hi?userName=test會返回不同的結(jié)果。
這里UserMapper接口中的方法并沒有實現(xiàn),真正的實現(xiàn)邏輯是在代理方法中根據(jù)方法名做的。
可以做一下合理的推測,除了Mapper之外,Spring Data JPA中的接口訪問數(shù)據(jù)庫的具體邏輯,也是在代理方法中實現(xiàn)的。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring中FactoryBean的高級用法實戰(zhàn)教程
FactoryBean是Spring框架的高級特性,允許自定義對象的創(chuàng)建過程,適用于復(fù)雜初始化邏輯,本文給大家介紹Spring中FactoryBean的高級用法實戰(zhàn),感興趣的朋友跟隨小編一起看看吧2024-09-09
Spring boot定時任務(wù)的原理及動態(tài)創(chuàng)建詳解
這篇文章主要給大家介紹了關(guān)于Spring boot定時任務(wù)的原理及動態(tài)創(chuàng)建的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
JAVA PDF操作之實現(xiàn)截取N頁和多個PDF合并
這篇文章主要為大家詳細介紹了java關(guān)于PDF的一些操作,例如截取N頁并生成新文件,轉(zhuǎn)圖片以及多個PDF合并,文中的示例代碼講解詳細,感興趣的可以了解下2025-01-01

