spring-mybatis
-
-
- 介紹
- 代碼
- jdk動态代理
- 注入mapper
- 優雅地注入mapper
- 像mybatis一樣工作
-
介紹
spring是spring,它是個容器的架構,mybatis是mybatis,它是個封裝jdbc的架構。它們兩個怎麼聯系起來呢?
當然是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的工作了。