背景
在使用Langchain4j的过程中发现,如果在启动类所在的classPath以外的目录中定义了AiService
,那么该类将不会被Langchain4j扫描并代理到,即使你使用@ComponentScan
或者其他方式指定了文件扫描路径
查看源码后发现,Langchain4j中 AiService
只会扫描启动类所在的classPath,而对于其他方式配置的扫描路径都没有进行处理,也没有实现其他配置手段能够让用户指定扫描路径,最后就导致了 AiService
不存在的Bug _(:з」∠)_
相关issue
Tips:这也是我第一次参加开源,虽然只是做了一个微不足道的贡献,但是还是感觉收获很多吧…一直在担心自己的水平不够所以反复理解反复查资料,但是作者很热心帮我修改了代码并且很快就被处理接受了😃
问题分析
项目中相关代码如下:
1 2 3 4 5 6 7 8 9
| private static Set<Class<?>> findAiServices(ConfigurableListableBeanFactory beanFactory) { String[] applicationBean = beanFactory.getBeanNamesForAnnotation(SpringBootApplication.class); BeanDefinition applicationBeanDefinition = beanFactory.getBeanDefinition(applicationBean[0]); String basePackage = applicationBeanDefinition.getResolvableType().resolve().getPackage().getName(); Reflections reflections = new Reflections((new ConfigurationBuilder()).forPackage(basePackage)); Set<Class<?>> classes = reflections.getTypesAnnotatedWith(AiService.class); classes.removeIf(clazz -> !clazz.getName().startsWith(basePackage)); return classes; }
|
大致逻辑就是,获取启动类所在的路径,然后使用反射找到该路径下所有带有AiService的class。
目前有两个问题:
- 扫描路径的获取。在不增加额外的用户配置的情况下,如何知道应该扫描全部的路径呢?用户指定扫描路径的方式有很多种,其中最常用的就是通过
@ComponentScan
指定,于是想到我们可以解析项目中所有@ComponentScan
注解,以此作为扫描路径
- 扫描的方式是通过Reflections,而不是通过Spring自带的方法。作者使用Reflections是因为Spring默认是不会扫描接口的,因为接口没有办法实例化,因此通过Spring自带的方法自然也无法获取到Bean。但是这么做属实有点不合理,有没有更加优雅的解决办法呢?
获取扫描路径
用户指定扫描路径的方式有很多种,其中最常用的就是通过@ComponentScan
指定,于是想到我们可以解析项目中所有@ComponentScan
注解,以此作为扫描路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private Set<String> getBasePackages(ConfigurableListableBeanFactory beanFactory) { Set<String> collectedBasePackages = new LinkedHashSet<>(); beanFactory.getBeansWithAnnotation(ComponentScan.class).forEach((beanName, instance) -> { Set<ComponentScan> componentScans = AnnotatedElementUtils.getMergedRepeatableAnnotations(instance.getClass(), ComponentScan.class); for (ComponentScan componentScan : componentScans) { Set<String> basePackages = new LinkedHashSet<>(); String[] basePackagesArray = componentScan.basePackages(); for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); Collections.addAll(basePackages, tokenized); } for (Class<?> clazz : componentScan.basePackageClasses()) { basePackages.add(ClassUtils.getPackageName(clazz)); } if (basePackages.isEmpty()) { basePackages.add(ClassUtils.getPackageName(instance.getClass())); } collectedBasePackages.addAll(basePackages); } }); return collectedBasePackages; }
|
Tips: 这里我只处理了@ComponentScan
的basePackages等相关的路径扫描属性、别名问题,过滤器之类的属性目前还没有解决(Spring太复杂了🥲)
自定义类扫描器
如果是要与Spring集成的话肯定是使用Spring的方式扫描Bean更好,但是Spring自带的类扫描器会直接过滤接口,这怎么办?我想到一个比较熟悉的场景:MyBatis的Mapper也是接口,但是他是怎么被扫描并且注册到Spring中的呢?
于是我去翻了MyBatis相关的源代码,发现其原理是自定义了一个类扫描ClassPathMapperScanner
,并重写了他的isCandidateComponent()
方法:
1 2 3
| protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent(); }
|
在Spring自带的类扫描器中,他们的isCandidateComponent()
方法是这样的:
1 2 3 4 5
| protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { AnnotationMetadata metadata = beanDefinition.getMetadata(); return (metadata.isIndependent() && (metadata.isConcrete() || (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName())))); }
|
isConcrete()
返回基础类是否表示具体类,即既不是接口也不是抽象类。这里我们也可以知道Spring不扫描接口的原因了,在这一步就被pass掉了
关于MyBatis代理的详细过程可以参考MyBatis创建代理全过程
接下来我们就可以参考MyBatis自定义类扫描器了:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class ClassPathAiServiceScanner extends ClassPathBeanDefinitionScanner { ClassPathAiServiceScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) { super(registry, useDefaultFilters); addIncludeFilter(new AnnotationTypeFilter(AiService.class)); }
@Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); return annotationMetadata.isInterface() && annotationMetadata.isIndependent(); } }
|
按照Mybatis的方式,我们可以通过自定义类扫描器的方式实现让Spring扫描接口,那么问题来了,如何让Spring使用我们自定义的类扫描器呢?
我们可以在Spring的BeanDefinition扫描流程结束后,再扫描一遍不就好了吗?这时候后置处理器就可以派上用场了:
1 2 3 4 5 6 7 8 9 10 11
| @Component public class AiServiceScannerProcessor implements BeanDefinitionRegistryPostProcessor {
@Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { ClassPathAiServiceScanner classPathAiServiceScanner = new ClassPathAiServiceScanner(registry, false); classPathAiServiceScanner.addIncludeFilter(new AnnotationTypeFilter(AiService.class)); Set<String> basePackages = getBasePackages((ConfigurableListableBeanFactory) registry); classPathAiServiceScanner.scan(StringUtils.toStringArray(basePackages)); } }
|
至此这个问题就大致解决了,但是一些细节肯定没有处理好,这个项目目前也是刚起步的阶段,期待有大佬给出更好的解决方案🥳
附一个贴图,纪念一下:
