码匠笔记

用心雕琢

通过 Spring 集成 MyBatis 源码理解 Java动态代理

访问

本文内容概要如下:

1, Spring 集成 Mybatis
2, 简述 Java 动态代理及源码分析
3, InvocationHandler里面invoke方法的proxy到底是什么
4, Java 动态代理在 Spring-mybatis 中的实现

前言

因为 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依赖,依赖配置如下。

pom.xml
1
2
3
4
5
6
7
8
9
10
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.1</version>
</dependency>

接下来会我们要创建工厂bean,放置下面的代码在 Spring 的 XML 配置文件中:

applicationContext.xml
1
2
3
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

这个工厂需要一个DataSource,就是我们熟知的数据源了。这里我们选择了阿里的Druid,同样我们需要引入两个配置

pom.xml
1
2
3
4
5
6
7
8
9
10
<dependency>
 <groupId>mysql</groupId>
 <artifactId>mysql-connector-java</artifactId>
 <version>5.1.41</version>
</dependency>
<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>druid</artifactId>
 <version>1.1.2</version>
</dependency>


添加Spring配置如下

applicationContext.xml
1
2
3
4
5
6
7
8
9
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本属性 url、user、password -->
        <property name="url">
            <value><![CDATA[${db.url}]]></value>
        </property>
        <property name="username" value="${db.username}"/>
        <property name="password" value="${db.password}"/>
        <!-- 省略其他配置 -->
</bean>

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

UserMapper.java
1
2
3
4
public interface UserMapper {
  @Select("SELECT * FROM users WHERE id = #{userId}")
  User getUser(@Param("userId") String userId);
}

接下来可以使用 MapperFactoryBean,像下面这样来把接口加入到 Spring 中,这样就把 UserMapperSessionFactory关联到一起了,原来使用xml配置的时候还需要Dao继承SqlSessionDaoSupport才能注入SessionFactory,这种方式直接通过Java动态代理SqlSessionFactory代理给了UserMapper,使得我们直接使用UserMapper即可。配置如下。

applicationContext.xml
1
2
3
4
<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>

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

FooServiceImpl.java
1
2
3
4
5
6
7
8
9
10
11
public class FooServiceImpl implements FooService {

private UserMapper userMapper;

public void setUserMapper(UserMapper userMapper) {
  this.userMapper = userMapper;
}

public User doSomeBusinessStuff(String userId) {
  return this.userMapper.getUser(userId);
}

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

applicationContext.xml
1
2
3
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="basePackage" value="com.github.codedrinker.mapper" />
</bean>

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

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

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

applicationContext.xml
1
2
3
4
5
6
7
8
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="configuration">
    <bean class="org.apache.ibatis.session.Configuration">
      <property name="mapUnderscoreToCamelCase" value="true"/>
    </bean>
  </property>
</bean>

至此关于Spring MyBatis的配置已经全部结束,后面我们会简单说下Spring MyBatis中的动态代理。

浅析 Java 动态代理

JDK自带的动态代理需要了解InvocationHandler接口和Proxy类,他们都是在java.lang.reflect包下。
InvocationHandler是代理实例的调用处理程序实现的接口。每个代理实例都具有一个关联的InvocationHandler。对代理实例调用方法时,这个方法会调用InvocationHandlerinvoke方法。 Proxy提供静态方法用于创建动态代理类和实例,同时后面自动生成的代理类都是Proxy对象。下面我们直接通过代码来分析Java动态代理InvocationInterceptor实现InvocationHandler接口,用于处理具体的代理逻辑。

InvocationInterceptor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * Created by codedrinker on 12/10/2017.
 */
public class InvocationInterceptor implements InvocationHandler {
    private Object target;

    public InvocationInterceptor(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before user create");
        method.invoke(target, args);
        System.out.println("end user create");
        return null;
    }
}

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

User.java
1
2
3
4
5
6
/**
 * Created by codedrinker on 12/10/2017.
 */
public interface User {
    void create();
}

UserImpl.java
1
2
3
4
5
6
7
8
9
/**
 * Created by codedrinker on 12/10/2017.
 */
public class UserImpl implements User {
    @Override
    public void create() {
        System.out.println("create user");
    }
}

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

DynamicProxyTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * Created by codedrinker on 12/10/2017.
 */
public class DynamicProxyTest {
    public static void main(String[] args) {
        User target = new UserImpl();
        InvocationInterceptor invocationInterceptor = new InvocationInterceptor(target);
        User proxyInstance = (User) Proxy.newProxyInstance(UserImpl.class.getClassLoader(),
                UserImpl.class.getInterfaces(),
                invocationInterceptor);
        proxyInstance.create();
    }
}

输入结果如下:

1
2
3
before user create
create user
end user create

很明显,我们通过proxyInstance这个代理类进行方法调用的时候,会在方法调用前后进行输出打印,这样就简单的实现了一个Java动态代理例子。动态代理不仅仅是打印输出这么简单,我们可以通过它打印日志,打开关闭事务, 权限检查了等等。当然它更是许多框架的钟爱,就如下文我们要说的MyBatisJava动态代理的实现。再多说一句SpringAOP也是使用动态代理实现的,当然它同时使用了Java动态代理CGLib两种方式。不过CGLIB不是本文要讨论的范围。
注意观察的同学看到上面代码的时候可能发现invoke方法的proxy参数并没有被使用,笔者查阅了一些相关文档也没有找到合理的说法,只能在源码中看看究竟喽,笔者当前的JDK版本是1.8。我们从入口开始,Proxy.newProxyInstance:

Proxy.java片段
1
2
3
4
5
6
7
8
9
10
11
/*
 * Look up or generate the designated proxy class.
 */
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    Class<?> cl = getProxyClass0(loader, intfs);
}

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

Proxy.java片段
1
2
3
4
5
6
7
8
9
10
private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }
    //If the proxy class defined by the given loader implementing
    //the given interfaces exists, this will simply return the cached copy;
    //otherwise, it will create the proxy class via the ProxyClassFactory
    return proxyClassCache.get(loader, interfaces);
}

其实上面写的已经很简单了,如果存在就在proxyClassCache里面获取到,如果不存在就使用ProxyClassFactory创建一个。当然我们如果看一下proxyClassCache变量的话其也是ProxyClassFactory对象。

1
2
   private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

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

Proxy.java片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static final class ProxyClassFactory
    implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
    // prefix for all proxy class names
    private static final String proxyClassNamePrefix = "$Proxy";
    //next number to use for generation of unique proxy class names
    private static final AtomicLong nextUniqueNumber = new AtomicLong();
    @Override
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

        String proxyName = proxyPkg + proxyClassNamePrefix + num;
        /*
         * Generate the specified proxy class.
         */
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
            proxyName, interfaces, accessFlags);
    }
}

由上代码便一目了然了,为什么我们Debug的时候Proxy对象是$Proxy0,是因为他通过$ProxyAtomicLong拼起来的类名,其实这不是重点。重点是ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags)。这就是生成class的地方,它把所有的条件组合好,生成class文件,然后再加载到内存里面以供使用。有兴趣的同学可以继续往深处查看。而我们需要做的是获取到他生成的字节码,看一下里面到底是什么?当saveGeneratedFilestrue的时候会保存class文件,所以我们在DynamicProxyTestmain函数添加一行即可:

DynamicProxyTest.java
1
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

通过Debug我们可以发现,它存储class文件的路径是com/sun/proxy/$Proxy0.class,所以直接在我们项目的目录下面就能找到它,然后通过Idea打开便得到如下代码:

$Proxy0.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public final class $Proxy0 extends Proxy implements User {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

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

    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 void create() throws  {
        try {
            super.h.invoke(this, m3, (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)).intValue();
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("local.dynimicproxy.User").getMethod("create", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

这样好多问题就迎刃而解。
为什么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的生命周期。

MapperScannerConfigurer.java片段
1
2
3
4
5
6
7
8
9
10
11
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
  /**
   * {@inheritDoc}
   * 
   * @since 1.0.2
   */
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

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

ClassPathMapperScanner.java
1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

if (beanDefinitions.isEmpty()) {
  logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
  processBeanDefinitions(beanDefinitions);
}

return beanDefinitions;
}

ClassPathMapperScanner继承了SpringClassPathBeanDefinitionScanner所以调用父类的doScan方法就可以加载Bean然后再通过processBeanDefinitions方法加工成MyBatis需要的Bean

ClassPathMapperScanner.java片段
1
2
3
4
5
6
7
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();
      definition.setBeanClass(this.mapperFactoryBean.getClass());
    }
  }

如上代码循环了所有由Spring容器解析出来的beanDefinitions然后把他们的BeanClass修改为mapperFactoryBean,这就进入了行文的重点。我们翻看到MapperFactoryBean:

MapperFactoryBean.java片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();

notNull(this.mapperInterface, "Property 'mapperInterface' is required");

Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
  try {
    configuration.addMapper(this.mapperInterface);
  } catch (Exception e) {
    logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
    throw new IllegalArgumentException(e);
  } finally {
    ErrorContext.instance().reset();
  }
}
}

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

DaoSupport.java片段
1
2
3
4
5
6
public abstract class DaoSupport implements InitializingBean {
    @Override
  public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
    checkDaoConfig();
  }
}

因为DaoSupport实现了InitializingBean并重写afterPropertiesSet方法,了解Spring生命周期的同学知道afterPropertiesSet方法会在资源加载完以后,初始化bean之前执行。我们继续查看addMapper方法。

MapperRegistry.java片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
}

addMapper方法最终创建了MapperProxyFactory对象,在MapperProxyFactory里面我们两眼泪汪汪地发现了似曾相识的代码:

MapperProxyFactory.java片段
1
2
3
4
5
6
7
8
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<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}

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

MapperProxy.java片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
}

参考链接

Spring Mybatis 配置
Spring Boot Mybatis
InvocationHandler Proxy Parameter

扩展阅读

  1. Spring 集成 Redis 扫雷
  2. 使用 Idea 创建 Spring Boot 项目
  3. 把《阿里巴巴Java开发手册》读薄
  4. JUC系列:ThreadPoolExecutor
  5. 细说 Java hashCode
  6. 使用 Idea 创建 Spring Boot 项目
  7. 优雅的使用 ThreadLocal 传递参数
  8. 构建 Java 应用内存级缓存

作者

本文作者麻酱,欢迎讨论,指正和转载,转载请注明出处。
原文地址:通过 Spring 集成 MyBatis 源码理解 Java动态代理
如果兴趣可以关注作者微信订阅号:码匠笔记
majiangbiji

评论