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结合在一起的地方了。
@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的工作了。