背景介绍

前面我们在一文带你掌握spring框架中import注解的作用中讲到了@import注解的作用,那么它和我们今天要讲的主题之间有什么联系呢?

今天我们我们这边文章主要是为了解决一个问题:“mybatis是如何将Mapper接口的代理类实例注册到spring容器中的?”。实际上mybatis批量注册Mapper的代理对象就使用了@import注解,下面我将手把手带大家看源码,帮助大家了解mybatis批量注册Mapper接口代理对象到spring容器中背后的原理。

Mybatis批量注册代理bean的原理

相信经常使用springboot开发项目的小伙伴对下面这段代码应该不陌生:

@SpringBootApplication
@MapperScan(basePackages = "com.lizemin.importcase.mapper")
public class ImportCaseApplication {

    public static void main(String[] args) {
        SpringApplication.run(ImportCaseApplication.class, args);
    }
}

我们正是通过@MapperScan(basePackages = "com.lizemin.importcase.mapper")将Mapper接口的代理对象注册到spring容器中,后面就能直接通过@Autowired使用Mapper代理对象了。那么为什么这个注解就能实现批量注册Mapper代理对象呢?我们点击去看这个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class) // 这个是核心部分
@Repeatable(MapperScans.class)
public @interface MapperScan {

  @AliasFor("basePackages")
  String[] value() default {};

  /**
   * 设置包扫描
   */
  @AliasFor("value")
  String[] basePackages() default {};
	
	// 指定创建代理bean的FactoyBean,默认是MapperFactoryBean
  Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;
}

可以看到该注解上方用到了@Import(MapperScannerRegistrar.class),接下来就要用到之前我们在一文带你掌握spring框架中import注解的作用中讲到的知识了。mybatis将通过这个MapperScannerRegistrar来往spring容器中添加BeanDefinition,我们看下这个类的源码,可以看到它实现了ImportBeanDefinitionRegistrar接口:

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

	/**
	 * 添加BeanDefinition
	 */
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	  var mapperScanAttrs = AnnotationAttributes
	      .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
	  if (mapperScanAttrs != null) {
	    registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
	        generateBaseBeanName(importingClassMetadata, 0));
	  }
	}
}

所以我们重点看上面registerBeanDefinitions这个方法,看mybatis注册了哪个类的BeanDefinition,只要我们认真看,就会发现mybatis注册的是org.mybatis.spring.mapper.MapperScannerConfigurer类的BeanDefinition,如下图所示:
在这里插入图片描述
然后我们点进去看这个类:

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
	
 /**
  * 添加BeanDefinition
  */
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    var scanner = new ClassPathMapperScanner(registry, getEnvironment());
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setExcludeFilters(this.excludeFilters = mergeExcludeFilters());
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
		
	// 这段代码非常关键,这里
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
      scanner.setLazyInitialization(Boolean.parseBoolean(lazyInitialization));
    }
    if (StringUtils.hasText(defaultScope)) {
      scanner.setDefaultScope(defaultScope);
    }
    scanner.registerFilters();

    // 开始进行包扫描,扫描MapperScan中指定的包下的Mapper接口
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }
}

上面是我贴出来的关键代码,这个类实现了org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor接口,这个接口的作用我在之前的一文带你快速了解spring的扩展点之beanFactoryPostProcessor文章中也讲过,主要就是往spring容器中动态添加BeanDefinition。在上面这段代码中,创建了一个org.mybatis.spring.mapper.ClassPathMapperScanner对象,扫描@MapperScan注解中指定的包下的Mapper接口。

  @Override
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    // 继承了spring的ClassPathBeanDefinitionScanner,拿到了所有Mapper接口的BeanDefinition
    var beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
      if (printWarnLogIfNotFoundMappers) {
        LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
            + "' package. Please check your configuration.");
      }
    } else {
      // 这段代码是关键,用来创建Mapper接口对应的MapperFactoryBean BeanDefinition
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }

在这个doScan方法中,主要做了两件事:

  1. 扫描basePackage下的所有mapper接口,得到对应的Mapper BeanDefinition。
  2. 执行processBeanDefinitions(beanDefinitions);方法,用来创建Mapper接口对应的MapperFactoryBean BeanDefinition,而MapperFactoryBean正是用来给Mapper接口创建代理对象的。所以,接下来我们重点看下这个方法。

这个方法很长,我只保留关键代码,如下所示:

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
  AbstractBeanDefinition definition;
  var registry = getRegistry();
  for (BeanDefinitionHolder holder : beanDefinitions) {
    definition = (AbstractBeanDefinition) holder.getBeanDefinition();
    var beanClassName = definition.getBeanClassName();
    // 为MapperFactoryBean设置mapperInterface
    definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
    // 这段代码非常关键,它将Mapper BeanDefinition变成了MapperFactoryBean的BeanDefinition
    definition.setBeanClass(this.mapperFactoryBeanClass);
  }
}

这段代码很有趣,它修改了原始的Mapper BeanDefinition,弄了一出狸猫换太子的好戏,通过definition.setBeanClass(this.mapperFactoryBeanClass);这行代码将Mapper BeanDefinition变成了MapperFactoryBean的BeanDefinition,也就是说:mybatis并不是直接注入Mapper接口的Bean Definition,而是先注入其对应的MapperFactoryBean的BeanDefinition。为什么要这样做呢?我们看下MapperFactoryBean这个类就知道了,代码如下:

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

	private Class<T> mapperInterface;

  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
	
 /*
  * Mybatis正是通过这个方法为Mapper接口创建代理对象,并将生成的代理对象注册到spring容器中
  */
  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }
}

至于为什么mybatis要使用FactoryBean来注册Mapper接口的代理对象,我在一文带你快速了解spring的FactoryBean中有讲过,主要还是跟springboot的自动化配置有关,因为mybatis依赖自动化配置创建出来的sqlSessionTemplate和sqlSessionFactory。

实际上到这里,我们已经基本上解决我们在一开始想要解决的问题,那就是mybatis是如何通过@MapperScan将Mapper接口的代理bean注册到spring容器中的。

接下来我们继续往下看,了解mybatis是如何创建代理对象的,这里就涉及到反射相关的知识了。到了这一步,剩下的内容就很简单了。

mybatis如何创建Mapper的代理bean对象

接下来我们顺腾摸瓜,看看org.mybatis.spring.SqlSessionTemplate#getMapper方法

@Override
public <T> T getMapper(Class<T> type) {
  return getConfiguration().getMapper(type, this);
}

这段没什么,我们继续往下看org.apache.ibatis.session.Configuration#getMapper方法:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}

这段没什么,我们继续往下看org.apache.ibatis.binding.MapperRegistry#getMapper方法:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  try {
    // 这个方法是重点,用来返回Mapper接口的代理对象
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

接下来我们重点看下return mapperProxyFactory.newInstance(sqlSession); ,这段代码会返回Mapper接口的代理对象,于是我们继续往下看org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)方法:

/**
 * 答案在这里,mybatis通过JDK反射创建了代理对象
 */
protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

如果我们还想继续了解mybatis是如何执行Mapper接口中的方法,如何查询数据库,并将查到的数据封装为对象的,就需要看org.apache.ibatis.binding.MapperProxy中的invoke方法了,代码如下:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    }
    // 这一段是关键,mybatis如何处理Mapper中每个方法的逻辑全在这里面
    return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

上面的代码中我们重点看return cachedInvoker(method).invoke(proxy, method, args, sqlSession);这段代码,尤其是cachedInvoker这个方法,点进去可以看到:

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
   try {
     return MapUtil.computeIfAbsent(methodCache, method, m -> {
       if (!m.isDefault()) {
         return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
       }
       try {
         if (privateLookupInMethod == null) {
           return new DefaultMethodInvoker(getMethodHandleJava8(method));
         }
         return new DefaultMethodInvoker(getMethodHandleJava9(method));
       } catch (IllegalAccessException | InstantiationException | InvocationTargetException
           | NoSuchMethodException e) {
         throw new RuntimeException(e);
       }
     });
   } catch (RuntimeException re) {
     Throwable cause = re.getCause();
     throw cause == null ? re : cause;
   }
 }

这里mybatis针对各种情况做了兼容,我们重点看下return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));这段代码,如下所示:

private static class PlainMethodInvoker implements MapperMethodInvoker {
  private final MapperMethod mapperMethod;

  public PlainMethodInvoker(MapperMethod mapperMethod) {
    this.mapperMethod = mapperMethod;
  }
	
 /**
  * 这段代码是关键,也是mybatis执行sql查询数据,并将数据封装为对象的关键实现
  */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
  }
}

接下来我们重点看下mapperMethod.execute(sqlSession, args);的逻辑,里面包含了mybatis执行sql查询数据,并将数据封装为对象的关键实现!

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = Optional.ofNullable(result);
        }
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

到这里,mybatis如何创建代理对象,以及Mapper代理对象中的方法如何执行sql的代码逻辑也讲完了。

最后的总结

今天这篇文章我们主要讲了mybatis如何往spring容器中注册Mapper的代理类实例,以及mybatis是如何创建代理对象的,以及mybatis如何通过反射的方式执行sql,并且查到的结果封装为对象。

觉得有收获的朋友可以点个赞,您的鼓励就是我最大的动力!

更多推荐