mybatis(1)- Springboot中的规范使用

@Mapper与@MapperScan如何正确使用

结论

测试证明
  • auto-configuration package 一定是启动类包路径
  • 测试1中:  @Mapper能正常启动是因为MybatisAutoConfiguration下的AutoConfiguredMapperScannerRegistrar来扫码启动类路径包下的带@Mapper对象。
  • 尽量不要自动创建SqlSessionTemplate,除非有特殊需要将SqlSessionTemplate归于容器管理。此时每个MapperFactoryBean将执行SqlSessionDaoSupport中的setSqlSessionFactory方法(会新建一个SqlSessionTemplate),但随后被再执行setSqlSessionTemplate方法注入的被容器管理的SqlSessionTemplate实例覆盖。
正确的使用方式有2种
  1. @Mapper + @ImportAutoConfiguration(MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class) + @SpringBootApplication(exclude = { MybatisAutoConfiguration.class })
  2. @MapperScan({ "com.noob.giant.dao" }) + @SpringBootApplication(exclude = { MybatisAutoConfiguration.class }) (package的路径要尽量精确到DAO层!)
分析

MybatisAutoConfiguration 在装载时会生成SqlSessionTemplate对象托管于容器中,导致MapperFactoryBean中setSqlSessionFactory、setSqlSessionTemplate 先后执行

引用javadoc对于 MybatisAutoConfiguration 的描述:
If {@link org.mybatis.spring.annotation.MapperScan} is used, or a  configuration file is specified as a property, those will be considered, otherwise this auto-configuration will attempt to register mappers based on  the interface definitions in or under the root auto-configuration package. 

如果@MapperScan 或者特定的资源文件被加载,那就要慎重考虑了。此类会尝试注册auto-configuration package路径下定义的接口为mappers

而对于MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar在执行过程中会扫描auto-configuration package路径下带@Mapper注解的类

 再来比对下MapperScannerConfigurer源码、@MapperScan import的MapperScannerRegistrar的源码,发现与AutoConfiguredMapperScannerRegistrar都是创建了ClassPathMapperScanner并确定扫描路径!
 详见:​​​​​mybatis的MapperScan到底做了什么

测试1

不使用@MapperScan和@Mapper:

public interface DecisionAdjustApplyMapper {
    DecisionAdjustApply selectUnqiue(@Param("applyNo") String applyNo);
}
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.ImportResource;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableDiscoveryClient
@EnableFeignClients
@EnableConfigurationProperties // 配合@ConfigurationProperties 合用
@EnableTransactionManagement(proxyTargetClass = true) // 启注解事务管理,等同于xml配置方式的 <tx:annotation-driven />
@EnableAsync //开启异步调用
@EnableAspectJAutoProxy //开启自定义切面
@ImportResource("classpath:dependence.xml")
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class})
//@MapperScan("com.noob.giant.dao")
public class GiantApplication {

    public static void main(String[] args) {
          //System.setProperty("DEPLOY_ENV", "test");
        SpringApplication.run(GiantApplication.class, args);
    }
}

结果: Mapper对象无法注入容器!
抛出异常

Description:
A component required a bean of type 'com.noob.giant.dao.DecisionAdjustApplyMapper' that could not be found.

Action:
Consider defining a bean of type 'com.noob.giant.dao.DecisionAdjustApplyMapper' in your configuration.

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.noob.giant.dao.DecisionAdjustApplyMapper' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@javax.annotation.Resource(shareable=true, lookup=, name=, description=, authenticationType=CONTAINER, type=class java.lang.Object, mappedName=)}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1493)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1104)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1066)
	at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.autowireResource(CommonAnnotationBeanPostProcessor.java:518)
	at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.getResource(CommonAnnotationBeanPostProcessor.java:496)
	at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor$ResourceElement.getResourceToInject(CommonAnnotationBeanPostProcessor.java:627)
	at org.springframework.beans.factory.annotation.InjectionMetadata$InjectedElement.inject(InjectionMetadata.java:169)
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
	at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessPropertyValues(CommonAnnotationBeanPostProcessor.java:318)
	... 54 common frames omitted

修正
后续测试发现: 单独  在Mapper类上增加@Mapper  或   在启动类上增加“@MapperScan("com.noob.giant.dao")” 都能启动注入成功。

SqlSessionTemplate 被处理了2次?

在测试过程中发现一个:

在对每一个Mapper对象进行beanWrapper过程中,创建MapperFactoryBean(extends SqlSessionDaoSupport)对象时,都会先执行SqlSessionDaoSupport中的setSqlSessionFactory方法根据传入的SqlSessionFactory (测试例子中实际对象是DefaultSqlSessionFactory创建了一个SqlSessionTemplate,而后再执行setSqlSessionTemplate方法又注入了一个SqlSessionTemplate来覆盖原有的!

a597bd58ff0332c3befa516e8b1c9289431.jpg

跟踪断点发现:
AbstractAutowireCapableBeanFactory.populateBean方法中

a7c4d5531c472e5ef4e339fec36785a7186.jpg
入参对象RootBeanDefinition的PropertyValues初始值正常:
5ea1ebe1a9f558d875692fe9d56df6b480c.jpg

 处理后最终得到的PropertyValues对象中却SqlSessionFactory SqlSessionTemplate !4357f02db894099f22308acfc1bc12210de.jpg

在测试中发现,MapperFactoryBean两方法中的SqlSessionTemplate 实例 内容是大致一样(构造方式默认相同:new SqlSessionTemplate(sqlSessionFactory))。 各个不同Mapper通过setSqlSessionTemplate方法注入的SqlSessionTemplate 实例是同一个对象,且setSqlSessionFactory的入参SqlSessionFactory实例也是同一个对象。

所以推测: sqlSessionTemplate在另外的地方被初始化托管在Spring容器中,被当做自动注入的属性了。

测试2 - AutoConfiguredMapperScannerRegistrar

经查验发现:
在运行环境中引用的mybatis-spring-boot-autoconfigure.jar中有一个MybatisAutoConfiguration自动装载了sqlSessionTemplate托管于容器,且提供了扫描@Mapper的处理类AutoConfiguredMapperScannerRegistrar。

    <parent>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot</artifactId>
        <version>1.3.2</version>
    </parent>
    <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
    <name>mybatis-spring-boot-autoconfigure</name>

与其他无此jar引用仍旧正常启动且只会执行setSqlSessionFactory方法的项目比对后, 发现没有MybatisAutoConfiguration加载也能启动,但其多了个配置:

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

第一次

@Mapper + 启动类排除自动装载: @SpringBootApplication(exclude = { MybatisAutoConfiguration.class })

结果:  失败!无法正常注入Mapper对象,报错同上。

第二次  

@Mapper + 启动类排除自动装载: @SpringBootApplication(exclude = { MybatisAutoConfiguration.class }) + 启动类增加自动装载@ImportAutoConfiguration(MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class)

@ImportAutoConfiguration(MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class)
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, MybatisAutoConfiguration.class,
        RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class })
//@MapperScan({ "com.noob.giant"})
public class GiantApplication { ...

@ImportAutoConfiguration 改为 @Import 就没用了!失败在:AutoConfiguredMapperScannerRegistrar的registerBeanDefinitions时 AutoConfigurationPackages.get()获取不到auto-configuration package 的路径。

结果: 正常启动, 且只执行了setSqlSessionFactory方法!!

跟踪断点发现: 

  1. AbstractAutowireCapableBeanFactory.populateBean()中处理得到的最后PropertyValues中只有SqlSessionFactory
    a6e9f7462e985297f7780e51c4acab23f2a.jpg

    后续测试时增入自己显示声明@Bean SqlSessionTemplate,又出现了!

    关键在于AbstractAutowireCapableBeanFactory的populateBean方法中执行autowireByType时对于resolveDependency结果是否为空的判定!

    详细分析见:https://my.oschina.net/u/3434392/blog/3010046 的【反向验证】

  2. MybatisAutoConfiguration 提供的 MapperScannerRegistrarNotFoundConfiguration 类自动加载的 AutoConfiguredMapperScannerRegistrar 获取到扫描mapper类的包路径。(该地址与启动application类包路径一致!)84a36a3a5c115f0ae88d377f6a5ea771569.jpg
第三次

启动类增加@MapperScan({ "com.noob.giant.dao" }) + 启动排除自动装载: @SpringBootApplication(exclude = { MybatisAutoConfiguration.class })

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class,MybatisAutoConfiguration.class, RedisAutoConfiguration.class,RedisRepositoriesAutoConfiguration.class })
@MapperScan({ "com.noob.giant.dao", "com.noob.xmagic.dao" })
public class GiantApplication { .......

结果: 具体呈现和第二次一致! 正常启动!!且创建MapperFactoryBean只执行了setSqlSessionFactory方法!!

测试3 - auto-configuration package 一定是启动类包路径

我们将启动的Application类移动到“com.noob.giant.hh”路径下 再次跟踪断点:
因为启动时默认扫描的是启动类包路径下的对象,所以本测试用例需要在启动Application类重新指定 @SpringBootApplication(scanBasePackages={"com.noob.giant"}, 否则加载不到bean
3a8ed5318cce62dfaacc07540c5793f3a42.jpg

测试4 -  单独使用@MapperScan时,扫描包路径一定要精确到Dao层!

@MapperScan({ "com.noob.giant" }):

一个大概的路径的话,在 ClassPathMapperScanner.doScan 时会把一些额外的接口也当作mapper。  如果精确到Dao层就没有这个现象!

bc065ddaa235cba5d269a31dc8e40e77a47.jpg

6835f2f3713361adfaa75cbd9d1c71a42c0.jpg

这是因为:

ClassPathBeanDefinitionScanner.doScan方法,在通过basePackage执行findCandidateComponens方法时使用了ClassPathMapperScannerisCandidateComponent方法对返回结果集判定: 但只是判定bean的定义描述是否是接口。所以导致doScan最终返回的BeanDefinitionHolder集合中含有很多不相干的接口信息。

  @Override
  protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
  }

@Param的使用

Java代码中定义了@Param("model")

	List<ProductInfo> queryByPage(@Param("model") ProductQueryReq queryModel);

mapper.xml中不写则启动报错:
162659_cCW3_3434392.png

BoundSql.getParameterObject():
e9d5f3dee0d67df50c5883541a01abe990c.jpg 

应该:

 <select id="queryByPage" resultMap="BaseResultMap">
    select 
    <include refid="Base_Column_List" />
    from product_info
    where is_deleted='N'
    <if test= "model.productStatus != null">
        and  status = #{model.productStatus,jdbcType=INTEGER}
    </if>
  </select>

只有一个参数可以不定义@Param, 且mapper.xml中可以直接用对象属性

List<ProductInfo> queryByPage(ProductQueryReq queryModel);

85ee7a42be5bf349c4c5a4c8f3c408daead.jpg

如果用了别名(@Param("model") )报错:

111115_2oYD_3434392.png

有多个相同的参数也需要定义@Param

	UserImageTransfer selectFirstHistoryOrcInfo(String productCode, String userId);

112425_f1R5_3434392.png184458_ytjN_3434392.png

collection的用法

collection 下 主表 和附表 都需要查出主键;其他字段没用,即使<id>标签写了其他字段,要用到表里面真实的主键.

<resultMap id="BaseResultMap" type="com.noob.domain.CmbClaimPackage" >
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="batch_no" property="batchNo" jdbcType="VARCHAR" />
    <result column="product_code" property="productCode" jdbcType="VARCHAR" />
    <result column="package_no" property="packageNo" jdbcType="VARCHAR" />
    <result column="serial_no" property="serialNo" jdbcType="VARCHAR" />
    <result column="amount" property="amount" jdbcType="DECIMAL" />
    <result column="total_count" property="totalCount" jdbcType="DECIMAL" />
    <result column="origin_info" property="originInfo" jdbcType="VARCHAR" />
    <result column="trade_status" property="tradeStatus" jdbcType="INTEGER" />
    <result column="return_info" property="returnInfo" jdbcType="VARCHAR" />
    <result column="upload_sign" property="uploadSign" jdbcType="CHAR" />
    <result column="last_upload_date" property="lastUploadDate" jdbcType="VARCHAR" />
    <result column="remark" property="remark" jdbcType="VARCHAR" />
    <result column="creator" property="creator" jdbcType="VARCHAR" />
    <result column="gmt_created" property="gmtCreated" jdbcType="TIMESTAMP" />
    <result column="modifier" property="modifier" jdbcType="VARCHAR" />
    <result column="gmt_modified" property="gmtModified" jdbcType="TIMESTAMP" />
    <result column="is_deleted" property="isDeleted" jdbcType="CHAR" />
  </resultMap>

	<resultMap id="PackageModelResultMap" type="com.noob.model.CmbClaimPackageModel" extends="BaseResultMap">
		<collection property="detailList" javaType="list" ofType="com.noob.domain.CmbClaimDetail">
	        <id column="d_id" property="id" jdbcType="BIGINT" />
	        <result  column="d_batch_no" property="batchNo" jdbcType="VARCHAR" />
	        <result  column="d_product_code" property="productCode" jdbcType="VARCHAR" />
		    <result column="d_amount" property="amount" jdbcType="DECIMAL" />
		    <result column="d_package_no" property="packageNo" jdbcType="VARCHAR" />
		    <result column="d_contract_no" property="contractNo" jdbcType="VARCHAR" />
            <result column="d_policy_no" property="policyNo" jdbcType="VARCHAR" />
            <result column="d_serial_no" property="serialNo" jdbcType="VARCHAR" />
		    <result column="d_trade_no" property="tradeNo" jdbcType="VARCHAR" />
		    <result column="d_report_amount" property="reportAmt" jdbcType="DECIMAL" />
		    <result column="d_trade_status" property="tradeStatus" jdbcType="INTEGER" />
		    <result column="d_resp_code" property="respCode" jdbcType="VARCHAR" />
		    <result column="d_resp_msg" property="respMsg" jdbcType="VARCHAR" />
		    <result column="d_report_no" property="reportNo" jdbcType="VARCHAR" />
		    <result column="d_claim_no" property="claimNo" jdbcType="VARCHAR" />
		    <result column="d_return_info" property="returnInfo" jdbcType="VARCHAR" />
		    <result column="d_agreed_repay_date" property="agreedRepayDate" jdbcType="VARCHAR" />
		    <result column="d_gmt_created" property="gmtCreated" jdbcType="TIMESTAMP" />
		</collection>
	</resultMap>


<select id="selectDoing" resultMap="PackageModelResultMap" parameterType="java.util.Date" >
    select  p.id,
            p.batch_no, 
            p.serial_no, 
            p.product_code, 
            p.package_no, 
            p.amount, 
            p.origin_info,  
            p.trade_status,  
            p.return_info,  
            d.id d_id,
            d.product_code d_product_code,
            d.package_no d_package_no,
            d.batch_no  d_batch_no,
            d.serial_no d_serial_no, 
            d.amount  d_amount, 
            d.contract_no d_contract_no, 
            d.policy_no  d_policy_no, 
            d.trade_no d_trade_no, 
            d.report_amount d_report_amount, 
            d.trade_status d_trade_status, 
            d.resp_code d_resp_code, 
            d.resp_msg d_resp_msg, 
            d.report_no d_report_no, 
            d.claim_no  d_claim_no, 
            d.agreed_repay_date d_agreed_repay_date,
            d.gmt_created d_gmt_created,
            d.return_info d_return_info
         from cmb_creditcard_claim_package p inner join cmb_creditcard_claim_detail d on p.batch_no = d.batch_no
           where   p.gmt_created >= #{beginDate,jdbcType=TIMESTAMP} 
                   and  <include refid="doing_condition"/>
                   and  <include refid="alive_condition"/>
  </select>

转载于:https://my.oschina.net/u/3434392/blog/3009819

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值