美团面试题:为什么能直接调用userMapper接口的方法?

关注“Java后端技术全栈”

回复“面试”获取全套面试资料
字数:2434,阅读耗时:3分40秒。

老规矩,先上案例代码,这样大家可以更加熟悉是如何使用的,看过Mybatis系列的小伙伴,对这段代码差不多都可以背下来了。

哈哈~,有点夸张吗?不夸张的,就这行代码。

public class MybatisApplication {
    public static final String URL = "jdbc:mysql://localhost:3306/mblog";
    public static final String USER = "root";
    public static final String PASSWORD = "123456";

    public static void main(String[] args) {
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        SqlSession sqlSession = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            sqlSession = sqlSessionFactory.openSession();
            //今天主要这行代码
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            System.out.println(userMapper.selectById(1));

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            sqlSession.close();
        }
    }

看源码有什么用?

图片

通过源码的学习,我们可以收获Mybatis的核心思想和框架设计,另外还可以收获设计模式的应用。
前两篇文章我们已经Mybatis配置文件解析到获取SqlSession,下面我们来分析从SqlSession到userMapper:

 UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

前面那篇文章已经知道了这里的sqlSession使用的是默认实现类DefaultSqlSession。所以我们直接进入DefaultSqlSession的getMapper方法。

//DefaultSqlSession中  
private final Configuration configuration;
//type=UserMapper.class
@Override
public <T> T getMapper(Class<T> type) {
  return configuration.getMapper(type, this);
}

这里有三个问题:

图片

问题1:getMapper返回的是个什么对象?
上面可以看出,getMapper方法调用的是Configuration中的getMapper方法。然后我们进入Configuration中

//Configuration中  
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
////type=UserMapper.class
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

这里也没做什么,继续调用MapperRegistry中的getMapper:

//MapperRegistry中
public class MapperRegistry {
  //主要是存放配置信息
  private final Configuration config;
  //MapperProxyFactory 的映射
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

  //获得 Mapper Proxy 对象
  //type=UserMapper.class,session为当前会话
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    //这里是get,那就有add或者put
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
   try {
      //创建实例
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  
  //解析配置文件的时候就会调用这个方法,
  //type=UserMapper.class
  public <T> void addMapper(Class<T> type) {
    // 判断 type 必须是接口,也就是说 Mapper 接口。
    if (type.isInterface()) {
        //已经添加过,则抛出 BindingException 异常
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            //添加到 knownMappers 中
            knownMappers.put(type, new MapperProxyFactory<>(type));
            //创建 MapperAnnotationBuilder 对象,解析 Mapper 的注解配置
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            //标记加载完成
            loadCompleted = true;
        } finally {
            //若加载未完成,从 knownMappers 中移除
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}
}

MapperProxyFactory对象里保存了mapper接口的class对象,就是一个普通的类,没有什么逻辑。

在MapperProxyFactory类中使用了两种设计模式:

单例模式methodCache(注册式单例模式)。

工厂模式getMapper()。

继续看MapperProxyFactory中的newInstance方法。

public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
 public T newInstance(SqlSession sqlSession) {
  //创建MapperProxy对象
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}
//最终以JDK动态代理创建对象并返回
 protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
}

从代码中可以看出,依然是稳稳的基于 JDK Proxy 实现的,而 InvocationHandler 参数是 MapperProxy 对象。

//UserMapper 的类加载器
//接口是UserMapper
//h是mapperProxy对象
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                       InvocationHandler h){
}

问题2:为什么就可以调用他的方法?
上面调用newInstance方法时候创建了MapperProxy对象,并且是当做newProxyInstance的第三个参数,所以MapperProxy类肯定实现了InvocationHandler。

进入MapperProxy类中:

//果然实现了InvocationHandler接口
public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }
  //调用userMapper.selectById()实质上是调用这个invoke方法
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //如果是Object的方法toString()、hashCode()等方法  
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        //JDK8以后的接口默认实现方法  
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //创建MapperMethod对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //下一篇再聊
    return mapperMethod.execute(sqlSession, args);
  }
}

也就是说,getMapper方法返回的是一个JDK动态代理对象(类型是$Proxy+数字)。这个代理对象会继承Proxy类,实现被代理的接口UserMpper,里面持有了一个MapperProxy类型的触发管理类。

当我们调用UserMpper的方法时候,实质上调用的是MapperProxy的invoke方法。

userMapper=$Proxy6@2355。
图片

为什么要在MapperRegistry中保存一个工厂类?

原来他是用来创建并返回代理类的。这里是代理模式的一个非常经典的应用。

MapperProxy如何实现对接口的代理?

JDK动态代理
我们知道,JDK动态代理有三个核心角色:

被代理类(即就是实现类)
接口
实现了InvocationHanndler的触发管理类,用来生成代理对象。
被代理类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。

而Mybatis中并没有Mapper接口的实现类,怎么被代理呢?它忽略了实现类,直接对Mapper接口进行代理。

MyBatis动态代理:
在Mybatis中,JDK动态代理为什么不需要实现类呢?

图片

这里我们的目的其实就是根据一个可以执行的方法,直接找到Mapper.xml中statement ID ,方便调用。

最后返回的userMapper就是MapperProxyFactory的创建的代理对象,然后这个对象中包含了MapperProxy对象,

问题3:到底是怎么根据Mapper.java找到Mapper.xml的?
最后我们调用userMapper.selectUserById(),本质上调用的是MapperProxy的invoke()方法。

请看下面这张图:

图片

如果根据(接口+方法名找到Statement ID ),这个逻辑在InvocationHandler子类(MapperProxy类)中就可以完成了,其实也就没有必要在用实现类了。

图片

总结
本文中主要是讲getMapper方法,该方法实质上是获取一个JDK动态代理对象(类型是Proxy+数字),这个代理类会继承MapperProxy类,实现被代理的接口UserMapper,并且里面持有一个MapperProxy类型的触发管理类。这里我们就拿到代理类了,后面我们就可以使用这个代理对象进行方法调用。

问题涉及到的设计模式:

代理模式。

工厂模式。

单例模式。

整个流程图:

图片

冰冻三尺,非一日之寒表面意义是冰冻了三尺,并不是一天的寒冷所能达到的效果。学习亦如此,你每一天的一点点努力,都是为你以后的成功做铺垫。

推荐阅读

面试官:Integer缓存最大范围只能是-128到127吗?
6000多字 | 秒杀系统设计注意点【理论】
面试官:说说你对Java异常的理解
《程序员面试宝典》.pdf下载

posted @ 2020-12-30 19:20  田维常TWC  阅读(296)  评论(0编辑  收藏  举报