在当前的日常开发中,mybatis这样的一个框架的使用,是很多程序员都无法避开的。大多数人都知道mybatis 的作用是为了避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。因为在开始接触使用Java操作数据库的时候,我们都是使用JDBC的。
自从有了持久化框架之后,使用持久化框架已经是“理所当然”的了,虽然我们已经脱离了使用JDBC是阶段了,但是这毕竟是基础的知识,所以本篇文章将会从JDBC入手。其实Mybatis就是对JDBC进行的封装。那就言归正传吧!
废话不多数,先来段代码来说明问题:
public class TestMain {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "zfy123456");
connection.setAutoCommit(false);
PreparedStatement ps = connection.prepareStatement("insert into dept values(?,?,?)");
ps.setInt(1,10000);
ps.setString(2,"test");
ps.setString(3,"test");
try{
ps.executeUpdate();
}catch (Exception e) {
connection.rollback();
e.printStackTrace();
}finally {
if(ps != null) {
ps.close();
}
if (connection != null) {
connection.close();
}
}
}
}
对于上面的代码中其大体流程就是:
mybatis学习日常文档:http://www.mybatis.org/mybatis-3/zh/index.html,以下代码参考mybatis官网。
测试类:
public class MybatisTest {
@Test
public void test() throws Exception {
User user = new User();
user.setAddress("北京市海淀区");
user.setBirthday(new Date(2000-10-01));
user.setSex("男");
user.setUsername("李清源");
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.insert("insertUser", user);
sqlSession.commit();
sqlSession.close();
}
}
实体类:
public class User {
private int id;
private String username;
private Date birthday;
private String sex;
private String address;
// 省略get、set、toString方法
}
Mapper接口:
public interface UserMapper {
void insertUser(User user) throws Exception;
}
Mapper配置文件:
insert into user(username,birthday,sex,address) values (#{username},#{birthday},#{sex},#{address})
mybatis配置文件:
此处由于代码太多就先省略config.properties配置文件,网上可自己参考。从上面的测试类代码中可以看出,mybatis的操作流程大体如下:
对于Resources.getResourceAsStream("mybatis-config.xml")代码中,关于对配置文件加载成输入流的代码,就不赘述了,直接来看SqlSessionFactoryBuilder中的build方法吧。来看看mybatis的核心配置文件时如何被加载的。那就先来看下build方法的源码:
SqlSessionFactoryBuilder.java
// 调用读取流的方法入口,这里的读取流就是指向所创建的工程中的核心配置文件
public SqlSessionFactory build(InputStream inputStream) {
// 调用重载的build方法,这三个参数的含义分别是:读取的配置文件的信息、将要指定的环境、所要使用的web的属性文件,
// 不过这里后面两个参数都是为null
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 首先创建XML解析对象,这对象实际上是对XPathParser封装的工具对象,这个对象主要是针对核心配置文件进行相关读取的
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// parser.parse()才是对XML进行真正的解析,解析完之后然后调用重载方法把parser对象放到DefaultSqlSessionFactory中去
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
// 关闭流,这就是我们使用流后不需要自己关闭的原因
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
上面的代码中,首先通过所传入的inputStream, environment, properties这几个参数创建XMLConfigBuilder 对象,然后嗲用这个对象中的parse()方法来进行解析,最后把解析完的对象放到DefaultSqlSessionFactory对象中去。代码如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
前面说到parser.parse()才是对mybatis核心配置文件的解析,那么继续看这个方法的代码到底做了什么。代码如下:
XMLConfigBuilder.java
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// parser.evalNode("/configuration")是为了定位核心配置文件中'configuration'元素的节点(根目录标签)
// 在获得根标签之后,然后对根标签下的信息进行解析
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
上面代码中首先只是做了一个防止多线程加载的操作,然后把parsed 设置为true,然后定位到mybatis核心配置文件的根标签configuration,在定位好根标签后,再对其根标签下的所有字标签进行逐一的解析。最后返回一个configuration对象。那来继续看下parseConfiguration方法中是如何解析根标签下的所有字标签。这里就先只对mappers字标签进行解析,对于上面开始的核心配置代码中的properties、typeAliases、environments,就先不赘述了,否则本篇文章将会太长。先不多说,继续看代码:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 对核心文件中的各种标签进行解析
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析mappers子标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
上面的代码中就是对configuration根标签下的所有字标签进行解析的,这里就以mappers标签的解析为例。那来继续看下 mapperElement(root.evalNode("mappers"))方法:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 因为mapper标签中的子标签存在两种写法,分别是:package、mapper
for (XNode child : parent.getChildren()) {
// 如果子标签的存在"package"的名称,则走此段代码
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
// 在和获取mapperPackage信息后,然后把它添加到configuration对象中,其实mapperPackage就是当前文件的路径
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 在获取到resource、url、mapperClass信息之后,下面便对这些信息是否存在进行判断,然后走相应的逻辑代码
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// 当resource != null时,在获取到对应的resource信息,后然后放到新建的XMLMapperBuilder对象中
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 最后通过mapperParser对象去解析,这后面所做的一切工作就是把mapper文件中的信息解析出来后,然后放到configuration对象中去,为后续最准备
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 同上
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class> mapperInterface = Resources.classForName(mapperClass);
// 同"package".equals(child.getName())的情况
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
这段代码首先判断parent != null的情况下,才执行后续操作,否则则什么都不做。当不为null的情况下,便开始对mappers下的所有子标签进行遍历并解析。后面的操作大体流程就是,如果子标签的存在"package"的名称,则执行相关代码,否则进入else代码,在else代码块中,先获取esource、url、mapperClass,然后对各自是否为空的条件,执行相关代码。具体看代码中的注释。因为开始所给的配置文件中的mappers标签下的子标签是package,所有这里我们就只对这个逻辑下的代码进行解析。
当子标签是package时,先获取其mapperPackage,然后放到mapper里。这里主要的工作在onfiguration.addMappers(mapperPackage),那就继续看下这块代码。
Configuration.java
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
MapperRegistry.java
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
public void addMappers(String packageName, Class> superType) {
ResolverUtil> resolverUtil = new ResolverUtil>();
// 获取此路径下后缀为.lass 的文件
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set>> mapperSet = resolverUtil.getClasses();
for (Class> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
上面代码中,先获取后缀为.lass 的文件,然后再把这些文件信息传递给Set,最后遍历Set,同时调用addMapper(mapperClass)方法。
public void addMapper(Class type) {
// 判断所获取到的类类型是否为Interface类型
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(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.
// 将type和config信息放到新建的MapperAnnotationBuilder对象中,config中主要包含environment这些信息
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 然后继续解析
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
上述代码中,首先判断锁获取到的类是否为接口类型的,如果是则先把loadCompleted 为false,然后以type为key放到knownMappers中去,后面再通过config和type创建MapperAnnotationBuilder,这里这个对象是设计注解的,因为我们没有用到注解,这里就不赘述了。那就看下parser.parse()的代码是如何做操作的。
MapperAnnotationBuilder.java
public void parse() {
String resource = type.toString();
// 判断configuration是否包含resource信息
if (!configuration.isResourceLoaded(resource)) {
// 重点:这里才真正的加载后缀为.xml文件的信息
loadXmlResource();
// 把resource添加到configuration中
configuration.addLoadedResource(resource);
// 设置MapperBuilderAssistant当前的namespace
assistant.setCurrentNamespace(type.getName());
// 解析缓存
parseCache();
// 解析缓存引用
parseCacheRef();
Method[] methods = type.getMethods();
// 对接口中的方法进行解析
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 解析方法
parsePendingMethods();
}
上面代码的逻辑很清晰,首先判断configuration是否包含resource信息,如果不包含,那么就继续后续的流程。当进入后续流程时,首先就是加载xml,这里的就开始正式的加载mapper的xml了。然后再进行一些设置和解析缓存等一些东西。这里主要看下loadXmlResource()这个方法:
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
// 判断configuration中是否包含了"namespace:" + type.getName())的mapper文件
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 获取到后缀信息为.xml的文件路径
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
// 获取mapper文件流
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
}
if (inputStream != null) {
// 如果流信息不为空,把流信息、assistant.getConfiguration()、xmlResource、configuration.getSqlFragments()和type的name放到XMLMapperBuilder中
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
// 然后进行解析
xmlParser.parse();
}
}
}
这里也没有什么复杂的逻辑,无非还是先判断下是否包含,然后获取到后缀为.xml的文件,当获取到后再通过锁获取到的xmlResource和ype.getClassLoader()这两个参数获取文件的输入流,在获得输入流后,再通过输入流等一些相关信息,去新建一个xml的解析对象,新建完成后便通过此对象中的解析方法去解析,那就来看看这个方法:
XMLMapperBuilder.java
public void parse() {
// 判断resource是否在configuration中
if (!configuration.isResourceLoaded(resource)) {
// 1.首先定位到mapper文件中的根节点mapper,2.然后对该节点下的所有节点进行解析
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 绑定mapper到工作空间
bindMapperForNamespace();
}
// 解析mapper文件中ResultMaps节点下的信息
parsePendingResultMaps();
// 解析缓存引用
parsePendingCacheRefs();
// 解析Statement
parsePendingStatements();
}
这段代码很简单,无非就是一些方法调用的逻辑,但这里所要关注的重点还是configurationElement(parser.evalNode("/mapper"))这个方法,这里便开始对于每个mapper文件的正式调用了,来看看这个方法中具体做了什么。代码如下:
private void configurationElement(XNode context) {
try {
// 获取mapper节点的namespace
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 接下来就是对mapper节点下的各种节点进行解析了,不准备赘述,但或许讲下buildStatementFromContext方法
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
// 解析"select|insert|update|delete"等标签的信息
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
上面代码中,基本上就是对mapper配置文件中的一些标签的解析,由于我开始所提供的配置文件中之涉及了 上面的两段代码的逻辑还是很简单的,还是只是做一个简单的判断,符合相关条件就执行相关的逻辑代码,buildStatementFromContext方法中所遍历的list的内容,其实就是"select|insert|update|delete"等标签下的SQL模板,然后再通过一些相关参数新建XMLStatementBuilder对象,新建对象之后再调用statementParser.parseStatementNode(),继续看这个方法的代码: 这段代码看上好像真的很复杂,因为这里涉及到很多参数的获取和标签的解析,但是在这段代码中现在所需要关注的只是SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass)这行代码,其他代码功能请看注释吧,有兴趣的同学可以自己在去细看一下里面的代码。 这段代码没什么说的,主要的还是builder.parseScriptNode()的调用。代码如下: 这里就是判断所使用的是否是动态SQL,这里并不是动态SQL,那就看非动态的逻辑代码了。 上面第一个方法代码里,主要要关注的是getSql(configuration, rootSqlNode)这个方法,下面的那个就是this 的构造函数了,先继续探索吧! 这段代码还是只是根据一些参数创建对象,context.getSql()和rootSqlNode.apply(context)其实只是把配置文件里的SQL模板变成个字符串,代码同学们可以自己看一下。那我们先看下构造函数代码做了什么。在构造函数的代码里会根据configuration来创建个SQLSource解析器对象,然后通过sqlSourceParser.parse(sql, clazz, new HashMap 在这段代码里首先创建ParameterMappingTokenHandler这个对象,然后通过这个对象先解析SQL模板中的#{username},#{birthday},#{sex},#{address}这些标签,然后就开始真正的SQL解析,其实就是字符串拼接过程,不信你点进去看看。 是吧,我没有骗你吧,从return builder.toString()这里就可以看出来了,这段代码的主要的流程只是拼接字符串而已,没什么好说的,如果对字符串拼接感兴趣的小伙伴感兴趣的话,可以自己去研究下。 在SqlSourceBuilder.java代码中的parse方法中,最后所返回的就是一个StaticSqlSource对象,这里就是这个对象中包含,我们所解析出来的SQL语句就是如下图中的sql这个所指向的SQL语句,已经不是originalSql所指向的SQL模板了。 本篇文章就先结束了,由于篇幅原因,对于SQL语句的执行这些操作,将会在下一篇文章进行解析。谢谢同学们的阅读,如果有错误,也请同学们指正。 private void buildStatementFromContext(List
private void buildStatementFromContext(List
XMLStatementBuilder.java
public void parseStatementNode() {
// 从这里开始,各种或许信息
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
// 获取驱动语言
LanguageDriver langDriver = getLanguageDriver(lang);
Class> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
// 获取SQL命令的类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 判断当前的SQL命令类型是否是select类型的
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
// 关于include标签的解析
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
// 解析 selectKey 标签
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre:
XMLLanguageDriver.java
public SqlSource createSqlSource(Configuration configuration, XNode script, Class> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
XMLScriptBuilder.java
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
// 判断是否是动态SQL
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 不是动态SQL
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
RawSqlSource.java
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class> parameterType) {
// 在这个构造函数里做getSql的操作
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class> parameterType) {
// 生成SQLSource解析器
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class> clazz = parameterType == null ? Object.class : parameterType;
// 对SQL进行具体的解析,这里的sqlSource中包含sql语句、字段属性映射信息、configuration信息
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
// 根据configuration对象,创建一个动态对象
DynamicContext context = new DynamicContext(configuration, null);
// 把节点中SQL模板变成一个字符串
rootSqlNode.apply(context);
return context.getSql();
}
SqlSourceBuilder.java
public SqlSource parse(String originalSql, Class> parameterType, Map
GenericTokenParser.java
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
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] == '\\') {
// this open token is escaped. remove the backslash and continue.
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
} else {
// found open token. let's search close token.
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] == '\\') {
// this close token is escaped. remove the backslash and continue.
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) {
// close token was not found.
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();
}