spring和mybatis整合为什么只定义了接口?为什么设置自动装配模型为BY_TYPE

背景

是不是还在疑惑为什么我们在工程中定义了接口mybatis就可以直接操作我们的数据库?
是不是想了解spring和mybaits整合的原理?
了解原理后我们能复用在工程上的东西是什么?换句话说怎么提高代码的逼格?

目的

基于上述背景,笔者准备深入源码带大家一探究竟,读完这篇文章大家可以的到的收获

  1. 了解Mybatis和Spring整合的底层原理
  2. 知道为什么只定义了接口就可以直接操作数据库
  3. 了解Spring中的拓展点和FactoryBean的使用
  4. 可以自己定义插件提高代码逼格
  5. Spring中自动装配的类型到底是什么

分析问题

准备

代码环境:

  • JDK :1.8
  • Spring Boot :2.3
  • 基于注解
  • 忽略一些不重要的细节 项目代码如下
//对象
public class AD {
    //id
    private int id;
    //名称
    private String name;
}
//mapper类
public interface AdMapper {

    @Select("select id , name  from " + " ad " + "where id=#{id} ")
    AD findADById(@Param("id") int id);
}
//接口
public interface AdService {
    /**
     * 通过id获取广告对象
     *
     * @param id
     * @return
     */
    AD findADbyId(int id);
}
//接口实现类
@Service
public class AdServiceImpl implements AdService {

    @Resource
    private AdMapper adMapper;

    @Override
    public AD findADbyId(int id) {
        return adMapper.findADById(id);
    }
}
//启动类
@MapperScan("com.learn.code.mybatis.mapper")
@SpringBootApplication
public class LearnCodeApplication {
    public static void main(String[] args) {
        SpringApplication.run(LearnCodeApplication.class, args);
    }
}

问题1 Mapper对象的BeanDefinition是怎么加入到工厂中的

问题由来:一个对象只有被Spring创建并且放入到工厂中才能被其他对象注入,比如AdServiceImpl就是加了@Service注解并且结合包扫描。这样环境中就会有这个对象,但是AdMapper没有加任何的注解,而我们的 AdServiceImpl却可以直接通过 @Resource注入进来。说明这个对象是被Spring创建的。

回想Spring创建Bean的过程,几乎所有的对象都是先变成BeanDefinition然后再通过工厂创建,所以我们只要找到这个Mapper类是什么时候变成BeanDefinition的。

我们通过看代码发现和Mybatis相关的只有开始在配置类中加入的注解@MapperScan,难道是这个注解起的作用吗?

没错,这个注解是个入口,就像是把钥匙,像只有我们带了钥匙才能开门一样,只有加了这个注解(注意本文基于注解配置)才能实现上述的功能

那现在我们重点看一下这个注解

@MapperScan

屏蔽掉一些非关键信息 注解结构如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {

  String[] value() default {};

  String[] basePackages() default {};

  Class<?>[] basePackageClasses() default {};
}

在这个注解类中发现其实也没有做太多事,但是我们会发现类上边有@Import(MapperScannerRegistrar.class)这行.对Spring启动源码有了解的同学可能知道,在准备工厂阶段,会把 @Import引入的类当作配置类,后期通过Spring创建这个bean。所以我们应该能感觉MapperScannerRegistrar这个类是有些作用的,照例点进去看一下源码。

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware

发现这个类继承了ImportBeanDefinitionRegistrar实现了这个接口,这个接口是Spring当中的一个扩展点,基于接口中的registerBeanDefinitions方法我们可以做到向bean工厂中注入BeanDefinition,现在看来我们的方向是对的。

下面解析一下 registerBeanDefinitions 方法

/**
* importingClassMetadata 注解元素
* registry  用来向 BeanDefinitionRegistry 加入 BeanDefinition
*/
@Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	//获取MapperScan注解中的属性信息 @MapperScan("com.learn.code.mybatis.mapper")
	// 类上可能会有很多注解  这里指定名称获取 
    AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    //初始化一个 scanner 用作扫描指定包下的类
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    
    // Spring 3.1 版本需要有这个判断 特殊逻辑
    if (resourceLoader != null) {
    // 设置资源加载器,作用:扫描指定包下的class文件。
      scanner.setResourceLoader(resourceLoader);
    }
	//===============下边是获取MapperScan中的属性值,赋值给scanner,以后会在doScan中使用============
    Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
    if (!Annotation.class.equals(annotationClass)) {
      scanner.setAnnotationClass(annotationClass);
    }

    Class<?> markerInterface = annoAttrs.getClass("markerInterface");
    if (!Class.class.equals(markerInterface)) {
      scanner.setMarkerInterface(markerInterface);
    }

    Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
    if (!BeanNameGenerator.class.equals(generatorClass)) {
      scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass));
    }

    Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
    if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
      scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
    }
    scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
    scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));
	//获取要扫描的路径
    List<String> basePackages = new ArrayList<String>();
    for (String pkg : annoAttrs.getStringArray("value")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (String pkg : annoAttrs.getStringArray("basePackages")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) {
      basePackages.add(ClassUtils.getPackageName(clazz));
    }
    scanner.registerFilters();
    //执行扫描,并且会把 BD放入到工厂中,并更改BD中的一些属性值
    scanner.doScan(StringUtils.toStringArray(basePackages));
  }

scanner.doScan(StringUtils.toStringArray(basePackages))方法

  @Override
 public Set<BeanDefinitionHolder> doScan(String... basePackages) {
 // 1
 // 调用父类的扫描方法 向容器中加入 BD  并返回 beanDefinitions 
   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 {
   // 2
   // 操作 BD 引用 修改其中的属性
     processBeanDefinitions(beanDefinitions);
   }
   return beanDefinitions;
 }

doScan方法首先做的是调用父类的扫描方法,根据指定的包的路径和一些其他的限制条件(比如只要接口不需要类),来决定是否读取这个BD加入到工厂中。由于这个方法比较简单就不跟进去看了。

因为扫描是基于spring的扫描器。扫描过程中应该会有类加入进来,但是断点的时候会发现结果是只有接口。其中有个条件是子类可以重写的比如:

//只扫描接口
  protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
  }

这样扫描出来的东西也不会和spring本身扫描的类重复

问题2 Mapper对象是个接口怎么进行实例化

通过上边的分析,我们已经解决了第一个问题,怎么把这些类的BD放入到工厂中的,但是这样就完了吗?我们现在BD里面的class类型是一个接口,这是没有办法被实例化的。所以我们需要解决对象实例化的问题。

其实解决对象实例化不外乎就是对接口进行动态代理,但是怎么进行?代理完成后是个代理对象又怎么通过@Resource注入进来。不妨来看一下Mybatis是怎么实现的。

入口位置即是上边代码块中我标注了2的位置。下边来看一下 2出的代码实现

processBeanDefinitions(beanDefinitions)

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    //对传进来的BD循环处理
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();

      if (logger.isDebugEnabled()) {
        logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
          + "' and '" + definition.getBeanClassName() + "' mapperInterface");
      }

      // the mapper interface is the original class of the bean
      // but, the actual class of the bean is MapperFactoryBean
      // 1 设置 mapperInterface 属性  也就是这个接口的原始类型 对那个接口进行代理
      definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
      // 2 设置 BeanClass 实例化时候用到的类型   能被实例化就是改了这个 原来是实际的接口类型,现在是 mapperFactoryBean 类型 因为 mapperFactoryBean 不是抽象类也不是接口所以可以被初始化
      // 通过spring的getBean方法创建对象就是创建的beanClass类型的对象
      definition.setBeanClass(this.mapperFactoryBean.getClass());
		//添加属性
      definition.getPropertyValues().add("addToConfig", this.addToConfig);

      boolean explicitFactoryUsed = false;
      //设置  sqlSessionFactory 
      if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
        definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionFactory != null) {
        definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
        explicitFactoryUsed = true;
      }
		//sqlSessionTemplate and sqlSessionFactor 都存在 会使用 sqlSessionTemplate
      if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
        explicitFactoryUsed = true;
      } else if (this.sqlSessionTemplate != null) {
        if (explicitFactoryUsed) {
          logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
        }
        definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
        explicitFactoryUsed = true;
      }

      if (!explicitFactoryUsed) {
        if (logger.isDebugEnabled()) {
          logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        }
		// 3 设置自动装配模型为 BY_TYPE  通过类型进行装配   
      definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }
    }
  }

这整个方法的作用实际是构造 mapperFactoryBean 类型 需要传入的参数

其中 需要重点关注的是我标记了1、2、3的部分

标注1的部分 设置代理的接口类型

在别的版本中是通过配置构造函数来做的如下,但是原理是一样的。

 definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59

需要注意的是通过这种形式加进去的是字符串类型,但是内部会帮我们转成class类型。所以我们去看这个类的的构造方法的时候会发现他是有一个class的构造方法而没有字符串的。

这部分的主要逻辑是 向 mapperFactoryBean 中传入我们要进行动态代理的接口,至于为什么稍后我会把 mapperFactoryBean 的源码贴出来 大家就明白了。

标注2的部分 设置实例化类的类型

开始我们再说的问题就是怎么创建对象的问题,因为BeanClass为接口类型,读过spring创建bean的源码的同学都知道,在getBean时就是创建的BeanClass类型的对象,所以我们需要修改这个类型 是个具体的类 。这个具体的类就是 MapperFactoryBean ,有些人可能会问,那创建的这个类型不是我们需要的类型啊?我们怎么使用?别急这些在我们说到 MapperFactoryBean 这个类的时候会做解释

标注3的部分 修改自动装配模型

自动装配你真的了解吗?

如果我们大家Spring中Bean的装配模型是什么?相信大部分人第一反应就是By_Type或者By_Name .为什么?因为我们通过@Resource等注解可以进行注入。这里我要给大家纠正一下概念。Spring中的装配模型为By_No,他只是依赖于自动装配的技术完成了自动装配。

看源码

org.springframework.beans.factory.support.AbstractBeanDefinition#setAutowireMode

	/**
	 * Set the autowire mode. This determines whether any automagical detection
	 * and setting of bean references will happen. Default is AUTOWIRE_NO,
	 * which means there's no autowire.
	 * @param autowireMode the autowire mode to set.
	 * Must be one of the constants defined in this class.
	 * @see #AUTOWIRE_NO
	 * @see #AUTOWIRE_BY_NAME
	 * @see #AUTOWIRE_BY_TYPE
	 * @see #AUTOWIRE_CONSTRUCTOR
	 * @see #AUTOWIRE_AUTODETECT
	 */
	public void setAutowireMode(int autowireMode) {
		this.autowireMode = autowireMode;
	}

看上边的注释我们会发现自动装配的模型有5中但是默认的是Default is AUTOWIRE_NO也就是不进行自动装配

是不是颠覆了你的认知呢?

为什么修改装配类型为AUTOWIRE_BY_TYPE

言归正传,这个地方修改为AUTOWIRE_BY_TYPE的目的是什么?

你们可以做个实验试一下,如果把某个类的装配类型改成 BY_TYPE ,那么这个类的所有的set方法都会进行自动装配

@Service
public class ByTypeTuan {

    //=============第一种========
    private AdService adService;

    public void setAdService(AdService adService) {
        this.adService = adService;
    }

    //=============第二种========
    @Autowired
    private AdService service;
}

如上述代码 我们要获得 adService 原来是通过 @Autowired注解,底层是后置处理器做的赋值,现在可以通过第一种 直接set注入。

想一下这样有什么好处?

我们要操作数据库还需要拿到数据库的SqlSession,SqlSession是怎么注入的呢?就是通过set方法注入到代理类中然后我们才可以进行操作。

所以说这个地方也是一个重点,但是别的博文中很少有人提到,看到这是不是该给作者一个赞呢?

问题3产生的对象为什么可以使用,并且能操作数据库

上边说到,其实我们向Spring中加入的是 MapperFactoryBean 类型的Bean.现在我们来看一下这个对象的实现:

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

  private Class<T> mapperInterface;
  private boolean addToConfig = true;

  //构造方法。传入 mapperInterface 
  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
  public MapperFactoryBean() {}
  /**
   * {@inheritDoc}
   */
  @Override
  protected void checkDaoConfig() {
    super.checkDaoConfig();
    notNull(this.mapperInterface, "Property 'mapperInterface' is required");
    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }
  /**
   * 创建代理对象
   */
  @Override
  public T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);}
  /**
   * 创建的对象类型是 mapperInterface 即mapper接口类型
   */
  @Override
  public Class<T> getObjectType() { return this.mapperInterface;}
  /**
   * 创建的对象是否是单例
   */
  @Override
  public boolean isSingleton() { return true; }
  //------------- mutators --------------
  /**
   * 设置 mapper interface 
   * @param mapperInterface class of the interface
   */
  public void setMapperInterface(Class<T> mapperInterface) { this.mapperInterface = mapperInterface;}
  
  public Class<T> getMapperInterface() {return mapperInterface;}
  
  public void setAddToConfig(boolean addToConfig) {this.addToConfig = addToConfig;}
  
  public boolean isAddToConfig() {return addToConfig;}
}

重要的代码位置已经加上了注释,通过这个类结构我们能更明确知道上一部在修改BD信息时的作用是什么

通过观察代码的继承结构,发现extends SqlSessionDaoSupport implements FactoryBean

FactoryBean比较了解的同学都知道 这个对象在Spring创建Bean时会创建两个对象,一个是它本身还有一个是通过getObject方法返回我们需要的对象。

  /**
   * 创建代理对象
   */
  @Override
  public T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);}

看 getObject 内部的实现 和我们自己整合mybatis一样 通过sqlSession.getMapper()来获得Mapper对象,其中参数中传的是 接口类。

还记得上边抛了一个问题,为什么要修改这个对象的装配属性为By_Type 因为 继承了 SqlSessionDaoSupport 而 SqlSessionDaoSupport 中有两个set方法 是用来注入SqlSessionFactory的 我们拿到了SqlSessionFactory 就可以获得SqlSession 从而获得Mapper对象 然后操作数据库。

总结一下:

  1. 定义一个类实现FactoryBean继承SqlSessionDaoSupport
  2. 定义一个实现ImportBeanDefinitionRegistrar,用来生成不同Mapper对象的FactoryBean
  3. @Import 用来创建上边实现的ImportBeanDefinitionRegistrar接口的对象
  4. 修改AUTOWIRE 可以通过set方法直接注入

我这篇文章 spring常用扩展点术语介绍大致说了一些拓展点的作用,以后会详细具体怎么使用,和Spring中的应用。大家可以关注一下

你可能感兴趣的:(spring,mybatis)