赞
踩
mybatis – MyBatis 3 | Getting started 在介绍前,先想想mybatis的用法,通常是写mapper接口与对应的xml,很明显这些接口要被动态代理,实现将请求参数转换成可执行的sql语句,使用sqlSession来进行操作,最后返回结果。这样在spring使用的话,应该是有注册beanDefination,里面是一个FactoryBean,由它的getObject()产生真正的接口实现类,这一点上,与dubbo的接口可以变成一个远程调用差不多机制。
下图就是最后根据分析,整理的结构索引图。分析完代码其实也很容易忘记,看图方便记忆。
对用户写的mapper类,典型的处理过程:
下面详细介绍分析的过程,我们还是先看一下官方的介绍,找到非spring环境中的通常使用方法,来构建出mybatis的运行时关系图,以及创建运行时关系的过程。
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);
- }
看看上面使用方法:session.selectOne(),所使用的selectList方法。几个参数很明确,大概可以分析出执行过程:
上述分析中要分清单例类(包括配置数据对象)与每一次处理产生的类/对象,最后理出一个运行关系图。
- //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);
- }
从上面可以看出,配置中的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()开始。
上述前两点,正好说明与前面的猜测一致。接口是要产生一个实现类,这个实现类用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就是类.方法名
- ...
- }
前面的分析,已经了解了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;
- }
-
前面的分析只是在非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);
通过跟踪发现:
其它就不分析了,关于mybatis-spring-boot-start中的autoconfiguration,后面会提到,现在快速进入mybatis-plus部分。
是不是上面的方式用着还不够爽?jpa是不是也有自己的方便之处?拿来一些给mybatis助力吧。另外如果在执行sql过程前后想插入自己的处理,怎么办?plus都提供了机制。
还是使用前面的mybatis plus的官方示例,发现用了这么一个base接口,还有泛型pojo类。
- public interface UserMapper extends BaseMapper<User> {
-
- }
这还是一个mapper接口,方法都在base类中,也对,每一个mapper中写的多数都一样,那整一个abstract类就行MybatisSqlSessionFactoryBean了。至于处理的数据对象不一样,那使用通用的反射代码,也都能获取各自的参数与sql,估计jpa也是这么弄的吧。想想这条路是可行的,也确实方便了使用者。自己有特殊的SQL,继承后另外写就行了。
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);
- }
前面说了,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使用。
说明executor在执行时,会使用这些handler分别处理请求的pojo泛型参数,还会处理返回值。
插件也是对终端用户提供的一个非常有用的功能。一般的扩展机制,最多的就是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方法等。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。