在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
。這里的關鍵就是ImportBeanDefinitionRegistrar
接口。
ImportBeanDefinitionRegistrar
Spring中最經(jīng)典的設計就是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應用程序的配置更加靈活和動態(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
應該知道,在通過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為代理對象。
當Spring容器啟動時,它會掃描應用程序中的所有Bean定義,并實例化那些需要實例化的Bean。如果遇到實現(xiàn)了FactoryBean接口的Bean定義,Spring將會為該Bean創(chuàng)建一個特殊的代理對象,以便在需要時調(diào)用FactoryBean的方法來創(chuàng)建實際的Bean實例。
當需要使用由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。
同樣地,先定義基礎注解,通過該注解引入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定義的功能需要自己實現(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方法設置進來的。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); } }
當系統(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)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Spring中FactoryBean的高級用法實戰(zhàn)教程
FactoryBean是Spring框架的高級特性,允許自定義對象的創(chuàng)建過程,適用于復雜初始化邏輯,本文給大家介紹Spring中FactoryBean的高級用法實戰(zhàn),感興趣的朋友跟隨小編一起看看吧2024-09-09Spring boot定時任務的原理及動態(tài)創(chuàng)建詳解
這篇文章主要給大家介紹了關于Spring boot定時任務的原理及動態(tài)創(chuàng)建的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-03-03JAVA PDF操作之實現(xiàn)截取N頁和多個PDF合并
這篇文章主要為大家詳細介紹了java關于PDF的一些操作,例如截取N頁并生成新文件,轉(zhuǎn)圖片以及多個PDF合并,文中的示例代碼講解詳細,感興趣的可以了解下2025-01-01