mybatis – MyBatis 3 | Getting started 在介绍前,先想想mybatis的用法,通常是写mapper接口与对应的xml,很明显这些接口要被动态代理,实现将请求参数转换成可执行的sql语句,使用sqlSession来进行操作,最后返回结果。这样在spring使用的话,应该是有注册beanDefination,里面是一个FactoryBean,由它的getObject()产生真正的接口实现类,这一点上,与dubbo的接口可以变成一个远程调用差不多机制。
下图就是最后根据分析,整理的结构索引图。分析完代码其实也很容易忘记,看图方便记忆。
对用户写的mapper类,典型的处理过程:
- ClassPathMapperScanner扫描包下的mapper接口类,产生beanDefination。
- beanDefination中设置为MapperFactoryBean,可用getObject()生成接口的实现类。
- getObject()中,又使用sessionFactory产生的sessionTemplate创建,创建时委托configuration创建的同时,把自己当this传进去。
- Configuration中又找到MapperRegistry后,从其map中找到mapper对应的MapperProxyFactory(MapperFactoryBean初始化时放入map中的)。
- MapperProxyFactory动态生成是,会用MapperProxy 这个invocationHandler来动态生成。
- MapperProxy的invoker方法中,又会用plainMethodInvoker和新的参数MapperMethod(包括了用户method与sessionTemplatemethod的关系)来处理。
- MapperMethod因为有了对应关系,最后还是前面this时放入的sessionTemplate处理。
- sessionTemplate内部又用动态代理生成sqlSessionProxy,委托它处理。
- sqlSessionProxy的invocationHandler是SqlSessionInterceptor,它又会产生一个DefaultSqlSession来处理,并用configuration产生一个executor给它。它会先从configuration中获取新的MappedStatement,再用executor使用MappedStatement处理。
- executor执行时,又从configuration获取一个新的StatementHandler来处理MappedStatement中的参数对象。
- StatementHandler内有生成的parameterHandler,resultSetHandler,typeHandlerRegistry等,可以处理请求参数,可以处理返回值,可以对参数中的属性,按类型进行转换。
- StatementHandler会用连接DB后生成的java.sql.Statement来处理转换后产生的真正的SQL语句,最后还会用resultSetHandler处理返回值。
下面详细介绍分析的过程,我们还是先看一下官方的介绍,找到非spring环境中的通常使用方法,来构建出mybatis的运行时关系图,以及创建运行时关系的过程。
一、mybatis-3.5.9.jar
1、官方说明与初步分析
mybatis – MyBatis 3 | Getting started
Every MyBatis application centers around an instance of SqlSessionFactory. A SqlSessionFactory instance can be acquired by using the SqlSessionFactoryBuilder. SqlSessionFactoryBuilder can build a SqlSessionFactory instance from an XML configuration file, or from a custom prepared instance of the Configuration class.
String resource = "org/mybatis/example/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); ......... //使用方式一: try (SqlSession session = sqlSessionFactory.openSession()) { Blog blog = session.selectOne( "org.mybatis.example.BlogMapper.selectBlog", 101); } //使用方式二: try (SqlSession session = sqlSessionFactory.openSession()) { BlogMapper mapper = session.getMapper(BlogMapper.class);//interface Blog blog = mapper.selectBlog(101); }
SqlSessionFactory就是核心,而且简单的看,就是从配置文件来构建这样一个核心类。怎么与前面的接口设想有点不一样了,当然sqlSesssion很重要,接口的实现都要使用它,所以它的工厂也是入口操作类也正常。从两种使用方式来看,可以设想mapper 就是那个接口实现类。
不如直接查一下DefaultSqlSession的getMapper怎么用,果然是把session传进去生成接口的实现类,并且是给这个实现类用。后面分别向下介绍session的操作,向上介绍接口的处理,最后理出一个关系出来。
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
2、Session的功能
看看上面使用方法:session.selectOne(),所使用的selectList方法。几个参数很明确,大概可以分析出执行过程:
- 一个从配置中获取MappedStatement,这些都是预先解析好的configuration的数据。
- StatementHandler是为每一个sql操作而产生的处理类对象,stmt也是如此。而configuration,mappedStatement,executor,connection,transaction这些都可以看做是单例对象。
- SimpleExecutor中的StatementHandler对象本来想的不需要在doQuery()这里出现吧? 产生的StatementHandler后,它又当参数生成stmt,stmt之后又当参数给StatementHandler处理用,感觉关系有点不清晰。
- 私以为类关系最好不要循环,但在prepareStatement()与getConnection()中,都用到了SimpleExecutor的transaction对象,所以不能下沉到StatementHandler中处理,当然我写的话,可能会把transaction传递下去,在StatementHandler中处理。也许是因为Executor或者其它接口有多个实现类,为了统一代码的结构而这么设计吧,暂不深入。
上述分析中要分清单例类(包括配置数据对象)与每一次处理产生的类/对象,最后理出一个运行关系图。
//DefaultSession.java
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
//SimpleExecutor.java
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
//SimpleStatementHandler.java
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.handleResultSets(statement);
}
3. MappedStatement
从上面可以看出,配置中的MappedStatement是一个重要的对象,可以认为是配置对象,从名字可以看到是mapped的声明,我们知道mapper有很多种方式,包括xml,包括接口,最终应该都被特定的parse类,解析成一个个MappedStatement,存起来来用吧。
这个过程应该是启动后的初始化中完成,之后可以正常处理用户过来的请求。
再回看一下官方的 Building SqlSessionFactory without XML,有助于我们理解类关系。
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource(); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(BlogMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
这里就比较清楚了,需要用配置configuration来产生SqlSessionFactory。而configuration中包括了:dataSource,TransactionFactory,以及BlogMapper.class这些接口类产生的mapper。所以前面说这些都是单例对象,包括解释用户接口产生的。我们猜测这个BlogMapper.class应该会产生MappedStatement与接口实现类。实现类中用方法名字与参数,使用sqlSession时,会找到MappedStatement,这样进行数据库操作。
从以下代码跟踪发现,会从XML配置文件中,产生MappedStatement。
//configuration.java
protected void buildAllStatements() {
...
incompleteStatements.removeIf(x -> {
x.parseStatementNode();
return true;
});
...
//这里还有从接口解析出statement,后面说明。
}
public void addIncompleteStatement(XMLStatementBuilder incompleteStatement) {
incompleteStatements.add(incompleteStatement);
}
//XMLMapperBuilder.java
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
//XMLStatementBuilder.java 可以发现产生MappedStatement对象了。
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
...
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
那么,如何从BlogMapper.class这样的接口中产生MappedStatement呢?我们从前面说的官方without XML的使用示例configuration.getMapper()开始。
- 解析的时候,按接口放入一个MapperProxyFactory。
- 使用的时候,用MapperProxyFactory产生实例,这时候传入sqlSession进去。
- sqlSession是在MapperMethod的executor中使用的,sqlSession会从configuration中得到MappedStatement。configuration.getMappedStatement(statement);
- 但是前面没有看到从接口产生MappedStatement的代码啊?只看了从xml产生地代码。实际上在buildAllStatements();中,之前只分析了一种情况,还有另两种情况,其中一个是incompleteMethods中解析。
上述前两点,正好说明与前面的猜测一致。接口是要产生一个实现类,这个实现类用sqlSession访问数据库。
//configuratin.java
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
//MapperRegistry.java
public <T> void addMapper(Class<T> type) {
...
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.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
}
...
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
...
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
//MapperProxyFactory.java
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
//MapperMethod.java
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
...
}
}
//MapperAnnotationBuilder.java
void parseStatement(Method method) {
final Class<?> parameterTypeClass = getParameterType(method);
final LanguageDriver languageDriver = getLanguageDriver(method);
...
final String mappedStatementId = type.getName() + "." + method.getName();//id就是类.方法名
...
}
4. configuration
前面的分析,已经了解了mybatis的大概类之间关系。configuration是一个重要的配置数据类,内容非常丰富。因为有足够的配置信息,它还会new一些实例类,有部分类工厂的功能。
这里我就仅分析一下TypeHandlerRegistry,因为我们生产环境出现了对enum类型参数,在高并发时解析出错的情况。enum的handler是使用时才注册的。此版本前一版本中,是发现jdbcHandlerMap中拿type对应的map为null时,new一个hashmap放进去,之后才设置map中的值,导致有可能后面线程发现已经不是null,却拿不到里面的值的情况。
//Configuration.java
protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
//TypeHandlerRegistry.java
public TypeHandlerRegistry(Configuration configuration) {
this.unknownTypeHandler = new UnknownTypeHandler(configuration);
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());
...
}
@SuppressWarnings("unchecked")
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
handler = jdbcHandlerMap.get(null);//在上一个版本中,如果null,会new HashMap并放进去,之后再给这个map设置handler,这样在高并发时,可能造成得不到正确的handler,比如enum时,因为这个类型的handler,是使用时才会设置,并不是一开始就设置好。
}
if (handler == null) {
// #591
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// type drives generics here
return (TypeHandler<T>) handler;
}
private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) {
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = typeHandlerMap.get(type);
if (jdbcHandlerMap != null) {
return NULL_TYPE_HANDLER_MAP.equals(jdbcHandlerMap) ? null : jdbcHandlerMap;
}
if (type instanceof Class) {
Class<?> clazz = (Class<?>) type;
if (Enum.class.isAssignableFrom(clazz)) {
Class<?> enumClass = clazz.isAnonymousClass() ? clazz.getSuperclass() : clazz;
jdbcHandlerMap = getJdbcHandlerMapForEnumInterfaces(enumClass, enumClass);
if (jdbcHandlerMap == null) {//当get不到时,会注册enum的handler。
register(enumClass, getInstance(enumClass, defaultEnumTypeHandler));
return typeHandlerMap.get(enumClass);
}
} else {
jdbcHandlerMap = getJdbcHandlerMapForSuperclass(clazz);
}
}
typeHandlerMap.put(type, jdbcHandlerMap == null ? NULL_TYPE_HANDLER_MAP : jdbcHandlerMap);
return jdbcHandlerMap;
}
二、mybatis-spring-2.0.6.jar
前面的分析只是在非spring的情况下分析的,通常我们使用的是spring的环境,这时候要使用mybatis-spring的包了。而且后面的mybatis-plus的分析也基于spring环境中的使用。
这里就直接从mybatis plus的官方的使用示例开始,也就是其中的@MapperScan。
@SpringBootApplication
@MapperScan("com.baomidou.mybatisplus.samples.quickstart.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class) //注解的处理类
@Repeatable(MapperScans.class)
public @interface MapperScan
//注解处理中,产生的bean定义:MapperScannerConfigurer
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
通过跟踪发现:
- MapperScan注解上导入MapperScannerRegistrar.class,里面再注册一个bean:MapperScannerConfigurer
- 这个类又实现了BeanDefinitionRegistryPostProcessor,InitializingBean等重要接口。从名字看,本身是一个configurer,所以用它在spring扩展中产生需要的bean是很合理的。
- postProcessBeanDefinitionRegistry中对找到的beanDefination进行了设置。比如:definition.setBeanClass(this.mapperFactoryBeanClass);//mapperFactoryBeanClass = MapperFactoryBean.class;
- 而class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T>。正好是一个FactoryBean,与最前面说的,类似dubbo的consumer接口,所有这种接口都会被定义为FactoryBean放入spring容器,这样用getObject()产生真正的实现类。
- getObject()中正好是:getSqlSession().getMapper(this.mapperInterface);与前面分析中非XML时使用的官方示例(使用方式二:BlogMapper mapper = session.getMapper(BlogMapper.class);//interface)一样了。
- spring就是给最终客户一种简便的方式,将客户的配置纳入原有的体系中来。
其它就不分析了,关于mybatis-spring-boot-start中的autoconfiguration,后面会提到,现在快速进入mybatis-plus部分。
三、mybatis-plus-3.0.jar
是不是上面的方式用着还不够爽?jpa是不是也有自己的方便之处?拿来一些给mybatis助力吧。另外如果在执行sql过程前后想插入自己的处理,怎么办?plus都提供了机制。
1. mybatis plus的扩展之一
还是使用前面的mybatis plus的官方示例,发现用了这么一个base接口,还有泛型pojo类。
public interface UserMapper extends BaseMapper<User> {
}
这还是一个mapper接口,方法都在base类中,也对,每一个mapper中写的多数都一样,那整一个abstract类就行MybatisSqlSessionFactoryBean了。至于处理的数据对象不一样,那使用通用的反射代码,也都能获取各自的参数与sql,估计jpa也是这么弄的吧。想想这条路是可行的,也确实方便了使用者。自己有特殊的SQL,继承后另外写就行了。
2. MybatisPlusAutoConfiguration中的变化
MybatisPlusAutoConfiguration代替了mybatis的MybatisAutoConfiguration,看看有什么变化呢?
//MybatisPlusAutoConfiguration.java
package com.baomidou.mybatisplus.autoconfigure;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
// TODO 入参使用 MybatisSqlSessionFactoryBean
private void applyConfiguration(MybatisSqlSessionFactoryBean factory) {
// TODO 使用 MybatisConfiguration
MybatisConfiguration configuration = this.properties.getConfiguration();
if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
configuration = new MybatisConfiguration();//使用扩展继承的configuration
}
...
factory.setConfiguration(configuration);
}
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();//使用扩展继承的
...
applyConfiguration(factory);//上面的方法
...
}
MybatisSqlSessionFactoryBean:中增加了一个globalConfig,配置了一些东西,包括MybatisConfiguration。
MybatisConfiguration:使用了新的mapperRegistry来处理用户的接口
@Override
public <T> void addMapper(Class<T> type) {
mybatisMapperRegistry.addMapper(type);
}
3. MybatisMapperRegistry
前面说了,plus中有了一个通用的接口,这里注册时,会使用pojo解析mappedStatement功能吗?实际上猜错了,这里没有找到,mappedstatement中还是记录的pojo对象,转化是后面executor中才有,在处理mappedstatement中的参数泛型对象时,而不是先转化好放入mappedstatement。
//这里跟踪,没有找到处理 pojo的@Table注解的功能,实际上是有一个chain来处理参数。
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
...
try {
// TODO 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
knownMappers.put(type, new MybatisMapperProxyFactory<>(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.
// TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
}...
}
//config中有parameterHandler,这里组成chain,来处理pojo等参数问题。
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
上面getLang(),提示我们在MybatisXMLLanguageDriver中的MybatisParameterHandler中找到了pojo注解的处理过程。并且interceptorChain中有三个handler组成,最后给executor使用。
- interceptorChain.pluginAll(parameterHandler);
- interceptorChain.pluginAll(resultSetHandler);
- interceptorChain.pluginAll(statementHandler);
- executor = (Executor) interceptorChain.pluginAll(executor);
说明executor在执行时,会使用这些handler分别处理请求的pojo泛型参数,还会处理返回值。
4. mybatis plus的扩展之二---插件
插件也是对终端用户提供的一个非常有用的功能。一般的扩展机制,最多的就是interceptor,filter这类的责任链设计模式的使用,而plus的插件有点不同,虽然也是写interceptor。
先看一下写的interceptor如果加载到框架中,给谁使用吧。有两种试,一种是配置xml中写plugin后解析,一种是注解为spring的bean。介绍后一种吧。在自动配置类的构造函数中,会用ObjectProvider找到容器中所有的Interceptor。如:MybatisPlusAutoConfiguration(...,ObjectProvider<Interceptor[]> interceptorsProvider...),下面的代码又说明了会从xml中找到interceptor,也都给configuration,放入interceptorChain中 。
configuration有一个功能就是产生成4个重要的类对象,executor,statementHandler,parameterHandler,resultSetHandler。生成时,会有例如:interceptorChain.pluginAll(executor);的处理,产生一个个代理对象。由于interceptor注解上有说明使用的对象以及方法签名信息,所以只会对应的有效果。
//MyplusAutoConfiguration.java中,会设置给MybatisSqlSessionFactoryBean
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
//MybatisSqlSessionFactoryBean.java 的buildSqlSessionFactory()中,会设置给mybatisConfiguration类自己的plugins。它的又是通过解析XNode 来的。而自动配置中从容器找到的,也会加过来,最后都给configuration。
if (!isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach(plugin -> {
targetConfiguration.addInterceptor(plugin);
LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
});
}
//MybatisXMLConfigBuilder.java中,会解析配置文件中的plugin
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
Plus extension包中的MybatisPlusInterceptor就是这样的功能,不过它内部又包含了一个innerInterceptor.java的内部子拦截器列表。怎么加载此内部拦截器?方法上有个注释说明了:
* 使用内部规则,拿分页插件举个栗子:
* <p>
* - key: "@page" ,value: "com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor"
* - key: "page:limit" ,value: "100"
* <p>
根据需要,可以自己定义拦截器,我们有组件使用拦截器,实现动态路由不同的数据库。
四、结束
上面的基本过程简单分析完了,我们项目还进一步扩展mybatis-plus,支持更多的base方法等。