剑指spring源码(二)补充篇之ImportBeanDefinitionRegistrar

19 篇文章 3 订阅
11 篇文章 1 订阅

剑指spring源码(二)补充篇之ImportBeanDefinitionRegistrar

例子

配置类

@ComponentScan("com.lry")
@Configuration//加了全配置注解会被cglib增强
@Import(MyImportBeanDefinitionRegistrar.class)
//@Conditional(MyCondition.class)这个条件不满足的话不会去扫描com.lry包
public class AppConfig {
	
}

入口类

public static void main(String[] args) {
	AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(AppConfig.class);
}

MyImportBeanDefinitionRegistrar类

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		System.out.println("importingClassMetadata:"+importingClassMetadata);
		System.out.println("registry:"+registry);
	}
}

源码解析

剑指spring二详细解析了bean工厂后置处理器的执行,但是在解析配置类的时候限于篇幅没有展开ImportBeanDefinitionRegistrar来说,故这里做一个补充。

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
				for (SourceClass candidate : importCandidates) {
					if (candidate.isAssignable(ImportSelector.class)) {
						//省略
					}
					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
						//candidate是MyImportBeanDefinitionRegistrar对应的SourceClass 文件
						//处理ImportBeanDefinitionRegistrar
						//MyImportBeanDefinitionRegistrar的class文件
						Class<?> candidateClass = candidate.loadClass();
						//BeanUtils根据class文件创建对象,可以理解registrar 是MyImportBeanDefinitionRegistrar的对象
						ImportBeanDefinitionRegistrar registrar =
								BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
						//这里可以忽略,没有实现Aware接口
						ParserStrategyUtils.invokeAwareMethods(
								registrar, this.environment, this.resourceLoader, this.registry);
						//在这里spring只是简单的放到importBeanDefinitionRegistrars的map集合中
						//是什么时候从map中取出来回调我们自己的MyImportBeanDefinitionRegistrar的registerBeanDefinitions方法呢
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					}
					else {
						//省略
					}
				}
			}
	}

其实是在ConfigurationClassPostProcessor类的processConfigBeanDefinitions方法的
this.reader.loadBeanDefinitions(configClasses);这一行代码遍历importBeanDefinitionRegistrars这个map来回调我们自己的MyImportBeanDefinitionRegistrar的registerBeanDefinitions方法

this.reader.loadBeanDefinitions(configClasses);这个方法一直追踪下去,会发现以下代码

private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
		registrars.forEach((registrar, metadata) ->
				registrar.registerBeanDefinitions(metadata, this.registry));
		//lamb遍历回调我们自己的MyImportBeanDefinitionRegistrar的registerBeanDefinitions方法

mybatis如何利用ImportBeanDefinitionRegistrar优雅兼容spring

mybatis版本一

查询的注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Select {//查询的注解
    String[] value();
}

mapper文件

public interface UserMapper {
    @Select(value = { "select * from user" })
    User queryUser(int id);
}

User类 ,get,set,构造器,tostring省略

public class User {
	private int id;
	private String username;
	private String password;
}

测试类

 public static void main(String[] args) {
        UserMapper userMapper = (UserMapper)Proxy.newProxyInstance(TestMybatis.class.getClassLoader(),new Class[]{UserMapper.class},new InvocationHandler(){
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("代理对象:"+proxy.getClass().getName());
                String sql = method.getAnnotation(Select.class).value()[0];
                System.out.println("sql:"+sql);
                System.out.println("参数:"+args[0]);
                Class clazz = method.getReturnType();
                System.out.println("返回类型:"+clazz.getSimpleName());
                //模拟查询数据库
                List<User> list = new ArrayList<>();
                for(int i=0;i<10;i++){
                    list.add(new User(i,"name"+i,"pwd"+i));
                }
                for (User user:list) {
                    if(user.getId()==(int)args[0]){
                        return user;
                    }
                }
                return null;
            }
        });
        //mybatis用户只需要写下面这行代码,和mapper文件就可以执行sql
        User user = userMapper.queryUser(1);
        System.out.println(user);
    }

至此为止mybatis第一版本完成了,但是缺点特别多,不适合作为框架使用,缺点如下:
不完善(当然这个是不可避免的,我不是mybatis的开发人员,不可能写个框架跟他媲美)
耦合性太大:测试类里面我要查询一个用户,就写了这么多代码,所以这种代码应该想办法抽出去。
没有和spring对接:这一点是致命的缺点,如何把产生的userMapper交给spring容器管理?
我们知道把一个对象交给spring有三种方法
第一种:@Bean
假若我们使用这种方式,代码如下
新增一个appConfig

@ComponentScan("easybatis")
@Configuration
public class AppConfig {
    @Bean
    UserMapper userMapper(){
        UserMapper userMapper = (UserMapper) Proxy.newProxyInstance(TestMybatis.class.getClassLoader(),new Class[]{UserMapper.class},new InvocationHandler(){
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("代理对象:"+proxy.getClass().getName());
                String sql = method.getAnnotation(Select.class).value()[0];
                System.out.println("sql:"+sql);
                System.out.println("参数:"+args[0]);
                Class clazz = method.getReturnType();
                System.out.println("返回类型:"+clazz.getSimpleName());
                //模拟查询数据库
                List<User> list = new ArrayList<>();
                for(int i=0;i<10;i++){
                    list.add(new User(i,"name"+i,"pwd"+i));
                }
                for (User user:list) {
                    if(user.getId()==(int)args[0]){
                        return user;
                    }
                }
                return null;
            }
        });
        return userMapper;
    }
}

测试类修改成这样

public class TestMybatis {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(AppConfig.class);
        //利用@Bean把userMapper交给spring容器,这里可以取出来
        UserMapper userMapper = app.getBean(UserMapper.class);
        User user = userMapper.queryUser(2);
        System.out.println(user);
    }
}

结果显示@Bean方案可行
但是这种方案有一个致命缺陷:
项目有多少个mapper接口就要在appConfig里写多少个@Bean,并且代码都是相似的,很显然这种方式非常不合适。
第二种:注册
AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(AppConfig.class);
app.getBeanFactory().registerSingleton(“userMapper”,userMapper);
这种注册方式就更加不合适了,因为既然你可以注册了,说明你的userMapper已经产生了,你再交给spring容器管理的意义就没有了
难道你要这样写代码吗?如下:

public static void main(String[] args) {
        AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(AppConfig.class);
        //动态代理生成userMapper代理对象
        UserMapper userMapper = (UserMapper)Proxy.newProxyInstance(TestMybatis.class.getClassLoader(),new Class[]{UserMapper.class},new InvocationHandler(){
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("代理对象:"+proxy.getClass().getName());
                String sql = method.getAnnotation(Select.class).value()[0];
                System.out.println("sql:"+sql);
                System.out.println("参数:"+args[0]);
                Class clazz = method.getReturnType();
                System.out.println("返回类型:"+clazz.getSimpleName());
                //模拟查询数据库
                List<User> list = new ArrayList<>();
                for(int i=0;i<10;i++){
                    list.add(new User(i,"name"+i,"pwd"+i));
                }
                for (User user:list) {
                    if(user.getId()==(int)args[0]){
                        return user;
                    }
                }
                return null;
            }
        });
        //明明可以直接用userMapper,你非要把他放到spring容器,再取出来,这不是脱光裤子放屁吗
        app.getBeanFactory().registerSingleton("userMapper",userMapper);
        userMapper = app.getBean(UserMapper.class);
        User user = userMapper.queryUser(1);
        System.out.println(user);
    }

第三种:FactoryBean
利用FactoryBean既可以很好的解决对一个对象交给spring管理的问题,见版本二

mybatis版本二

MyFactoryBean

@Component
public class MyFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        UserMapper userMapper = (UserMapper) Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),new Class[]{UserMapper.class},new InvocationHandler(){
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("代理对象:"+proxy.getClass().getName());
                String sql = method.getAnnotation(Select.class).value()[0];
                System.out.println("sql:"+sql);
                System.out.println("参数:"+args[0]);
                Class clazz = method.getReturnType();
                System.out.println("返回类型:"+clazz.getSimpleName());
                //模拟查询数据库
                List<User> list = new ArrayList<>();
                for(int i=0;i<10;i++){
                    list.add(new User(i,"name"+i,"pwd"+i));
                }
                for (User user:list) {
                    if(user.getId()==(int)args[0]){
                        return user;
                    }
                }
                return null;
            }
        });
        return userMapper;
    }

    @Override
    public Class<?> getObjectType() {
        return UserMapper.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

测试类

public static void main(String[] args) {
        AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(AppConfig.class);
        UserMapper userMapper = app.getBean(UserMapper.class);
        User user = userMapper.queryUser(1);
        System.out.println(user);
    }

结果是正确的,可惜这种方式虽然解决了把对象交给spring管理的问题,但是还有两个问题
问题一:MyFactoryBean里面写死了UserMapper,如果是其他mapper文件怎么办,我们希望可以传一个Mapper.class文件进来,这样我们就可以自适应生成代理对象
问题二:第二个问题很难察觉到,因为我们没有把自己看成是mybatis的开发人员。思考如下问题
@Component放在MyFactoryBean上真的合适吗?
想一下这个注解是干嘛,这个注解是spring扫描用的,我们现在需要把自己想象成mybatis的开发人员,把MyFactoryBean想象成是你自己写的一个类,用户在用mybatis集成spring框架的时候,写的包扫描路径下根本不可能包含你的MyFactoryBean,那么扫描到这个类也就不可能了。所以不可以用Component注解,矛盾就来了,不能用注解,又要交给spring管理,怎么办,这个时候就要请出我们的ImportBeanDefinitionRegistrar了,这个接口很厉害,可以动态注册bd,也就是可以用这个接口去注册我们的MyFactoryBean。
解决这两个问题请看版本三

mybatis版本三

配置文件改成这样

@ComponentScan("easybatis")
@Configuration
@MapperScan("easybatis.mapper")//这个MapperScan是自己写的
public class AppConfig {

}

再加一个anno

@Retention(RetentionPolicy.RUNTIME)
@Import(MyImportBeanDefinitionRegistrar.class)
public @interface MapperScan {//mybatis也是叫MapperScan 
    String[] value() default {};
}

MyFactoryBean

//注意此时不用加@Component
public class MyFactoryBean implements FactoryBean {
    Class mapperInterface;
    public MyFactoryBean(Class mapperInterface){
        this.mapperInterface = mapperInterface;
    }
    public MyFactoryBean(){
        System.out.println(0);
    }
    @Override
    public Object getObject() throws Exception {
        Object Object =  Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),new Class[]{mapperInterface},new InvocationHandler(){
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("代理对象:"+proxy.getClass().getName());
                String sql = method.getAnnotation(Select.class).value()[0];
                System.out.println("sql:"+sql);
                System.out.println("参数:"+args[0]);
                Class clazz = method.getReturnType();
                System.out.println("返回类型:"+clazz.getSimpleName());
                //模拟查询数据库
                List<User> list = new ArrayList<>();
                for(int i=0;i<10;i++){
                    list.add(new User(i,"name"+i,"pwd"+i));
                }
                for (User user:list) {
                    if(user.getId()==(int)args[0]){
                        return user;
                    }
                }
                return null;
            }
        });
        return Object;
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

MyImportBeanDefinitionRegistrar

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
        GenericBeanDefinition genericBeanDefinition =(GenericBeanDefinition)builder.getBeanDefinition();
        registry.registerBeanDefinition("userMapper", genericBeanDefinition);
        genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
        //走public构造器(且要求参数最多的且参数是在spring容器中)
        genericBeanDefinition.setAutowireMode(3);
    }
}

到这里我画个图方便理解

在这里插入图片描述
到此为止mybatis和spring是如何兼容的就基本说清楚了。
还有一个问题就是MyImportBeanDefinitionRegistrar还是写死了UserMapper,我们可以获取到MapperScan的value值,
然后包扫描得到所有的class,遍历进行注册。

mybatis版本四

mapper扫描遍历注册的代码如下

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
			                            BeanDefinitionRegistry registry) {
		AnnotationAttributes mapperScanAttrs = AnnotationAttributes
				.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
		String[] strs = mapperScanAttrs.getStringArray("value");//得到@MapperScan上面所有的value
		//包扫描 便利mapper
		List<String> mapperList = new ArrayList<String>();
		for (String str:strs) {
			mapperList.addAll(basePackageScan(str));
		}
		for (String mapper:mapperList) {
			//mybatis.mapper.UserMapper
			String [] s = mapper.split("\\.");
			Class clazz = null;
			try {
				 clazz = Class.forName(mapper);
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			}
			//bd建造者
			BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
			
			GenericBeanDefinition genericBeanDefinition =(GenericBeanDefinition)builder.getBeanDefinition();
			//beanName=类名首字母小写
			registry.registerBeanDefinition(toLowerCaseFirstOne(s[s.length-1]), genericBeanDefinition);
			//通过构造方法传mapper Class文件
			genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(clazz);
			//走public构造器(且要求参数最多的且参数是在spring容器中)
			//装配模式设置为3
			genericBeanDefinition.setAutowireMode(3);
			//还有其他装配模式,
			//AUTOWIRE_BY_NAME=1 
			//AUTOWIRE_BY_TYPE = 2;
			//AUTOWIRE_NO = 0;
			//AUTOWIRE_CONSTRUCTOR = 3;
		}
	}
	//首字母转小写
	public  String toLowerCaseFirstOne(String s){
		if(Character.isLowerCase(s.charAt(0)))
			return s;
		else
			return (new StringBuilder()).append(Character.toLowerCase(s.charAt(0))).append(s.substring(1)).toString();
	}
	private List<String> basePackageScan(String packName){
		List<String>classNames = new ArrayList<String>();
		URL url = this.getClass().getClassLoader().getResource(packName.replaceAll("\\.", "/"));
		String fileUrl = url.getFile()+"/";// E:/workspace/com/spring/mvc/
		File file = new File(fileUrl);//把路径转为文件目录
		String[] fileStr = file.list();//列出文件目录所有文件和文件夹的名字
		for (String path : fileStr) {//遍历所有的文件或文件夹的名字
			File pathFile = new File(fileUrl+path);//根据url和名字拼装成完整的url路径E:/workspace/com/spring/mvc
			if(pathFile.isDirectory()){
				basePackageScan(packName+"."+path);
			}else{//class 文件
				//mybatis.mapper.UserMapper
				classNames.add(packName+"."+pathFile.getName().replace(".class", ""));
			}
		}
		return classNames;
	}
}

总结

本文主要介绍了ImportBeanDefinitionRegistrar 的源码以及mybatis兼容spring的问题,
但是没有具体写出mybatis执行sql的过程,也就是SqlSession,Executor。
其实之前自己有写过mybaits框架,增删查改都可以通过配置mapper文件和mapper接口来完成,但是那时候没有考虑和spring对接的问题,导致我后面想把二者结合起来(即自己的mybatis框架和spring结合起来)变的异常困难,也思考了许久,还是没能解决,看看在研究一段时间mybatis源码能不能解决这个问题,如果解决了,到时候再出一篇博客发布自己的mybatis-spring框架。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值