Mybatis 有两类配置文件,全局配置文件,映射信息配置文件
全局配置文件
就是 Mybatis-config.xml,用于指定 数据库连接池,事务属性,参数配置信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration > <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" /> <property name ="username" value ="root" /> <property name ="password" value ="root" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="com/test/mapping/userMapper.xml" /> </mappers > </configuration >
Mapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace ="com.test.mapping.userMapper" > <select id ="getUser" parameterType ="int" resultType ="com.test.domain.User" > select * from users where id=#{id} </select > </mapper >
Mybatis都做了些什么
说完上述配置文件后,就来说说 Mybatis 到底做了什么事,这就需要结合 JDBC 来分析,先来看看没有 Mybatis 时,JDBC 做了些什么:
加载数据库驱动,建立获取数据库连接对象
创建statment对象,设置Sql语句传入参数
执行SQL语句并获得查询结果
对查询结果进行转换并将处理结果返回
释放数据库资源
管理数据库连接对象
在加载全局配置文件后,Mybatis 使用连接池管理数据库连接对象(SqlSession)。
为什么必须使用连接池来获取数据库连接对象?
JDBC 数据库连接 (Connection 对象) 使用 DriverManager 来获取,每次向数据库建立连接都要讲 Connection 加载到内存,再验证用户名密码。非常耗时,创建成本大。
对于每一次数据库连接,使用完后都要断开,否则程序出现异常未关闭,会导致数据库系统内存泄露。最终将导致重启数据库。
当前的操作数据库的方式,花费了大量资源和时间创建数据库连接对象,使用一次就关闭,没有充分使用数据库连接对象。
连接池不仅管理数据库连接对象的创建,也管理其销毁,因此在使用完毕后,我们可以直接通过 close 将连接对象还给连接池即可。
注入 SQL 语句
JDBC 中 SQL 写死了,每次改变 SQL 都需要改 java 文件,而结果集的查询也不友好。
在获取到 SqlSession 后,通过解析 mapper.xml 的方式,将 mapper 中对应 id 的 sql 语句注入到 SqlSession,mapper 同样可以解析出 结果集对象,通过结果集的 resultType或者 resultMap 拿到类全限定名,使用反射机制获取类的所有属性并且赋值。可以直接返回 java 对象。DML 只需要注入 sql 即可。
当然这里的赋值是嵌套的,并不是简单值,包括引用对象以及数组列表等。
这极大的优化了 JDBC 中的预加载以及结果集处理的过程。
手写 mybatis 框架
在 JDBC 代码发现2个硬编码问题,一个是连接池硬编码,一个是 sql 语句硬编码。因此需要两个配置文件解决这两个硬编码问题。
框架设计,JDBC 繁琐的代码,框架应对外提供一个接口,接口实现类,实现了JDBC代码,做CRUD。
项目代码:
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 public class User { private Integer id; private String username; private Date birthday; private Integer sex; private String address; public String toString () { return "User [id=" + id + ", username=" + username + ", birthday=" + birthday + ", sex=" + sex + ", address=" + address + "]" ; } } public interface UserDao { User queryUserById (User u) ; } public class UserDaoImpl implements UserDao { private SqlSessionFactory sqlSessionFactory; public UserDaoImpl (SqlSessionFactory sqlSessionFactory) { this .sqlSessionFactory = sqlSessionFactory; } public User queryUserById (User u) { SqlSession session = sqlSessionFactory.openSession(); User user = session.selectOne("test.findUserById" , u); return user; } } public class UserDaoTest { private SqlSessionFactory sqlSessionFactory; @Before public void init () throws IOException { String resource = "sqlMapConfig.xml" ; InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } @Test public void testQueryUserById () { UserDao userDao = new UserDaoImpl(sqlSessionFactory); User queryUser = new User(); queryUser.setId(1 ); User user = userDao.queryUserById(queryUser); System.out.println(user); } }
Mybatis 框架主要看SqlSession的初始化 以及 session.selectOne
,中的代码是框架的核心内容。
接口如何设计
SqlSession 接口来规范CRUD,接口的作用就是规范框架调用,方便开发者使用框架,只传入需要的参数,在JDBC中,如果要完成一次数据库操作,需要 sql 以及入参,还需要选择使用 preparement 预编译语句 还是直接执行sql。因此,在设计时,sql肯定是在配置文件中的,配置文件被解析后,找到对应的 sql,需要一个id来记录sql,还需要传入执行的参数。如果没有参数,即为直接执行的 sql 语句。
1 2 3 4 User user = session.selectOne("test.findUserById" , id); Object selectOne (String statementId,Object object) List<Object> selectList (String statementId) void insert (String statementId,Object object)
配置文件的设计
配置文件的作用是解决硬编码,将除规范外需要自定义的东西通过配置文件表达出来,因此,可以知道配置文件需要传递哪些内容。
主配置文件 作用:配置环境,包括 mybatis 数据源环境,可能有多个,可以根据id来选择。每个数据源都需要连接池以及配置连接池的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <configuration > <environments default ="dev" > <environment id ="dev" > <dataSource type ="DBCP" > <property name ="driver" value ="com.mysql.jdbc.Driver" > </property > <property name ="url" value ="jdbc:mysql://localhost:3306/ssm" > </property > <property name ="username" value ="root" > </property > <property name ="password" value ="123456" > </property > </dataSource > </environment > </environments > <mappers > <mapper resource ="mapper/UserMapper.xml" > </mapper > </mappers > </configuration >
sql 映射配置文件 在映射文件中,需要命名空间,在调用时才知道需要在哪个 mapper 映射文件中找 sql。可能会有多个 mapper 映射文件。用 select 标签表示查询,id 表示 sql标识,parameterType 属性表示传入的参数类型,以及 resultType 表示返回结果的类型,拿到类型才能通过反射来赋值成员变量或者取值。最后是 sqlText 文本。这样就可以通过解析 mapper 配置文件获取执行一次 sql 语句所有的信息了。
1 2 3 4 5 6 7 <mapper namespace ="test" > <select id ="findUserById" parameterType ="cn.lizhaoloveit.po.User" resultType ="cn.lizhaoloveit.po.User" statementType ="prepared" > SELECT * FROM user WHERE id = #{id} </select > </mapper >
用配置文件类来存储 xml 中的信息 configuration
1 2 3 4 5 6 7 8 public class Configuration { private DataSource dataSource; private Map<String, MappedStatement> statementMap = new HashMap<>(); public void addMappedStatement (String statementId, MappedStatement mappedStatement) { statementMap.put(statementId, mappedStatement); } }
实现
思路:将全局配置文件信息封装到一个对象中,后期只需要在对象中读取配置文件信息。(configuration 对象)。
在创建 sqlSession 过程中,需要完成复杂的初始化工作(configuration 的创建),而在获取 sqlSession 的时候,通常只需要创建对象并赋值 configuration 即可,所以使用工厂模式 SqlSessionFactory 创建 SqlSession,而 SqlSessionFactory 的创建需要完成一系列初始化工作,因此需要构建者 SqlSessionFactoryBuild 类来定制 SqlSessionFactory。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class SqlSessionFactoryBuilder { private Configuration configuration; public SqlSessionFactoryBuilder () { configuration = new Configuration(); } public SqlSessionFactory build (InputStream inputStream) { Document document = DocumentReader.createDocument(inputStream); XMLConfigParser configParser = new XMLConfigParser(configuration); configParser.parseConfiguration(document.getRootElement()); return build(); } public SqlSessionFactory build (Reader reader) { return build(); } private SqlSessionFactory build () { return new DefaultSqlSessionFactory(configuration); } }
获取连接:读取配置文件,获取数据源对象,根绝数据源获取连接对象。也就是通过 Configuration 对象获取 DataSource 对象,通过 DataSource 对象获取 Connection
执行 statement 操作(考虑执行那种 statement,不同的 statement 操作不同,参数也不同),读取配置文件,获取要执行的 sql 语句的 statement 类型,如何获取呢?要根据 statementID 查找 statement 对象(MappedStatement),进而获取 statement 类型。
根据以上需求,Configuration 对象中,不仅要有一个 DataSouce 信息。还应该有一个 Map<String, MappedStatement>
的集合存放所有的 sql 语句信息。Configuration (DataSource, Map<String, MappedStatement>)
解析 configuration 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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 public class XMLConfigParser { private Configuration configuration; public XMLConfigParser (Configuration configuration) { this .configuration = configuration; } public Configuration parseConfiguration (Element element) { parseEnviroments(element.element("environments" )); parseMappers(element.element("mappers" )); return configuration; } private void parseEnviroments (Element element) { String defaultId = element.attributeValue("default" ); List<Element> elements = element.elements("environment" ); for (Element enviroment : elements) { String envId = enviroment.attributeValue("id" ); if (envId == null || !envId.equals(defaultId)) return ; parseDataSource(enviroment); } } private void parseDataSource (Element element) { Element dataSourceEle = element.element("dataSource" ); String type = dataSourceEle.attributeValue("type" ); List<Element> elements = dataSourceEle.elements("property" ); Properties properties = new Properties(); for (Element propertyEle : elements) { String name = propertyEle.attributeValue("name" ); String value = propertyEle.attributeValue("value" ); properties.setProperty(name, value); } BasicDataSource dataSource = null ; if (type.equals("DBCP" )) { dataSource = new BasicDataSource(); dataSource.setDriverClassName(properties.getProperty("driver" )); dataSource.setUrl(properties.getProperty("url" )); dataSource.setUsername(properties.getProperty("username" )); dataSource.setPassword(properties.getProperty("password" )); } configuration.setDataSource(dataSource); } private void parseMappers (Element element) { List<Element> elements = element.elements("mapper" ); for (Element mapperEle : elements) { parseMapper(mapperEle); } } private void parseMapper (Element mapperEle) { String resource = mapperEle.attributeValue("resource" ); InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resource); Document document = DocumentReader.createDocument(inputStream); XMLMapperParser xmlMapperParser = new XMLMapperParser(configuration); xmlMapperParser.parse(document.getRootElement()); } }
解析完主配置文件后,configuration 中的 dataSource 就有值了。接下来解析 mappers。根据 mappers 标签,找到 mapper 配置文件的路径,再解析很多个 mapper 配置文件。
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 64 65 66 67 68 69 70 71 72 73 public class XMLMapperParser { private Configuration configuration; private String nameSpace; public XMLMapperParser (Configuration configuration) { this .configuration = configuration; } public void parse (Element rootElement) { nameSpace = rootElement.attributeValue("namespace" ); parseStatement(rootElement.elements("select" )); } private void parseStatement (List<Element> elements) { for (Element selectEle : elements) { String statementId = selectEle.attributeValue("id" ); String id = nameSpace + "." + statementId; String paramType = selectEle.attributeValue("parameterType" ); Class<?> paramClass = getClassType(paramType); String resultType = selectEle.attributeValue("resultType" ); Class<?> resultClass = getClassType(resultType); String statementType = selectEle.attributeValue("statementType" ); String sqlText = selectEle.getTextTrim(); SqlSource sqlSource = new SqlSource(sqlText); MappedStatement mappedStatement = new MappedStatement( id, paramClass, resultClass, statementType, sqlSource); configuration.addMappedStatement(id, mappedStatement); } } private Class<?> getClassType(String paramType) { if (paramType == null || paramType.equals("" )) return null ; try { Class<?> cls = Class.forName(paramType); return cls; } catch (ClassNotFoundException e) { return null ; } } }
将解析的内容封装成对象 MappedStatement,其中,SqlSource 类解析 sql,BoundSql 用来存放 sql 语句和占位符参数。
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 public class MappedStatement { private String id; private Class<?> parameterTypeClass; private Class<?> resultTypeClass; private String statementType; private SqlSource sqlSource; } public class SqlSource { private String sqlText; public SqlSource (String sqlText) { this .sqlText = sqlText; } public BoundSql getBoundSql () { ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler(); GenericTokenParser genericTokenParser = new GenericTokenParser("#{" , "}" , tokenHandler); String sql = genericTokenParser.parse(sqlText); return new BoundSql(sql, tokenHandler.getParameterMappings()); } } public class BoundSql { private String sql; private List<ParameterMapping> parameterMappings = new ArrayList<>(); public void addParamMapping (ParameterMapping parameterMapping) { this .parameterMappings.add(parameterMapping); } }
ParamenterMapping 类表示 sql 中的参数名,ParameterMappingTokenHandler 的作用是将占位名{id}
换成 ?
并将 id 存储到 ParamenterMapping 中的 name
中。
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 public interface TokenHandler { String handleToken (String content) ; } public class ParameterMapping { private String name; public ParameterMapping (String content) { this .name = content; } } public class ParameterMappingTokenHandler implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<>(); @Override public String handleToken (String content) { parameterMappings.add(buildParameterMapping(content)); return "?" ; } private ParameterMapping buildParameterMapping (String content) { ParameterMapping parameterMapping = new ParameterMapping(content); return parameterMapping; } } public class GenericTokenParser { private final String openToken; private final String closeToken; private final TokenHandler handler; public GenericTokenParser (String openToken, String closeToken, TokenHandler handler) { this .openToken = openToken; this .closeToken = closeToken; this .handler = handler; } public String parse (String text) { if (text == null || text.isEmpty()) { return "" ; } int start = text.indexOf(openToken, 0 ); if (start == -1 ) { return text; } char [] src = text.toCharArray(); int offset = 0 ; final StringBuilder builder = new StringBuilder(); StringBuilder expression = null ; while (start > -1 ) { if (start > 0 && src[start - 1 ] == '\\' ) { builder.append(src, offset, start - offset - 1 ).append(openToken); offset = start + openToken.length(); } else { if (expression == null ) { expression = new StringBuilder(); } else { expression.setLength(0 ); } builder.append(src, offset, start - offset); offset = start + openToken.length(); int end = text.indexOf(closeToken, offset); while (end > -1 ) { if (end > offset && src[end - 1 ] == '\\' ) { expression.append(src, offset, end - offset - 1 ).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } else { expression.append(src, offset, end - offset); offset = end + closeToken.length(); break ; } } if (end == -1 ) { builder.append(src, start, src.length - start); offset = src.length; } else { builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); } }
GenericTokenParser 是一个解析器,会调用 Tokenhandler 的 handleToken
方法,将整个内容替换,并且把去掉 openToken
和 closeToken
之后的文本通过 content 传递给 Tokenhandler。
最终 configuration 中的 statementMap 也被赋值,key为 statementId,value 是 mappedStatement 将 sql 的全部信息封装了进去。
配置文件的初始化就完成了。
SqlSession接口的实现类
上面提到 SqlSession 接口提供方法让开发者调用,
User user = session.selectOne("test.findUserById", id);
下面说,selectedOne 是如何完成 JDBC 工作的。
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 public interface Executor { <T> List<T> query (Configuration configuration, MappedStatement mappedStatement, Object param) ; } public class DefaultSqlSession implements SqlSession { private Configuration configuration; public DefaultSqlSession (Configuration configuration) { this .configuration = configuration; } @Override public <T> T selectOne (String statementId, Object param) { List<Object> list = selectAll(statementId, param); if (list != null && list.size() == 1 ) { return (T) list.get(0 ); } return null ; } @Override public <T> List<T> selectAll (String statementId, Object param) { Executor executor = new SimpleExecutor(); MappedStatement mappedStatement = configuration.getStatementMap().get(statementId); return executor.query(configuration, mappedStatement, param); } }
mybatis 库中还有一个重要的角色,Executor,执行者,专门用来执行 JDBC 操作,
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 64 65 66 public class SimpleExecutor implements Executor { @Override public <T> List<T> query (Configuration configuration, MappedStatement mappedStatement, Object param) { Connection connection = null ; List<T> result = new ArrayList<>(); DataSource dataSource = configuration.getDataSource(); try { connection = dataSource.getConnection(); SqlSource sqlSource = mappedStatement.getSqlSource(); BoundSql boundSql = sqlSource.getBoundSql(); String sql = boundSql.getSql(); String statementType = mappedStatement.getStatementType(); if (!"prepared" .equals(statementType)) return result; PreparedStatement preparedStatement = connection.prepareStatement(sql); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); Class<?> parameterTypeClass = mappedStatement.getParameterTypeClass(); if (parameterTypeClass == Integer.class) preparedStatement.setObject(1 , param); else { for (int i = 0 ; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); String name = parameterMapping.getName(); Field field = parameterTypeClass.getDeclaredField(name); field.setAccessible(true ); Object value = field.get(param); preparedStatement.setObject(i + 1 , value); } } ResultSet resultSet = preparedStatement.executeQuery(); Class<?> resultTypeClass = mappedStatement.getResultTypeClass(); while (resultSet.next()) { Object returnObj = resultTypeClass.newInstance(); ResultSetMetaData metaData = resultSet.getMetaData(); int count = metaData.getColumnCount(); for (int i = 1 ; i <= count; i++) { String columnName = metaData.getColumnName(i); Field field = resultTypeClass.getDeclaredField(columnName); field.setAccessible(true ); field.set(returnObj, resultSet.getObject(columnName)); } result.add((T) returnObj); } } catch (Exception e) { e.printStackTrace(); } return result; } }
原理就是运用反射给参数赋值,运用反射给结果集赋值。进行 JDBC 操作。关于 JDBC 跳转传送门:JDBC
源码阅读方法
找主线
找入口
记笔记(类名#方法名(数据成员变量))
参考其他人的源码阅读经验
通过阅读源码,提升对设计模式的理解,提升编程能力,找到问题根源解决问题。
Executor Mybatis 执行器,是MyBatis 调度核心,负责SQL语句的生成和查询缓存的维护
StatementHandler 封装了 JDBC Statement 操作,负责对 JDBC statement 的操作,如设置参数、将 statement 结果集转换成 List 集合。
ParameterHandler 负责对用户传递的参数转换成 JDBC Statement 所需要的参数。
ResultSetHandler 将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合
TypeHandler java 数据类型和 jdbc 数据类型之间的映射和转换