MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。
MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。(这是官网解释)
当框架启动时,通过configuration解析config.xml配置文件和mapper.xml映射文件,映射文件可以使用xml方式或者注解方式,然后由configuration获得sqlsessionfactory对象,再由sqlsessionfactory获得sqlsession数据库访问会话对象,通过会话对象获得对应DAO层的mapper对象,通过调用mapper对象相应方法,框架就会自动执行SQL语句从而获得结果。
其实整体流程就是这么简单,我们来一起实现一个简单版本的 mybatis。
(1)深入学习 mybatis 的原理
一千个读者就有一千个哈姆雷特,一千个作者就有一千个莎士比亚。——老马
(2)实现属于自己的 mybatis 工具。
数据库的种类实际上有几百种,比如工作中就用到过 GreenPlum 这种相对小众的数据库,这时候 mybatis 可能就不能使用了。
感觉大可不必,符合 SQL 标准都应该统一支持下,这样更加方便实用。
本系列目前共计 17 个迭代版本,基本完成了 mybatis 的核心特性。
耗时大概十天左右,相对实现的方式比较简单。
采用 mvp 的开发策略,逐渐添加新的特性。
本系列将对核心代码进行讲解,完整代码已经全部开源
https://github.com/houbb/mybatis
不是本系列重点,请自行找资料。
版本:使用的是 v5.7 版本,v8.0 之后依赖的驱动包会有所不同。
-- auto-generated definition create table user ( id int auto_increment primary key, name varchar(100) not null, password varchar(100) not null ); insert into user (name, password) value ('ryo', '123456');
<dependency> <groupId>com.github.houbb</groupId> <artifactId>mybatis</artifactId> <version>0.0.1</version> </dependency>
<?xml version="1.0" encoding="UTF-8"?> <configuration> <dataSource> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> <mappers> <mapper resource="mapper/UserMapper.xml"/> </mappers> </configuration>
Config config = new XmlConfig("mybatis-config-5-7.xml"); SqlSession sqlSession = new DefaultSessionFactory(config).openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.selectById(1L); System.out.println(user);
输出结果:
User{id=1, name='ryo', password='123456'}
是不是有种 mybatis 初恋般的感觉呢?
到这里都是引子,下面我们来讲述下一些核心实现。
这里我们需要访问 mysql,也需要解析 xml。
需要引入如下的依赖:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.29</version> </dependency> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency>
上述的测试代码中,我们演示用到的几个核心接口如下:
配置接口
/** * 配置信息 * @author binbin.hou * @since 0.0.1 */ public interface Config { /** * 获取数据源信息 * @return 数据源配置 * @since 0.0.1 */ DataSource getDataSource(); /** * 获取映射类信息 * @param clazz 类信息 * @return 结果 * @since 0.0.1 */ MapperClass getMapperData(final Class clazz); /** * 获取映射类信息 * @param clazz 类信息 * @param methodName 方法名称 * @return 结果 * @since 0.0.1 */ MapperMethod getMapperMethod(final Class clazz, final String methodName); /** * 数据库连接信息 * @return 连接信息 * @since 0.0.1 */ Connection getConnection(); }
public interface SqlSession { /** * 查询单个 * @param mapperMethod 方法 * @param args 参数 * @param <T> 泛型 * @return 结果 * @since 0.0.1 */ <T> T selectOne(final MapperMethod mapperMethod, Object[] args); /** * Retrieves a mapper. * @param <T> the mapper type * @param type Mapper interface class * @return a mapper bound to this SqlSession * @since 0.0.1 */ <T> T getMapper(Class<T> type); /** * 获取配置信息 * @return 配置 * @since 0.0.1 */ Config getConfig(); }
UserMapper 就是我们经常定义的 mapper
public interface UserMapper { User selectById(final long id); }
下面我们来看看对应的几个比较重要的实现。
我们的很多配置放在 config.xml 文件中,肯定是通过解析 xml 实现的。
public class XmlConfig extends ConfigAdaptor { /** * 文件配置路径 * * @since 0.0.1 */ private final String configPath; /** * 配置文件信息 * * @since 0.0.1 */ private Element root; /** * 数据源信息 * * @since 0.0.1 */ private DataSource dataSource; /** * mapper 注册类 * * @since 0.0.1 */ private final MapperRegister mapperRegister = new MapperRegister(); public XmlConfig(String configPath) { this.configPath = configPath; // 配置初始化 initProperties(); // 初始化数据连接信息 initDataSource(); // mapper 信息 initMapper(); } @Override public DataSource getDataSource() { return this.dataSource; } @Override public Connection getConnection() { try { Class.forName(dataSource.driver()); return DriverManager.getConnection(dataSource.url(), dataSource.username(), dataSource.password()); } catch (ClassNotFoundException | SQLException e) { throw new MybatisException(e); } } @Override public MapperMethod getMapperMethod(Class clazz, String methodName) { return this.mapperRegister.getMapperMethod(clazz, methodName); } }
这里就是解析 xml 文件的 root 节点,便于后续使用:
root 节点的初始化如下:
/** * 获取根节点 * @param path 配置路径 * @return 元素 * @since 0.0.1 */ public static Element getRoot(final String path) { try { // 初始化数据库连接信息 InputStream inputStream = StreamUtil.getInputStream(path); SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); return document.getRootElement(); } catch (DocumentException e) { throw new MybatisException(e); } }
这就是解析 xml 中对于 dataSource 的配置信息:
/** * 初始化数据源 * * @since 0.0.1 */ private void initDataSource() { // 根据配置初始化连接信息 this.dataSource = new DataSource(); Element dsElem = root.element("dataSource"); Map<String, String> map = new HashMap<>(4); for (Object property : dsElem.elements("property")) { Element element = (Element) property; String name = element.attributeValue("name"); String value = element.attributeValue("value"); map.put("jdbc." + name, value); } dataSource.username(map.get(DataSourceConst.USERNAME)) .password(map.get(DataSourceConst.PASSWORD)) .driver(map.get(DataSourceConst.DRIVER)) .url(map.get(DataSourceConst.URL)); }
解析 xml 中的 mapper 配置。
/** * 初始化 mapper 信息 * * @since 0.0.1 */ private void initMapper() { Element mappers = root.element("mappers"); // 遍历所有需要初始化的 mapper 文件路径 for (Object item : mappers.elements("mapper")) { Element mapper = (Element) item; String path = mapper.attributeValue("resource"); mapperRegister.addMapper(path); } }
mapperRegister 就是对方法的元数据进行一些构建,比如出参,入参的类型,等等,便于后期使用。
比如我们的 UserMapper.xml 方法内容如下:
<select id = "selectById" paramType="java.lang.Long" resultType = "com.github.houbb.mybatis.domain.User"> select * from user where id = ? </select>
sql 就是:select * from user where id = ?
方法标识:selectById
入参:Long
出参:User
SqlSession sqlSession = new DefaultSessionFactory(config).openSession();
这句话实际执行的是:
@Override public SqlSession openSession() { return new DefaultSqlSession(config, new SimpleExecutor()); }
UserMapper userMapper = sqlSession.getMapper(UserMapper.class)
这里获取 mapper,实际获取的是什么呢?
实际上获取到的是一个代理。
mybatis 将我们的接口,和实际 xml 中的 sql 二者通过动态代理结合,让我们调用 xml 中的 sql 和使用接口方法一样自然。
getMapper 实际上是一个动态代理。
@Override @SuppressWarnings("all") public <T> T getMapper(Class<T> clazz) { MapperProxy proxy = new MapperProxy(clazz, this); return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, proxy); }
MapperProxy 的实现如下:
public class MapperProxy implements InvocationHandler { /** * 类信息 * * @since 0.0.1 */ private final Class clazz; /** * sql session * * @since 0.0.1 */ private final SqlSession sqlSession; public MapperProxy(Class clazz, SqlSession sqlSession) { this.clazz = clazz; this.sqlSession = sqlSession; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MapperMethod mapperMethod = this.sqlSession.getConfig() .getMapperMethod(clazz, method.getName()); if (mapperMethod != null) { return this.sqlSession.selectOne(mapperMethod, args); } return method.invoke(proxy, args); } }
当我们执行 userMapper.selectById(1L)
时,实际执行的是什么?
实际执行的是 sqlSession.selectOne(mapperMethod, args)
selectOne 是比较核心的内容了。
整体如下
public <T> T query(final Config config, MapperMethod method, Object[] args) { try(Connection connection = config.getConnection(); PreparedStatement preparedStatement = connection.prepareStatement(method.getSql());) { // 2. 处理参数 parameterHandle(preparedStatement, args); // 3. 执行方法 preparedStatement.execute(); // 4. 处理结果 final Class resultType = method.getResultType(); ResultSet resultSet = preparedStatement.getResultSet(); ResultHandler resultHandler = new ResultHandler(resultType); Object result = resultHandler.buildResult(resultSet); return (T) result; } catch (SQLException ex) { throw new MybatisException(ex); } }
我们获取到 xml 中的 sql,然后构建 jdbc 中大家比较熟悉的 PreparedStatement。
然后对出参和入参进行处理,最后返回结果。
public void setParams(final Object[] objects) { try { for(int i = 0; i < objects.length; i++) { Object value = objects[i]; // 目标类型,这个后期可以根据 jdbcType 获取 // jdbc 下标从1开始 statement.setObject(i+1, value); } } catch (SQLException throwables) { throw new MybatisException(throwables); } }
针对我们非常简单的例子:
select * from user where id = ?
那就是直接把入参中的 1L 设置到占位符 ?
即可。
这里主要用到反射,将查询结果和 javaBean 做一一映射。
/** * 构建结果 * @param resultSet 结果集合 * @return 结果 * @since 0.0.1 */ public Object buildResult(final ResultSet resultSet) { try { // 基本类型,非 java 对象,直接返回即可。 // 可以进行抽象 Object instance = resultType.newInstance(); // 结果大小的判断 // 为空直接返回,大于1则报错 if(resultSet.next()) { List<Field> fieldList = ClassUtil.getAllFieldList(resultType); for(Field field : fieldList) { Object value = getResult(field, resultSet); ReflectFieldUtil.setValue(field, instance, value); } // 返回设置值后的结果 return instance; } return null; } catch (InstantiationException | IllegalAccessException | SQLException e) { throw new MybatisException(e); } }
到这里,一个简易版的 myabtis 就可以跑起来了。
当然这里还有很多的不足之处,我们后续都会一一优化。
为了便于学习,完整版本代码以开源:
https://github.com/houbb/mybatis