引言
有关于Spring整合Mybatis其实一直是一个很具有典型代表性的Spring实际应用,今天就带着大家由浅入深手撸一遍整合的代码
手撕代码
准备工作
首先准备两个Mapper作为今天演示的操作对象
import org.apache.ibatis.annotations.Select;
public interface UserMapper {
	@Select("select username from user where id = 1")
	String selectById();
}
import org.apache.ibatis.annotations.Select;
public interface OrderMapper {
	@Select("select 'user_login_platfrom")
	void selectById();
}
然后是UserService
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class UserService implements BeanNameAware,InitializingBean{
	@Autowired
	private UserMapper userMapper;
	public void test(){
		System.out.println(userMapper.selectById());
	}
}
紧接着定义Spring配置类
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
@ComponentScan("com.zzy")
@EnableScheduling
public class AppConfig {
}
最后,新建一个测试类,创建Spring容器并启动
import com.zzy.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(AppConfig.class);
		context.refresh();
		UserService userService = (UserService)context.getBean("userService");
		userService.test()
	}
}
至此,所有的业务类已经创建结束了,这也符合我们日常使用Mybatis的使用,在service层注入mapper的Bean,并执行mapper中的方法执行最终sql
现在摆在我们面前最大的问题就是如何将UserMapper这个接口(interface)注入到userSerivce中,众所周知,Spring在生成BeanDefinition会过滤掉接口类
思路1:FactoryBean
在Spring中,给我们提供了一个FactoryBean的接口类,通过实现FactoryBean并改写getObject()及getObjectType()方法可以返回任意类型的实例,代码如下:
import com.zzy.mapper.UserMapper;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.stereotype.Component;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@Component
public class UserMapperFactoryBean implements FactoryBean {
	@Override
	public Object getObject() throws Exception {
		Object proxyInstance = Proxy.newProxyInstance(UserMapperFactoryBean.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				System.out.println(method.getName());
				// todo完成相关数据库操作
				return null;
			}
		});
		return proxyInstance;
	}
	@Override
	public Class<?> getObjectType() {
		return UserMapper.class;
	}
}
在getObject中使用了JDK的动态代理,生成了代理对象进行返回,并且再代理类中加入了我们需要的逻辑(即执行数据库操作),这里因为不是最终方案,因此就不写完整了。
这种方式实现起来比较简单,比容易理解,不过不能作为mybatis这种组件实现的方式,因为丧失了扩展性,试想一下项目中会有很多的Mapper,那总不能每一个Mapper都去定义一个相应的BeanFactory吧,所以这就引出了下面一种方案
思路2:可扩展的FactoryBean
我们是否可以只是用一个FactoryBean,然后将类型作为入参实现呢,当然也是可以的,可以利用属性+构造方法的方式实现,代码如下:
import org.springframework.beans.factory.FactoryBean;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class MapperFactoryBean implements FactoryBean {
	private Class mapperInterface;
	public MapperFactoryBean(Class mapperInterface) {
		this.mapperInterface = mapperInterface;
	}
	@Override
	public Object getObject() throws Exception {
		Object proxy = Proxy.newProxyInstance(MapperFactoryBean.class.getClassLoader(), new Class[]{mapperInterface}, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				System.out.println("method = " + method.getName());
				// todo 执行数据库操作
				return null;
			}
		});
		return proxy;
	}
	@Override
	public Class<?> getObjectType() {
		return mapperInterface;
	}
}
注意此处的MapperFactoryBean没有Component注解,因为我们需要多个Bean,因此需要去手动注册BeanDefinition,返回我们的Test类,修改main方法,代码如下:
public class Test {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(AppConfig.class);
		AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
		beanDefinition.setBeanClass(MapperFactoryBean.class);
		beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
		context.registerBeanDefinition("userMapper",beanDefinition);
		context.refresh();
		UserService userService = (UserService)context.getBean("userService");
		userService.test();
		
	}
}
我们利用了AnnotationConfigApplicationContext注册了一个名为userMapper类型为MapperFactoryBean的BD
当然也可以使用BeanDefinitionRegistryPostProcessor来注册BD,代码如下:
@Component
public class BeanDefinitionRegister implements BeanDefinitionRegistryPostProcessor {
	
	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
	}
	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
		AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
		beanDefinition.setBeanClass(MapperFactoryBean.class);
		beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
		registry.registerBeanDefinition("userMapper",beanDefinition);
	}
}
其实代码写到这里,大家肯定又有疑惑了,因为我们还是需要去手动注册,虽然不需要去创建很多个FactoryBean,但是仍然需要去手动创建很多个BD
那么有没有一种方法能够自动将所有的Mapper都解析出来呢?
了解Spring的第一时间肯定会想到 - 扫描
思路3:扫描
既然要扫描,那肯定需要确定扫描的路径,mybatis中提供了MapperScan接口,那我们自己写一个ZzyMapperScan接口吧
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZzyImportBeanDefinitionRegistrar.class)
public @interface ZzyMapperScan {
	String value();
}
在AppConfig中指定扫描路径:
@ComponentScan("com.zzy")
@ZzyMapperScan("com.zzy.mapper")
public class AppConfig {
}
在MapperScan中我们import了一个类ZzyImportBeanDefinitionRegistrar,具体代码如下:
public class ZzyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(ZzyMapperScan.class.getName());
		String path = (String)annotationAttributes.get("value");
		System.out.println("path = " + path);
		ZzyClassPathBeanDefintionScan zzyClassPathBeanDefintionScan = new ZzyClassPathBeanDefintionScan(registry);
		zzyClassPathBeanDefintionScan.addIncludeFilter(new TypeFilter() {
			@Override
			public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
				return true;
			}
		});
 		zzyClassPathBeanDefintionScan.scan(path);
}
ZzyImportBeanDefinitionRegistrar实现了ImportBeanDefintionRegsitrar,并重写了registerBeanDefinitions方法,该方法在import类导入时会执行,而方法其实一共就做了两件事:
- 首先获取了导入类的ZzyMapperScan注解,并取得了扫描路径
- 利用自定义的扫描器对包进行扫描
自定义扫描器继承于ClassPathBeanDefinitionScanner,代码如下
public class ZzyClassPathBeanDefintionScan extends ClassPathBeanDefinitionScanner {
	public ZzyClassPathBeanDefintionScan(BeanDefinitionRegistry registry) {
		super(registry);
	}
	@Override
	protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
		for(BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders){
			BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
			beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
			beanDefinition.setBeanClassName(MapperFactoryBean.class.getName());
		}
		return beanDefinitionHolders;
	}
	@Override
	protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
		return beanDefinition.getMetadata().isInterface();
	}
}
其中:
- 实现isCandidateComponent接口是为了让扫描器只去扫描接口类
- 实现doScan接口可以完成自定义BD的注入
在doScan逻辑中,我们将当前类类型(接口)作为构造方法入参传入,而将BD的bean类型改为了上文所写的MapperFactoryBean
至此,我们完成了基于扫描的可扩展的Mapper注入,接下去就是执行接口注解定义的sql了,我们需要进行一些简单的改造,首先是MapperFactoryBean,我们现在直接使用Mybatis生成的代理对象:
public class MapperFactoryBean implements FactoryBean {
	private Class mapperInterface;
	private SqlSession sqlSession;
	@Autowired
	public void setSqlSession(SqlSessionFactory sqlSessionFactory) {
		sqlSessionFactory.getConfiguration().addMapper(mapperInterface);
		this.sqlSession = sqlSessionFactory.openSession();
	}
	public MapperFactoryBean(Class mapperInterface) {
		this.mapperInterface = mapperInterface;
	}
	@Override
	public Object getObject() throws Exception {
		return sqlSession.getMapper(mapperInterface);
	}
	@Override
	public Class<?> getObjectType() {
		return mapperInterface;
	}
}
这里我们利用到了mybatis的sqlSession,而想要注入sqlSession,我们可以利用构造方法自动注入sqlSessionFactory,并且使用openSession的方法返回sqlSession实例
而sqlSessionFactory就需要我们去手动创建该Bean了,我们可以直接写到AppConfig中:
@ComponentScan("com.zzy")
@ZzyMapperScan("com.zzy.mapper")
public class AppConfig {
	@Bean
	public SqlSessionFactory sqlSessionFactory() throws IOException{
		InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		return sqlSessionFactory;
	}
}
mybaits.xml配置文件如下:
<?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">
      <!-- 使用jdbc事务管理 -->
      <transactionManager type="JDBC"/>
      <!-- 数据库连接池 -->
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url"
                  value="jdbc:mysql://{ip}:{port}/{datasource}?characterEncoding=utf-8&useSSL=false"/>
        <property name="username" value="{username}"/>
        <property name="password" value="{password}"/>
      </dataSource>
    </environment>
  </environments>
</configuration>
最后运行,控制台输出:
 
总结
至此手写Spring-mybtias的过程就结束了,我们经过了一步步的推导过程,十分直观详细的向大家展示了其中的核心逻辑,当然mybatis内部实现的代码写的也是十分好的,例如生成代理对象执行sql等,以后有机会的话也可以带大家手撕一下代码~



















