前言

MyBatis-Plus(简称MP)相对于MyBatis简化了很多,开发效率也提升了不少,这也是Mybatis-Plus框架实现的初衷,愿景是成为MyBatis最好的搭档,就像魂斗罗中提到的1P、2P,基友搭配,效率翻倍。
image-1675778086965

MyBatis-Plus实际是在MyBatis的基础上进行了功能的加强,包括:

  • 内置通用CRUD,无序XML配置
  • 支持分页、多租户等插件
  • 强大的条件构造器,满足大多场景

今天主要聊一聊通用CRUDBaseMapper如何实现,单表的数据库操作基本不用编写sql。


一个例子

项目github传送门>

1、定义model

@Data
@TableName("user")
public class User {
    /**
     * 配置type = IdType.ASSIGN_ID,默认雪花算法,可以通过实现IdentifierGenerator自定义
     */
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;

    private String name;
    private Integer age;
    private String email;
    private String telPhone;
    @TableLogic
    private int deleted;
}

2、定义mapper

public interface UserMapper extends BaseMapper<User>{}

3、定义service

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}

4、配置

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

    @Bean
    public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() {
        return properties -> {
            GlobalConfig globalConfig = properties.getGlobalConfig();
            globalConfig.setBanner(false);
            MybatisConfiguration configuration = new MybatisConfiguration();
            configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
            properties.setConfiguration(configuration);
        };
    }

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer(){
        return builder -> builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
    }
}

5、单测执行

    @Test
    public void testFind() {
        Wrapper queryWrapper = new LambdaQueryWrapper<User>()
                .eq(User::getName, "Jack");
        List<User> users = userService.list(queryWrapper);
        System.out.println(users);
    }

BaseMapper实现原理

类图

image-1675780789702

源码

通过上面的示例项目,跟一下源代码:

1、首先spring boot starter项目都会有一个启动类
找到mybatis plus boot start包下面有个METE-INF/spring.factories文件

# Auto Configure
org.springframework.boot.env.EnvironmentPostProcessor=\
  com.baomidou.mybatisplus.autoconfigure.SafetyEncryptProcessor
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.baomidou.mybatisplus.autoconfigure.IdentifierGeneratorAutoConfiguration,\
  com.baomidou.mybatisplus.autoconfigure.MybatisPlusLanguageDriverAutoConfiguration,\
  com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration

关注类MybatisPlusAutoConfiguration,它会替代MyBatisAutoConfiguration,进行自动化配置。

2、看下MybatisPlusAutoConfiguration配置类

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisPlusProperties.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
	@Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    	// TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        // 省略部分代码
        GlobalConfig globalConfig = this.properties.getGlobalConfig();
        // 省略部分代码
        this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
        // 省略部分代码
        factory.setGlobalConfig(globalConfig);
        return factory.getObject();
    }
}

类上注解解析

  • @Configuration:表示一个配置类,会初始化容器管理,且Spring加载@Bean注解的方法。
  • @ConditionalOnClass:表示classpath下必须有SqlSessionFactory和SqlSessionFactoryBean两个类才能初始化当前类,因为这两个类来自MyBatis的包,也及需要引用对应的包(starter已经帮我们做了这个工作)。
  • @ConditionalOnSingleCandidate:当bean的容器中只有一个DataSource或者一个首选的DataSource时才会加载当前类。
  • @EnableConfigurationProperties(MybatisPlusProperties.class):表示使加了@ConfigurationProperties 注解的类MybatisPlusProperties生效,并且将该类注入到 IOC 容器中,让bean对应上yaml里的配置。
  • @AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})表示先加载DataSourceAutoConfiguration和MybatisPlusLanguageDriverAutoConfiguration两个类,再加载其注解的类

看到sqlSessionFactory方法,说明spring会加载该方法,实例化SqlSessionFactory

3、在sqlSessionFaction中,主要是将配置信息注入到MybatisSqlSessionFactoryBean属性中,包括GlobalConfig,包括了很多扩展功能

其中mapperLocations便配置了mapper xml的路径,后续进行加载到配置中

4、接下来,MybatisSqlSessionFactoryBean的对象factory.getObject()通过该方式来创建SqlSessionFactory

//com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean
@Override
public SqlSessionFactory getObject() throws Exception {
     if (this.sqlSessionFactory == null) {
     	// 初次加载时sqlSessionFactory为null,调用afterPropertiesSet方法
         afterPropertiesSet();
      }
      return this.sqlSessionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
        notNull(dataSource, "Property 'dataSource' is required");
        state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property 'configuration' and 'configLocation' can not specified with together");
        //TODO 清理掉资源  建议不要保留这个玩意了
        SqlRunner.DEFAULT.close();
        // 核心在buildSqlSessionFactory方法
        this.sqlSessionFactory = buildSqlSessionFactory();
    }
private Resource[] mapperLocations;

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
    // 省略部分代码
    for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
            continue;
        }
        try {
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
            xmlMapperBuilder.parse();
        } catch (Exception e) {
            throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
     // 省略部分代码
}

XMLMapperBuilder属于MyBatis的类,通过MyBatis进行mapper xml的解析,不过传入的targetConfiguration是MyBatis-Plus的配置类MybatisConfiguration(MybatisConfiguration继承了MyBatis的Configuration类,实现了部分方法)。

5、接着进入到XMLMapperBuilder的parse方法进行mapper的解析

先插一句,解析完便通过MybatisSqlSessionFactoryBuilder进行SqlSessionFactory的创建,默认是new DefaultSqlSessionFactory,并设置到GlobalConfig中进行缓存。

// org.apache.ibatis.builder.xml.XMLMapperBuilder
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // 核心方法
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
private void bindMapperForNamespace() {
	// namespace,即你配置的mapper xml中的namespace配置的类限定名
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        // ignore, bound type is not required
      }
      if (boundType != null && !configuration.hasMapper(boundType)) {
        // Spring may not know the real resource name so we set a flag
        // to prevent loading again this resource from the mapper interface
        // look at MapperAnnotationBuilder#loadXmlResource
        configuration.addLoadedResource("namespace:" + namespace);
        // configuration是MyBatis-Plus的MybatisConfiguration,重写了MyBatis的对于方法,进行mapper解析
        configuration.addMapper(boundType);
      }
    }
  }

6、进入到MybatisConfiguration类

public class MybatisConfiguration extends Configuration {
    protected final MybatisMapperRegistry mybatisMapperRegistry = new MybatisMapperRegistry(this);

    @Override
    public <T> void addMapper(Class<T> type) {
        mybatisMapperRegistry.addMapper(type);
    }
}

MybatisConfiguration 重点在于使用了自定义的MybatisMapperRegistry 添加 Mapper。

public class MybatisMapperRegistry extends MapperRegistry {
    @Override
    public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
            boolean loadCompleted = false;
            // 省略部分代码
            try {
                knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
                MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
                parser.parse();
                loadCompleted = true;
            } finally {
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
}

7、自定义的MybatisMapperRegistry使用了自定义的MybatisMapperAnnotationBuilder类进行mapper的解析

public class MybatisMapperAnnotationBuilder extends MapperAnnotationBuilder {

    @Override
    public void parse() {
        String resource = type.toString();
        if (!configuration.isResourceLoaded(resource)) {
            // 省略解析 Mapper xml方法及方法上注解sql代码
            // TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
            try {
                if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
                	// 
                    parserInjector();
                }
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new InjectorResolver(this));
            }
        }
    }
    
    void parserInjector() {
  	GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
    }    
}

8、到了这里解析 Mapper 接口的时候,MyBatis-Plus 会判断接口是否继承 Mapper 接口,如果是的话就会注入动态 CURD 动态 SQL。这里我们可以看一下注入 SQL 的接口。

public interface ISqlInjector {
    void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);
}

接口的默认实现

public class DefaultSqlInjector extends AbstractSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        return Stream.of(
            new Insert(),
            new Delete(),
            new DeleteByMap(),
            new DeleteById(),
            new DeleteBatchByIds(),
            new Update(),
            new UpdateById(),
            new SelectById(),
            new SelectBatchByIds(),
            new SelectByMap(),
            new SelectOne(),
            new SelectCount(),
            new SelectMaps(),
            new SelectMapsPage(),
            new SelectObjs(),
            new SelectList(),
            new SelectPage()
        ).collect(toList());
    }
}

9、最终MyBatis-Plus循环使用AbstractMethod的上述实现类进行构造常用的CRUD方法。方法最终解析到MyBatis-Plus的MyBatisConfiguration的mappdStatement属性中。
image-1675868145117

调用类方法流程图

image-1675868243223