天天看點

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的工作了。