乐趣区

Mybatis是如何跟Spring整合的

写在前面

Mybatis 官网中,有这么一节专门介绍如何注入一个 mapper


对于单个 mapper,有两种方式可以注入,分别是 xml 和注解

其中,xml 这种方式耐人寻味。

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

不妨我们今天就手写一个小框架,来实现 mapper 注入的功能。

动态代理

我们都知道,mybatis 通过动态代理来实现将 interface 接口转为具体的类,来执行相应的 mapper。具体是怎样做的呢?
SqlSession.java

  /**
   * Retrieves a mapper.
   * @param <T> the mapper type
   * @param type Mapper interface class
   * @return a mapper bound to this SqlSession
   */
  <T> T getMapper(Class<T> type);

一路追查下去
DefaultSqlSession.java

  @Override
  public <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);
  }

Configuration.java

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);
  }

MapperProxyFactory.java

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface}, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

这时我们可以看到真身了,mybatis 正是使用了 JDK 的动态代理实现了对 mapper 的代理。那 JDK 的动态代理是怎么回事呢?
MapperProxy 实现了 InvocationHandler 接口,需要实现 invoke 方法。这里我们来探究一下,为什么 JDK 的动态代理,实现 InvocationHandler 接口就可以了?
先来一个接口

public interface IHello {void sayHello();
}

被代理对象

public class RealSubject implements IHello {
    @Override
    public void sayHello() {System.out.println("我是被逼说 hello 的");
    }
}

InvocationHandler 增强日志

public class MyHandler implements InvocationHandler {
    private Object target;

    public MyHandler(Object subject) {this.target = subject;}

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("前置日志");
        Object res = method.invoke(target, args);
        System.out.println("后置日志");
        return res;
    }
}

在 client 中跑一下

    public static void main(String[] args) throws IOException {RealSubject realSubject = new RealSubject();
        MyHandler myHandler = new MyHandler(realSubject);

        IHello iHello = (IHello) Proxy.newProxyInstance(realSubject.getClass().getClassLoader(), new Class[]{IHello.class}, myHandler);
        iHello.sayHello();}

得到输出

前置日志
我是被逼说 hello 的
后置日志

Proxy.newProxyInstance 可以动态地获取一个代理对象,代理对象调用接口中的方法时,会进入 InvokationHandler 里的 invoke 方法,而我们的实现是,先打印前置日志,再调用被代理对象的方法,最后输出后置日志。这一切是怎么运转起来的呢?
这里我们需要借助 ProxyGenerator,来一窥代理的真容

    private static void createProxyClass() throws IOException {byte[] proxyBytes = ProxyGenerator.generateProxyClass("IHello$Proxy", new Class[]{IHello.class});
        Files.write(new File("YOUR_PATH/IHello$Proxy.class").toPath(), proxyBytes);
    }

这时,我们可以得到 Proxy.class,这个代理对象继承了 Proxy 并实现了 IHello 接口,由于 Java 是单继承且已经继承自 Proxy,所以 JDK 的动态代理是基于接口的

 public final class IHello$Proxy extends Proxy implements IHello {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public IHello$Proxy(InvocationHandler var1) throws  {super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);
        }
    }

    public final void sayHello() throws  {
        try {super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("your.package.IHello").getMethod("sayHello");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

我们看到 sayHell()方法,好眼熟啊!这个 h 是父类 Proxy 中的protected InvocationHandler h;。那这个 h 是怎么传进来的呢?前方高能!前方高能!前方高能!

    @CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {if (sm != null) {checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {Throwable t = e.getCause();
            if (t instanceof RuntimeException) {throw (RuntimeException) t;
            } else {throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {throw new InternalError(e.toString(), e);
        }
    }

这里会拿到代理类的构造方法,这个构造方法的参数是 constructorParams,这个 constructorParams 是什么呢?private static final Class<?>[] constructorParams = { InvocationHandler.class}; 那我们就可以从 IHello$Proxy 中找到这个构造函数:

    public IHello$Proxy(InvocationHandler var1) throws  {super(var1);
    }

这个 super(var1)就是

    protected Proxy(InvocationHandler h) {Objects.requireNonNull(h);
        this.h = h;
    }

对上了有木有!有木有!!激动不激动!!!
这样,当我们使用 Proxy.newInstance()获取到的代理对象调用相应方法时,就会跑到我们自己写的 InvocationHandler 实现类里 invoke 方法!我不管,我要为自己的牛逼点个赞????

与 Spring 整合

好了,不要脸半天了,收!
我们现在可以通过一个 UserMapper 的接口来创建一个代理对象了,但是我们怎么把这个对象交给 Spring 托管呢?
注意,我们这边是 已经 new 好了对象了,然后交给 Spring 去管理 ,而不是 提供给 Spring 一个类,让 Spring 帮我们 new 一个对象是管理 ,这还是有本质区别的!
Spring 会先使用 BeanDefinition 来保存 Bean 的信息,其中包含 bean 的 scope、class 相关信息、是否懒加载等等内容,当 BeanDefinition 对象创建好后,会先存入一个 map。
为什么 Spring 不直接 new 呢?
直接 new 的话可能不符合条件,比如我们提供的 interface;另外,直接 new 的话,提供给程序员可自由发挥的空间就小了。
这里,我们要导出 Spring 的一个后置处理器了

BeanFactoryPostProcessor

实现 BeanFactoryPostProcessor 接口,我们就可以对 BeanDefinition 进行处理了。
假设有 A、B 两个类,我们在 postProcessBeanFactory 获取到 A 这个 BeanDefinition,然后将其 BeanClass 设置为 B,然后从 ApplicationContext 中获取 A

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {GenericBeanDefinition a = (GenericBeanDefinition) beanFactory.getBeanDefinition("a");
        a.setBeanClass(B.class);
    }
}

Spring 会毫不留情地告诉我们

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'your.package.domain.A' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:350)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:341)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1123)
    at com.meituan.Starter.main(Starter.java:22)

我们可以通过这个后置处理器来处理我们的动态代理类,可以似乎还是不够

FactoryBean 与 ImportBeanDefinitionRegistrar

BeanFactoryPostProcessor 的局限是我们只能从 BeanFactory 中获取一个 BeanDefinition,然后修改它的属性,而不能直接往里面添加一个新的 BeanDefinition,这时我们要再挖掘些新玩意儿。

将 new 出来的对象交给 Spring 托管有三种常用的方法:

  1. BeanFactory.registerSingleton(String beanName, Object singletonObject);
  2. 基于 @Bean,自己在方法中 new 出一个对象来
  3. FactoryBean!

这里我们用第三种方法

public class MyFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {return Proxy.newProxyInstance(CityMapper.class.getClassLoader(), new Class[]{CityMapper.class}, new MyInvocationHandler());
    }

    @Override
    public Class<?> getObjectType() {return CityMapper.class;}
}

这样可以解决一个 mapper 的注入问题,可以如果 mybatis 要提供一个框架,这样写可扩展性不好,这时我们可以把 class 作为一个参数注入进来

public class MyFactoryBean implements FactoryBean {
    private Class mapperInterface;

    public void setMapperInterface(Class mapperInterface) {this.mapperInterface = mapperInterface;}

    @Override
    public Object getObject() throws Exception {return Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, new MyInvocationHandler());
    }

    @Override
    public Class<?> getObjectType() {return mapperInterface;}
}

这时我们回看 mybatis 官网的那个 xml 配置,不禁恍然大悟!把这里的 MyFactoryBean 替换成 MapperFactoryBean 不就是了嘛!

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

那我们如何将这个 MyFactoryBean 交给 Spring 呢?

这时我们还需要写一个类,实现 ImportBeanDefinitionRegistrar,这个接口可以帮我们把一个 BeanDefinition 放入 Spring 的 map

public class MyBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
        GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue("your.package.dao.CityMapper");
        registry.registerBeanDefinition("xxx", beanDefinition);
    }
}

这个类我们通过一个注解引入,这时会用到 @Import 注解

@Retention(RetentionPolicy.RUNTIME)
@Import(MyBeanDefinitionRegistrar.class)
public @interface MyScan {}

将这个注解放到

@ComponentScan("your.package")
@Configuration
@MyScan
public class AppConfig {}

大功告成,撒花!

参考文档

http://mybatis.org/spring/map…

退出移动版