我们使用Mybatis框架的时候,只需要定义一个mapper接口,然后在类上面加上@Mapper或者在启动类加上@MappScan,配置需要扫描的路径,就能得到一个对数据库表进行CRUD的Bean。

众所周知Java里的接口并不能实例化,那Mybatis是怎么实例化接口的?以及实例化完成之后的对象怎么放入IOC容器?

如何实例化接口?

其实很简单,就是生成一个类,然后去实现接口。因为我们的mapper没有业务逻辑,只是对数据库操作的封装(连接数据库、执行sql、返回值),所以Mybatis可以为我们的mapper生成实现类。

生成类的方式也有很多(反射、字节码、代理),我们的mapper是接口,用JDK的动态代理最适合不过了,Mybatis也是这么做的。

原生Mybatis使用文档:入门_MyBatis中文网

mybatisplus注入basemapper失败_mybatis

这里官方给的使用文档里一小段代码就是我们入手看Mybatis怎么实现的地方。

// SqlSession就是数据库连接
try (SqlSession session = sqlSessionFactory.openSession()) {
  // 把我们的Mapper接口实例化了,在这个getMapper()里使用JDK的动态代理实例化一个代理对象
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 使用代理对象进行数据库操作
  Blog blog = mapper.selectBlog(101);
}

 getMapper就包含了创建代理对象的逻辑

mybatisplus注入basemapper失败_mybatis_02

 SqlSession的getMapper有两个实现,但是实现都是一样的,随便点一个进去就可以了。进去两层之后来到这个方法。

位置:org.apache.ibatis.binding.MapperRegistry#getMapper

mybatisplus注入basemapper失败_实例化_03

进入newInstance()之后答案就看眼前了

mybatisplus注入basemapper失败_spring_04

 Proxy.newProxyInstance的三个入参,第一个是类加载器,第二个是需要代理的接口,也就是我们的mapper,第三个是代理方法的逻辑,就是我们mapper.select的时候,调的是mapperProxy里的invoke方法,Mybatis就是在这里为我们做数据库操作的。

mybatisplus注入basemapper失败_mybatis_05

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());
    }

}

mybatisplus注入basemapper失败_mybatis_06

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());
    }

}

效果

mybatisplus注入basemapper失败_实例化_07

成功的把我们的接口注册到了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());
    }

}

mybatisplus注入basemapper失败_spring_08

两个类都在容器中找到了, 对于工厂类,需要在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;
    }
}

mybatisplus注入basemapper失败_java_09

 具体使用可以参考这个demo:

 从图灵学院白嫖的资源,不是我自己写的。

示例demo代码:

最后

 在学习FactoryBean的时候,想起了那时为了面试背八股文遇到了一个问题。

FactoryBean和BeanFactory有什么区别?

 

mybatisplus注入basemapper失败_实例化_10

 直接上一波ChatGPT的回答,因为我不会回答这个问题,但曾经会过。八股文就是这样,背了就会,过不了多久又忘了,等到需要跳槽面试的时候,又重新捡起来。没办法,Java就是这么卷,为了能面试通过、涨工资就必须得这么做。为了面试而背八股文,就是我们经常所说的为了做而做,为了写代码而写代码。只为了达到某种目的而一股脑的去做,中间没有任何思考的过程,最后目的达到了,可是也没有任何的收获和成长。背八股文就是这样,目的是就是面试,记住这个答案,哪有去思考为什么这样回答,答案为什么是这样来的,过程是什么样的,如何写代码复现,怎么找到源码去一步一步debug证实。这也是八股文容易忘记的原因,根本没有去理解它,只是靠单纯的记住。

所以平时还是得多学习、积累,钻研一些技术的原理,这样面试跳槽的时候才不会临时抱佛脚,不会的太多,背起来也很痛苦。程序员这行就是这样学无止境,既然干这行就要爱这行,得保持学习才不至于太早被淘汰。学习过程中对知识有自己的思考、见解、总结,在面试回答的时候才不至于像八股文一样呆板,面试官听着也不会厌烦,对于一些自己不懂或者没有接触过的东西,也能根据自己已有的知识和思考给出一个令面试官满意的回答。

之前看到这个问题的时候,会想着FactoryBean是什么东西,BeanFactory是什么东西,然后就百度搜,搜完知道概念之后就在那想有什么区别,想破脑袋也没想出来, 然后就开始吭哧吭哧的背八股文。现在我已经知道了这两个是什么东西了,也知道它们是怎么用的,它们被创造出来是为了解决什么问题、什么场景,回头在看这个问题,就发现这个问题非常的扯淡,这两个东西毫无关联,既然毫无关联,那何来比较,没有比较又哪来的区别,就因为名字的顺序相反被强制关联起来,只能说这八股文真害人。