目录
一、Mybatis代码实践
mybatis使用的简单代码大致如下:
public static void main(String[] args) {
try {
//SqlSessionFactory 在实际项目使用中只需要初始化一次,适合单例模式
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sf = new SqlSessionFactoryBuilder().build(is);
try(SqlSession sqlSession = sf.openSession()){
UserDao userDao = sqlSession.getMapper(UserDao.class);
System.out.println(userDao.queryUserAll());
}
} catch (IOException e) {
e.printStackTrace();
}
}
主要涉及三个类:
1、SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,负责加载配置,有多个重载的build方法用来创建SqlSessionFactory,一旦创建了 SqlSessionFactory,就不再需要它了。
2、SqlSessionFactory
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例,适合用单例模式创建。如果是需要配置多个数据源,则每个数据源都需要一个SqlSessionFactory。
3、SqlSession
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。使用完需要关闭它,可以在finally中主动使用close方法关闭,也可以使用try(){}语法自动关闭。
二、配置加载
创建SqlSessionFactory的过程为:
1、由XMLConfigBuilder类解析xml配置文件,得到一个Configuration配置类实例;
2、new DefaultSqlSessionFactory(config);
这一环节最重要的就是这个Configuration类,每一个SqlSessionFactory对应一个Configuration实例,它包含了mybatis运行期间所有的配置信息,一部分属于内置,在new Configuration()的时候默认装载,另一部分从配置文件中解析。
XMLConfigBuilder类聚合了Configuration类,通过XMLConfigBuilder的parseConfiguration方法,将配置文件中的内容依次解析装载到Configuration实例中,parseConfiguration源码如下:
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
其中部分常见的标签解析逻辑如下:
1、解析properties
properties属性可以在build时传入,也可以在xml中配置,设置好的属性,在配置文件其它地方可以使用占位符${}动态替换。
properties对应配置文件中的配置格式为:
<properties resource="com/lin/config.properties">
<property name="username" value="apps"/>
<property name="password" value="123456"/>
</properties>
源码逻辑:
1)解析properties标签下所有property子元素;
2)解析properties标签中resource属性指定的properties文件或url属性指定的properties文件;
3)装载程序入口方法指定的properties变量;
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(input, props);
4) 设置到Configuration实例的variables变量中;
protected Properties variables = new Properties();
注:
1)按解析顺序,后者覆盖前者,也就是说properties变量使用优先级为:
程序入口方法传递的变量 > resource或url指定文件的配置 > property元素指定的配置;
2)properties中的resource和url属性只能最多指定其中之一,否则抛出BuilderException;
2、解析typeAliases
typeAliases用来指定类型别名,目的是为了降低xml配置中冗余的全限定名,仅仅只是为了在xml文件中书写简便。
typeAliases对应的配置文件格式为:
<typeAliases>
<package name="com.lin"/>
<typeAlias alias="User" type="com.lin.User"/>
<typeAlias type="com.lin.User"/>
</typeAliases>
源码逻辑:
1)如果子元素是package,获取name属性指定的包名下所有类(不包含接口和内部类),别名默认为类名,如果类用了 Alias注解,则取注解的别名;
2)子元素是typeAlias,加载由type属性指定的类,如果元素指定了alias属性,则用alias属性的值作为别名,否则别名使用规则同上述package解析;
3)设置到Configuration的typeAliasRegistry变量中:
protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
而TypeAliasRegistry内部使用的是一个HashMap:
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
4)typeAlias有很多默认配置,在 TypeAliasRegistry的无参构造器和Configuration的无参构造器中默认装载,包括我们在resultType中常用的"string","int"或指定事务管理器type="JDBC"等;
public TypeAliasRegistry() {
registerAlias("string", String.class);
registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
//还有更多,此处不全部展示
}
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
//还有更多,此处不全部展示
}
注:
1)别名是不区分大小写的,所有的别名在保存和比较的时候都会转为小写;
String key = alias.toLowerCase(Locale.ENGLISH);
2)别名必须唯一,否则抛出TypeException;
3、解析settings
settings主要用来配置mybatis运行策略,更改其运行行为。
其配置文件格式为:
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
</settings>
源码解读:
每一个setting元素中name属性对应值,都是Configuration中的一个变量,value属性对应的值就是变量需要赋予的值。
其中部分设置详细如下:
1)cacheEnabled
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
该属性用来全局性地开启或关闭所有映射器配置文件中已配置的任何缓存,这里的缓存指二级缓存,默认是true,如果配置为了false,则全局禁用二级缓存。
2)defaultExecutorType
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
该属性用来配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。
对应的源码:
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
3)mapUnderscoreToCamelCase
configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
该属性用来配置是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn,默认为false。
4)localCacheScope
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
该属性用来配置本地缓存(一级缓存),值有SESSION和STATEMENT,默认SESSION时,sqlSession执行期间缓存查询结果,同样的sql查询将从缓存获取,sql查询期间,若中途有insert/update/delete操作,则清空缓存,本地缓存是mybatis代码强制规则,无法关闭,但是可以设置STATEMENT后每次查询都会清理掉缓存,这样等于没有缓存,源码如下:
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
4、解析environments
environments用来配置数据库连接,可以配置多个environment。
配置文件格式:
<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://192.168.0.11:33063/test_db"/>
<property name="username" value="test"/>
<property name="password" value="test"/>
</dataSource>
</environment>
</environments>
源码解析:
1)判断是否指定了要加载的envrionment,如果没有指定,则使用environments元素中default属性指定的环境;
2)遍历所有environment元素,如果其属性id指定的环境就是需要加载的环境,则开始解析;
3)根据transactionManager元素中type属性指定的全限定的类名或别名,加载对应的事务管理器;
4)根据dataSource加载指定的数据源;
5)将解析的Environment对象赋值给Configuration的environment变量;
protected Environment environment;
5、解析mappers
配置文件格式:
<mappers>
<mapper class="lin.db.test.mybatis.User01Dao"/>
<package name="lin.db.test.mybatis"/>
<mapper resource="mapper/UserDao.xml"/>
<mapper url="file:///var/mappers/UserDao.xml"/>
</mappers>
源码解析大致如下图:
三、sql执行
Mybatis通过Executor执行器来最终执行sql,Executor是一个接口,有多种不同实现,以下在默认配置下会使用到二级缓存CachingExecutor和BaseExecutor。
对于增删改的操作统一由Executor的update方法执行:
select操作交由CachingExecutor的query方法执行:
四、缓存
Mybatis通过缓存来提升自己的查询性能,在缓存都开启的情况下,其查询顺序为:二级缓存>一级缓存>数据库。
1、一级缓存
一级缓存是mybatis的本地缓存,它是在SqlSession创建时底层创建的一个缓存对象,所以其作用域是sqlSession级别的,一旦sqlSesssion关闭,缓存对象也便消亡,该缓存对象是PerpetualCache,实现了Cache接口,其内部的数据结构就是一个HashMap,其中key对应着查询的sql,key值由全限定查询ID+offset+limit+sql语句按照一定的规则计算而来,value对应着查询的结果。
一级缓存是mybatis代码默认逻辑,不可被关闭,但可通过指定setting配置中的 localCacheScope=STATEMENT,来达到每次查询并缓存后就及时清理掉缓存的目的,从而变相达成不使用缓存的目的。
每次查询会缓存,而每次增删改操作则会清空缓存,这是默认的行为,无法改变。
2、二级缓存
二级缓存是mapper级别的,每一个mapper配置文件或多个mapper文件共享同一个缓存对象,该缓存对象实现了Cache接口;
二级缓存的使用受限于如下几个条件:
1)setting配置中的cacheEnabled,需要设置为true,默认也为true,这是一个全局开关,如果设置为false,则关闭全局的缓存使用策略;
2)mapper文件中配置了cache标签,或mapper接口加了CacheNamespace注解,这样,在mapper文件加载时会自动生成一个cache对象,用来缓存该mapper下的所有查询结果,也可以配置cache-ref标签或在接口上加CacheNamespaceRef注解,来共享另一个mapper的cache对象;
3)默认情况下select操作会自动缓存,但是如果select标签中配置了useCache=false,则该查询不会使用二级缓存。
二级缓存的数据可以存放在本地,也可以存放在其他地方如redis中 ,只要实现cache接口,缓存怎么做完全可以自己掌控;默认如果只配置一个简单的<cache/>标签, 则使用的是本地缓存对象,该对象同一级缓存一样,也是PerpetualCache,只是它用来保存mapper级别的缓存;
不同于一级缓存对象,二级缓存对象通过装饰器模式提供了额外的功能,包括:缓存过期算法,缓存大小等;
默认二级缓存过期策略是最近最少使用原则,其装饰类为LruCache,也可以在cache标签中指定eviction="FIFO"使用先入先出算法;
默认二级缓存大小为1024个(map size),也可以在cache标签中指定size="2048"更改缓存大小;
同一级缓存一样,默认在增删改的时候,会清空二级缓存。