spring-mybatis

  • 介绍
  • 代码
  • jdk动态代理
  • 注入mapper
  • 优雅地注入mapper
  • 像mybatis一样工作


介绍

spring是spring,它是个容器的框架,mybatis是mybatis,它是个封装jdbc的框架。它们两个怎么联系起来呢?

当然是spring来收纳mybatis呀。我会给出一个小例子,你必须注意,哪些是spring的东西,哪些是mybatis的东西,以及代码中奇怪的地方。

代码

springbatch 状态 springbatis_mybatis

先讲mybatis的东西:

@Data
public class Entity implements Serializable {

	private int locationId;

	private String streetAddress;

	private String postalCode;

	private String city;

	private String stateProvince;

	private String countryId;

	@Override
	public String toString() {
		return "Entity{" +
				"locationId=" + locationId +
				", streetAddress='" + streetAddress + '\'' +
				", postalCode='" + postalCode + '\'' +
				", city='" + city + '\'' +
				", stateProvince='" + stateProvince + '\'' +
				", countryId='" + countryId + '\'' +
				'}';
	}
}

一个实体类。

@Mapper
public interface LocationMapper {

	@Select("select * from locations")
	List<Entity> queryAll();
}

一个mapper并配上sql语句。

这都是mybatis固有的套路,在spring里也不用改。

神奇的是配置类:

@Configuration
@ComponentScan(value = "com.ocean.test_spring_mybatis")
@MapperScan(value = "com.ocean.test_spring_mybatis.mapper")
public class MyBatisConfig {

	@Bean
	public SqlSessionFactory sqlSessionFactory() throws Exception {
		SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
		org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
		configuration.setMapUnderscoreToCamelCase(true);
		factoryBean.setConfiguration(configuration);
		factoryBean.setDataSource(dataSource());
		return factoryBean.getObject();
	}

	@Bean
	public DataSource dataSource(){
		DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
		driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		driverManagerDataSource.setUsername("root");
		driverManagerDataSource.setPassword("123456");
		driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/myemployees?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT");
		return driverManagerDataSource;
	}

}

datasource还是mybatis的节奏,不过我们用了DriverManagerDataSource 这样一个连接池,这是spring-jdbc的连接池。

在mybatis中,我们得到SqlSessionFactory 是通过SqlSessionFactoryBuilder build出来的。

在spring中使用SqlSessionFactoryBeangetObject拿到。

这显然是一个FactoryBean。我们发现了mybatis和spring结合在一起的地方了。

@MapperScan(value = "com.ocean.test_spring_mybatis.mapper")

这行代码是用来扫描注入mapper的,就是那些mapper接口。

具体的业务类:

@Service
public class LocationService {

	@Autowired
	private LocationMapper locationMapper;

	public void selectAllTheLocations(){
		List<Entity> locations = locationMapper.queryAll();
		locations.stream().forEach(System.out::println);
	}

}

只有spring容器中有LocationMapper ,我们才能自动注入进来。

测试:

public class SpringMyBatisTest {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyBatisConfig.class);

		LocationService locationService = applicationContext.getBean(LocationService.class);

		locationService.selectAllTheLocations();

	}
}

通过。


jdk动态代理

首先是mybatis是如何工作的。这个我们曾经讲过,就是通过jdk动态代理,为mapper接口产生一个代理类来执行数据库查询。

只要能拿到sql语句,执行sql语句就很简单了。

public class OceanSession {

	public static Object getMapper(Class interfaceName){
		Class[] clazz = new Class[]{interfaceName};
		return Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), clazz, new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				Select select = method.getAnnotation(Select.class);
				
				if(select!=null){
					String sql = select.value()[0];
					System.out.println("executing sql : " +sql);
				}
				
				if(method.getName().equals("toString")){
					return proxy.getClass().getSimpleName();
				}
				return null;
			}
		});
	}

}

我们通过LocationMapper生成一个代理类,并且在回调方法中解析出sql语句。所用的方法就是反射。拿到@Select注解,拿到里面的值就行了。

测试:

public class SpringMyBatisTest {
	public static void main(String[] args) {
		LocationMapper mapperProxy = (LocationMapper) OceanSession.getMapper(LocationMapper.class);
		mapperProxy.queryAll();

	}}

打印出了sql语句。

executing sql : select * from locations

我们只是简单说一下mybatis的原理,这和spring无关。


注入mapper

复习了mybatis的原理,现在的关键是LocationMapper是如何注入进来的。因为只有容器中有LocationMapper,我们才能基于它产生代理对象。

我们可以在LocationService中打印它:

public void printLocationMapper(){
		System.out.println("location mapper from autowire : " + locationMapper);
	}

测试:

public class SpringMyBatisTest {
	public static void main(String[] args) {
		/*LocationMapper mapperProxy = (LocationMapper) OceanSession.getMapper(LocationMapper.class);
		mapperProxy.queryAll();*/

		AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyBatisConfig.class);
		LocationService locationService = applicationContext.getBean(LocationService.class);
		locationService.printLocationMapper();
	}}

结果是:

location mapper from autowire : org.apache.ibatis.binding.MapperProxy@646d64ab

autowired的时候已经是代理对象了。

现在我注释掉配置类中的

//@MapperScan(value = "com.ocean.test_spring_mybatis.mapper")

/*@Bean
	public SqlSessionFactory sqlSessionFactory() throws Exception {
		SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
		org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
		configuration.setMapUnderscoreToCamelCase(true);
		factoryBean.setConfiguration(configuration);
		factoryBean.setDataSource(dataSource());
		return factoryBean.getObject();
	}*/

	/*@Bean
	public DataSource dataSource(){
		DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
		driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
		driverManagerDataSource.setUsername("root");
		driverManagerDataSource.setPassword("123456");
		driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/myemployees?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT");
		return driverManagerDataSource;
	}*/

单纯地思考如何注入LocationMapper

  • 使用@Bean
@Bean
	public LocationMapper locationMapper(){
		return (LocationMapper) OceanSession.getMapper(LocationMapper.class);
	}

再次调用locationService.printLocationMapper();

结果:

location mapper from autowire : $Proxy15

能够注入进mapper。

问题是,要是有很多个mapper呢?难道写很多个@Bean吗?

  • 使用FactoryBean

一个能产生其他bean的bean。

@Component
public class OceanSqlFactoryBean implements FactoryBean {
	@Override
	public Object getObject() throws Exception {
		return OceanSession.getMapper(LocationMapper.class);
	}

	@Override
	public Class<?> getObjectType() {
		return LocationMapper.class;
	}
}

这么一写,@autowired下面的LocationMapper也有值。

问题还是,如果有多个mapper,我写多个FactoryBean吗?

优雅地注入mapper

如果我们能够在OceanSqlFactoryBean 中动态地传入mapper接口,那不就实现了一个FactoryBean生产不同的mapper代理类的目标了吗?

public class OceanSqlFactoryBean implements FactoryBean {

	private Class interfaceName;
	
	public OceanSqlFactoryBean(Class interfaceName){
		this.interfaceName=interfaceName;
	}
	
	@Override
	public Object getObject() throws Exception {
		return OceanSession.getMapper(interfaceName);
	}

	@Override
	public Class<?> getObjectType() {
		return interfaceName;
	}
}

我们还要将@Component删掉,因为一旦spring扫描到这个注解,它就会去new这个bean。如此一来便不能动态地传入interfaceName了?

现在的问题是,如何让spring知道OceanSqlFactoryBean

我们要用到BeanDefinition@Import的技术。

public class OceanImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(OceanSqlFactoryBean.class);
		AbstractBeanDefinition beanDefinition 
		= beanDefinitionBuilder.getBeanDefinition();
		beanDefinition.getConstructorArgumentValues()
		.addGenericArgumentValue("com.ocean.test_spring_mybatis.mapper.LocationMapper");
		registry.registerBeanDefinition("oceanSqlFactoryBean",beanDefinition);

	}
}

我们拿到OceanSqlFactoryBean的BeanDefinition之后,给它的构造函数传入LocationMapper,这样就把LocationMapper动态传入OceanSqlFactoryBean了。

为了让spring能够认识我们的OceanImportBeanDefinitionRegistrar ,需要在配置类上加注解:

@Import(OceanImportBeanDefinitionRegistrar.class)
public class MyBatisConfig {
}

再跑一下

locationService.printLocationMapper();

LocationMapper注入进来了。

为了证明这种动态性,我再写一个UserMapper

public interface UserMapper {
	
	@Select("select * from user")
	void testUser();
}

并且在LocationService中加入:

@Autowired
	private UserMapper userMapper;

	public void testUserMapper(){
		userMapper.testUser();
	}

我们希望能够打印出invoke方法里执行sql的话。

我们只需改动OceanImportBeanDefinitionRegistrar中的

beanDefinition.getConstructorArgumentValues().addGenericArgumentValue("com.ocean.test_spring_mybatis.mapper.UserMapper");

即可。

测试:

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyBatisConfig.class);
		LocationService locationService = applicationContext.getBean(LocationService.class);
	    locationService.testUserMapper();

打印了sql。

成功。


像mybatis一样工作

也许你会说

beanDefinition.getConstructorArgumentValues().addGenericArgumentValue("com.ocean.test_spring_mybatis.mapper.UserMapper");

不也是写死了吗?我们在用mybatis的MapperScan时,会有一个基包,根据那个包名我们去找到下面所有的mapper,然后遍历,这就解决问题了。

这个工作我们暂时不做了。

现在写一个我们自己的MapperScan:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(OceanImportBeanDefinitionRegistrar.class)
public @interface OceanMapperScan {


}
@OceanMapperScan()
public class MyBatisConfig {
}

这样就模拟了mybatis的工作了。