目录
需求
标题有些许拗口
简单来说,就是实现mybatis框架中的MapperScan功能(接口扫描与代理对象注册)。
在Spring环境中,我们常通过XML文件 或者 @Controller、@Component、@Service、@Bean等注解进行bean的注册。
这么做比较简单便捷,但同时我们自己的代码与服务与Spring框架紧密耦合,只要SpringBoot启动,我们的类就会被加载、注册进容器,无法进行策略的替换。
比如某个场景下,我需要注册package1中的bean;另一个场景下,我需要注册package2中的bean。
当然,用SpringBoot神奇的@Conditional注解,也可以达到条件注册的目的。但这种方法不符合“批量”这一特征。
在Mybatis中也有类似的需求。Mybatis与SpringBoot集成时,Mapper接口(其实注入容器的是代理对象)是如何被批量注册进Bean容器的?
通过MapperScan注解指定包名,将该包下的所有类加载到内存中,生成代理对象后注册进spring容器。
源码有兴趣的可以自己阅读一下,下面仿造mybatis写一个bean批量注册的功能。
实现
项目结构
大概结构如下
待注入类
listener包下有两个测试类,是我们需要注入的对象。
UserInfo无关紧要,只是用来测试Autowired注解后续能否生效的,忽略即可。
public class HelloListener {
@Autowired
UserInfo userInfo;
public void hello() {
System.out.println("hello");
}
}
public class WorldListener {
public void world() {
System.out.println("world");
}
}
ClassScanner工具类
比较关键的工具类,用于加载指定package下的class
public class ClassScanner {
private final String packageName;
private final String packageUrl;
private final ClassLoader classLoader;
private final Charset charset;
private final Set<Class<?>> classes = new HashSet<>();
public ClassScanner(String packageName, ClassLoader classLoader) {
this.packageName = packageName;
this.classLoader = classLoader;
this.charset = StandardCharsets.UTF_8;
this.packageUrl = packageName.replace(".", File.separator);
}
public ClassScanner(String packageName, ClassLoader classLoader, Charset charset) {
this.packageName = packageName;
this.classLoader = classLoader;
this.charset = charset;
this.packageUrl = packageName.replace(".", File.separator);
}
public Set<Class<?>> scan() {
try {
Enumeration<URL> resources = classLoader.getResources(packageUrl);
EnumerationIter<URL> urls = new EnumerationIter<>(resources);
for (URL url : urls) {
scanFile(new File(URLDecoder.decode(url.getFile(), charset.name())));
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
private void scanFile(File file) {
if(file == null) {
return;
}
if(file.isFile()) {
final String fileName = file.getName();
if(fileName.endsWith(".class")) {
String className = packageName + "." + fileName.substring(0, fileName.lastIndexOf('.'));
addClass(className);
}
}else if(file.isDirectory()) {
if(file.listFiles() == null){
return;
}
for (File listFile : file.listFiles()) {
scanFile(listFile, packageName);
}
}
}
/**
* 嵌套扫描
* @param file 文件
* @param basePackage 基础包名
*/
private void scanFile(File file, String basePackage) {
if(file == null) {
return;
}
if(file.isFile()) {
final String fileName = file.getName();
if(fileName.endsWith(".class")) {
String className = basePackage + "." + fileName.substring(0, fileName.lastIndexOf('.'));
addClass(className);
}
}else if(file.isDirectory()) {
if(file.listFiles() == null){
return;
}
basePackage = basePackage + "." + file.getName();
for (File listFile : file.listFiles()) {
scanFile(listFile, basePackage);
}
}
}
public void addClass(String className) {
Class<?> aClass = null;
try {
aClass = classLoader.loadClass(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
classes.add(aClass);
}
}
迭代器
public class EnumerationIter<E> implements Iterator<E>, Iterable<E>, Serializable {
private static final long serialVersionUID = 1L;
private final Enumeration<E> e;
public EnumerationIter(Enumeration<E> enumeration) {
this.e = enumeration;
}
@Override
public boolean hasNext() {
return e.hasMoreElements();
}
@Override
public E next() {
return e.nextElement();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public Iterator<E> iterator() {
return this;
}
}
ListenerScan注解
仿造@MapperScan,定义一个@ListenerScan
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({BeanDefinitionRegister.class})
public @interface ListenerScan {
@AliasFor("basePackages")
String[] value() default {} ;
@AliasFor("value")
String[] basePackages() default {};
}
BeanDefinitionRegister
批量注册Bean需要集成Spring提供的ImportBeanBeanDefinitionRegistrar接口。
(mybatis-spring也是这么做的)
public class BeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(ListenerScan.class.getName()));
if(annoAttrs != null) {
String[] stringArray = annoAttrs.getStringArray("value");
for (String packageName : stringArray) {
ClassScanner classScanner = new ClassScanner(packageName, Thread.currentThread().getContextClassLoader());
Set<Class<?>> scan = classScanner.scan();
for (Class<?> aClass : scan) {
if(aClass.isInterface() || aClass.isAnonymousClass() || aClass.isEnum()) {
continue;
}
registry.registerBeanDefinition(aClass.getName(), new RootBeanDefinition(aClass));
}
}
}
}
}
配置文件
配置文件引入我们自定义的Register
@Import({BeanDefinitionRegister.class})
@Configuration
@ConditionalOnBean
public class CustomerConfig {
}
启动类
启动类加上我们的注解,并尝试获取容器中的对象。
@ListenerScan("com.dayrain.springbootdemo.listener")
@SpringBootApplication
public class SpringbootDemoApplication {
public static void main(String[] args){
ConfigurableApplicationContext context = SpringApplication.run(SpringbootDemoApplication.class, args);
Map<String, HelloListener> helloListenerMap = context.getBeansOfType(HelloListener.class);
Map<String, WorldListener> worldListenerMap = context.getBeansOfType(WorldListener.class);
System.out.println(helloListenerMap);
System.out.println(worldListenerMap);
}
}
结果
可以看到,即使我们没加@Component之类的注解,HelloListener、WorldListener也被注册进spring容器了。