spring整合myabtis无非就是,把@Mapper修饰的接口扫描到容器中,然后spring中接口是不能呗扫描成为bean定义的,还有spring中connection与mybatis中connection的整合无非就是同一个线程spring生成的connection放到ThreadLocal中,mybatis用到时直接从中取保证了同一个业务方法中spring的连接与Mybatis时同一个。(此文只针对相应的@Mapper接口扫描beandefinition)
一、如何把@Mapper接口扫描到容器中
(1) spring中接口是不能扫描成bean定义的,而且bean定义的class肯定不能设置为接口类型,因接口不能实例化。spring提供了FactoryBean,因此我们可以在生成bean定义的时候指定beanClass为FactoryBean类型当getObject时根据当前接口生成代理对象
看下伪代码:
public class MybatisFactoryBean implements FactoryBean {
private Class classInterface;
public MybatisFactoryBean(Class<?> classInterface) {
this.classInterface = classInterface;
}
@Override
public Object getObject() throws Exception {
return 生成相应代理对象;
}
@Override
public Class<?> getObjectType() {
return classInterface;
}
}
/// 扫描无非就是把没过bean定义的先把构造参数设置当前的接口的,再把beanClass设置FactoryBean类型因为需要实例化
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
beanDefinition.setBeanClassName(MybatisFactoryBean.class.getName());
beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
生成代理对象mybatis中sqlSession.getMapper(classInterface);
所以FactoryBean需要有SqlSession属性
public class MybatisFactoryBean implements FactoryBean {
private Class classInterface;
private DefaultSqlSession sqlSession;
//设置by_type注入会扫描所有set方法进行注入不多做解释
public void setSqlSession(SqlSessionFactory sqlSessionFactory) {
this.sqlSession = (DefaultSqlSession)sqlSessionFactory.openSession();
//此处必须把这个接口加入进来负责报错
sqlSession.getConfiguration().addMapper(classInterface);
}
public MybatisFactoryBean(Class<?> classInterface) {
this.classInterface = classInterface;
}
@Override
public Object getObject() throws Exception {
return sqlSession.getMapper(classInterface);
}
@Override
public Class<?> getObjectType() {
return classInterface;
}
}
因SqlSessionFactory 需要在容器中
(2)、SqlSessionFactory 需要交给spring容器
@ComponentScan("com.spring.demo.component")
@Configuration
@MapperScan("com.spring.demo.mybatis")
public class ApplicationConfig {
@Bean
public SqlSessionFactory sqlSessionFactory() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
return sqlSessionFactory;
}
}
mybatis配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test_db?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
</configuration>
(3)、考虑如何把接口扫描成bean定义呢
先来了解下 spring中扫描bean定义:
1、org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
2、org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponents
3、org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
try {
MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
//1此处就是改造关键点
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setSource(resource);
//2、此处会把接口类型过滤掉
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
candidates.add(sbd);
}
}
}
return candidates;
}
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
return (metadata.isIndependent() && (metadata.isConcrete() ||
(metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
}
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
//此处扫描只会this.includeFilters.add(new AnnotationTypeFilter(Component.class));@Componet注解的bean
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
因此我们自定义扫描器肯定需要把1、不是@Component注解修饰的也需要扫描进来,2、还要把接口类行不要过滤掉
为了方便自定义扫描器继承ClassPathBeanDefinitionScanner只需要把上面注释中的1、2两处重写即可
public class MybatisMapperScan extends ClassPathBeanDefinitionScanner {
public MybatisMapperScan(BeanDefinitionRegistry registry) {
super(registry);
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
beanDefinition.setBeanClassName(MybatisFactoryBean.class.getName());
beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
return beanDefinitionHolders;
}
@Override//把接口类型放开
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
AnnotationMetadata metadata = beanDefinition.getMetadata();
return metadata.isInterface();
}
}
public class MybatisImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<String,Object> mapScan= importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName());
String value= (String) mapScan.get("value");
MybatisMapperScan mybatisMapperScan=new MybatisMapperScan(registry);
//2、添加过滤条件此处把所有条件都放开,spring中此处之后把@Copmonet注解放开,此处后续可以改造只扫描@Mapper注解的放开
mybatisMapperScan.addIncludeFilter(new TypeFilter(){
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
return true;
}
});
mybatisMapperScan.doScan(value);
}
}
何时会触发这个扫描器呢?
ImportBeanDefinitionRegistrar接口Spring启动时候调用该接口的registerBeanDefinitions()因此我们只需在配置类上导入该类即可
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MybatisImportBeanDefinitionRegistrar.class)
public @interface MapperScan {
String value() default "";
}
@ComponentScan("com.spring.demo.component")
@Configuration
@MapperScan("com.spring.demo.mybatis")
public class ApplicationConfig {
@Bean
public SqlSessionFactory sqlSessionFactory() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
return sqlSessionFactory;
}
}
一切准备就绪测试(注意我的TestMapper接口没有加@Mapper注解因为上面代码过滤中会把所有类都扫描到,此处可以扩展,即加上@Mapper,上面过滤条件只扫描带此注解的):
public interface TestMapper {
@Select("select 'hello' from dual ")
String select();
}
@Component
public class UserService {
@Autowired
private TestMapper testMapper;
public void hello(){
System.out.println(testMapper.select());
}
}
二、代码demo路径
https://github.com/kangchangchang/SpringMybatis.git