Spring+Mybatis + Mybatis-Plus 自定义无XML的sql生成及MapperProxy代理生成

问题产生背景

现在新服务ORM框架是使用mybatis3.4.6mybatis-plus2.2.0

最近在项目中偶然发现CouponRecord实体类中增加了这样一行代码如下,导致在Service中调用this.selectCount出现NPE。当然出现NPE很好解决,直接判断下是否为null就OK了。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("coupon_record")
public class CouponRecord {
...
@TableField(value = "product_quantity")
private BigDecimal productQuantity;
public BigDecimal getProductQuantity() {
// 提交上的代码
return this.productQuantity.setScale(2, RoundingMode.HALF_DOWN);
// 解决方式如下
//return this.productQuantity == null ? null : this.productQuantity.setScale(2, RoundingMode.HALF_DOWN);
}
...
}

调用链:CouponRecordServiceImpl#count->ServiceImpl#selectCount->BaseMapper#selectCount,主要代码如下:

ServiceImpl的部分代码如下:

public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
@Autowired
protected M baseMapper;
...
@Override
public int selectCount(Wrapper<T> wrapper) {
return SqlHelper.retCount(baseMapper.selectCount(wrapper));
}
...
}

BaseMapper所有接口如下:

public interface BaseMapper<T> {
Integer insert(T entity);
Integer insertAllColumn(T entity);
Integer deleteById(Serializable id);
Integer deleteByMap(@Param("cm") Map<String, Object> columnMap);
Integer delete(@Param("ew") Wrapper<T> wrapper);
Integer deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);
Integer updateById(@Param("et") T entity);
Integer updateAllColumnById(@Param("et") T entity);
Integer update(@Param("et") T entity, @Param("ew") Wrapper<T> wrapper);
T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
T selectOne(@Param("ew") T entity);
Integer selectCount(@Param("ew") Wrapper<T> wrapper);
List<T> selectList(@Param("ew") Wrapper<T> wrapper);
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> wrapper);
List<Object> selectObjs(@Param("ew") Wrapper<T> wrapper);
List<T> selectPage(RowBounds rowBounds, @Param("ew") Wrapper<T> wrapper);
List<Map<String, Object>> selectMapsPage(RowBounds rowBounds, @Param("ew") Wrapper<T> wrapper);
}

我们在业务代码CouponRecordServiceImpl#count中直接调用,可能会产生如下疑问?

  • 我们没有配置XML为什么调用selectCount可以查询?既然可以查询那么生成的SQL长成什么样子?
  • 通过看ServiceImpl中的代码,会发现是直接注入baseMapper,baseMapper明明是接口咋个就可以使用了呢?

对于工作了这么多年的老司机,猜也猜的出百分之八九十吧。在整理这篇文章之前,以前浏览过,我确实忘记的差不多了。感谢公司能提供给大家不管是组内分享还是部门分享机会,分享总会给自己和他人的很大进步。不扯淡这些了。下面将对此这些疑问来逐一解决。但是这里要说明下,这里只看我们关心的内容,其他比如在与spring整合后有些为什么要这样写,可以找学习spring组来做分享或者后面整理好文章后在分享。

框架是如何使用

任何框架学习,首先要会用,不然就是扯淡。框架都是在实际的应用中逐渐抽象出来的,简化我们工作。

Service主要代码如下:

@Service
public class CouponRecordService extends ServiceImpl<CouponRecordDao, CouponRecord> {
public int count(Date endTime) {
CouponRecord conditionCouponRecord = CouponRecord.builder().status(CouponStatus.USED).isDelete(YesNo.NO.getValue()).build();
return selectCount(new EntityWrapper<>(conditionCouponRecord).le("create_time", endTime).isNotNull("order_no"));
}
}

Dao(或者叫Mapper)

public interface CouponRecordDao extends BaseMapper<CouponRecord> {
}

spring的相关配置如下:

<bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/> <!-- 自动扫描entity目录, 省掉Configuration.xml里的手工配置 -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath*:mapper/**/*.xml"/>
<property name="plugins">
<array>
<!-- 分页插件配置 -->
<bean id="paginationInterceptor" class="com.baomidou.mybatisplus.plugins.PaginationInterceptor">
<property name="dialectType" value="mysql"/>
</bean>
<bean id="limitInterceptor" class="com.common.mybatis.LimitInterceptor"/>
</array>
</property>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.merchant.activity.**.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<context:component-scan base-package="com.common.**,com.merchant.activity.**">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

用法+大致配置就是这样的。接下来看看这些无Xml的SQL是怎么生成的以及生成出来的SQL长成什么样?

无Xml的SQL是如何生成生成及SQL长成什么样

在如何使用中,可以看到XML中有如下一段配置:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.merchant.activity.**.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

这段的配置作用就是扫描我们的Mapper或者Dao的入口。

大概类图如下:

接下来对源码做分析

BeanDefinition解析阶段

MapperScannerConfigurer

MapperScannerConfigurer得继承关系如下图:

从图中看出MapperScannerConfigurer实现了我们关注的BeanDefinitionRegistryPostProcessor、InitializingBean接口,Spring在初始化Bean的时候会执行对应的方法。

ClassPathMapperScanner构造

构造ClassPathMapperScanner扫描类,扫描basePackage包下的Mapper或者Dao并注册我们的Mapper Bean到容器中.

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
...
@Override
public void afterPropertiesSet() throws Exception {
// 验证是否配置了basePackage
notNull(this.basePackage, "Property 'basePackage' is required");
} @Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// left intentionally blank
} @Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 是否有占位符,处理之
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
// 扫描
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
// 注册一些过滤器,包括和不包括。有部分可以在xml中配置,比如:annotationClass、markerInterface
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
...
}
ClassPathMapperScanner#scan

扫描类并生成BeanDefinition注入到Spring容器中,注意这里的ClassPathMapperScanner继承ClassPathBeanDefinitionScanner,在ClassPathMapperScanner中未实现scan,所以直接调用父类的scan方法。为了便于阅读这里将源码中的日志删除了。大致源码如下:

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
...
public int scan(String... basePackages) {
// 获取之前容器中bean的数量
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
// 真正干事的---扫描, 调用子类ClassPathMapperScanner#doScan(basePackages)方法
doScan(basePackages);
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
// 返回注册bean的数量
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
// 真正干事的扫描 生成BeanDefinition集合
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
// BeanDefinitionHolder 的集合
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
for (String basePackage : basePackages) {
// 通过查找候选bean定义
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
// 遍历进行部分逻辑处理
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
// 设置作用域
candidate.setScope(scopeMetadata.getScopeName());
// 生成beanName
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
// 增加默认值,autowireCandidate
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
// 注册BeanDefinition到容器中。
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
...
}
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 调用父类的ClassPathBeanDefinitionScanner#doScaner(basePackages)方法,扫描生产BeanDefinitionHolder集合
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
// MapperBean 需要一些额外的处理,查看这个方法
processBeanDefinitions(beanDefinitions);
} return beanDefinitions;
} //对每个Mapper的BeanDefinition定义处理,
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
// 构造器参数,下一行代码将Bean设置为MapperFactoryBean,MapperFactoryBean的构造器中有个参数是mapperInterface
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
// 这一步非常重要,把我们的Bean设置为MapperFactoryBean,接下来会看到MapperFactoryBean的继承关系
definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig); boolean explicitFactoryUsed = false;
// 在bean中增加sqlSessionFactory
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
// 在bean中增加sqlSessionTemplate
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
// 设置自动注入模式
if (!explicitFactoryUsed) {
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
}
}
}

写到代码的注释可能都不怎么关注,这里再次强调下重点,如果不注意后续可能有些会懵逼的。这是怎么来的。

  1. BeanDefinition的class设置为MapperFactoryBean
  2. 将原始mapper的接口类型以MapperFactoryBean构造器的参数传入,也就是后面你将看到参数是mapperInterface.

BeanDefinition初始化阶段

MapperFactoryBean

经过上面的扫描并注册,现在容器中已经存在了我们的Mapper Bean了,在上面的说构建Mapper BeanDefinition的时候注意这些BeanDefinition的class类型设置为了MapperFactoryBean,先看看MapperFactoryBean的继承关系如下:

从图中,看出MapperFactoryBean是实现了InitializingBean接口。DaoSupport对afterPropertiesSet()实现了。我们都知道Spring在初始化会Bean的时候将会调用afterPropertiesSet()方法。那么看看这个方法干了什么事

public abstract class DaoSupport implements InitializingBean {
protected final Log logger = LogFactory.getLog(getClass());
@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
// 检查Dao配置
checkDaoConfig();
// Let concrete implementations initialize themselves.
try {
initDao();
}
catch (Exception ex) {
throw new BeanInitializationException("Initialization of DAO failed", ex);
}
}
protected abstract void checkDaoConfig() throws IllegalArgumentException;
protected void initDao() throws Exception {
}
}

一看典型的模板设计模式,真正处理在子类中。这里我们关心的是checkDaoConfig(),看看子类MapperFactoryBean#checkDaoConfig实现干了些什么事

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
...
protected void checkDaoConfig() {
super.checkDaoConfig();//调用父类的方法,父类就是检查sqlSession是否为null。null的话抛出异常
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
// 通过sqlSession获取MybatisConfiguration,相当于我们每一个MapperBean都是由SqlSession的,否则你想咋个查询呢
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
// 将mapperInterface注册到configuration中。
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();
}
}
}
...
}

MybatisConfiguration#addMapper干的就是将类型注册到我们Mapper容器中,便于后续取

public class MybatisConfiguration extends Configuration {
...
public final MybatisMapperRegistry mybatisMapperRegistry = new MybatisMapperRegistry(this);
public <T> void addMapper(Class<T> type) {
mybatisMapperRegistry.addMapper(type);
}
...
}

接下来就要看看MybatisMapperRegistry#addMapper注册到底干了何事。猜猜应该就是自定义无XML的sql生产注入。哪些是自定义?就是我们BaseMapper中的那一堆方法。

XXXRegistry 类的名字起的真好,看名字就是一个注册器。这里的注册器有一箭双雕的作用

  1. 定义了一个Map,缓存所知道的Mapper,后面初始化MapperProxy代理用的着,不然后面不好取哦
  2. 将解析出来的SQL,注册到Configuration中
public class MybatisMapperRegistry extends MapperRegistry {
...
// 这个knownMappers之前以为起的不够好。。当再次看的时候发现还真不错,known翻译就是众所周知,那么在这里就是我们已经扫描并且已经注册了的Mapper了,在内部来说当然是都知道的。
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
// 注入过就不再执行了。
if (hasMapper(type)) {
return;
}
boolean loadCompleted = false;
try {
// 这里先记着,后面查看我们MapperProxy代理用的着哦
knownMappers.put(type, new MapperProxyFactory<>(type));
// mybatisMapper注解构建器
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
// 解析
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
...
}
MybatisMapperAnnotationBuilder#parse

接下来将是生成无xml对应的SQL了。

Mybatis-Plus BaseMapper自动生成SQL及MapperProxy的更多相关文章

  1. 基于eclipse的mybatis映射代码自动生成的插件

    基于eclipse的mybatis映射代码自动生成的插件 分类: JAVA 数据库 工具相关2012-04-29 00:15 2157人阅读 评论(9) 收藏 举报 eclipsegeneratori ...

  2. 基于eclipse的mybatis映射代码自动生成的插件http://blog.csdn.net/fu9958/article/details/7521681

    基于eclipse的mybatis映射代码自动生成的插件 分类: JAVA 数据库 工具相关2012-04-29 00:15 2157人阅读 评论(9) 收藏 举报 eclipsegeneratori ...

  3. 使用Java注解开发自动生成SQL

    使用注解开发的好处就是减少配置文件的使用.在实际过程中,随着项目越来越复杂,功能越来越多,会产生非常多的配置文件.但是,当配置文件过多,实际维护过程中产生的问题就不容易定位,这样就会徒劳的增加工作量. ...

  4. 使用Excel自动生成sql语句

    在近一段日子里,进入了新的项目组,由于项目需要,经常要将一些Excel表中的数据导入数据库中,以前并没有过多的接触过数据导入与数据处理,对于我来说比较痛苦,今天下午花了几个小时处理数据,但是同事给我提 ...

  5. Eclipse 使用mybatis generator插件自动生成代码

    Eclipse 使用mybatis generator插件自动生成代码 标签: mybatis 2016-12-07 15:10 5247人阅读 评论(0) 收藏 举报 .embody{ paddin ...

  6. springboot整合mybatis,利用mybatis-genetor自动生成文件

    springboot整合mybatis,利用mybatis-genetor自动生成文件 项目结构: xx 实现思路: 1.添加依赖 <?xml version="1.0" e ...

  7. springboot+mybatis+mysql 利用mybatis自动生成sql语句

    工具和环境 idea,mysql,JDK1.8 效果图如下 结构图如下 java resources sql文件 /* Navicat MySQL Data Transfer Source Serve ...

  8. spring和mybatis集成,自动生成model、mapper,增加mybatis分页功能

    软件简介 Spring是一个流行的控制反转(IoC)和面向切面(AOP)的容器框架,在java webapp开发中使用广泛.http://projects.spring.io/spring-frame ...

  9. MyBatis使用Generator自动生成代码

    MyBatis中,可以使用Generator自动生成代码,包括DAO层. MODEL层 .MAPPING SQL映射文件. 第一步: 配置好自动生成代码所需的XML配置文件,例如(generator. ...

随机推荐

  1. iOS 更改状态栏文字颜色

    第一步:在info.plist中添加一个字段:view controller -base status bar 设置为NO 第二步: 在AppDelegate.m的 didFinishLaunchin ...

  2. phpspider爬虫框架的使用

    这几天使用PHP的爬虫框架爬取了一些数据,发现还是挺方便的,先上爬虫框架的文档 phpspider框架文档 使用方法其实在文档中写的很清楚而且在demo中也有使用示例,这里放下我自己的代码做个笔记 & ...

  3. 个人学习HTML以及CSS所得体会

    拥有自己样式的浏览器: 苹果,欧朋,谷歌,IE,火狐 form标签<form></form> 表单属性: 1,action主要同来规定表单的作用,提交到处理器上面处理URL,默 ...

  4. ASP.NET---如何使用web api创建web服务

    1 首先创建asp.net web空项目,并且创建模拟数据,我在工程下面创建了一个Models文件夹,在文件夹Nodels下面创建类Product和Repository 具体如下: [Serializ ...

  5. 三 HashSet

    HashSet无序且不能重复 1.HashSet类的字段属性 //HashSet集合中的内容是通过 HashMap 数据结构来存储的 private transient HashMap<E,Ob ...

  6. 4.Servlet(动态web资源)

    Servlet (动态web资源) 开发一个动态web资源(即开发一个Java程序向浏览器输出数据) 需完成以下两个步骤 1.编写一个Java类,实现servelet接口 2.把开发好的Java类部署 ...

  7. 【Java并发】并发队列与线程池

    并发队列 阻塞队列与非阻塞队 ConcurrentLinkedQueue BlockingQueue ArrayBlockingQueue LinkedBlockingQueue PriorityBl ...

  8. str 文本函数的调用

    方法 说明 S.isdigit() 判断字符串中的字符是否全为数字 S.isalpha() 判断字符串是否全为英文字母 S.islower() 判断字符串所有字符是否全为小写英文字母 S.isuppe ...

  9. 关于rtos中任务切换时的程序流程

    今天和一个小伙伴讨论了一下基于cortex-m3内核的RTOS在任务切换时的程序流程,小伙伴说国内某搜索引擎都搜不到这类的信息,所以我才打算写下来,硬件平台是stm32f1​. 这里的切换有两种情况: ...

  10. SpringCloud01——服务的注册和发现

    SpringCloud01--服务的注册和发现 一.微服务的注册和发现 我们在微服务中,往往有服务提供者,服务消费者和服务注册中心.我们之前学习的Zookeeper就是一个注册中心.但是在官方的Spr ...