mybatis mapper 接口原理(mybatis 注解原理)

mybatis mapper 接口的使用方法

Address.java:

public class Address implements Serializable {
    private int id;
    private String name;
    private String postCode;

    public Address() {
    }

    public Address(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public Address(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPostCode() {
        return postCode;
    }

    public void setPostCode(String postCode) {
        this.postCode = postCode;
    }
}

AddressMapper.java:

public interface AddressMapper {
    @Select("select * from address where id=#{id}")
    Address queryById(int id);
}

mybatis-config.xml:

<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis-demo"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper class="shfq.annotation.AddressMapper"/>
    </mappers>
</configuration>

address 表结构:

CREATE TABLE `address` (
  `id` int(32) NOT NULL,
  `name` varchar(100) DEFAULT NULL,
  `post_code` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Test.java:

public class Test {
    public static void main(String[] args) {
        SqlSession session = null;
                try {
                    Reader reader = Resources.getResourceAsReader("shfq/annotation/mybatis-config.xml");
                    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
                    session = sqlSessionFactory.openSession();
                    AddressMapper mapper = session.getMapper(AddressMapper.class);
                    Address address = mapper.queryById(1);
                    System.out.println("id:" + address.getId());
                    System.out.println("name:" + address.getName());
                    System.out.println("post code:" + address.getPostCode());
                    System.out.println("");
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (session != null) {
                        session.close();
                    }
                }
    }
}

输出结果:

id:1
name:海淀
post code:1111

mybatis mapper 接口的原理

上面的代码很简单,不过在我没有阅读 mybatis 源码之前我有个疑惑:

AddressMapper mapper = session.getMapper(AddressMapper.class);

AddressMapper 是一个接口,大家都知道接口是不能实例化对象的。那么赋值给 mapper 引用的对象到底是个什么东西呢?我当时很纳闷,看了 mybatis 源码之后,我就清楚了背后的原理。

赋值给 mapper 引用的对象是一个代理对象,这个代理对象是由 JDK 动态代理创建的。

我写了两篇有关动态代理的博客:代理模式和 Java 动态代理深入理解 Java 动态代理,对 Java 动态代理不熟悉的同学可以阅读我的博客。

在解析 mybatis-config.xml 配置文件的时候:

<mappers>
    <mapper class="shfq.annotation.AddressMapper"/>
</mappers>

会循环处理 < mappers/> 内的所有< mapper/>,在上面的例子中只有一个 mapper 并且是个接口,在解析 mapper 的时候,mybatis 会通过 Java 反射,获取到接口的所有方法。然后循环处理每一个方法。接口中的方法包含的信息主要有:参数、返回类型、方法注解、方法名称。

  • 返回类型的作用主要是根据返回类型创建对象,然后把结果集中的数据通过调用 Java 反射把相关的数据设置到对象中。
  • 参数的主要作用是为 sql 语句传递参数。
  • 方法注解里包含了 sql 语句信息,方法注解里还可以包含很多其他的信息。
  • 方法名称的作用主要是为了标志唯一性,接口名称和方法名称拼接在一起就可以唯一地区分一个方法(Java 里允许方法重载,但 mybatis 不支持方法名称相同但参数不同的情况)

在处理每一个方法时会把上面说的这几种信息都封装并保存起来。

在调用:

Address address = mapper.queryById(1);

的时候最终是交给 InvocationHandler 去执行,InvocationHandler 执行的时候通过反射获取到当前调用方法的相关信息,然后根据这些信息取出之前解析封装好的数据(sql 语句等),然后再把这些数据和参数封装成为 Statement(PreparedStatement)最终交给 jdbc 去执行。
JDBC 执行查询之后会返回一个结果集。比如结果集中只有一行这样的数据:

idnamepost_code
1海淀1111

那么结果集是怎么映射成为一个对象的呢?

因为这个例子中返回的对象中的每个属性都是简单属性(String、int)而不是自定义的复杂类型,mybatis 默认会为这些属性自动映射。

自动映射的原理:
大家知道可以从结果集中取出列名(别名),在处理结果集中的每行数据时,首先创建一个对象,然后依次遍历所有的列名,在遍历的时候看这个列名是不是某个类的属性名称。那么怎样判断一个列名是不是某一个类的属性名称呢?

一个类的所有的属性名称都会放在一个 map 中,键是大写的属性名称,值是属性名称,所以判断一个列名是不是某个类的属性名称的时候会先把这个列名做个大写转换,然后根据键取出值。如果取出的值不为空的话,说明这个列名是某一个类的属性名称。
如果一个列名是某一个类的属性名称的话,然后再根据取出的属性名称找到其 set 方法。找到 set 方法后会根据列名从结果集中取出该行数据中该列所对应的值。然后利用反射,把属性值设置到刚才创建的对象中。

大家会注意到有一列的名称是 post_code 而我们的字段名称是 postCode ,这是怎么映射的呢?

在上面的 mybatis-config.xml 配置文件中有这样的一个配置:

<settings>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

上面的这行配置的意思是映射下划线到驼峰类型,有了这个配置之后,在取出列名的时候会把列名中的 _ 都去掉,列名就变成 postcode 很显然 postcode 是在列名 map 中的,然后就像其他属性一样反射调用来设置属性值。

mybatis mapper 接口主要还是用了 Java 动态代理,Java 动态代理很有用,在很多地方会用到它,比如 mybatis 懒加载(不仅是 mybatis ,hibernate 懒加载应该也会用到它) spring AOP、事务管理等。

这是我的讲解动态代理的一篇博客:深入理解 Java 动态代理,理解了这篇博客就理解了 Java 动态代理。


版权声明:本文为shfqbluestone原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。