从 Spring 集成 MyBatis 到浅析 Java 动态代理

前言
因为 MyBatis 的易上手性和可控性,使得它成为了 ORM框架中的首选。近日新起了一个项目,所以重新搭建了一下 Spring-mybatis, 下面是搭建笔记和从 Spring-mybatis源码分析其如何使用 Java动态代理,希望对大家有帮助。

Spring 集成 Mybatis

Spring 集成 Mybatis的方式有很多种,大家耳熟能详的 xml配置方式或者本文的采用的方式:
首先需要添加 MyBatis的和 MyBatis-Spring的依赖,本文使用的 Spring-mybatis版本是1.3.1。在 mvnrepository里面我们可以找到当前 Spring-mybatis依赖的 springmybatis版本,最好是选择匹配的版本以避免处理不必要的兼容性问题。因为 MyBatis-Spring中对 mybatis的依赖选择了 provided模式,所以我们不得不额外添加 mybatis依赖,依赖配置如下。
  1. <dependency>
  2.    <groupId>org.mybatis</groupId>
  3.    <artifactId>mybatis-spring</artifactId>
  4.    <version>1.3.1</version>
  5. </dependency>
  6. <dependency>
  7.      <groupId>org.mybatis</groupId>
  8.      <artifactId>mybatis</artifactId>
  9.      <version>3.4.1</version>
  10. </dependency>
接下来会我们要创建工厂bean,放置下面的代码在 Spring 的 XML 配置文件中:
  1. <beanid="sqlSessionFactory"class="org.mybatis.spring.SqlSessionFactoryBean">
  2.  <propertyname="dataSource"ref="dataSource"/>
  3. </bean>
这个工厂需要一个 DataSource,就是我们熟知的数据源了。这里我们选择了阿里的 Druid,同样我们需要引入两个配置
  1. <dependency>
  2. <groupId>mysql</groupId>
  3. <artifactId>mysql-connector-java</artifactId>
  4. <version>5.1.41</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.alibaba</groupId>
  8. <artifactId>druid</artifactId>
  9. <version>1.1.2</version>
  10. </dependency>
添加 Spring配置如下
  1. <beanid="dataSource"class="com.alibaba.druid.pool.DruidDataSource"init-method="init"destroy-method="close">
  2.        <!-- 基本属性 url、user、password -->
  3.        <propertyname="url">
  4.            <value><![CDATA[${db.url}]]></value>
  5.        </property>
  6.        <propertyname="username"value="${db.username}"/>
  7.        <propertyname="password"value="${db.password}"/>      
  8.        <!-- 省略其他配置 -->  
  9. </bean>
接下来我们要编写数据库访问对象,大多数人会把它叫做 DAO或者 Repository,在这里其被称为
Mapper,也是因为它的实现方式所决定。要注意的是所指定的映射器类必须是一个接口,而不是具体的实现类。这便因为 Mybatis的内部实现使用的是 Java动态代理,而 Java动态代理只支持接口,关于 动态代理我们下文有更详细的描述。
  1. publicinterfaceUserMapper{
  2.  @Select("SELECT * FROM users WHERE id = #{userId}")
  3.  User getUser(@Param("userId")String userId);
  4. }
接下来可以使用 MapperFactoryBean,像下面这样来把接口加入到 Spring 中,这样就把 UserMapperSessionFactory关联到一起了,原来使用 xml配置的时候还需要Dao继承 SqlSessionDaoSupport才能注入 SessionFactory,这种方式直接通过 Java动态代理SqlSessionFactory代理给了 UserMapper,使得我们直接使用 UserMapper即可。配置如下。
  1. <beanid="userMapper"class="org.mybatis.spring.mapper.MapperFactoryBean">
  2.  <propertyname="mapperInterface"value="org.mybatis.spring.sample.mapper.UserMapper"/>
  3.  <propertyname="sqlSessionFactory"ref="sqlSessionFactory"/>
  4. </bean>
这样我们已经完成了90%,就差调用了,前提是你 Spring环境是OK的。调用 MyBatis 数据方法现在只需一行代码:
  1. publicclassFooServiceImplimplementsFooService{
  2. privateUserMapper userMapper;
  3. publicvoid setUserMapper(UserMapper userMapper){
  4.  this.userMapper = userMapper;
  5. }
  6. publicUser doSomeBusinessStuff(String userId){
  7.  returnthis.userMapper.getUser(userId);
  8. }
那么问题又来了,每次写一个DAO都需要为其写一个 Bean配置,那不是累死?于是我们又寻找另一种方案,代替手动声明 *MapperMapperScannerConfigurer的出现解决了这个问题, 它会根据你配置的包路径自动的扫描类文件并自动将它们创建成 MapperFactoryBean,可以在 Spring 的配置中添加如下代码:
  1. <beanclass="org.mybatis.spring.mapper.MapperScannerConfigurer">
  2.  <propertyname="basePackage"value="com.github.codedrinker.mapper"/>
  3. </bean>
basePackage属性是让你为映射器接口文件设置基本的包路径。你可以使用分号或逗号作为分隔符设置多于一个的包路径。这个时候如果想自定义 sqlSessionFactory可以添加如下配置:
  1. <propertyname="sqlSessionFactoryBeanName"value="sqlSessionFactory"/>
这样以后还有一点点小瑕疵,如果我们数据的 column名字是 _连接的,那么它不会那么聪明自动转换为驼峰的变量,所以我们需要对 SqlSessionFactoryBean做如下配置,但是在1.3.0以后才可以通过xml配置,如果用早起版本的需要注意了。
  1. <beanid="sqlSessionFactory"class="org.mybatis.spring.SqlSessionFactoryBean">
  2.  <propertyname="dataSource"ref="dataSource"/>
  3.  <propertyname="configuration">
  4.    <beanclass="org.apache.ibatis.session.Configuration">
  5.      <propertyname="mapUnderscoreToCamelCase"value="true"/>
  6.    </bean>
  7.  </property>
  8. </bean>
至此关于 SpringMyBatis的配置已经全部结束,后面我们会简单说下 SpringMyBatis中的动态代理。

浅析 Java 动态代理

JDK自带的动态代理需要了解InvocationHandler接口和Proxy类,他们都是在java.lang.reflect包下。
InvocationHandler是代理实例的调用处理程序实现的接口。每个代理实例都具有一个关联的 InvocationHandler。对代理实例调用方法时,这个方法会调用 InvocationHandlerinvoke方法。 Proxy提供静态方法用于创建动态代理类和实例,同时后面自动生成的代理类都是 Proxy对象。下面我们直接通过代码来分析 Java动态代理InvocationInterceptor实现 InvocationHandler接口,用于处理具体的代理逻辑。
  1. /**
  2. * Created by codedrinker on 12/10/2017.
  3. */
  4. publicclassInvocationInterceptorimplementsInvocationHandler{
  5.    privateObject target;
  6.    publicInvocationInterceptor(Object target){
  7.        this.target = target;
  8.    }
  9.    @Override
  10.    publicObject invoke(Object proxy,Method method,Object[] args)throwsThrowable{
  11.        System.out.println("before user create");
  12.        method.invoke(target, args);
  13.        System.out.println("end user create");
  14.        returnnull;
  15.    }
  16. }
UserUserImpl是被代理对象的接口和类
  1. /**
  2. * Created by codedrinker on 12/10/2017.
  3. */
  4. publicinterfaceUser{
  5.    void create();
  6. }
  1. /**
  2. * Created by codedrinker on 12/10/2017.
  3. */
  4. publicclassUserImplimplementsUser{
  5.    @Override
  6.    publicvoid create(){
  7.        System.out.println("create user");
  8.    }
  9. }
DynamicProxyTest是测试类,用于创建 InvocationInterceptorProxy类以便测试。
  1. /**
  2. * Created by codedrinker on 12/10/2017.
  3. */
  4. publicclassDynamicProxyTest{
  5.    publicstaticvoid main(String[] args){
  6.        User target =newUserImpl();
  7.        InvocationInterceptor invocationInterceptor =newInvocationInterceptor(target);
  8.        User proxyInstance =(User)Proxy.newProxyInstance(UserImpl.class.getClassLoader(),
  9.                UserImpl.class.getInterfaces(),
  10.                invocationInterceptor);
  11.        proxyInstance.create();
  12.    }
  13. }
输入结果如下:
  1. before user create
  2. create user
  3. end user create
很明显,我们通过proxyInstance这个代理类进行方法调用的时候,会在方法调用前后进行输出打印,这样就简单的实现了一个 Java动态代理例子。动态代理不仅仅是打印输出这么简单,我们可以通过它打印日志,打开关闭事务, 权限检查了等等。当然它更是许多框架的钟爱,就如下文我们要说的 MyBatisJava动态代理的实现。再多说一句 SpringAOP也是使用动态代理实现的,当然它同时使用了 Java动态代理CGLib两种方式。不过 CGLIB不是本文要讨论的范围。
注意观察的同学看到上面代码的时候可能发现 invoke方法的 proxy参数并没有被使用,笔者查阅了一些相关文档也没有找到合理的说法,只能在源码中看看究竟喽,笔者当前的JDK版本是1.8。我们从入口开始, Proxy.newProxyInstance:
  1. /*
  2. * Look up or generate the designated proxy class.
  3. */
  4. @CallerSensitive
  5. publicstaticObject newProxyInstance(ClassLoader loader,
  6.                                      Class<?>[] interfaces,
  7.                                      InvocationHandler h)
  8.    throwsIllegalArgumentException
  9. {
  10.    Class<?> cl = getProxyClass0(loader, intfs);
  11. }
如上代码由此可见,它调用了 getProxyClass0来获取 ProxyClass,那我们继续往下看。
  1. privatestaticClass<?> getProxyClass0(ClassLoader loader,
  2.                                           Class<?>... interfaces){
  3.    if(interfaces.length >65535){
  4.        thrownewIllegalArgumentException("interface limit exceeded");
  5.    }
  6.    //If the proxy class defined by the given loader implementing
  7.    //the given interfaces exists, this will simply return the cached copy;
  8.    //otherwise, it will create the proxy class via the ProxyClassFactory
  9.    return proxyClassCache.get(loader, interfaces);
  10. }
其实上面写的已经很简单了,如果存在就在 proxyClassCache里面获取到,如果不存在就使用 ProxyClassFactory创建一个。当然我们如果看一下 proxyClassCache变量的话其也是 ProxyClassFactory对象。
  1.   privatestaticfinalWeakCache<ClassLoader,Class<?>[],Class<?>>
  2.        proxyClassCache =newWeakCache<>(newKeyFactory(),newProxyClassFactory());
那么我们直接就去查看 ProxyClassFactory的实现问题不就解决了吗?
  1.    privatestaticfinalclassProxyClassFactory
  2.        implementsBiFunction<ClassLoader,Class<?>[],Class<?>>
  3.    {
  4.        // prefix for all proxy class names
  5.        privatestaticfinalString proxyClassNamePrefix ="$Proxy";
  6.        //next number to use for generation of unique proxy class names
  7.        privatestaticfinalAtomicLong nextUniqueNumber =newAtomicLong();
  8.        @Override
  9.        publicClass<?> apply(ClassLoader loader,Class<?>[] interfaces){
  10.            String proxyName = proxyPkg + proxyClassNamePrefix + num;
  11.            /*
  12.             * Generate the specified proxy class.
  13.             */
  14.            byte[] proxyClassFile =ProxyGenerator.generateProxyClass(
  15.                proxyName, interfaces, accessFlags);
  16.        }
  17.    }
由上代码便一目了然了,为什么我们 Debug的时候 Proxy对象是 $Proxy0,是因为他通过 $ProxyAtomicLong拼起来的类名,其实这不是重点。重点是 ProxyGenerator.generateProxyClass(proxyName,interfaces,accessFlags)。这就是生成 class的地方,它把所有的条件组合好,生成 class文件,然后再加载到内存里面以供使用。有兴趣的同学可以继续往深处查看。而我们需要做的是获取到他生成的字节码,看一下里面到底是什么?当 saveGeneratedFilestrue的时候会保存 class文件,所以我们在 DynamicProxyTestmain函数添加一行即可:
  1. System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
通过 Debug我们可以发现,它存储 class文件的路径是 com/sun/proxy/$Proxy0.class,所以直接在我们项目的目录下面就能找到它,然后通过 Idea打开便得到如下代码:
  1. publicfinalclass $Proxy0 extendsProxyimplementsUser{
  2.    privatestaticMethod m1;
  3.    privatestaticMethod m2;
  4.    privatestaticMethod m3;
  5.    privatestaticMethod m0;
  6.    public $Proxy0(InvocationHandler var1)throws  {
  7.        super(var1);
  8.    }
  9.    publicfinalboolean equals(Object var1)throws  {
  10.        try{
  11.            return((Boolean)super.h.invoke(this, m1,newObject[]{var1})).booleanValue();
  12.        }catch(RuntimeException|Error var3){
  13.            throw var3;
  14.        }catch(Throwable var4){
  15.            thrownewUndeclaredThrowableException(var4);
  16.        }
  17.    }
  18.    publicfinalString toString()throws  {
  19.        try{
  20.            return(String)super.h.invoke(this, m2,(Object[])null);
  21.        }catch(RuntimeException|Error var2){
  22.            throw var2;
  23.        }catch(Throwable var3){
  24.            thrownewUndeclaredThrowableException(var3);
  25.        }
  26.    }
  27.    publicfinalvoid create()throws  {
  28.        try{
  29.            super.h.invoke(this, m3,(Object[])null);
  30.        }catch(RuntimeException|Error var2){
  31.            throw var2;
  32.        }catch(Throwable var3){
  33.            thrownewUndeclaredThrowableException(var3);
  34.        }
  35.    }
  36.    publicfinalint hashCode()throws  {
  37.        try{
  38.            return((Integer)super.h.invoke(this, m0,(Object[])null)).intValue();
  39.        }catch(RuntimeException|Error var2){
  40.            throw var2;
  41.        }catch(Throwable var3){
  42.            thrownewUndeclaredThrowableException(var3);
  43.        }
  44.    }
  45.    static{
  46.        try{
  47.            m1 =Class.forName("java.lang.Object").getMethod("equals",newClass[]{Class.forName("java.lang.Object")});
  48.            m2 =Class.forName("java.lang.Object").getMethod("toString",newClass[0]);
  49.            m3 =Class.forName("local.dynimicproxy.User").getMethod("create",newClass[0]);
  50.            m0 =Class.forName("java.lang.Object").getMethod("hashCode",newClass[0]);
  51.        }catch(NoSuchMethodException var2){
  52.            thrownewNoSuchMethodError(var2.getMessage());
  53.        }catch(ClassNotFoundException var3){
  54.            thrownewNoClassDefFoundError(var3.getMessage());
  55.        }
  56.    }
  57. }
这样好多问题就迎刃而解。
为什么 Java动态代理必须是接口,因为生成的类要去实现这个接口。
invoke方法的 proxy是干嘛的,通过 super.h.invoke(this,m3,(Object[])null);我们可以发现传递给 invoke方法的就是 Proxy本身。
同时 Proxy类也通过反射实现了 toString, equals,和 hashcode等方法。
自此关于 Java动态代理的讲解已经告段落,下面让我们简单看一下 Spring-mybatis中关于 Java动态代理的使用。

Java动态代理在Spring-mybatis中的实现

关于 Spring-mybatis的实现我们得从 MapperScannerConfigurer说起,首先 MapperScannerConfigurer实现了 BeanDefinitionRegistryPostProcessor接口。而 BeanDefinitionRegistryPostProcessor依赖于 Spring框架,简单的说 BeanDefinitionRegistryPostProcessor使得我们可以将 BeanDefinition添加到 BeanDefinitionRegistry中,而 BeanDefinition描述了一个Bean实例所拥有的实例、结构参数和参数值,简单点说拥有它就可以实例化 Bean了。 BeanDefinitionRegistryPostProcessorpostProcessBeanDefinitionRegistry方法在 Bean被定义但还没被创建的时候执行,所以 Spring-mybatis也是借助了这一点。需要想需要更深入的了解可以查看 Spring的生命周期。
  1. publicclassMapperScannerConfigurerimplementsBeanDefinitionRegistryPostProcessor,InitializingBean,ApplicationContextAware,BeanNameAware{
  2.  /**
  3.   * {@inheritDoc}
  4.   *
  5.   * @since 1.0.2
  6.   */
  7.  @Override
  8.  publicvoid postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry){
  9.    ClassPathMapperScanner scanner =newClassPathMapperScanner(registry);
  10.    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage,ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  11.  }
由上代码我们可以看到在 postProcessBeanDefinitionRegistry里面得到 registry然后使用 ClassPathMapperScanner开始扫描包路径得到的 Bean并且注册到 registry里面。我们接着往里面看。
  1. @Override
  2. publicSet<BeanDefinitionHolder> doScan(String... basePackages){
  3. Set<BeanDefinitionHolder> beanDefinitions =super.doScan(basePackages);
  4. if(beanDefinitions.isEmpty()){
  5.  logger.warn("No MyBatis mapper was found in '"+Arrays.toString(basePackages)+"' package. Please check your configuration.");
  6. }else{
  7.  processBeanDefinitions(beanDefinitions);
  8. }
  9. return beanDefinitions;
  10. }
ClassPathMapperScanner继承了 SpringClassPathBeanDefinitionScanner所以调用父类的 doScan方法就可以加载 Bean然后再通过 processBeanDefinitions方法加工成 MyBatis需要的 Bean
  1. privatevoid processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions){
  2.    GenericBeanDefinition definition;
  3.    for(BeanDefinitionHolder holder : beanDefinitions){
  4.      definition =(GenericBeanDefinition) holder.getBeanDefinition();
  5.      definition.setBeanClass(this.mapperFactoryBean.getClass());
  6.    }
  7.  }
如上代码循环了所有由 Spring容器解析出来的 beanDefinitions然后把他们的 BeanClass修改为 mapperFactoryBean,这就进入了行文的重点。我们翻看到 MapperFactoryBean:
  1. @Override
  2. protectedvoid checkDaoConfig(){
  3. super.checkDaoConfig();
  4. notNull(this.mapperInterface,"Property 'mapperInterface' is required");
  5. Configuration configuration = getSqlSession().getConfiguration();
  6. if(this.addToConfig &&!configuration.hasMapper(this.mapperInterface)){
  7.  try{
  8.    configuration.addMapper(this.mapperInterface);
  9.  }catch(Exception e){
  10.    logger.error("Error while adding the mapper '"+this.mapperInterface +"' to configuration.", e);
  11.    thrownewIllegalArgumentException(e);
  12.  }finally{
  13.    ErrorContext.instance().reset();
  14.  }
  15. }
  16. }
其调用了 ConfigurationaddMapper方法,这样就把 Bean交给 MyBatis管理了。那么 checkDaoConfig是什么时候调用的呢?我们翻看其父类 DaoSupport可以看到:
  1. publicabstractclassDaoSupportimplementsInitializingBean{
  2.    @Override
  3.  publicfinalvoid afterPropertiesSet()throwsIllegalArgumentException,BeanInitializationException{
  4.    checkDaoConfig();
  5.  }
  6. }
因为 DaoSupport实现了 InitializingBean并重写 afterPropertiesSet方法,了解 Spring生命周期的同学知道 afterPropertiesSet方法会在资源加载完以后,初始化bean之前执行。我们继续查看 addMapper方法。
  1. public<T>void addMapper(Class<T> type){
  2.    if(type.isInterface()){
  3.      if(hasMapper(type)){
  4.        thrownewBindingException("Type "+ type +" is already known to the MapperRegistry.");
  5.      }
  6.      boolean loadCompleted =false;
  7.      try{
  8.        knownMappers.put(type,newMapperProxyFactory<T>(type));
  9.        // It's important that the type is added before the parser is run
  10.        // otherwise the binding may automatically be attempted by the
  11.        // mapper parser. If the type is already known, it won't try.
  12.        MapperAnnotationBuilder parser =newMapperAnnotationBuilder(config, type);
  13.        parser.parse();
  14.        loadCompleted =true;
  15.      }finally{
  16.        if(!loadCompleted){
  17.          knownMappers.remove(type);
  18.        }
  19.      }
  20.    }
  21. }
addMapper方法最终创建了 MapperProxyFactory对象,在 MapperProxyFactory里面我们两眼泪汪汪地发现了似曾相识的代码:
  1. protected T newInstance(MapperProxy<T> mapperProxy){
  2.    return(T)Proxy.newProxyInstance(mapperInterface.getClassLoader(),newClass[]{ mapperInterface }, mapperProxy);
  3. }
  4. public T newInstance(SqlSession sqlSession){
  5.    finalMapperProxy<T> mapperProxy =newMapperProxy<T>(sqlSession, mapperInterface, methodCache);
  6.    return newInstance(mapperProxy);
  7. }
MapperProxy实现了 InvocationHandler方法,最终实现对 Bean的代理,同时获取到上下文的 sqlSession以供使用。具体生成过程我们不再累述,直接通过其源码结束本篇文章:
  1. publicclassMapperProxy<T>implementsInvocationHandler,Serializable{
  2.  privatestaticfinallong serialVersionUID =-6424540398559729838L;
  3.  privatefinalSqlSession sqlSession;
  4.  privatefinalClass<T> mapperInterface;
  5.  privatefinalMap<Method,MapperMethod> methodCache;
  6.  publicMapperProxy(SqlSession sqlSession,Class<T> mapperInterface,Map<Method,MapperMethod> methodCache){
  7.    this.sqlSession = sqlSession;
  8.    this.mapperInterface = mapperInterface;
  9.    this.methodCache = methodCache;
  10.  }
  11.  @Override
  12.  publicObject invoke(Object proxy,Method method,Object[] args)throwsThrowable{
  13.    try{
  14.      if(Object.class.equals(method.getDeclaringClass())){
  15.        return method.invoke(this, args);
  16.      }elseif(isDefaultMethod(method)){
  17.        return invokeDefaultMethod(proxy, method, args);
  18.      }
  19.    }catch(Throwable t){
  20.      throwExceptionUtil.unwrapThrowable(t);
  21.    }
  22.    finalMapperMethod mapperMethod = cachedMapperMethod(method);
  23.    return mapperMethod.execute(sqlSession, args);
  24.  }
  25. }
本站所有文章均来自互联网,如有侵权,请联系站长删除。极客文库 » 从 Spring 集成 MyBatis 到浅析 Java 动态代理
分享到:
赞(0)

评论抢沙发

评论前必须登录!