是不是还在疑惑为什么我们在工程中定义了接口mybatis就可以直接操作我们的数据库?
是不是想了解spring和mybaits整合的原理?
了解原理后我们能复用在工程上的东西是什么?换句话说怎么提高代码的逼格?
基于上述背景,笔者准备深入源码带大家一探究竟,读完这篇文章大家可以的到的收获
代码环境:
//对象
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);
}
}
Mapper
对象的BeanDefinition
是怎么加入到工厂中的问题由来:一个对象只有被Spring
创建并且放入到工厂中才能被其他对象注入,比如AdServiceImpl
就是加了@Service
注解并且结合包扫描。这样环境中就会有这个对象,但是AdMapper
没有加任何的注解,而我们的 AdServiceImpl
却可以直接通过 @Resource
注入进来。说明这个对象是被Spring创建的。
回想Spring创建Bean的过程,几乎所有的对象都是先变成BeanDefinition
然后再通过工厂创建,所以我们只要找到这个Mapper类是什么时候变成BeanDefinition
的。
我们通过看代码发现和Mybatis
相关的只有开始在配置类中加入的注解@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本身扫描的类重复
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
的部分
在别的版本中是通过配置构造函数来做的如下,但是原理是一样的。
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
需要注意的是通过这种形式加进去的是字符串类型,但是内部会帮我们转成class类型。所以我们去看这个类的的构造方法的时候会发现他是有一个class的构造方法而没有字符串的。
这部分的主要逻辑是 向 mapperFactoryBean 中传入我们要进行动态代理的接口,至于为什么稍后我会把 mapperFactoryBean 的源码贴出来 大家就明白了。
开始我们再说的问题就是怎么创建对象的问题,因为BeanClass为接口类型,读过spring创建bean的源码的同学都知道,在getBean时就是创建的BeanClass类型的对象,所以我们需要修改这个类型 是个具体的类 。这个具体的类就是 MapperFactoryBean ,有些人可能会问,那创建的这个类型不是我们需要的类型啊?我们怎么使用?别急这些在我们说到 MapperFactoryBean 这个类的时候会做解释
如果我们大家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方法注入到代理类中然后我们才可以进行操作。
所以说这个地方也是一个重点,但是别的博文中很少有人提到,看到这是不是该给作者一个赞呢?
上边说到,其实我们向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对象 然后操作数据库。
总结一下:
我这篇文章 spring常用扩展点术语介绍大致说了一些拓展点的作用,以后会详细具体怎么使用,和Spring中的应用。大家可以关注一下