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. winfrom_权限设置_TreeView的相关问题

    1.获取TreeView的值: 循环TreeView,获取checked每个节点的Text,串起来用逗号“,”隔开,保存到数据库. List<string> list = new List ...

  2. DDD 理解

    DDD提倡充血模型,业务放在类中,而不是服务中,刚开始是比较不清楚的.突然明白,以前开发桌面程序的时候,不就是这样处理了吗?业务分析和代码实现一一对应.因为桌面程序没有数据库,他就是纯粹的面向对象的实 ...

  3. 单变量图形的pandas方法

    数据加载与展示: 1. 类别数据的Bar图 1.1 每一类对应有多少个 1.2 每类数量占整体的比值 1.3 对X轴进行排序

  4. ASP.NET如何接收清楚缓存的通知

    1 如果使用cache的add方法或者接受CacheItemPriority值得Insert方法重载接收通知,可以提供CacheItemRemovedCallBack对象,选择在清除对象时接收通知 u ...

  5. BigDecimal与Long、int之间的相互转换

    //bigDecimal 转换成 Long类型 public static Long bigDecimalToLong(BigDecimal b){ BigDecimal c = new BigDec ...

  6. 一个简单的例子学会github repository的webhook

    github的webhook是个有用的功能,允许开发人员指定一个服务器的url.当开发者对github仓库施加操作,比如提交代码,创建issue时,github网站会自动向该url指定的服务器推送事件 ...

  7. DDOS 攻击的防范教程--转载自阮一峰的博客

    一个多月前,我的个人网站遭受 DDOS 攻击,下线了50多个小时.这篇文章就来谈谈,如何应对这种攻击. 需要说明的是,我对 DDOS 并不精通,从没想过自己会成为攻击目标.攻击发生以后,很多素昧平生的 ...

  8. Image Processing and Analysis_8_Edge Detection: Optimal edge detection in two-dimensional images ——1996

    此主要讨论图像处理与分析.虽然计算机视觉部分的有些内容比如特 征提取等也可以归结到图像分析中来,但鉴于它们与计算机视觉的紧密联系,以 及它们的出处,没有把它们纳入到图像处理与分析中来.同样,这里面也有 ...

  9. C# List集合去除重复数据

    实例如下: using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; ...

  10. wampserver apache 500 Internal Server Error解决办法

    Internal Server ErrorThe server encountered an internal error or misconfiguration and was unable to ...