源码架构分析
Mapper映射器的配置
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {//packge优先
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
//表示 resource 值不为 null,且url 值和 mapperClass 值都为null。
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
//表示 url 值不为 null,且 resource 值和 mapperClass 值都为null。
} 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();
//表示 mapperClass 值不为 null,且 resource 值和 url 值都为null。
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
//表示如果三个都为null或者都不为null,或者有两个不为null,都会抛出异常。
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
document type definition 文档类型定义
mapper子标签必须在package标签前面。实际应用中,package标签使用的比较少,这里就不贴源码对package进行分析了(需要注意的是,如果两个子标签同时存在,前面解析完mapper标签后,存在相同的接口名,会抛出异常)
数据源的配置
实质上,Mybatis-config.xml这个主配置文件就代表我们如何连接数据库源,那么哪个负责读取xml就是哪个源码负责连接数据库源
显然这个方法是读取environments标签用的
private void environmentsElement(XNode context) throws Exception {
//如果<environments>标签不为null
我们可以不在 mybatis-configuration.xml 文件中配置标签,这是为了和spring整合时,在spring容器中进行配置。
if (context != null) {
//如果 environment 值为 null
//获取中的default属性值,注意第 5 行 首先判断 environment == null 。因为我们可以配置多个环境,也就是连接多个数据库。
if (environment == null) {
//获取<environments default="属性值">中的default属性值
environment = context.getStringAttribute("default");
}
//遍历<environments />标签中的子标签<environment />
for (XNode child : context.getChildren()) {
//获取<environment id="属性值">中的id属性值
String id = child.getStringAttribute("id");
//遍历所有<environment>的时候一次判断相应的id是否是default设置的值
if (isSpecifiedEnvironment(id)) {
//获取配置的事务管理器
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//获取配置的数据源信息
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
private boolean isSpecifiedEnvironment(String id) {
if (environment == null) {
throw new BuilderException("No environment specified.");
} else if (id == null) {
throw new BuilderException("Environment requires an id attribute.");
} else if (environment.equals(id)) {
return true;
}
return false;
}
而在mybatis源码内也是需要通过configxml文件来生产相应的数据源工厂的其实就是我们配置的type属性
private DataSourceFactory dataSourceElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties props = context.getChildrenAsProperties();
DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}
mybatis中的数据源
mybatis连接池为我们提供了3种方式的配置:
POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现
UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。
注意:如果不是web或者maven的war工程,JNDI是不能使用的。
在mybatis中配置数据源
POOLED数据源的配置方式
<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>
UNPOOLED数据源的配置方式
<dataSource type="UNPOOLED">
<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>
JNDI数据源的配置方式
将数据源的配置文件 context.xml 放到工程的webapp/META-INF/下
context.xml
<Resource
name="jdbc/mybatis" 数据源的名称
type="javax.sql.DataSource" 数据源类型
auth="Container" 数据源提供者
maxActive="20" 最大活动数
maxWait="10000" 最大等待时间
maxIdle="5" 最大空闲数
username="root" 用户名
password="1234" 密码
driverClassName="com.mysql.jdbc.Driver" 驱动类
url="jdbc:mysql://localhost:3306/mybatis" 连接url字符串
/>
然后在mybatis的主配置文件中配置数据源,其中前缀"java:comp/env/ "是固定的,后缀 “jdbc/mybatis” 是在context.xml中取的JNDI数据源名称
<dataSource type="JNDI">
<property name="data_source" value="java:comp/env/jdbc/mybatis"/>
</dataSource>
Mybatis访问数据库步骤
1.读取配置文件(配置文件要包含数据库连接信息以及mapper文件地址)
2.创建SqlSessionFactoryBuild对象
3.通过SqlSessionFactoryBuild创建SqlSessionFactory工厂
private static SqlSessionFactory getSqlSessionFactory() {
String resource = "mybatis/mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);//读取配置文件
} catch (IOException e) {
e.printStackTrace();
}
//创建SqlSessionFactoryBuild对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
return sqlSessionFactory;
}
4.通过SqlSessionFactory创建SqlSession对象
SqlSessionFactory的功能就是获取SqlSession,默认实现类DefaultSqlSession,DefaultSqlSession可以执行curd,也可以通过获取mapper来执行curd DefaultSqlSession是直接执行sql操作数据库,mapper是通过接口方法(实际上还是要映射sql) sqlSession可以指定事务隔离级别:TransactionIsolationLevel:
NONE(0),
READ_COMMITTED(2),
READ_UNCOMMITTED(1),
REPEATABLE_READ(4),
SERIALIZABLE(8);
DefaultSqlSession的属性和方法
private Configuration configuration;
private Executor executor;
private boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = this.configuration.getMappedStatement(statement);
this.executor.query(ms, this.wrapCollection(parameter), rowBounds, handler);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}
}
public int update(String statement, Object parameter) {
int var4;
try {
this.dirty = true;
MappedStatement ms = this.configuration.getMappedStatement(statement);
var4 = this.executor.update(ms, this.wrapCollection(parameter));
} catch (Exception var8) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + var8, var8);
} finally {
ErrorContext.instance().reset();
}
return var4;
}
public void commit(boolean force) {
try {
this.executor.commit(this.isCommitOrRollbackRequired(force));
this.dirty = false;
} catch (Exception var6) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + var6, var6);
} finally {
ErrorContext.instance().reset();
}
}
public <T> T getMapper(Class<T> type) {
return this.configuration.getMapper(type, this);
}
5.为DAO接口生成代理类
6.动态代理回调SqlSession某方法
- SqlSession将要操作的方法转发给Executor(并组装好cacheKey)(paramHandle运用插件的原理)
Configuration是对mybatis-config.xml解析后的封装,配置文件的节点对应Configuration的属性
例如:<environment 对应 protected Environment environment;
注意Executor是在SqlSessionFactory中就获取的:Executor executor = this.configuration.newExecutor(tx, execType);
实际上调用的configuration中的方法:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object 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 (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
根据参数生成对应的executor的实现 看看抽象类BaseExecutor的方法:
看看其中的SimpleExecutor:还是调用的configuration的方法
8.去查询cacheKey是否命中,如果命中直接返回,如果没有命中
9.Executor交给StatementHandler去执行(里面有个statement.execute(sql)真正执行sql的操作-JDBC访问数据库操作数据)代理模式
- Executor通过反射将数据封装POJO返回给sqlsession(resultParamHandle运用插件的原理)
11.把数据返回给调用者
处理sql相关的类 MappedStatement,获取BoundSql
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = this.sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(this.configuration, boundSql.getSql(), this.parameterMap.getParameterMappings(), parameterObject);
}
Iterator i$ = boundSql.getParameterMappings().iterator();
while(i$.hasNext()) {
ParameterMapping pm = (ParameterMapping)i$.next();
String rmId = pm.getResultMapId();
if (rmId != null) {
ResultMap rm = this.configuration.getResultMap(rmId);
if (rm != null) {
this.hasNestedResultMaps |= rm.hasNestedResultMaps();
}
}
}
return boundSql;
}
BoundSql,获取执行的sql
private String sql;
private List<ParameterMapping> parameterMappings;
private Object parameterObject;
private Map<String, Object> additionalParameters;
private MetaObject metaParameters;
BaseStatementHandler,获取Statement
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(this.boundSql.getSql());
Statement statement = null;
try {
statement = this.instantiateStatement(connection);
this.setStatementTimeout(statement, transactionTimeout);
this.setFetchSize(statement);
return statement;
} catch (SQLException var5) {
this.closeStatement(statement);
throw var5;
} catch (Exception var6) {
this.closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + var6, var6);
}
}
SimpleStatementHandler,获取Statement
protected Statement instantiateStatement(Connection connection) throws SQLException {
return this.mappedStatement.getResultSetType() != null ? connection.createStatement(this.mappedStatement.getResultSetType().getValue(), 1007) : connection.createStatement();
}
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = this.boundSql.getSql();
statement.execute(sql);
return this.resultSetHandler.handleResultSets(statement);
}
dafulatSqlSession->getMapper方法->mapperRegistry.getMapper->mapperProxyFactory.newInstance->Proxy.newProxyInstance(生成代理对象)
MapperProxy->都会去执行invoke方法->invoke里面都会执行mapperMethod.execute(this.sqlSession, args)
插件原理
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。
在 Spring 中使用 MyBatis
数据源配置 到 Spring 配置文件中。配置完数据源,接下来配置 SqlSessionFactory。再接下来是配置 MapperScannerConfigurer,这个类用于 扫描某个包下的数据访问接口,并将这些接口注册到 Spring 容器中。这样,我们就可以在其 他的 bean 中注入 Dao 接口的实现类,无需再从 SqlSession 中获取接口实现类
Mybatis分页插件安全性问题:
PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。
只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。
如果代码在进入 Executor 前发生异常,就会导致线程不可用,这属于人为的 Bug(例如接口方法和 XML 中的不匹配,导致找不到 MappedStatement 时), 这种情况由于线程不可用,也不会导致 ThreadLocal 参数被错误的使用。
//代码不安全
PageHelper.startPage(1,10);
List<Object> list;
if(param != null){
list = Mapper.selectIf(param);
} else {
list = Lists.newArrayList();
}
//这种情况下由于param 存在nul1 的情况,就会导致Pagelelper生产了-一个分页参数,
// 但是没有被消费,这个参数就会- -直保留在这个线程上。当这个线程再次被使用时,
// 就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
//代码安全
List<Object> list;
if(param != null){
PageHelper.startPage(1,10);
list = Mapper.selectIf(param);
} else {
list = Lists.newArrayList();
}