我们使用Mybatis框架的时候,只需要定义一个mapper接口,然后在类上面加上@Mapper或者在启动类加上@MappScan,配置需要扫描的路径,就能得到一个对数据库表进行CRUD的Bean。
众所周知Java里的接口并不能实例化,那Mybatis是怎么实例化接口的?以及实例化完成之后的对象怎么放入IOC容器?
如何实例化接口?
其实很简单,就是生成一个类,然后去实现接口。因为我们的mapper没有业务逻辑,只是对数据库操作的封装(连接数据库、执行sql、返回值),所以Mybatis可以为我们的mapper生成实现类。
生成类的方式也有很多(反射、字节码、代理),我们的mapper是接口,用JDK的动态代理最适合不过了,Mybatis也是这么做的。
原生Mybatis使用文档:入门_MyBatis中文网
这里官方给的使用文档里一小段代码就是我们入手看Mybatis怎么实现的地方。
// SqlSession就是数据库连接
try (SqlSession session = sqlSessionFactory.openSession()) {
// 把我们的Mapper接口实例化了,在这个getMapper()里使用JDK的动态代理实例化一个代理对象
BlogMapper mapper = session.getMapper(BlogMapper.class);
// 使用代理对象进行数据库操作
Blog blog = mapper.selectBlog(101);
}
getMapper就包含了创建代理对象的逻辑
SqlSession的getMapper有两个实现,但是实现都是一样的,随便点一个进去就可以了。进去两层之后来到这个方法。
位置:org.apache.ibatis.binding.MapperRegistry#getMapper
进入newInstance()之后答案就看眼前了
Proxy.newProxyInstance的三个入参,第一个是类加载器,第二个是需要代理的接口,也就是我们的mapper,第三个是代理方法的逻辑,就是我们mapper.select的时候,调的是mapperProxy里的invoke方法,Mybatis就是在这里为我们做数据库操作的。
Mybatis作为一个框架,需要考虑到拓展性、性能之类的问题,代码封装了很多,其实原理就是下面几行
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
interface Test {
String value();
}
public class JdkProxyDemo {
public static final void main(String[] args) {
Test test = (Test) Proxy.newProxyInstance(JdkProxyDemo.class.getClassLoader(),
new Class[] {Test.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
return "我代理返回的字符串,跟接口没有关系";
}
});
System.out.println(test.value());
}
}
Mapper接口实例化的代理对象怎么放入IOC容器?
现在我们知道了mapper接口是用jdk动态代理去实例化的,但是要怎么放入IOC容器呢?Spring可没有提供直接放入实例化好的bean到IOC容器中,在mapper接口上面加入@Component也不行,因为Spring不知道怎么去实例化它。所以,Mybatis要告诉Spring怎么去实例化mapper,这就需要用到Spring提供的FactoryBean的接口。
这个接口的作用跟字面意思一样,就是制造bean的工厂,也就是说把mapper接口用JDK动态代理生成对象的逻辑写在这里,Spring就能知道mapper是怎么实例化的。然后我们注册的时候也不是注册我们的mapper接口了,而是注册实现了FactoryBean的类。
TestMapper接口
public interface TestMapper {
String select();
}
TestFactoryBean 类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.stereotype.Component;
// 向Spring容器注册TestFactoryBean,beanName为testMapper
// 这里不改的话,beanName就是testFactoryBean了
// 通过byName注入TestMapper的时候就会有问题
@Component("testMapper")
public class TestFactoryBean implements FactoryBean<TestMapper> {
// 这里就是告诉Spring,对象是怎么实例化的
@Override
public TestMapper getObject() throws Exception {
return (TestMapper) Proxy.newProxyInstance(TestFactoryBean.class.getClassLoader(), new Class[]{TestMapper.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理" + method.getName());
return "TestFactoryBean的返回";
}
});
}
@Override
public Class<?> getObjectType() {
return TestMapper.class;
}
}
测试类
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
public class MybatisDemoApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MybatisDemoApplication.class);
TestMapper testMapper = (TestMapper) applicationContext.getBean("testMapper");
System.out.println(testMapper.select());
}
}
效果
成功的把我们的接口注册到了Spring容器中, 其实这里往Spring注册了两个类,一个是我们的TestFactoryBean,一个是getObject()返回的我们的代理类。
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
public class MybatisDemoApplication {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MybatisDemoApplication.class);
TestFactoryBean testFactoryBean = (TestFactoryBean) applicationContext.getBean("&testMapper");
System.out.println(testFactoryBean);
TestMapper testMapper = (TestMapper) applicationContext.getBean("testMapper");
System.out.println(testMapper.select());
}
}
两个类都在容器中找到了, 对于工厂类,需要在beanName前面加一个'&'去获取这个bean。
applicationContext.getBean("&testMapper");
Mybatis把代理对象放入Spring容器的原理就是这样,但是真正到Mybatis的实现代码可不是这样写的,现在的代码还无法做到可拓展,因为现在每加一个mapper就需要加一个FactoryBean,完全无法拓展,然后还需要修改一下beanName。
所以我们可以在TestFactoryBean加一个Class类型的字段,记录一下要代理的类型,这样我们只需定义一个FactoryBean就可以为所有mapper生成代理了。然后定义一个@Mapper或者@MapperScan注解去标识哪些接口是mapper接口,动态的为这些mapper生成FactoryBean对象注册到Spring容器中。然后第二步的逻辑借助ClassPathBeanDefinitionScanner扫描的时候把TestFactoryBean注入进去。
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import java.util.Set;
public class MyScanner extends ClassPathBeanDefinitionScanner {
public MyScanner(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface();
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
beanDefinition.setBeanClassName(ZhouyuFactoryBean.class.getName());
}
return beanDefinitionHolders;
}
}
具体使用可以参考这个demo:
从图灵学院白嫖的资源,不是我自己写的。
示例demo代码:
最后
在学习FactoryBean的时候,想起了那时为了面试背八股文遇到了一个问题。
FactoryBean和BeanFactory有什么区别?
直接上一波ChatGPT的回答,因为我不会回答这个问题,但曾经会过。八股文就是这样,背了就会,过不了多久又忘了,等到需要跳槽面试的时候,又重新捡起来。没办法,Java就是这么卷,为了能面试通过、涨工资就必须得这么做。为了面试而背八股文,就是我们经常所说的为了做而做,为了写代码而写代码。只为了达到某种目的而一股脑的去做,中间没有任何思考的过程,最后目的达到了,可是也没有任何的收获和成长。背八股文就是这样,目的是就是面试,记住这个答案,哪有去思考为什么这样回答,答案为什么是这样来的,过程是什么样的,如何写代码复现,怎么找到源码去一步一步debug证实。这也是八股文容易忘记的原因,根本没有去理解它,只是靠单纯的记住。
所以平时还是得多学习、积累,钻研一些技术的原理,这样面试跳槽的时候才不会临时抱佛脚,不会的太多,背起来也很痛苦。程序员这行就是这样学无止境,既然干这行就要爱这行,得保持学习才不至于太早被淘汰。学习过程中对知识有自己的思考、见解、总结,在面试回答的时候才不至于像八股文一样呆板,面试官听着也不会厌烦,对于一些自己不懂或者没有接触过的东西,也能根据自己已有的知识和思考给出一个令面试官满意的回答。
之前看到这个问题的时候,会想着FactoryBean是什么东西,BeanFactory是什么东西,然后就百度搜,搜完知道概念之后就在那想有什么区别,想破脑袋也没想出来, 然后就开始吭哧吭哧的背八股文。现在我已经知道了这两个是什么东西了,也知道它们是怎么用的,它们被创造出来是为了解决什么问题、什么场景,回头在看这个问题,就发现这个问题非常的扯淡,这两个东西毫无关联,既然毫无关联,那何来比较,没有比较又哪来的区别,就因为名字的顺序相反被强制关联起来,只能说这八股文真害人。