天天看点

spring整合mybatis的简单例子及原理

spring-mybatis

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

介绍

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

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

代码

spring整合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中使用

SqlSessionFactoryBean

getObject

拿到。

这显然是一个

FactoryBean

。我们发现了mybatis和spring结合在一起的地方了。

这行代码是用来扫描注入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();
	}}

           

结果是:

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

现在我注释掉配置类中的

/*@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 {
}


           

再跑一下

LocationMapper

注入进来了。

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

UserMapper

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

并且在

LocationService

中加入:

@Autowired
	private UserMapper userMapper;

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

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

我们只需改动

OceanImportBeanDefinitionRegistrar

中的

即可。

测试:

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


           

打印了sql。

成功。

像mybatis一样工作

也许你会说

不也是写死了吗?我们在用mybatis的

MapperScan

时,会有一个基包,根据那个包名我们去找到下面所有的mapper,然后遍历,这就解决问题了。

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

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

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


}
           
@OceanMapperScan()
public class MyBatisConfig {
}
           

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