前言
mybatis是目前进行java开发 dao层较为流行的框架,其较为轻量级的特性,避免了类似hibernate的重量级封装。同时将sql的查询与与实现分离,实现了sql的解耦。学习成本较hibernate也要少很多。
我们可以先简单的回顾下mybatis的使用方式。一般两种方式,单独使用或者配合spring使用。当然了 我们一般都是使用Spring集成的方式 。下面简要写明下两种的关键步骤
单独使用
这儿我们采用手动添加xml配置文件的形式,先加载mybatis配置文件,这儿简要列一下配置文件以及说明
<?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> <!--用来进行属性配置--> <properties> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://192.168.0.1:3306/test"/> <property name="username" value="root"/> <property name="password" value="mysql"/> <!--如果为true 则可以有默认配置 例如下面的password--> <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/> </properties> <!--这是非常重要的设置 会改变mybatis的行为--> <settings> <!--全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存。 默认为true--> <setting name="cacheEnabled" value="true"></setting> <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态 类似于hibernate的懒加载 默认false--> <setting name="lazyLoadingEnabled" value="false"></setting> <!--当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载 默认false 但是<=3.4.1为true--> <setting name="aggressiveLazyLoading" value="false"></setting> <!-- 是否允许单一语句返回多结果集(需要兼容驱动)。 默认为true 目前测试都不起作用--> <setting name="multipleResultSetsEnabled" value="true"></setting> <!--使用列标签代替列名。不同的驱动在这方面会有不同的表现, 具体可参考相关驱动文档或通过测试 这两种不同的模式来观察所用驱动的结果。 默认为true 为false则不能使用别名--> <setting name="useColumnLabel" value="true"></setting> <!-- 在执行添加记录之后可以获取到数据库自动生成的主键ID。(如果支持自动生成 如自增) 默认为false--> <setting name="useGeneratedKeys" value="true"></setting> <!--指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射;PARTIAL 只会自动映射 没有定义嵌套结果集映射的结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套)。--> <setting name="autoMappingBehavior" value="PARTIAL"></setting> <!--指定发现自动映射目标未知列(或者未知属性类型)的行为。即查询的数据在返回值有没映射上的结果 NONE: 不做任何反应 WARNING: 输出提醒日志 ('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN) FAILING: 映射失败 (抛出 SqlSessionException)--> <setting name="autoMappingUnknownColumnBehavior" value="NONE"></setting> <!--配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。--> <setting name="defaultExecutorType" value="SIMPLE"></setting> <!--等待数据库响应的时间--> <setting name="defaultStatementTimeout" value="5000"></setting> <!--获取的连接数--> <setting name="defaultFetchSize" value="5"></setting> <!--允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为false--> <setting name="safeRowBoundsEnabled" value="false"></setting> <!--允许在嵌套语句中使用分页(ResultHandler)。如果允许使用则设置为false。--> <setting name="safeResultHandlerEnabled" value="true"></setting> <!--是否自动开启驼峰命名与bean映射 默认为false--> <setting name="mapUnderscoreToCamelCase" value="true"></setting> <!--MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。 即使一级缓存 局部缓存 若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。--> <setting name="localCacheScope" value="SESSION"></setting> <!--当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。 某些驱动需要指定列的 JDBC 类型, 多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。--> <setting name="jdbcTypeForNull" value="OTHER"></setting> <!--懒加载的方法--> <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"></setting> <!--指定动态 SQL 生成的默认语言。 目前系统只有xml 默认即为下面的配置--> <setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"></setting> <!--即默认的类型处理器 可以处理pojo类中的枚举类型 插入以及查询 也可以在xml中使用typeHandler EnumOrdinalTypeHandler即不会使用自己定义的code EnumTypeHandler会使用自定义的code--> <setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"></setting> <!--为空时需不需要调用set为null的方法 这个map则为put方法 注意map的话为空则不会有为null值的key 所以最好为true 默认false--> <setting name="callSettersOnNulls" value="true"></setting> <!--即如果所有的列都为空会返回null 如果为true 则会新建空实例返回 --> <setting name="returnInstanceForEmptyRow" value="false"></setting> <!--mybatis 日志前缀 这儿只有查询有关的日志--> <setting name="logPrefix" value="megalith-hamizz: "></setting> <!--指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING --> <setting name="logImpl" value="SLF4J"></setting> <!--mybatis创建具有延迟加载能力的工具 CGLIB | JAVASSIST 默认后者--> <setting name="proxyFactory" value="JAVASSIST"></setting> <!--自定义的虚拟文件系统--> <!--<setting name="vfsImpl" value=""></setting>--> <!--貌似这儿如果为true 那么在不能在参数不加注解,直接使用#{0} 或者#{param1} 这种,如果为false则可以 3.4.1开始--> <setting name="useActualParamName" value="true"></setting> <!--指定一个提供Configuration实例的类。 这个被返回的Configuration实例用来加载被反序列化对象的懒加载属性值。 这个类必须包含一个签名方法static Configuration getConfiguration(). (从 3.2.3 版本开始)--> <!--<setting name="configurationFactory" value=""></setting>--> </settings> <!--entity 别名 在使用type或者resultType可以直接用这个--> <typeAliases> <!--这个是只扫描某个包--> <!--<package name="com.code.analysis.mybatis.entity" ></package>--> <!--可以直接在bean上添加 @Alias--> <typeAlias type="com.code.analysis.mybatis.entity.Test" alias="Test"></typeAlias> <!--还有内置的一些别名 如map等--> </typeAliases> <!--类型处理器 注意如果查询用这个 则必须使用resultMap--> <typeHandlers> <!--<typeHandler handler=""></typeHandler>--> </typeHandlers> <!--即mybatis--> <objectFactory type="com.code.analysis.mybatis.config.MyObjectFactory"> <property name="dilg" value="100"/> </objectFactory> <!--插件配置--> <plugins> <plugin interceptor="com.code.analysis.mybatis.plugin.ExecutorPlugin"></plugin> </plugins> <!--环境选择 可以配置多个环境 在构建sqlSessionFacotry的时候可以选择--> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="com.code.analysis.mybatis.config.DuidDataSource"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/tjfx"/> <property name="username" value="root"/> <property name="password" value="mysql"/> </dataSource> </environment> <environment id="prod"> <!--如果使用了spring集成 那么spring会覆盖掉这儿的事物--> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password:123456}"/> </dataSource> </environment> </environments> <!--sql.xml文件扫描--> <mappers> <mapper resource="mappers/UserInfoMapper.xml"/> </mappers> </configuration>
然后新建MybatisConfig类,使用静态代码块读取mybatisCofig.xml文件的流,并根据这个流构建SqlSessionFactory
/** * @Description: * @author: zhoum * @Date: 2019-02-14 * @Time: 15:38 */ public class MybatisConfig { private static SqlSessionFactory sqlSessionFactory; static { try { InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml"); //这儿主要可以传入的参数有configuration 或者配置文件流 或者环境 或者配置属性 很灵活 sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream,"development"); } catch (IOException e) { e.printStackTrace(); } } public static SqlSession getSession(){ return sqlSessionFactory.openSession(); } }
使用方式,假设已经建立了数据库,并且建立了名为user_info的表,系统也建立了对应的mapper.xml 且已经写了对应的sql ,mapper接口 且已经写了对应的方法,则可以按如下方式使用mybatis
public class MainTest { public static Logger logger = LoggerFactory.getLogger(MainTest.class); public static void main(String[] args) throws IOException { SqlSession session = MybatisConfig.getSession(); UserInfoMapper mapper = session.getMapper(UserInfoMapper.class); UserInfo user = mapper.seleceCase(); System.out.println(user); logger.info("查询成功"); session.close(); } }
到这儿我们就成功的使用了mybatis,根据这个main方式我们就可以得知,我们是根据SqlSessionFactory打开一个SqlSession 根据这个SqlSession拿到对应的接口,然后执行方法即可执行对应mapper.xml中的sql命令 并封装数据返回。核心有两个地方,1是构造SqlSessionFactory,2是根据SqlSession获得mapper。 关于2我们后面的文章再详细分析。本文先主要讲如何构造一个SqlSessionFactory,根据上面的源码我们接着看
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //根据传入的参数获取对应的XMLConfigBilder XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //执行构造方法 构造一个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. } } } public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
上面的代码可以得知,程序根据我们的配置文件流创建了一个XmlConfigBuilder对象,并执行对应的parse()方法生成一个Configuration对象,然后使用这个config创建了一个默认的DefaultSqlSessionFactory。核心又有两个地方 生成创建XmlConfigBuilder 以及他的parse()方法,这两个方法因为在使用Spring集成的时候也会用上,所以这里不讲,下面集成Spring时到这儿了则一块讲,使用Spring集成的方式也是我们标准的用法,下面简单说下Spring集成的方式。
Spring集成
这儿使用springboot的方式,可以简化下其他配置。这儿主要说明一些核心步骤,
需要几个核心依赖包
//添加jdbc compile group: 'org.springframework.boot', name: 'spring-boot-starter-jdbc', version: '2.0.3.RELEASE' //添加mysql驱动 compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.18' compile group: 'com.alibaba', name: 'druid', version: '1.1.21' //添加mybatis compile group: 'org.mybatis', name: 'mybatis', version: '3.5.3' compile group: 'org.mybatis', name: 'mybatis-spring', version: '2.0.3'
springboot配置文件
spring: application: name: bootjar datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource logging: level: root: info
mybatis的配置类 由于一些配置功能在上面的config文件中已经说明了,所以这儿就比较的简化配置
@Configuration public class MybatisConfig { @Bean @Autowired public SqlSessionFactoryBean initMybatis(DataSource dataSource) throws IOException { org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); //设置配置 sqlSessionFactoryBean.setConfiguration(configuration); //设置数据源 这个数据源是spring加载的 sqlSessionFactoryBean.setDataSource(dataSource); //扫描xml路径 sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**.xml")); return sqlSessionFactoryBean; } }
使用的方式依然如下,当然spring有默认可以注入mapper接口的方式,不过本文主要探究SqlSessionFactory加载原理,所以暂时不讲
@Autowired private SqlSessionFactory sqlSessionFactory; public UserInfo getUser(){ SqlSession sqlSession = sqlSessionFactory.openSession(); UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class); UserInfo userInfo = mapper.selectInfo(); sqlSession.close(); return userInfo; }
上面就是普通使用,和spring集成的方式。接下来我主要通过Spring集成的案例来讲解
正文
1.加载mapper文件数据
mapper文件即我们编写sql语句的地方,也是mybatis解耦合的设计标志
我们在spring配置mybatis的方法中可以看到这行代码
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**.xml"));
而设置的mypperLocation点进源码查看,可以得知通过 new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**.xml")方法获取到了我们系统中所有mapper.xml文件的资源,并赋值给了SqlSessionFactoryBean。当然这儿我选用了PathMatchingResourcePatternResolver查找器,也可以使用其他的查找器
private Resource[] mapperLocations; public void setMapperLocations(Resource... mapperLocations) { this.mapperLocations = mapperLocations; }
所以我们可以得知加载mapper.xml的主要方法就在这儿,我们接着往里面看逻辑
String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; @Override public Resource getResource(String location) { return getResourceLoader().getResource(location); } @Override public Resource[] getResources(String locationPattern) throws IOException { //判断下获取资源的地址不能为空 Assert.notNull(locationPattern, "Location pattern must not be null"); //判断是否是根据类加载路径地址来加载 if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { // //判断后面的有用地址中是否有通配符 if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { //根据通配符 查找满足通配符的文件资源 return findPathMatchingResources(locationPattern); } else { // 查找下面所有的文件资源 return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); } } else { //解析出有用的地址开头 int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : locationPattern.indexOf(':') + 1); //判断解析后的地址开头后的地址中是否有 "*"或者 "?" 即是否有通配符 if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { // 则返回所有满足条件的资源 return findPathMatchingResources(locationPattern); } else { // 查询单个资源 return new Resource[] {getResourceLoader().getResource(locationPattern)}; } } }
可以看到这个方法主要的作用就是加载到传入的指定的location中满足条件的文件,我们传入的是classpath*:/mapper/**.xml,所以这儿会执行findPathMatchingResources方法 并会传入/mapper/**.xml参数,我们接着源码看
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { //根据我们传入的地址 解析出顶级文件夹地址 本例即class*:/mapper/ 去掉了后面的匹配规则 String rootDirPath = determineRootDir(locationPattern); //根据顶级地址截取后面的匹配规则,如本例则为**.xml String subPattern = locationPattern.substring(rootDirPath.length()); //获取顶级文件夹的转换为Resource 这儿又会回去执行刚才的即上面的findAllClassPathResources(String location)方法 Resource[] rootDirResources = getResources(rootDirPath); Set<Resource> result = new LinkedHashSet<>(16); //遍历找到的顶级文件夹资源 for (Resource rootDirResource : rootDirResources) { //这个方法可以自己继承 做一下自定义处理 默认不处理 直接返回 rootDirResource = resolveRootDirResource(rootDirResource); //获取到文件夹绝对路径 URL rootDirUrl = rootDirResource.getURL(); //对特殊的文件夹做的一些额外处理 if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) { URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl); if (resolvedUrl != null) { rootDirUrl = resolvedUrl; } rootDirResource = new UrlResource(rootDirUrl); } //是否是jboss的vfs资源 if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) { result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher())); } //是否是jar资源 如果是做处理 else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) { result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern)); } //否则进行普通处理 else { result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern)); } } if (logger.isDebugEnabled()) { logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result); } //将找到的结果结果转换为数组返回 return result.toArray(new Resource[0]); }
可以看到该方法主要是根据我们传入的location地址,解析到其根文件夹,以及匹配规则,根据这两者来执行doFindPathMatchingFileResources方法获取到满足匹配规则的文件,我们接着往下看
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { File rootDir; try { //根据resource解析出对应的绝对路径的文件夹 rootDir = rootDirResource.getFile().getAbsoluteFile(); } catch (IOException ex) { if (logger.isWarnEnabled()) { logger.warn("Cannot search for matching files underneath " + rootDirResource + " because it does not correspond to a directory in the file system", ex); } return Collections.emptySet(); } //继续执行 return doFindMatchingFileSystemResources(rootDir, subPattern); } protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Looking for matching resources in directory tree [" + rootDir.getPath() + "]"); } //获取到所有的满足条件的文件 Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern); Set<Resource> result = new LinkedHashSet<>(matchingFiles.size()); for (File file : matchingFiles) { //将所有的文件转换为Resource放入result并返回 result.add(new FileSystemResource(file)); } return result; }
这两步都是做了一些普通的转换,关键在于retrieveMatchingFiles方法获取到的满足条件的文件,我们接着看
protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException { //判断文件是否存在 if (!rootDir.exists()) { // Silently skip non-existing directories. if (logger.isDebugEnabled()) { logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist"); } return Collections.emptySet(); } //判断是否是文件夹 if (!rootDir.isDirectory()) { // Complain louder if it exists but is no directory. if (logger.isWarnEnabled()) { logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory"); } return Collections.emptySet(); } //判断文件夹是否可读 if (!rootDir.canRead()) { if (logger.isWarnEnabled()) { logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() + "] because the application is not allowed to read the directory"); } return Collections.emptySet(); } //得到完整的文件夹路径 并且将不同机器的分隔符统一为/ String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/"); //如果匹配规则前每加/ 则路径后面需要加上 为的是拼凑城一个完整路径的匹配规则 即类似d:/test/mapper/**.xml if (!pattern.startsWith("/")) { fullPattern += "/"; } //拼接完整匹配规则 并且将不同机器的分隔符统一为/ fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/"); Set<File> result = new LinkedHashSet<>(8); //继续执行 doRetrieveMatchingFiles(fullPattern, rootDir, result); return result; }
这个方法主要对路径做了一些适应处理 继续看关键的
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Searching directory [" + dir.getAbsolutePath() + "] for files matching pattern [" + fullPattern + "]"); } //找到文件夹下所有的文件或者文件夹 File[] dirContents = dir.listFiles(); if (dirContents == null) { if (logger.isWarnEnabled()) { logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]"); } return; } //排个序 Arrays.sort(dirContents); //遍历文件 for (File content : dirContents) { //得到每个文件的绝对路径 String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/"); //如果是文件夹 且文件夹满足通配规则 if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) { if (!content.canRead()) { //如果不可读 那就记录下日志即可 if (logger.isDebugEnabled()) { logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() + "] because the application is not allowed to read the directory"); } } else { //并且可读 则传入文件夹 以及通配符规则 set集合递归收集 doRetrieveMatchingFiles(fullPattern, content, result); } } //如果是文件且满足我们的通配符规则 则 添加进去 if (getPathMatcher().match(fullPattern, currPath)) { result.add(content); } } }
千呼万唤啊,做了这么多铺垫终于来到了最核心的加载方法了,通过这个方法可以得知,通过我们传入的根目录,遍历下面的文件或者文件夹,如果是文件且名字满足通配符就添加进我们的set,如果是文件夹且满足我们的通配符路径 则继续递归这个方法找到根目录下所有满足条件的文件夹 加入set并返回
好了 至此所有的mapper文件终于是加载到我们的SqlSessionFactoryBean中了 并由 mapperLocations(即Resource[]类型) 进行接收
2.SqlSessionFactorybean.buildSqlSessionFactory()方法
系统最终会调用我们配置的SqlSessionFactoryBeand.buildSqlSessionFactory()方法类创建需要的SqlSessionFactory 我们的接着看这个方法,这个方法总体如下
protected SqlSessionFactory buildSqlSessionFactory() throws Exception { //声明config final Configuration targetConfiguration; //声明xmlBuilder 解析config XMLConfigBuilder xmlConfigBuilder = null; //判断我们是否传入了config if (this.configuration != null) { targetConfiguration = this.configuration; if (targetConfiguration.getVariables() == null) { targetConfiguration.setVariables(this.configurationProperties); } else if (this.configurationProperties != null) { targetConfiguration.getVariables().putAll(this.configurationProperties); } } //如果没传入config 那是否传入了config文件地址 else if (this.configLocation != null) { //将传入的configLocation resource进行解析 xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties); //获得解析后的config targetConfiguration = xmlConfigBuilder.getConfiguration(); } else { //都没有的话就直接用系统默认的config LOGGER.debug( () -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration"); targetConfiguration = new Configuration(); Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables); } //如果objectFactory不为空则写入config Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory); //如果objectWrapperFactory不为空则写入config Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory); //如果vfs不为空则写入config Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl); //如果别名扫描包地址不为空 则注入别名 if (hasLength(this.typeAliasesPackage)) { //扫描包下所有类 并去除掉匿名类 非接口的类 成员类 ,将剩下的写入config的alias scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream() .filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface()) .filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias); } //如果执行别名不为空 则也写入config if (!isEmpty(this.typeAliases)) { Stream.of(this.typeAliases).forEach(typeAlias -> { targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias); LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'"); }); } //如果拦截器不为空 则将拦截器全部写入config if (!isEmpty(this.plugins)) { Stream.of(this.plugins).forEach(plugin -> { targetConfiguration.addInterceptor(plugin); LOGGER.debug(() -> "Registered plugin: '" + plugin + "'"); }); } //如果typeHandlersPackage包不为空 即类别转换器 扫描包下所有类 也写入config if (hasLength(this.typeHandlersPackage)) { scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass()) .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers())) .forEach(targetConfiguration.getTypeHandlerRegistry()::register); } //如果typeHandlers类不为空 即类别转换器 扫描包下所有类 也写入config if (!isEmpty(this.typeHandlers)) { Stream.of(this.typeHandlers).forEach(typeHandler -> { targetConfiguration.getTypeHandlerRegistry().register(typeHandler); LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'"); }); } //如果scriptingLanguageDrivers类不为空 也写入config if (!isEmpty(this.scriptingLanguageDrivers)) { Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> { targetConfiguration.getLanguageRegistry().register(languageDriver); LOGGER.debug(() -> "Registered scripting language driver: '" + languageDriver + "'"); }); } //默认的scriptingLanguageDrivers不为null的话也写入 Optional.ofNullable(this.defaultScriptingLanguageDriver) .ifPresent(targetConfiguration::setDefaultScriptingLanguage); //如果指定的数据库id不为空 则写入当前配置支持的数据库id if (this.databaseIdProvider != null) {// fix #64 set databaseId before parse mapper xmls try { targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource)); } catch (SQLException e) { throw new NestedIOException("Failed getting a databaseId", e); } } //缓存不为空则写入缓存 Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache); //如果xmlConfigBuilder不为空 即系统有ConfigLocation 则先解析找到的xml文件信息写入config if (xmlConfigBuilder != null) { try { //先将获取到的config信息写入config xmlConfigBuilder.parse(); LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'"); } catch (Exception ex) { throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex); } finally { ErrorContext.instance().reset(); } } //为config设置环境 以及事物处理工厂 如果没有设置默认使用Spring的事物管理 targetConfiguration.setEnvironment(new Environment(this.environment, this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory, this.dataSource)); //mapper扫描器如果不为空 即扫描mapper.xml文件的地址不为空 if (this.mapperLocations != null) { if (this.mapperLocations.length == 0) { //如果长度为0 说明虽然设置了 但是没找到对应的地址 LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found."); } else { //遍历所有的resource 即xml文件资源 for (Resource mapperLocation : this.mapperLocations) { //判断一下空 if (mapperLocation == null) { continue; } try { //为每个xml文件创建Mapper解析器 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments()); //进行解析 xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'"); } } } else { LOGGER.debug(() -> "Property 'mapperLocations' was not specified."); } return this.sqlSessionFactoryBuilder.build(targetConfiguration); } }
系统根据我们创建SqlSessionFactoryBean的 查看是否有传入Configuration ,然后根据情况分别处理,然后再将我们传入的mybatis功能组件加载到configuration中,然后如果我们设置了configLocation 则会根据这个加载对应的文件流然后解析。最后将我们加载的mapper文件,解析每个mapper.xml文件 并将信息加载到configuration 可见这个configuration保存了mybatis需要的所有信息。
关于其中的功能组件如拦截器会在后面介绍每个组件的时候专门说明,所以这儿就不讲了,主要针对几个核心的加载方法再说明下
构造XMLConfigBuilder
在我们没有声明configuration 而设置了configLocation时有如下代码
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties); //获得解析后的config targetConfiguration = xmlConfigBuilder.getConfiguration();
这儿即和我们上面手动配置mybatis时构建的xmlConfigBuilder一模一样 ,所以我们直接看他的构造到底如何
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); } private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 这儿创建了一个默认的configuration super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; }
上面代码执行了两方法,一个是新建了一个XPathParser 然后将这个解析器和环境值,参数传入了有参构造, 在有参构造中注意除了赋值操作外,还初始化了一个默认的configuration。 XPath我们都知道是一种xml处理器,所以很明显 这个XPathParse是用来解析xml文件的类,我们接着看new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()) ,这儿注意传入了一个xml解析实体器XMLMapperEntityResolver 。这个处理器注意是专门用来解析mybatis的config文件和mapper文件的,可以看下其中的部分定义信息
private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd"; private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd"; private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd"; private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd"; private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd"; private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";
好了 我们接着看新建xml解析器的逻辑
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) { this.validation = validation; this.entityResolver = entityResolver; this.variables = variables; XPathFactory factory = XPathFactory.newInstance(); this.xpath = factory.newXPath(); } public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) { //执行了一个初始化赋值方法 commonConstructor(validation, variables, entityResolver); //创建我们需要的document this.document = createDocument(new InputSource(inputStream)); }
这儿终于看到了我们需要的xml对象文件 document ,后面的xmlConfigBuilder肯定也是使用类中parse所带的document对象进行解析xml节点 获取对应的配置。 我们接着看
private Document createDocument(InputSource inputSource) { // important: this must only be called AFTER common constructor try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); //设置一些document的通用信息 factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); factory.setValidating(validation); factory.setNamespaceAware(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(false); factory.setCoalescing(false); factory.setExpandEntityReferences(true); //创建一个文件构造器 DocumentBuilder builder = factory.newDocumentBuilder(); //这儿注意设置了我们刚才传入的配置文件或者mapper文件的解析器 builder.setEntityResolver(entityResolver); builder.setErrorHandler(new ErrorHandler() { @Override public void error(SAXParseException exception) throws SAXException { throw exception; } @Override public void fatalError(SAXParseException exception) throws SAXException { throw exception; } @Override public void warning(SAXParseException exception) throws SAXException { // NOP } }); //根据文件流返回包含所有xml信息的document return builder.parse(inputSource);
到这儿 xmlConfiBuilder构造函数中就已经获取到了包含config文件所有信息的docment对象并创建对象成功,注意其中创建了一个默认的configuration。
XMLConfigBuilder.parse()方法
在构造SqlSessionfactory中我们可以看到如下的代码
//如果xmlConfigBuilder不为空 即系统有ConfigLocation 则先解析找到的xml文件信息写入config if (xmlConfigBuilder != null) { try { //先将获取到的config信息写入config xmlConfigBuilder.parse(); LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'"); } catch (Exception ex) { throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex); } finally { ErrorContext.instance().reset(); } }
可以看到 系统在判定我们手动设置了config文件的话 最后会执行这个方法,也就是执行上面我们创建的xmlConfigBuilder方法,由此可知如果我们既创建了configuration 又设置了config文件地址,系统最终不会加载config文件中的东西。我们直接往下分析
public Configuration parse() { //不能重复解析 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } //设置解析标志 parsed = true; //执行解析方法并传入config节点 parseConfiguration(parser.evalNode("/configuration")); //返回这个config return configuration; }
这儿主要设置下是否解析标志,然后拿到xml中的configuration节点信息进行解析
private void parseConfiguration(XNode root) { try { //拿到properties节点 设置configuration的variables propertiesElement(root.evalNode("properties")); //拿到setting配置 Properties settings = settingsAsProperties(root.evalNode("settings")); //根据对应的setting配置设置configuration的vfs loadCustomVfs(settings); //根据对应的setting配置设置configuration的日志配置 loadCustomLogImpl(settings); //拿到typeAliases节点 设置configuration的别名 typeAliasesElement(root.evalNode("typeAliases")); //拿到plugins节点 设置configuration的拦截器 pluginElement(root.evalNode("plugins")); //拿到objectFactory节点 设置configuration的对象工厂 objectFactoryElement(root.evalNode("objectFactory")); //拿到objectWrapperFactory节点 设置configuration的对象包装工厂 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); //拿到reflectorFactory节点 设置configuration的反射工厂 reflectorFactoryElement(root.evalNode("reflectorFactory")); //设置其他所有的setting参数到对应的mybatis中 settingsElement(settings); //拿到environments节点 设置configuration的所有环境 environmentsElement(root.evalNode("environments")); //拿到databaseIdProvider节点 设置configuration的数据库id databaseIdProviderElement(root.evalNode("databaseIdProvider")); //拿到typeHandlers节点 设置configuration的类型处理器 typeHandlerElement(root.evalNode("typeHandlers")); //拿到mappers节点 设置configuration的mapper扫描规则并添加mapper mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
这里面可以看到是根据config文件的节点信息分别设置,囿于篇幅我就不每个细说了,大概意思相信大家也能明白 这儿只着重说下mapperElement方法,这个方法主要是解析mapper文件并将mapper文件中对应的信息放入configuration中对应的装载类
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { //如果是package标签 即指定接口包 此时映射文件和包必须在一个文件夹下且名字要对应 //这儿如果有不明白可以查看这篇博文https://www.cnblogs.com/canger/p/9911958.html if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); //根据包名添加mapper文件与接口 configuration.addMappers(mapperPackage); } else { //否则都是mapper标签 mapper有三种 url resource class String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); //第一种如果是resource类型 即引入classpath路径的相对资源 注意此方法是获取xml文件 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); //获取mapper文件流 InputStream inputStream = Resources.getResourceAsStream(resource); //创建XMLMapperBuilder并解析 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } //第二种如果是url类型 通过url引入网络资源或者本地磁盘资源 注意此方法是获取xml文件 else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); //获取mapper文件流 InputStream inputStream = Resources.getUrlAsStream(url); //创建XMLMapperBuilder并解析 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } //第二种如果是class类型 通过class即接口找到对应的mapper.xml 注意此方法是获取接口,所以此时映射文件和包必须在一个文件夹下且名字要对应 else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); //调用configuration自己的addMapper方式解析 configuration.addMapper(mapperInterface); } else {
//mapper下只能有一种节点 throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
可以看到 根据我们在config配置文件中配置的mapper寻找策略加载,这儿主要为两种 根据接口加载,根据xml加载,根据接口的加载和xml加载类似,所以我们主要讲解下xml方式加载
XMLMapperBuilder
在SqlSessionFactoryBean中 最后可以看到有如下代码
//mapper扫描器如果不为空 即扫描mapper.xml文件的地址不为空 if (this.mapperLocations != null) { if (this.mapperLocations.length == 0) { //如果长度为0 说明虽然设置了 但是没找到对应的地址 LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found."); } else { //遍历所有的resource 即xml文件资源 for (Resource mapperLocation : this.mapperLocations) { //判断一下空 if (mapperLocation == null) { continue; } try { //为每个xml文件创建Mapper解析器 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments()); //进行解析 xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'"); } } } else { LOGGER.debug(() -> "Property 'mapperLocations' was not specified."); }
这里和之前的xmlConfigBuilder原理类似,都是根据传入的文件流格式根据指定的xml解析器 转化为Document对象存储xml文件的节点信息,然后执行parse()方法,将需要的信息从document中拿出来 装载进我们的configuration中,我们依旧看源代码
由于构建xmlMapperBuilder的方式和xmlConfigBuilder几乎一致,所以这儿不再讲解,主要讲解parse()方法,我们接着看parse方法
XMLMapperBuilder.parse()方法
public void parse() { //判断下是否加载了过 if (!configuration.isResourceLoaded(resource)) { //拿到mapper.xml文件下mapper节点并配置 configurationElement(parser.evalNode("/mapper")); //加入已装载过的资源中 configuration.addLoadedResource(resource); //将mapper绑定namespace 即接口 bindMapperForNamespace(); } //执行resultMap装载 parsePendingResultMaps(); //执行缓存装载 parsePendingCacheRefs(); //执行sql语句装载 parsePendingStatements(); }
该方法执行几个重要的装载方法。 我们挨个说明
configurationElement方法
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"); } //设置当前处理的namespace builderAssistant.setCurrentNamespace(namespace); //设置当前nameSpace的缓存引用 cacheRefElement(context.evalNode("cache-ref")); //设置当前nameSpace的缓存 cacheElement(context.evalNode("cache")); //设置当前nameSpace的所有parameterMap parameterMapElement(context.evalNodes("/mapper/parameterMap")); //设置当前nameSpace的所有resultMap resultMapElements(context.evalNodes("/mapper/resultMap")); //设置当前nameSpace的所有sql语句 sqlElement(context.evalNodes("/mapper/sql")); //设置当前nameSpace 每个sql方法的方法类型 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); } }
bindMapperForNamespace()方法
private void bindMapperForNamespace() { //获取到之前设置的namespace 即接口的全限定名一般 String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { //反射获取接口 boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { //ignore, bound type is not required } if (boundType != null) { if (!configuration.hasMapper(boundType)) { // Spring may not know the real resource name so we set a flag // to prevent loading again this resource from the mapper interface // look at MapperAnnotationBuilder#loadXmlResource //添加加载过的资源 configuration.addLoadedResource("namespace:" + namespace); //将当前通过namespace的获取到的接口添加到mapper中 即之前的接口添加 configuration.addMapper(boundType); } } } }
这里面主要通过namespace反射获取到接口加载到config中,这儿可以看下简要看下里面的部分逻辑
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 {
//创建mapper接口 已经其对应的代理工厂 knownMappers.put(type, new MapperProxyFactory<>(type)); //创建对应的builder 再次加载一下 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
//解析 parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } }
可以看到主要对namespace对应的接口创建代理工厂并存储系统缓存,然后再次加载一下,像之前的接口加载,防止还没有加载到xml文件。至于剩下的三个方法,则是分别将对应的数据加载到对应的MapperBuilderAssistant中
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
到此config所需要的mapper和其他有关信息已经被全部加载进去
至此config已经参数装载完毕 最后将config作为参数传入DefaultSqlSessionFactory中创建我们所需要的SqlSessionFactory
return this.sqlSessionFactoryBuilder.build(targetConfiguration); public class SqlSessionFactoryBuilder { public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); } }
完结
我们到这儿可以理一下思路(基于spring的方式,手动方式也基本一致)
- 创建mybatis配置文件或者自定义config
- 创建mapper资源扫描器,扫描到所有满足我们的匹配条件的mapper信息并将资源加载到SqlSessionFactoryBean中
- SqlSessionFactoryBean执行构建方法
- 判断我们是否传入了configuration类,如果没有且设置了configLocation,则创建一个xmlConfigBuilder初始化一个默认的configuration,创建过程中会将config文件信息加载为Document对象以备使用
- 为configuration设置系统功能组件
- 判断xmlConfigBuilder是否为空,如果不为空则执行parse()方法,内部会将config文件信息即document根据功能全部解析并设置到configuration对应的值,并且会根据设置了mapper扫描策略扫描mapper.xml或者接口,最后装载mapper信息
- 判断之前加载的mapper资源是否为空,如果不为空,则为每个资源创建xmlMapperBuilder,内部会解析mapper.xml文件为一个Document对象。然后调用parse()方法,将document中的信息设置到configuration中对应的值
- 创建一个DefaultSqlSessionFactory返回
可以看到主要就是为了创建configuration,而这个configuration也是存储所有信息的地方
至此 我们成功的解析了config文件,或者我们自定义的config,并成功的装载进了所需要的所有参数与组件,并且拿到了所有的mapper文件信息并获取到了其namespace中对应的接口加载到config中。可以看到SqlSessionFactoryBean主要充当了构建的作用,而我们所需要的SqlSessionFactory也被创建好了。本文主要说明了两种mybatis配置模式,并根据spring模式的创建方法中四个核心的方法逐一分析,就完全了解了SqlSessionFactory中的config构建过程。下一章我们主要分析下SqlSessionFactory