码匠笔记

用心雕琢

从 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 hashCode
  4. 使用 Idea 创建 Spring Boot 项目
  5. 优雅的使用 ThreadLocal 传递参数
  6. 构建 Java 应用内存级缓存

作者

本文作者麻酱,欢迎讨论,指正和转载,转载请注明出处。
原文地址:从 Spring 集成 Mybatis 到 浅析Java动态代理
如果兴趣可以关注作者微信订阅号
majiangbiji

评论