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

132 阅读8分钟
原文链接: mp.weixin.qq.com

前言

因为 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. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

  2.  <property name="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. <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">

  2.         <!-- 基本属性 url、user、password -->

  3.         <property name= "url">

  4.             <value><![CDATA[${db.url}]]> </value>

  5.         </property>

  6.         <property name= "username" value= "${db.username}"/>

  7.         <property name= "password" value= "${db.password}"/>      

  8.         <!-- 省略其他配置 -->  

  9. </bean>

接下来我们要编写数据库访问对象,大多数人会把它叫做 DAO或者 Repository,在这里其被称为 Mapper,也是因为它的实现方式所决定。要注意的是所指定的映射器类必须是一个接口,而不是具体的实现类。这便因为 Mybatis的内部实现使用的是 Java动态代理,而 Java动态代理只支持接口,关于 动态代理我们下文有更详细的描述。

                                            
  1. public interface UserMapper {

  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. <b ean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">

  2.   <property name= "mapperInterface" value= "org.mybatis.spring.sample.mapper.UserMapper" />

  3.   <property name= "sqlSessionFactory" ref= "sqlSessionFactory" />

  4. </bean>

这样我们已经完成了90%,就差调用了,前提是你 Spring环境是OK的。调用 MyBatis 数据方法现在只需一行代码:

  1. public class FooServiceImpl implements FooService {

  2. private UserMapper userMapper;

  3. public void setUserMapper(UserMapper userMapper) {

  4.  this.userMapper = userMapper;

  5. }

  6. public User doSomeBusinessStuff(String userId) {

  7.  return this.userMapper.getUser(userId);

  8. }

那么问题又来了,每次写一个DAO都需要为其写一个 Bean配置,那不是累死?于是我们又寻找另一种方案,代替手动声明 *MapperMapperScannerConfigurer的出现解决了这个问题, 它会根据你配置的包路径自动的扫描类文件并自动将它们创建成 MapperFactoryBean,可以在 Spring 的配置中添加如下代码:

  1. <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">

  2.  <property name="basePackage" value="com.github.codedrinker.mapper" />

  3. </bean>

basePackage属性是让你为映射器接口文件设置基本的包路径。你可以使用分号或逗号作为分隔符设置多于一个的包路径。这个时候如果想自定义 sqlSessionFactory可以添加如下配置:

                                                    
  1. < property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />

这样以后还有一点点小瑕疵,如果我们数据的 column名字是 _连接的,那么它不会那么聪明自动转换为驼峰的变量,所以我们需要对 SqlSessionFactoryBean做如下配置,但是在1.3.0以后才可以通过xml配置,如果用早起版本的需要注意了。

                                                        
  1. <b ean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

  2.   <property name= "dataSource" ref= "dataSource" />

  3.   <property name= "configuration">

  4.     <bean class= "org.apache.ibatis.session.Configuration">

  5.       <property name= "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. public class InvocationInterceptor implements InvocationHandler {

  5.     private Object target;

  6.     public InvocationInterceptor( Object target) {

  7.         this.target = target;

  8.    }

  9.     @Override

  10.     public Object invoke( Object proxy, Method method, Object[] args) throws Throwable {

  11.         System.out.println( "before user create");

  12.        method.invoke(target, args);

  13.         System.out.println( "end user create");

  14.         return null;

  15.     }

  16. }

UserUserImpl是被代理对象的接口和类

                                                                    
  1. /**

  2. * Created by codedrinker on 12/10/2017.

  3. */

  4. public interface User {

  5.     void create();

  6. }

  1. /**

  2. * Created by codedrinker on 12/10/2017.

  3. */

  4. public class UserImpl implements User {

  5.    @Override

  6.    public void create() {

  7.        System.out.println("create user");

  8.    }

  9. }

DynamicProxyTest是测试类,用于创建 InvocationInterceptorProxy类以便测试。

                                                                        
  1. /**

  2. * Created by codedrinker on 12/10/2017.

  3. */

  4. public class DynamicProxyTest {

  5.     public static void main( String[] args) {

  6.         User target = new UserImpl();

  7.         InvocationInterceptor invocationInterceptor = new InvocationInterceptor(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. public static Object newProxyInstance(ClassLoader loader,

  6.                                       Class<?>[] interfaces,

  7.                                       InvocationHandler h)

  8.     throws IllegalArgumentException

  9. {

  10.     Class<?> cl = getProxyClass0(loader, intfs);

  11. }

如上代码由此可见,它调用了 getProxyClass0来获取 ProxyClass,那我们继续往下看。

                                                                                
  1. p rivate static Class<?> getProxyClass0(ClassLoader loader,

  2.                                           Class<?>... interfaces) {

  3.     if (interfaces.length > 65535) {

  4.         throw new IllegalArgumentException( "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.   private static final WeakCache <ClassLoader, Class<?>[], Class<?>>

  2.        proxyClassCache = new WeakCache<>( new KeyFactory(), new ProxyClassFactory());

那么我们直接就去查看 ProxyClassFactory的实现问题不就解决了吗?

                                                                                        
  1.     private static final class ProxyClassFactory

  2.         implements BiFunction< ClassLoader, Class<?>[], Class<?>>

  3.    {

  4.         // prefix for all proxy class names

  5.         private static final String proxyClassNamePrefix = "$Proxy";

  6.         //next number to use for generation of unique proxy class names

  7.         private static final AtomicLong nextUniqueNumber = new AtomicLong();

  8.         @Override

  9.         public Class<?> 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. public final class $Proxy0 extends Proxy implements User {

  2.     private static Method m1;

  3.     private static Method m2;

  4.     private static Method m3;

  5.     private static Method m0;

  6.     public $Proxy0( InvocationHandler var1) throws  {

  7.         super(var1);

  8.    }

  9.     public final boolean equals( Object var1) throws  {

  10.         try {

  11.             return (( Boolean) super.h.invoke( this, m1, new Object[]{var1})).booleanValue();

  12.        } catch ( RuntimeException | Error var3) {

  13.             throw var3;

  14.        } catch ( Throwable var4) {

  15.             throw new UndeclaredThrowableException(var4);

  16.        }

  17.    }

  18.     public final String 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.             throw new UndeclaredThrowableException(var3);

  25.        }

  26.    }

  27.     public final void 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.             throw new UndeclaredThrowableException(var3);

  34.        }

  35.    }

  36.     public final int 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.             throw new UndeclaredThrowableException(var3);

  43.        }

  44.    }

  45.     static {

  46.         try {

  47.            m1 = Class.forName( "java.lang.Object").getMethod( "equals", new Class[]{ Class.forName( "java.lang.Object")});

  48.            m2 = Class.forName( "java.lang.Object").getMethod( "toString", new Class[ 0]);

  49.            m3 = Class.forName( "local.dynimicproxy.User").getMethod( "create", new Class[ 0]);

  50.            m0 = Class.forName( "java.lang.Object").getMethod( "hashCode", new Class[ 0]);

  51.        } catch ( NoSuchMethodException var2) {

  52.             throw new NoSuchMethodError(var2.getMessage());

  53.        } catch ( ClassNotFoundException var3) {

  54.             throw new NoClassDefFoundError(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. pu blic class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {

  2.   /**

  3.   * {@inheritDoc}

  4.   *

  5.   * @since 1.0.2

  6.   */

  7.   @Override

  8.   public void postProcessBeanDefinitionRegistry( BeanDefinitionRegistry registry) {

  9.     ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

  10.    scanner.scan( StringUtils.tokenizeToStringArray( this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));

  11.  }

由上代码我们可以看到在 postProcessBeanDefinitionRegistry里面得到 registry之后,随后我们便可以使用 ClassPathMapperScanner开始扫描包路径得到的 Bean并且注册到 registry里面。我们接着往里面看。

                                                                                                                
  1. @Override

  2. public Set <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. p rivate void 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. protected void 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.     throw new IllegalArgumentException(e);

  12.  } finally {

  13.     ErrorContext.instance().reset();

  14.  }

  15. }

  16. }

其调用了 ConfigurationaddMapper方法,这样就把 Bean交给 MyBatis管理了。那么 checkDaoConfig是什么时候调用的呢?我们翻看其父类 DaoSupport可以看到:

                                                                                                                            
  1. public abstract class DaoSupport implements InitializingBean {

  2.     @Override

  3.   public final void afterPropertiesSet() throws IllegalArgumentException, 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.         throw new BindingException( "Type " + type + " is already known to the MapperRegistry.");

  5.      }

  6.       boolean loadCompleted = false;

  7.       try {

  8.        knownMappers.put(type, new MapperProxyFactory<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 = new MapperAnnotationBuilder(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(), new Class[] { mapperInterface }, mapperProxy);

  3. }

  4. public T newInstance(SqlSession sqlSession) {

  5.     final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);

  6.     return newInstance(mapperProxy);

  7. }

MapperProxy实现了 InvocationHandler方法,最终实现对 Bean的代理,同时获取到上下文的 sqlSession以供使用。具体生成过程我们不再累述,直接通过其源码结束本篇文章:

                                                                                                                                        
  1. public class MapperProxy <T> implements InvocationHandler , Serializable {

  2.   private static final long serialVersionUID = - 6424540398559729838L;

  3.   private final SqlSession sqlSession;

  4.   private final Class<T> mapperInterface;

  5.   private final Map< Method, MapperMethod> methodCache;

  6.   public MapperProxy( 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.   public Object invoke( Object proxy, Method method, Object[] args) throws Throwable {

  13.     try {

  14.       if ( Object. class.equals(method.getDeclaringClass())) {

  15.         return method.invoke( this, args);

  16.      } else if (isDefaultMethod(method)) {

  17.         return invokeDefaultMethod(proxy, method, args);

  18.      }

  19.    } catch ( Throwable t) {

  20.       throw ExceptionUtil.unwrapThrowable(t);

  21.    }

  22.     final MapperMethod mapperMethod = cachedMapperMethod(method);

  23.     return mapperMethod.execute(sqlSession, args);

  24.  }

  25. }

声明:本文由订阅号「码匠笔记」的小编投稿,他在两个月内拿到了阿里、京东等公司的offer,搜索「majiangbiji」即可关注码匠笔记。

1.  从零学习Spring Boot

2.  Maven学习笔记(三)

3.  从零开发内容管理系统

4.  从零学习Spring MVC框架