手写spring-mybatis(一文搞懂spring如何引入mybatis)

1. 背景

手写一下spring-mybatis 包,方便理解spring的加载过程。
(希望能够耐着心看完,学习源码毕竟枯燥,只有死磕才是终点)

2. 代码环境

引入mybatis 的jar 包

    compile group: 'org.mybatis', name: 'mybatis', version: '3.4.5'

3. 准备基础代码

       3.1 :手写mapper类----DogAppleMapper
public interface DogAppleMapper {

	@Select("select 'apple'")
	String query();
}
        3.2: 准备测试类 service
@Service
public class DogUserService {

	@Autowired
	private DogAppleMapper dogAppleMapper;

	public String getUser(){
		System.out.println(dogAppleMapper.query());
		return "success";
	}
}
        3.3 : 准备容器
 
@ComponentScan("com.dog.dog_mybatis")
public class DogMybatisTest {
	public static void main(String[] args) {
		/** 获得上下文容器 **/
		 AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(DogMybatisTest.class);
		context.refresh();

		DoguserService dogUserService = (DogUserService) context.getBean("dogUserService");
		dogUserService.getUser();
}
}
        3.4: 启动后分析

                启动后报错 如下:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.dog.dog_mybatis.mapper.DogAppleMapper' available:

原因很简单,就是DogAppleMapper 没有实现类,在spring 找到注入点的时候,因为没有对应的实现类,所以生成不了对应的BeanDefinition,所以就会报错 NoSuchBeanDefinition。

所以需要给DogAppleMapper 找到一个实现类/代理类/或则直接把DogAppleMapper手动 注册到BeanDefinitionMap中。

      4. 改造,使用BeanDefinition直接注册

        4.1: 尝试手动注册到BeanDefinition (❌)
​
@ComponentScan("com.dog.dog_mybatis")
public class DogMybatisTest {
	public static void main(String[] args) {
		/** 获得上下文容器 **/
		 AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(DogMybatisTest.class);

        //获取一个新的beanDefinition
		AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
		beanDefinition.setBeanClass(DogAppleMapper.class);
		context.registerBeanDefinition(DogAppleMapper.class.getName(), beanDefinition);
		
		context.refresh();

		DoguserService dogUserService = (DogUserService) context.getBean("dogUserService");
		dogUserService.getUser();
}
}

​

这样也是不行的,因为DogAppleMapper是一个 interface, 不能直接注册成 beanDefinition. 我们只能考虑给 DogAppleMapper 生成一个代理类。这里就要使用FactoryBean了

5. FactoryBean接手

        5.1: 关于FactoryBean

                FactoryBean可以理解为是一个spring的扩展类,我们可以用来生成一些复杂的bean, 或则就是为接口,类生成代理类使用。 在实例化刚开始的时候一段代码如下

            /** 只有非懒加载的单例才会走这里 **/
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
	/** 这里是处理特殊的对象 FactoryBean的逻辑 **/
	if (isFactoryBean(beanName)) {
		// 获取FactoryBean对象,这个 Bean获取是要加一个 &
		Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
		if (bean instanceof FactoryBean) {
			FactoryBean<?> factory = (FactoryBean<?>) bean;
			boolean isEagerInit;
		    if (){...}
			else {
				isEagerInit = (factory instanceof SmartFactoryBean &&
					((SmartFactoryBean<?>) factory).isEagerInit());}
						if (isEagerInit) {
							// 创建真正的Bean对象(getObject()返回的对象)
							getBean(beanName);
						}
					}

也就是我们如果实现的是FactoryBean接口,在Spring启动的时候是不会执行getBean()方法的,也就是不会生成内部的getObject()产生的对象。 只有实现SmartFactoryBean的时候,并且重写了isEager()方法返回true的时候,才会在Spring启动过程中生成getObject()中的bean.

接着往下看getBean(). 里面调用的是doGetBean()。 这个先处理FactoryBean实现类,如果不是,再去处理正常的业务Bean等。处理后面业务Bean的时候会先扫描生成BeanDefinition。这里就不再描述了。直接看doGetBean()方法中对FactoryBean的处理。

if (sharedInstance != null && args == null) {
    ......
		// 如果sharedInstance是FactoryBean,那么就调用getObject()返回对象
		beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
	}
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
....
object = postProcessObjectFromFactoryBean(object, beanName);

代码太长了,就是用两行看一下。 先从factoryBeanObjectCache  缓存中获取,获取不到调用FactoyBean实现类中的getObject()方法获取对象,然后存到 factoryBeanObjectCache缓存中。所以只要factoryBean是单例的,我们getObject()获取的对象也是单例的,因为后续获取getBean(factoryBean)的时候,都是走的缓存。

结论就是:通过factoryBean接口getObject()方法创建的对象,不会放入到单例池中,也不会生成BeanDefinition. 只是在需要该对象的时候通过getObject()获取然后放入缓存。   factoryBean接口getObject()创建的实例,是不拥有spring生命周期的。但是实现了factoryBean的类是在容器中的。

        5.2: 使用FactoryBean获取DogAppleMapper接口的代理
@Component
public class DogFactoryBean implements FactoryBean {
	
	@Override
	public Object getObject() throws Exception {

		Object proxyInstance = Proxy.newProxyInstance(DogFactoryBean.class.getClassLoader(), new Class[]{DogAppleMapper.class}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				System.out.println("factory bean 中的getObject()");
				return null;
			}
		});
		return proxyInstance;
	}

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

这样我们使用基础代码测试,就能返回 null. 说明DogAppleMapper的代理类是生成成功了。

按照上面写,会存在两个问题

  • 我们会有很多的mapper,不能把 DogAppleMapper.class写死,写死的话,需要创建很多的FactoryBean
  • DogFactoryBean不能加@Component注解,加了就是单例,单例的对象是固定一个。但是会有多mapper,需要很多的mapper代理对象factoryBean.

更改上面的代码如下

public class DogFactoryBean implements FactoryBean {

	private Class instance;

	public DogFactoryBean(Class instance) {
		this.instance = instance;
	}

	@Override
	public Object getObject() throws Exception {

		Object proxyInstance = Proxy.newProxyInstance(DogFactoryBean.class.getClassLoader(), new Class[]{instance}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				System.out.println("factory bean 中的getObject()");
				return null;
			}
		});
		return proxyInstance;
	}

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

改动了两个地方,第一使用动态的class. 使用方mapper通过构造方法设置

第二去掉了@Component注解,使用方可以创建不同的实例对象,作为mapper代理类

这样还是存在问题,我们的代理类DogFactoryBean没有了@Component,没办法注册到spring容器中,但是作为mapper的代理对象,spring初始化的时候是要加载创建的。这个时候可以使用BeanDefinition去实例化这个factoryBean

我们更改启动类代码如下:

@ComponentScan("com.dog.dog_mybatis")
public class DogMybatisTest {

	public static void main(String[] args) {
		/** 获得上下文容器 **/
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(DogMybatisTest.class);

		/** 生成一个新的BeanDefinition **/
		AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
		/** 设置beanClass,  beanClass 就是一个BeanDefinition实例化的时候要去找的对象 **/
		beanDefinition.setBeanClass(DogFactoryBean.class);
        /** 设置构造方法的参数引用,spring会根据推断 构造方法机型匹配 **/		beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(DogAppleMapper.class);
        /** 注册到BeanDifinitionMap中 **/
		context.registerBeanDefinition("dogAppleMapper", beanDefinition);
		context.refresh();


		DogUserService dogUserService = (DogUserService) context.getBean("dogUserService");
		dogUserService.getUser();
}

跑一下测试,可以拿到DogAppleMapper的代理类的。

        5.3 插入一点BeanDefinition 说明

        上面的插入的代码拿出来

/** 生成一个新的BeanDefinition **/
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
/** 设置beanClass,  beanClass 就是一个BeanDefinition实例化的时候要去找的对象 **/
beanDefinition.setBeanClass(DogFactoryBean.class);
     
/** 注册到BeanDifinitionMap中 **/
context.registerBeanDefinition("dogAppleMapper", beanDefinition);

巩固一下这个流程,Spring启动过程中,对Bean的初始化大概的过程是 

  1. 创建BeanFactory,默认是DefaultListableBeanFactory 
  2. 创建注解对象BeanDefinition 读取器  AnnotationBeanDefinitionReader
  3. 创建扫描对象注册BeanDefinition 注册器 ClasspathBeanDefinitionScanner
  4. 把class文件加载到 JVM中,Class.forName();
  5. 扫描配置类,在invokerBeanFactoryPostProcessor()方法中,先通过@Component,@ComponentScaner,@Import,@ImportSource, @Bean找到配置类。
  6. 解析配置类,把配置类都转换成BeanDefinition, 并且注册到BeanDefinitionMap<String name, beanDefinition>中,这个时候我们可以随便指定一个name,随表起名字,只不过后续使用的时候也要用这个名字getBean()。
  7. 然后进行实例化,实例化的时候会根据BeanDefinitionMap中的BeanDefinition的 beanClass属性找到类,对类进行实例化。Class.invoke() 
  8. 实例化后,进行属性填充,属性填充包含配置文件,依赖注入。 寻找注入点,然后注入实例
  9. 实例化完成后,进行初始化。初始化后会把实例放入到单例池中。

6. 引入ImportBeanDefinitionRegistrar 注册BeanDefinition

在看一下5.2 步骤的代码,可以看到,如果存在多个Mapper,我们是要生成多个FactoryBean实现类的代理类,需要生成很多的BeanDefinition 去处理。我们不可能在启动类上无限的添加BeanDefinition是吧。所以需要考虑一下注册BeanDefinition还有什么办法。 

        6.1: 使用BeanDefinitionRegistryPostProcessor 来注册BeanDefinition
@Component
public class DogBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		/** 用不到,不实现 **/
	}

	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
		AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
		beanDefinition.setBeanClass(DogFactoryBean.class);
		beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(DogAppleMapper.class);
		registry.registerBeanDefinition("dogAppleMapper", beanDefinition);
	}
}

把5.2步骤中的 BeanDefinition 那块去掉,测试发现DogAppleMapper依然可以注册。

自此,创建BeanDefinition的工作交给了 BeanDefinitionRegistryPostProcessor

还是要注册不同mapper的BeanDefinition。 如果使用扫描的方式,循环赋值就可以规避,扫描包下面的类。Spring中有一个 ClassPathBeanDefinitionScanner。 

上面代码面临什么问题呢?我们接下来改造需要使用扫描。但是这个postProcessor不支持获取注解,想一下还有什么方式可以注册BeaDefinition呢?可以使用@Import注解范畴内 ImportBeanDefinitionRegistrar。

        6.2:自定义MapperScanner注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(DogImportBanDefinitionRegistrar.class)
public @interface DogMapperScanner {

   String value() default "";
}

自定义的注解需要加到3.3步骤容器类上 @DogMapperScanner("com.dog.mybatis.mapper")指定到mapper所在的包。

注解上上面还多了一个注解 @Import(DogImportBeanDefinitionRegistrar.class) ,需要知道前提@Import的注解会把类的信息生成BeanDefinition。

        6.3:自定义 ClassPathBeanDefinitionScanner

上面我们自定义了MapperScanner,还需要注解解析器,也就是说需要这个注解做什么事情,需要自定义一个注解解析器,告诉程序这个注解接下来需要做什么。 这个场景下我们就是需要把扫描到mapper 注册成BeanDefinition。

其实Spring源码中已经提供了类似的功能,例如 @ComponentScanner注解。就是通过扫描包下面的类,然后注册成BeanDefinition,所以这里只需要集成Spring中的ClassPathBeanDefinitionScanner,重写内部的方法就可以了。

public class DogClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {

    /** 构造方法,集成ClassPathBeanDefinitionScanner 必选的 **/
	public DogClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
		super(registry);
	}


	@Override
	protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);

		for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
        /** 获取到扫描到的  单个BeanDefinition, 此处就不用我们手动生成新的BeanDefinition了,扫描会得到一个 **/
			BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
        /** 设置构造方法参数,这个是要和下一行放一起分析的 **/	beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
        /** 替换了这个 BeanDefinition的 beanClass。 下面这个set就是该beanClass的。 接下来根据BeanDefinition实例化对象的时候,就会使用DogFactoryBean, 推断的出来的构造方法就是上面一句。 上面这一句就可以把扫描到的mapper 传进去生成 **/

			beanDefinition.setBeanClassName(DogFactoryBean.class.getName());
		}

		return beanDefinitionHolders;
	}

上面代码中的doScan()方法就是直接调用CalssPathBeanDefinitionScanner生成 BeanDefinition。

Spring 后续流程中会处理这个set集合中对象 到BeanDefinitionMap中。

        6.4:处理 ImportBeanDefinitionRegistrar。

这个类中实现的方法先去获取 @DogMapperScanner注解上的path, 根据path, 使用自定义的DogClassPathBeanDefinitionScanner去扫描。代码如下

public class DogImportBanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	
		/** 获取注解 **/
		Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(DogMapperScanner.class.getName());
        /** 获取注解属性值**/
		String path = (String)annotationAttributes.get("value");

		/** 使用自定义扫描器扫描,参数就是一个 beanFactory **/
		DogClassPathBeanDefinitionScanner scanner = new DogClassPathBeanDefinitionScanner(registry);

        /** 这个扫描就是我们重新的扫描方法 **/
		scanner.scan(path);
}

这个类上面是不需要添加 @Componnet的。因为注解 @Import的是会被Spring自己解析加载到容器的。

启动尝试,这个时候会报错。这是因为Spring 扫描在解析配置类的时候,是会忽略接口的,mapper包下都是接口,所以不行。看一下这块的源码:

doScan()---->findCondidateComponent()----->scanCondidateComponent()在这个方法中,是寻找配置类。

MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
// excludeFilters、includeFilters判断
if (isCandidateComponent(metadataReader)) { // @Component-->includeFilters判断
	ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
	sbd.setSource(resource);

	if (isCandidateComponent(sbd)) {
						
		candidates.add(sbd);
	}

isCandidateCOmponent() 源码中就是判断 includefilter, excludeFilter, Conditional这些注解。 而@Component注解在Spring刚开始启动的时候,都加到includerFilter。 也就是说,这个地方会判断是不是配置类,mapper接口上面没有添加@Component注解,是没办法成为配置类的。当然是可以在mapper类上加上@Component注解的,也就是配置类了,但是一般没有这样写的。所以我们要重写这个判断。

先看第二个isCandidateComponent(),我们在6.3 步骤中的DogClassPathBeanDefinitionScanner中重写方法

	@Override
	protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        /** 只要是接口就返回 true **/
		return beanDefinition.getMetadata().isInterface();
	}

还有第一个isCandidateComponent()判断,我们有很多种方法解决,也可以重写,直接返回true。 但是在源码中是在DogImportBeanDefinitionRegistrar中处理的,代码如下:

scanner.addIncludeFilter(new TypeFilter() {
			@Override
			public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
				return true;
			}
		});

也就是直接返回true了。也就是校验这个类的时候返回true,直接理解为忽略。

现在启动测试,发现还是没问题的。经过上面的处理,我们把spring 怎么引入 mybatis的代码写完了。主要就是完成了怎么无感注册代理类。接下来就是mybatis相关的了

7.  引入SqlSession

        7.1: 再看DogFactoryBean
public class DogFactoryBean implements FactoryBean {

	private Class instance;

	public DogFactoryBean(Class instance) {
		this.instance = instance;
	}

	@Override
	public Object getObject() throws Exception {

		Object proxyInstance = Proxy.newProxyInstance(DogFactoryBean.class.getClassLoader(), new Class[]{instance}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				System.out.println("factory bean 中的getObject()");
				return null;
			}
		});
		return proxyInstance;
	}

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

在这里,只是生成了一个代理类,也就是在代码中注入dogAppleMapper的时候,会找到一个factoryBean,具体的执行对象,还是要通过getObject()方法获得,但是现在的这个是null.

此时你可以在启动类上添加下面代码就能看到效果。

Object dogAppleMapper = context.getBean("dogAppleMapper");

这样,还没有生成真正的代理类。

接下来就可以直接使用,mybatis中操作mapper的代理类了,也就是getMapper()方法。

        7.2: 使用mybatis 包中的代理类

这个DogFactoryBean就要改造成下面的代码了

public class DogFactoryBean implements FactoryBean {

	private Class mapperInstance;

    /** sql session连接**/
	private SqlSession sqlSession;

    /** 这个是构造方法,是BeanDefinition 推断构造方法的时候使用 **/
	public DogFactoryBean(Class instance) {
		this.mapperInstance = instance;
	}

    
	@Autowired
	public void setSqlSession(SqlSessionFactory sqlSessionFactory) {
		sqlSessionFactory.getConfiguration().addMapper(mapperInstance);
		this.sqlSession = sqlSessionFactory.openSession();
	}

	@Override
	public Object getObject() throws Exception {

		return sqlSession.getMapper(mapperInstance);
	}

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

上面的代码,在setSqlSession()方法上添加了@Autowired,就是要注入这个属性了。属性注入byName,byType等等这些,不重点说哈。

这里这样注入 sqlSession也是可以的。但还有另外一种方式,就是使用BeanDefinition的 setAutowiredMode() 设置注入类型。在 DogClassPathBeanDefinitionScanner类中的for循环中方改成如下. 记得去掉上面set方法的@Autowired注解

AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition)beanDefinitionHolder.getBeanDefinition();

beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());

/** 添加了属性注入模型,按照byType的形式,也就是会调用setSqlSession()方法了 **/
beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
beanDefinition.setBeanClassName(DogFactoryBean.class.getName());

通过上面的set方法,可以看到还有一个类,就是SqlSessionFactory还没有初始化,这个类对象是读取配置文件 mybatis.xml生成的。在启动类上注入一个@Bean

	@Bean
	public SqlSessionFactory sqlSessionFactory() throws IOException {
		InputStream resourceAsStream = Resources.getResourceAsStream("mybatis.xml");
		SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
		return factory;
	}

另外,mybatis.xml文件附上

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
  <environments default="development">
    <environment id="development">
      <!-- 使用jdbc事务管理 -->
      <transactionManager type="JDBC"/>
      <!-- 数据库连接池 -->
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url"
                  value="jdbc:mysql://127.0.0.1:3306/atm?characterEncoding=utf-8&amp;useSSL=false"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>
    </environment>
  </environments>


</configuration>

8. 完成

启动测试类,这个时候已经可以正常使用了。

再提一下,这里最终使用的链接数据库session是 DefaultSession,应该是这个名字吧。这里面的操作线程是不安全的。spring-mybatis源码中使用的sessionTemplate,里面用ThreadLocal封装了一下。

over. 个人总结。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值