Mybatis的源码与流程解析
2018-01-02 09:43
537 查看
使用Mybatis需要配置好数据源与SqlSessionFactory,如果要使用事务则需额外配置事务管理器,我自己的小框架使用的是基于Java形式的配置,也可以使用XML文件形式配置。我把我自己的配置文件放上来,想尝试这种方式的可以借鉴下。
配置Mybatis最主要的是配置SqlSessionFactory实例,当前配置基于Java形式,通过sqlSessionFactoryBean.getObject()方法生成SqlSessionFactory 实例,在此方法中会解析指定的Mybatis Mapper文件,将XML文件内的元素节点按类别解析生不同的java类对象,存入SqlSessionFactory的Configuration中。
如何解析,流程很繁琐,我就不一一列举了,感兴趣的可以就着XMLMapperBuilder 的方法自行查看。
解析Mybatis的Mapper文件,将生成的结果存入Conguration中,Conguration就相当于Myabtis的配置容器,Conguration又被SqlSessionFactory持有,当通过SqlSessionFactory生成SqlSession实例时,也会持有Conguration对象。
解析Mapper配置文件和扫描Mapper接口没有必然的先后顺序,这里只是先讲解解析,后讲解扫描。
通过@MapperScan注解,指定扫描的Mapper接口包。@MapperScan会引入MapperScannerRegistrar这个Bean,它会根据注解的属性相应的执行自己的逻辑。MapperScannerRegistrar扫描指定包下的所有接口,根据指定的规则对这些接口做过滤,针对这个接口注册BeanDefinition,如果注解上有自定义信息,这个将这些信息注入接口的BeanDefinition中,在通过这些BeanDefinition生成Bean实例时会从容器中寻找相应的Bean注入到接口的Bean中。这些接口的Bean实例是MapperFactoryBean对象(一个FactoryBean),真正注入Mapper对象时,实际注入的是通过MapperFactoryBean对象的getObject()方法生成的对象。
MapperFactoryBean对象在实例化时 :
第一步会调用其父类SqlSessionDaoSupport 的setSqlSessionFactory()方法, 当@MapperScan指定SqlSessionFactory或者SqlSessionTemplate的BeanName时,从Spring容器中获取相应名称的Bean;当未指定时,Mapper接口的BeanDefinition的autowireMode=AUTOWIRE_BY_TYPE,Spring通过BeanDefinition实例化Bean时,会从解析BeanDefinition内未赋值的非简单属性,从容器中获取SqlSessionFactory,SqlSessionTemplate类型的Bean注入其中,当你定义了相应类型的Bean就能赋值入Mapper接口生成的Bean中。属性的赋值使用setter方法。当赋值SqlSessionFactory时将sqlSession属性初始化为一个SqlSessionTemplate对象。
第二步会调用Configuration的addMapper()方法,这个方法会将当前接口的Class和一个MapperProxyFactory对象关联映射起来,间接存入Configuration中。
通过MapperFactoryBean的getObject()方法获取Mapper接口实际注入的对象,这个方法最终是调用MapperProxyFactory的newInstance(SqlSession)方法,
先通过sqlSession及Mapper接口生成MapperProxy对象,MapperProxy是一个InvocationHandler实现类,最后通过JDK Proxy的newProxyInstance方法,以MapperProxy,Mapper接口,及ClassLoader为参数,为Mapper接口生成动态代理对象,这个对象就是实际被注入到Service层的Mapper对象,所以实际的Mybatis执行逻辑都在MapperProxy对象的invoke()方法中。
MapperProxy在执行时会先通过当前方法生成MapperMethod,在MapperMethod里会先通过此方法找到对应的Mapper.xml里的相应节点解析成的MappedStatement,接口名称对应namespace,方法名称对应节点名称,获取这个节点的sql类型,然后提取出当前方法的参数,然后通过sqlSession和方法参数执行相应的操作。
执行增删改查的哪一种由sql节点的类型确定,返回单个还是多个由方法的返回值类型确定,这两个因素确定后通过sqlSession的相应方法执行,获取结果。
这里的sqlSession实际上是一个DefaultSqlSession对象,SqlSessionTemplate持有一个sqlSession类型的属性,名称是sqlSessionProxy,自身调用方法均会传递给此属性执行,这个属性是通过JDK动态代理生成的,代理了DefaultSqlSession对象,DefaultSqlSession对象时每个线程独立的,当没有事务时立即通过sessionFactory实时生成一个,当有事务时可以复用此DefaultSqlSession对象。
DefaultSqlSession在执行时会先通过从Configuration中找到当前方法对应的MappedStatement对象,然后MappedStatement根据当前方法的参数,动态的生成BoundSql对象,基本是通过当前参数集合,与DynamicSqlSource的MixSqlNode相匹配(循环递归式),如果匹配上,就加上SqlNode里的sql字符串,匹配完之后就生成一个sql字符串,然后将里面的参数”#{}”替换为”?”,同时生成有序的List< ParameterMapping > ,这个顺序一定要和?的顺序相同。之后将替换过的sql字符串和参数集合放进一个BoundSql对象里返回。
当得到了BoundSql对象时,就可以去执行了,此时需要执行器对象,默认配置是SimpleExecutor实例。
一般的MappedStatement的StatementType是PREPARED,也就是说会有预编译的语句类型,每一个sql在执行前会预编译(若sql存在字符拼接或者不存在参数则不预编译),这样就能防止sql注入。
SimpleExecutor先用PreparedStatementHandler去预编译sql语句,生成Statement,如果当前方法配置了事务,且执行器配置为ReuserExexutor,则这个Statement可以实现复用,若没有则不能复用。
PreparedStatementHandler之后将参数设置到编译后的sql中,形成当前参数下的Statement。
最后PreparedStatementHandler执行此Statement,解析结果,将得到的结果返回。
关键点总结:
通过配置文件或者Configuration的配置实例化SqlSessionFactory。
在实例化SqlSessionFactory时解析Mapper.xml文件,按节点类型不同生成不同的Java对象存入Configuration中。
扫描指定包下的Mapper接口,注册BeanDefinition,最后这些Bean被注入到Service层时是通过JDK Proxy动态生成的接口代理类对象。MapperProxy是InvocationHandler,Mapper的执行逻辑都在其Invoke方法中。
MapperProxy根据当前方法生成MapperMethod。
MapperMethod在实例化时会确定当前方法的执行类型,时增删改查的哪种,然后提取参数,最后由SqlSession去执行此操作。
SqlSession执行时,先找到当前方法对应的MapperStatement,然后由Executor去执行。
Executor在执行时,先根据当前参数从MapperStatement生成BoundSql,然后由StatementHandler根据BoundSql生成Statement,MappedStatement的StatementType是PREPARED,也就是在Statement生成后会预编译sql语句,这样能防止sql注入。
StatementHandler执行相应的Statement,解析结果,返回。
@Configuration @MapperScan(value = "com.bob.mvc.mapper", markerInterface = BaseMapper.class) public class DataAccessContextConfig { private static final String DRIVER_CLASS_NAME = "com.mysql.jdbc.Driver"; private static final String USERNAME = "root"; private static final String PASSWORD = "lanboal"; /** * MySQL的JDBC URL编写方式:jdbc:mysql://主机名称:连接端口/数据库的名称?参数=值 * 避免中文乱码要指定useUnicode和characterEncoding * 执行数据库操作之前要在数据库管理系统上创建一个数据库,名字自己定, * 下面语句之前就要先创建project数据库 */ private static final String URL = "jdbc:mysql://localhost:3306/project?useUnicode=true&characterEncoding=UTF8&useSSL=false"; @Bean(destroyMethod = "close") public DataSource dataSource() { BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName(DRIVER_CLASS_NAME); //针对mysql获取字段注释 dataSource.addConnectionProperty("useInformationSchema", "true"); //dataSource.addConnectionProperty("remarksReporting","true"); 针对oracle获取字段注释 dataSource.setUrl(URL); dataSource.setUsername(USERNAME); dataSource.setPassword(PASSWORD); dataSource.setMaxTotal(50); dataSource.setMinIdle(5); dataSource.setMaxIdle(10); return dataSource; } @Bean public DataSourceTransactionManager txManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } /** * @param dataSource * @return * @throws Exception */ @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { // 配置MapperConfig org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); //当数据库集群时,配置多个数据源,通过设置不同的DatebaseId来区分数据源,同时sql语句中通过DatabaseId来指定匹配哪个数据源 //configuration.setDatabaseId("Mysql-1"); // 这个配置使全局的映射器启用或禁用缓存 configuration.setCacheEnabled(true); // 允许 JDBC 支持生成的键,需要适合的驱动(如MySQL,SQL Server,Sybase ASE)。 // 如果设置为 true 则这个设置强制生成的键被使用,尽管一些驱动拒绝兼容但仍然有效(比如 Derby)。 // 但是在 Oracle 中一般不需要它,而且容易带来其它问题,比如对创建同义词DBLINK表插入时发生以下错误: // "ORA-22816: unsupported feature with RETURNING clause" 在 Oracle // 中应明确使用 selectKey 方法 //configuration.setUseGeneratedKeys(false); // 配置默认的执行器: // SIMPLE :> SimpleExecutor 执行器没有什么特别之处; // REUSE :> ReuseExecutor 执行器重用预处理语句,在一个Service方法中多次执行SQL字符串一致的操作时,会复用Statement及Connection, // 也就是说不需要再预编译Statement,不需要重新通过DataSource生成Connection及释放Connection,能大大提高操纵数据库效率; // BATCH :> BatchExecutor 执行器重用语句和批量更新 configuration.setDefaultExecutorType(ExecutorType.REUSE); // 全局启用或禁用延迟加载,禁用时所有关联对象都会即时加载 configuration.setLazyLoadingEnabled(false); // 设置SQL语句执行超时时间缺省值,具体SQL语句仍可以单独设置 configuration.setDefaultStatementTimeout(5000); SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setConfiguration(configuration); // 匹配多个 MapperConfig.xml, 使用mappingLocation属性 sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/bob/mvc/mapper/*Mapper.xml")); return sqlSessionFactoryBean.getObject(); } }
配置Mybatis最主要的是配置SqlSessionFactory实例,当前配置基于Java形式,通过sqlSessionFactoryBean.getObject()方法生成SqlSessionFactory 实例,在此方法中会解析指定的Mybatis Mapper文件,将XML文件内的元素节点按类别解析生不同的java类对象,存入SqlSessionFactory的Configuration中。
public class XMLMapperBuilder extends BaseBuilder { ...... private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); //解析resultMap节点,生成ResultMap对象,存入Configuraion中 resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); //解析增删改查节点,生成MappedStatement对象,存入Configuraion中 buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e); } } }
如何解析,流程很繁琐,我就不一一列举了,感兴趣的可以就着XMLMapperBuilder 的方法自行查看。
解析Mybatis的Mapper文件,将生成的结果存入Conguration中,Conguration就相当于Myabtis的配置容器,Conguration又被SqlSessionFactory持有,当通过SqlSessionFactory生成SqlSession实例时,也会持有Conguration对象。
解析Mapper配置文件和扫描Mapper接口没有必然的先后顺序,这里只是先讲解解析,后讲解扫描。
通过@MapperScan注解,指定扫描的Mapper接口包。@MapperScan会引入MapperScannerRegistrar这个Bean,它会根据注解的属性相应的执行自己的逻辑。MapperScannerRegistrar扫描指定包下的所有接口,根据指定的规则对这些接口做过滤,针对这个接口注册BeanDefinition,如果注解上有自定义信息,这个将这些信息注入接口的BeanDefinition中,在通过这些BeanDefinition生成Bean实例时会从容器中寻找相应的Bean注入到接口的Bean中。这些接口的Bean实例是MapperFactoryBean对象(一个FactoryBean),真正注入Mapper对象时,实际注入的是通过MapperFactoryBean对象的getObject()方法生成的对象。
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { private Class<T> mapperInterface; private boolean addToConfig = true; public MapperFactoryBean() { //intentionally empty } public MapperFactoryBean(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } /**第2步 * {@inheritDoc} */ @Override protected void checkDaoConfig() { super.checkDaoConfig(); notNull(this.mapperInterface, "Property 'mapperInterface' is required"); Configuration configuration = getSqlSession().getConfiguration(); if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { configuration.addMapper(this.mapperInterface); } catch (Exception e) { logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e); throw new IllegalArgumentException(e); } finally { ErrorContext.instance().reset(); } } } /**第3步 * {@inheritDoc} */ @Override public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); } } public abstract class SqlSessionDaoSupport extends DaoSupport { private SqlSession sqlSession; private boolean externalSqlSession; //第1步 public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { if (!this.externalSqlSession) { //这步其实很关键,但是需要配合着事务来说,在下一篇博客中会重点讲解。 this.sqlSession = new SqlSessionTemplate(sqlSessionFactory); } } }
MapperFactoryBean对象在实例化时 :
第一步会调用其父类SqlSessionDaoSupport 的setSqlSessionFactory()方法, 当@MapperScan指定SqlSessionFactory或者SqlSessionTemplate的BeanName时,从Spring容器中获取相应名称的Bean;当未指定时,Mapper接口的BeanDefinition的autowireMode=AUTOWIRE_BY_TYPE,Spring通过BeanDefinition实例化Bean时,会从解析BeanDefinition内未赋值的非简单属性,从容器中获取SqlSessionFactory,SqlSessionTemplate类型的Bean注入其中,当你定义了相应类型的Bean就能赋值入Mapper接口生成的Bean中。属性的赋值使用setter方法。当赋值SqlSessionFactory时将sqlSession属性初始化为一个SqlSessionTemplate对象。
第二步会调用Configuration的addMapper()方法,这个方法会将当前接口的Class和一个MapperProxyFactory对象关联映射起来,间接存入Configuration中。
通过MapperFactoryBean的getObject()方法获取Mapper接口实际注入的对象,这个方法最终是调用MapperProxyFactory的newInstance(SqlSession)方法,
先通过sqlSession及Mapper接口生成MapperProxy对象,MapperProxy是一个InvocationHandler实现类,最后通过JDK Proxy的newProxyInstance方法,以MapperProxy,Mapper接口,及ClassLoader为参数,为Mapper接口生成动态代理对象,这个对象就是实际被注入到Service层的Mapper对象,所以实际的Mybatis执行逻辑都在MapperProxy对象的invoke()方法中。
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); ...... @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
MapperProxy在执行时会先通过当前方法生成MapperMethod,在MapperMethod里会先通过此方法找到对应的Mapper.xml里的相应节点解析成的MappedStatement,接口名称对应namespace,方法名称对应节点名称,获取这个节点的sql类型,然后提取出当前方法的参数,然后通过sqlSession和方法参数执行相应的操作。
public class MapperProxy<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache; public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; } ...... }
执行增删改查的哪一种由sql节点的类型确定,返回单个还是多个由方法的返回值类型确定,这两个因素确定后通过sqlSession的相应方法执行,获取结果。
这里的sqlSession实际上是一个DefaultSqlSession对象,SqlSessionTemplate持有一个sqlSession类型的属性,名称是sqlSessionProxy,自身调用方法均会传递给此属性执行,这个属性是通过JDK动态代理生成的,代理了DefaultSqlSession对象,DefaultSqlSession对象时每个线程独立的,当没有事务时立即通过sessionFactory实时生成一个,当有事务时可以复用此DefaultSqlSession对象。
DefaultSqlSession在执行时会先通过从Configuration中找到当前方法对应的MappedStatement对象,然后MappedStatement根据当前方法的参数,动态的生成BoundSql对象,基本是通过当前参数集合,与DynamicSqlSource的MixSqlNode相匹配(循环递归式),如果匹配上,就加上SqlNode里的sql字符串,匹配完之后就生成一个sql字符串,然后将里面的参数”#{}”替换为”?”,同时生成有序的List< ParameterMapping > ,这个顺序一定要和?的顺序相同。之后将替换过的sql字符串和参数集合放进一个BoundSql对象里返回。
当得到了BoundSql对象时,就可以去执行了,此时需要执行器对象,默认配置是SimpleExecutor实例。
一般的MappedStatement的StatementType是PREPARED,也就是说会有预编译的语句类型,每一个sql在执行前会预编译(若sql存在字符拼接或者不存在参数则不预编译),这样就能防止sql注入。
SimpleExecutor先用PreparedStatementHandler去预编译sql语句,生成Statement,如果当前方法配置了事务,且执行器配置为ReuserExexutor,则这个Statement可以实现复用,若没有则不能复用。
PreparedStatementHandler之后将参数设置到编译后的sql中,形成当前参数下的Statement。
最后PreparedStatementHandler执行此Statement,解析结果,将得到的结果返回。
关键点总结:
通过配置文件或者Configuration的配置实例化SqlSessionFactory。
在实例化SqlSessionFactory时解析Mapper.xml文件,按节点类型不同生成不同的Java对象存入Configuration中。
扫描指定包下的Mapper接口,注册BeanDefinition,最后这些Bean被注入到Service层时是通过JDK Proxy动态生成的接口代理类对象。MapperProxy是InvocationHandler,Mapper的执行逻辑都在其Invoke方法中。
MapperProxy根据当前方法生成MapperMethod。
MapperMethod在实例化时会确定当前方法的执行类型,时增删改查的哪种,然后提取参数,最后由SqlSession去执行此操作。
SqlSession执行时,先找到当前方法对应的MapperStatement,然后由Executor去执行。
Executor在执行时,先根据当前参数从MapperStatement生成BoundSql,然后由StatementHandler根据BoundSql生成Statement,MappedStatement的StatementType是PREPARED,也就是在Statement生成后会预编译sql语句,这样能防止sql注入。
StatementHandler执行相应的Statement,解析结果,返回。
相关文章推荐
- mybatis源码解析以及执行的流程
- MyBatis源码解析(一)——执行流程
- mybatis源码解析 - 通过一个简单查询例子分析流程
- Mybatis源码解析之写流程
- mybatis源码解析 - 通过一个简单查询例子分析流程
- mybatis源码解析-----执行流程1
- Mybatis源码详解之接口方法被执行流程源码解析
- Mybatis源码解析之查询流程
- Mybatis源码解析-Mapper执行SQL过程
- HDFS追本溯源:HDFS操作的逻辑流程与源码解析
- Mybatis源码解析-MapperRegistry注册mapper接口
- 安卓5.1源码解析 : RecyclerView解析 从绘制流程,ViewHolder复用机制,LayoutManger,ItemAnimator等流程全面讲解
- 【MyBatis源码分析】insert方法、update方法、delete方法处理流程(下篇)
- Mybatis3源码分析(14)-Sql解析执行-StatementHandler
- mybatis_初始化过程源码解析
- Android源码解析之(八)-->Zygote进程启动流程
- 【MyBatis 源码解析】 | Chat · 预告
- Mybatis源码解析四、SqlSession运行过程
- mybatis源码解析------Configuration类
- Mybatis源码解析-BoundSql