这里写目录标题
- 前言
- 温馨提示
- 手把手带你解析 @MapperScan 源码
- 手把手带你解析 @MapperScan 源码细节剖析
- 工厂模式+Jdk 代理手撕脚手架,复刻 BeanDefinitionRegistryPostProcessor
- 手撕 FactoryBean
- 代理 Mapper 在 Spring 源码中的生成流程
- 手撕 MapperProxyFactory
- 手撕增强逻辑 InvocationHandler
- 源码级别解读 Mapper 要被设计成接口的原因
- 自定义 Executor 实现,框架 Dao 层
- 手写基础架构效果演示
- 总结
前言
最近在码云搜 Es 的开源项目学学技术,无意间搜到 Easy-Es 这么一个项目,里面的用法和 Mybatis-Plus 一模一样,当时心想我擦,这个人是直接悟透了 Mybatis-Plus 吗,虽然老早前看过源码。之前大概看了一下,就是对 Mapper 对象进行代理,植入了一些自定义逻辑而已,没仔细看过实现细节,现在网上居然有人直接又造了一个轮子,直呼 666,于是乎深入看了 Mybatis-Plus 是如何生成 Mapper 代理对象的全部源码,并且一比一复刻出来了。
温馨提示
阅读以下文章了解前置知识对理解本文更有帮助
- 深入jdk动态代理源码解析
- 模拟jdk动态代理(完整版)
- Factorybean与BeanFactory的区别
- 手把手debug自动装配源码、顺带弄懂了@Import等相关的源码(全文3w字、超详细)
- spring 源码解析(配图文讲解)顺带搞懂了循环依赖、aop底层实现
手把手带你解析 @MapperScan 源码
废话不多说直接步入正题,我们在使用 Mybatis 的时候要要设置 @MapperScan 扫描对应的 Mapper 接口,一步步点进去
发现其实就是注册了 MapperScannerConfigurer 这个 Bean ,都是些常用套路。然后发现 MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor、InitializingBean。
-
BeanDefinitionRegistryPostProcessor:Spring 为我们提供的扩展点,让程序员可以自己干预 Bean 的生成
-
InitializingBean:在 Bean 填充属性(populateBean)完成后会调用
直接看重写了 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法就行,看下图可以看到就是利用 ClassPathMapperScanner (路径扫描器)去扫描指定包下面的类,然后生成对应的 BeanDefinition 注册到 BeanDefinitionMap 中,然后 Spring 会将 BeanDefinitionMap 中的所有 BeanDefinition 生成 Bean 放到 Spring 单例池里面提供给程序员使用。
然后来到 scan 源码,cmd+art+b 查看 doScan 实现类,点进第二个,第一个是 Spring 实现的,而我们看的是 Mybatis 的源码这里大家要注意一下!
然后你会发现扫描完 basePackages 下的类生成对应的 BeanDefinition ,后还会去处理一下这些 BeanDefinition,click 进去。
发现得到的所有 Mapper 的 BeanDefinition 的 BeanClass 都被替换成了mapperFactoryBeanClass (工厂 bean)
到这里我大概就明白了,所有的 Mapper BeanDefinition 统一设置为 MapperFactoryBean 类型,最终生成的 Bean 本质 Class 是 MapperFactoryBean 但是名字依然是原来的名字,然后通过代理工厂统一生成代理对象(这也是很多开源框架的常用套路)。接下来验证一下我的猜想。看一下 MapperFactoryBean 构造实现了 FactoryBean 。
当我们的项目中使用了如下代码时,拿到的 Bean 其实是在紧挨上图一中的 getObject 方法中创建的。
@Autowired
UserMapper userMapper;
然后进入 getMapper 方法里面。看到确实是通过 MapperProxyFactory (代理工厂)生成的代理对象 Mapper。
看到这你是不是觉得源码也不过如此,对于整个简单的流程虽然走完了,但是作为一个要进行开发整个轮子的开发者来说,还远远不够。还需要了解更多细节
- 如何将指定包路径下的所有类生成 BeanDefinition ?。
- MapperProxyFactory 如何初始化,并且 MapperProxyFactory 如何根据感知生产什么类型的代理对象等
手把手带你解析 @MapperScan 源码细节剖析
这部分的文章读者可选择自行跳过,
knownMappers 中的数据什么时候初始化的?
回到 MapperFactoryBean 类中可以看到 checkDaoConfig 方法左侧有一个这个小图标,说明就是抽象接口的实现类,一般为了简化操作很多框架包括我也喜欢利用抽象接口封装逻辑
点击来到了上层的实现类,发现还被包裹了一层逻辑接着点向上的那个图标
来到最顶层的 checkDaoConfig 发现原来 MapperFactoryBean 居然实现了 InitializingBean 接口,当 MapperFactoryBean 属性填充完成以后,进行调用 afterPropertiesSet 方法,触发我们的 checkDaoConfig 方法调用。
最终会发现在进行 addMapper 的时候会以 key:mapperInterface ,value:MapperProxyFactory 的键值对放到 knownMappers 里面,而 mapperInterface 其实就UserMapper 的 Class。
Mapper 是使用什么代理创建的?
答:看一下 MapperProxyFactory 源码得知是用的 Jdk 代理,直接代理接口
如何动态的批量创建、修改 Bean ?
答:通过实现 Spring 提供的扩展接口 BeanDefinitionRegistryPostProcessor 动态注册、修改 BeanDefinition 即可。
如何实现动态的将一个普通 Bean 改成工厂 Bean ?
答:通过设置 BeanDefinition 的 BeanClass、ConstructorArgumentValues 替换成工厂 Bean 的 Class 即可。关键代码如下
genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(((GenericBeanDefinition) beanDefinition).getBeanClass());
genericBeanDefinition.setBeanClass(mapperFactoryBeanClass);
源码中的 mapperInterface 是什么东西?
答:Mapper 对象的 Class。举个例子当项目中用到了
@Autowired
UserMapper userMapper;
此时的 mapperInterface 就是 UserMapper.Class
为什么 Mapper 的代理对象能转换成目标对象?
了解 Jdk 动态代理的都知道,代理对象不能转换成目标对象,只能装换成目标对象的接口实现类或者 Proxy 对象,原因就是如下,可以看到代理对象和目标对象半毛钱关系都没有。
代理对象 extends proxy implments 目标对象实现接口
那为什么 UserMapper 的代理对象但是还能用 UserMapper 接收呢?项目中应该这样使用才对啊!!
@Autowired
Proxy userMapper;
工厂模式+Jdk 代理手撕脚手架,复刻 BeanDefinitionRegistryPostProcessor
里面的逻辑主要就是扫描指定包下面的类,生成对应的 BeanDefinition,然后自定义一个我们自己的后置处理器,将所有 BeanDefinition 替换成工厂 Bean。读者可自行封装对应的后置处理器,方便其他使用者进行扩展。整个流程对标 ClassPathMapperScanner 源码中的 doScan 逻辑。
/**
* 扫描哪些包是 mapper,并统一设置类型为 BaseFactoryBean
*/
@Slf4j
@Component
public class RegistryPostProcessorConfig implements BeanDefinitionRegistryPostProcessor {
private Class<? extends BaseFactoryBean> mapperFactoryBeanClass = BaseFactoryBean.class;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
//扫描指定路径下的 BeanDefinition
Set<BeanDefinitionHolder> beanDefinitions = scan();
if (beanDefinitions.size() >= 1) {
//后置处理器:全部替换成工厂 Bean
factoryBeanDefinitionPostProcess(beanDefinitions);
}
//注册 BeanDefinition
register(beanDefinitions,registry);
log.info("自定义 Mapper 扫描注册完成");
}
/**
* 扫描指定包下面的类,包装成一个个的 BeanDefinitionHolder,我这里就简单写写直接指定了
*/
public Set<BeanDefinitionHolder> scan() {
HashSet<BeanDefinitionHolder> beanDefinitions = new HashSet<>();
GenericBeanDefinition scanBeanDefinition = new GenericBeanDefinition();
scanBeanDefinition.setBeanClassName("userMapper");
scanBeanDefinition.setBeanClass(UserMapper.class);
GenericBeanDefinition scanBeanDefinition2 = new GenericBeanDefinition();
scanBeanDefinition2.setBeanClassName("studentMapper");
scanBeanDefinition2.setBeanClass(StudentMapper.class);
beanDefinitions.add(new BeanDefinitionHolder("userMapper",scanBeanDefinition));
beanDefinitions.add(new BeanDefinitionHolder("studentMapper",scanBeanDefinition2));
return beanDefinitions;
}
public void factoryBeanDefinitionPostProcess(Set<BeanDefinitionHolder> beanDefinitions) {
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitions) {
GenericBeanDefinition genericBeanDefinition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
genericBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
genericBeanDefinition.setLazyInit(false);
/**
* 设置 bean 创建的构造 class,必须设置不然 bean 无法被创建
*/
genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(((GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition()).getBeanClass());
genericBeanDefinition.setBeanClass(mapperFactoryBeanClass);
}
}
public void register(Set<BeanDefinitionHolder> beanDefinitions, BeanDefinitionRegistry registry) {
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitions) {
/**
* BeanDefinition 重置了 BeanClass 为 BaseFactoryBean 后,对应的 BeanClassName 会自动变成 com.zzh.service2.structure.factory.bean.BaseFactoryBean
* 造成所有的 Mapper 接口的 BeanDefinition 的 BeanClassName 都是 com.zzh.service2.structure.factory.bean.BaseFactoryBean 导致注册报错!!!
* 因此自定义包装 BeanDefinitionHolder 对象,设置原始 BeanName
* 例如:BeanDefinitionHolder(key->userMapper,value->BeanDefinition)
* BeanDefinitionHolder(key->studentMapper,value->BeanDefinition)
*/
registry.registerBeanDefinition(beanDefinitionHolder.getBeanName(), beanDefinitionHolder.getBeanDefinition());
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}
手撕 FactoryBean
实现 FactoryBean 接口,同时设置成泛型,让任何类型的 Mapper 接口都是转换成此 FactoryBean,当 Spring 进行属性填充完成之后,进行初始化 Bean 的时候会调用 InitializingBean 接口里面的方法,此时我们将 UserMapper.Class 放到一个临时容器中,等 BaseFactoryBean.getObject 方法被调用的时候,再去容器里面拿到 UserMapper.Class 进行 Jdk 代理创建代理对象。
@Data
public class BaseFactoryBean<T> implements FactoryBean<T>, InitializingBean {
/**
* > 如何实现动态的将一个普通 Bean 改成工厂 Bean ?
* 通过设置 BeanDefinition 的 BeanClass、ConstructorArgumentValues 替换成工厂 Bean 的 Class 即可。关键代码如下
* ```javascript
* genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(((GenericBeanDefinition) beanDefinition).getBeanClass());
* genericBeanDefinition.setBeanClass(mapperFactoryBeanClass);
* ```
*/
public BaseFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
private Class<T> mapperInterface;
/**
* 通过 MapperProxyFactory 工厂统一生产代理对象
*/
@Override
public T getObject() throws Exception {
return (T) BaseMapperRegistry
.getMapper(mapperInterface)
.newInstance(DaoTemplateFactory.getInstance().getDaoTemplate());
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
@Override
public void afterPropertiesSet() {
BaseMapperRegistry.addMapper(this.mapperInterface);
}
@Override
public boolean isSingleton() {
return true;
}
}
补充一嘴 Spring 中的源码逻辑,BeanDefinitionMap 中所有的 BeanDefinition 都会走
CreateBean 的流程,先是调用 createBeanInstance 方法创建一个实例对象,然后调用 populateBean 方法为实例对象填充属性,接着才是调用 InitializingBean 里面的方法。可以看到此时的 mapperInterface 是 UserMapper.Class
代理 Mapper 在 Spring 源码中的生成流程
当创建好 UserMapper 这个 Bean 的时候,会调用 getObjectForBeanInstance 方法获取其实例,发现 UserMapper 是个工厂 Bean,于是乎调用 getObject 方法,走我们的 Jdk 创建代理对象的逻辑,最终放到 Ioc 容器里面的是我们自己创建的代理对象!
然后顺着栈帧来到 getObject,到此整个流程结束!
手撕 MapperProxyFactory
根据目标对象的 Class 生成代理对象,同时 InvocationHandler 里面织入我们手写的 DaoTemplate,用来与数据库进行交互。
亮点代码只有一行:由于目标对象自身是一个 Mapper 接口,参数二实现类的接口用的是自己本身 new Class[]{mapperInterface} 这样生成的代理对象就可以转换成目标对象了。
T proxyInstance = (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, baseMapperProxy);
@Slf4j
public class MapperProxyFactory<T> {
/**
* 被代理对象 Class
*/
@Getter
private final Class<T> mapperInterface;
private ConcurrentHashMap methodCaches = new ConcurrentHashMap<Object, Object>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public T newInstance(MapperProxyInvoke<T> baseMapperProxy) {
T proxyInstance = (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, baseMapperProxy);
log.info("proxyInstance instanceof BaseMapper:{}", proxyInstance instanceof BaseMapper);
log.info("proxyInstance instanceof BaseMapper:{}", proxyInstance instanceof UserMapper);
return proxyInstance;
}
/**
* Mybatis-Plus 封装了 SqlSession 对象操作 db,我这里也简单封装一个 DaoTemplate 做做样子
* @param daoTemplate
* @return
*/
public T newInstance(DaoTemplate daoTemplate) {
MapperProxyInvoke<T> baseMapperProxy = new MapperProxyInvoke<T>(daoTemplate, mapperInterface, methodCaches);
return newInstance(baseMapperProxy);
}
/**
* 为啥jdk生成的代理对象居然不支持类型转换为目标对象?
* https://blog.csdn.net/qq_42875345/article/details/115413716
*/
public static void main(String[] args) {
test1();
test2();
}
/**
* 强制代理对象实现 UserMapper 接口,从而实现 jdk生成的代理对象支持转换为目标对象!!!!!!!
* 关键代码:new Class[]{UserMapper.class}
* 这也是为什么 Mapper 要设计成接口的原因!!!!!!!
* 代理对象结构:代理对象 extends proxy implments UserMapper
*/
static void test1() {
Mapper proxyInstance = (Mapper) Proxy.newProxyInstance(UserMapper.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.err.println("代理前置输出");
return null;
}
});
System.err.println(proxyInstance instanceof UserMapper); //true
System.err.println(proxyInstance instanceof BaseMapper);
System.err.println(proxyInstance instanceof Mapper);
}
/**
* 普通 Jdk 代理对象只实现目标对象的实现接口
* 代理对象结构:代理对象 extends proxy implments 目标对象实现接口
*/
static void test2() {
Mapper proxyInstance = (Mapper) Proxy.newProxyInstance(UserMapper.class.getClassLoader(), UserMapper.class.getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.err.println("代理前置输出");
return null;
}
});
System.err.println(proxyInstance instanceof UserMapper); //false
System.err.println(proxyInstance instanceof BaseMapper);
System.err.println(proxyInstance instanceof Mapper);
}
}
手撕增强逻辑 InvocationHandler
实现了 InvocationHandler 接口,每当代理 Mapper 中的方法被调用的时候,都会执行 invoke 中的逻辑。里面分默认方法(被 default 修饰的方法)与 db 查询的方法
/**
* 代理 Mapper 增强逻辑
*/
@Slf4j
public class MapperProxyInvoke<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final DaoTemplate daoTemplate;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxyInvoke(DaoTemplate daoTemplate, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.daoTemplate = daoTemplate;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
/**
*
* @param proxy 生成的代理对象
* @param method 被调用的目标对象方法
* @param args 被调用的目标对象方法中的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("代理对象前置输出!!!!");
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) {
/**
* Mapper 自带的默认方法走这调用(userMapper.say())
*/
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
/**
* (userMapper.seleteById(1))走这调用
* 此处需要 Method 与 daoTemplate 中的方法名称、参数做匹配然后调用 daoTemplate 中的方法
* 源码中也是这么干的,我懒这里直接硬编码匹配 seleteById 了
*/
return daoTemplate.seleteById(1);
// mybatis 源码中还做了方法缓存加快处理速度
// final MapperMethod mapperMethod = cachedMapperMethod(method);
// return mapperMethod.execute(sqlSession, args);
}
private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
throws Throwable {
final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Class<?> declaringClass = method.getDeclaringClass();
return constructor
.newInstance(declaringClass,
MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
.unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
}
}
源码级别解读 Mapper 要被设计成接口的原因
这里也贴一下 UserMapper 的代码吧,也解释一下 Mapper 为什么要采用接口的形式
/**
* UserMapper 只能是接口,如果 UserMapper 为类,生成的代理对象不能转换为 UserMapper
* 只能转换为 proxy、或者 BaseMapper ,原因:代理对象 extends proxy implments BaseMapper
* 但是我们需要 @Autowire UserMapper 这样使用。需要代理对象为 UserMapper 类型,因此 UserMapper 只能是接口
* 让生成的代理对象 extends proxy implments UserMapper
*/
public interface UserMapper extends BaseMapper<User> {
default String say() {
return "UserMapper say";
}
}
自定义 Executor 实现,框架 Dao 层
代理对象会根据被调用的方法匹配 DaoTemplate 中的方法进行执行,在这里面可以自行封装类似于 Mybatis 二级缓存,多级 Executor ,动态数据源切换的逻辑,工程量巨大,我这里只提供思路,简单查个库给大家演示一下设计原理。到此所有组件全部开发完成。
/**
* 封装原始的 jdbc 逻辑,可扩展组件:多级缓存查询、多级 Executor 查询、数据库连接池切换等等
*/
public class DaoTemplate {
String driver = "com.mysql.cj.jdbc.Driver";
String url = "url";
String username = "root";
String password = "pwd";
public Connection getConnection() throws SQLException, ClassNotFoundException {
Class.forName(driver);
return DriverManager.getConnection(url, username, password);
}
//随便写写了,直接拼接
public User seleteById(Integer id) throws SQLException, ClassNotFoundException {
String sql = "select * from user where id = " + id;
ResultSet resultSet = getConnection().createStatement().executeQuery(sql);
resultSet.next();
return new User()
.setId(resultSet.getInt("id"))
.setName(resultSet.getString("name"));
}
}
手写基础架构效果演示
基础依赖包如下
可以看到使用我们手撕的 UserMapper 可以成功的查到 db 中的数据
总结
本文基于源码分析了 Mybatis 中代理 Mapper 创建的详细流程,基于理解一比一手撕复刻了出来,期间遇到的问题都总结在注释里面了。有人说会这个有啥用,会这个你可以将所有和数据库打交道的技术,都封装成类似于 Mybatis-Plus 的框架造福全宇宙!!!让技术不在复杂让小学生都会写代码,你就是明日之星!!!!