MyBatis配置原理
如何整合Spring与Mybatis?
- 初始化spring环境。(AppConfig.class)
- 创建SqlSessionFactory。(注入数据源)
- @MapperScan(com.bafan.spring.mybatis.mapper)。(Mapper接口的扫描路径)
- TDao。(Mapper接口)
@Configuration
@ComponentScan("com.bafan.spring.mybatis")
@MapperScan("com.bafan.spring.mybatis.mapper")
public class AppConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
//配置数据源
@Bean
public DataSource dataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUrl("jdbc:mysql://10.70.6.34:3306/finance_mall");
driverManagerDataSource.setUsername("pay");
driverManagerDataSource.setPassword("pay123");
return driverManagerDataSource;
}
}
//Mapper
public interface TDao {
@Select("select * from BankInfo limit 10")
public List<Map<String, Object>> list();
}
@Service
public class IndexService {
@Autowired
private TDao tDao;
public List<Map<String, Object>> getList() {
return tDao.list();
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(ac.getBean(IndexService.class).getList());
}
/**
FactoryBean是一个特殊的bean,里面有三个方法
getObject():返回一个你自己定义的bean,在获取它的时候,你可以自己随意处理,例如做个代理之类的
getObjectType():在spring容器初始化的时候,如果你将FactoryBean注入到spring容器中,那么会初始化这个FactoryBean,在当你想要获取到这个FactoryBean的时候,会调用getObjectType()这个方法,这个方法返回一个Class,假如getObjectType()返回的是FactoryService,在获取的时候通过名字"factoryService",判断相等,则会去调用getObject()方法,也即是factoryService这个类是你自己在getObject()中按照你的处理方式返回的类,如果传的是"&factoryService",那么判断就不相等了,返回则是FactoryBean这个类。
isSingleton():是否是单例模式,不重写的话默认就是true。
*/
public interface FactoryBean<T> {
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
Mybatis到底是怎么能访问到数据库的?为什么通过tDao.list()就能获取到结果呢?tDao本来是一个接口,也没有实现类,为什么能够属性注入到IndexService中呢?
带着这几个问题和上面对FactoryBean的描述来看下Mybatis是怎么做的。
首先看一个类MapperFactoryBean,它继承了FactoryBean
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
//需要被代理的类(这里传入的就是Mapper类,在例子中对应着TDao的接口类)
private Class<T> mapperInterface;
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public void setMapperInterface(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
//在初始化过程中,每个Mapper接口在初始化的时候都会调用到这里
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
//按照Mapper接口的初始化顺序将每个接口顺序放到configuration中
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
//实际上要返回的代理类,这里把Mapper传进去,用SqlSession的getMapper方法来对这个类进行代理
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
@Override
public Class<T> getObjectType() {
return this.mapperInterface;
}
}
通过MapperFactoryBean这个类可以看出,Mybatis会将我们的Mapper接口传到这个类中,并对它进行了代理,然后,在这个类被自动注入的时候,返回这个类的代理类,然后,通过这个代理类来调用操作数据库的方法。
那么有下面两个问题:
- 代理类都做了什么?
- Spring是怎么初始化这个类的?也可以说是MapperScan这个注解做了什么?
//代理类做了什么?找到这个getMapper的实现类DefaultSqlSession
public class DefaultSqlSession implements SqlSession {
//上段代码中的Configuration
private final Configuration configuration;
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
}
//我们沿着这个方法一直点下去,最终会走到MapperProxyFactory的newInstance方法
public class MapperProxyFactory<T> {
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
}
//我们看下mapperProxy做了什么
public class MapperProxy<T> implements InvocationHandler, Serializable {
//invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
//实际去执行的方法,点到execute里面去
return mapperMethod.execute(sqlSession, args);
}
}
//这里就可以很清楚的看到,Mybatis底层在执行sql的时候,会根据Mapper接口中我们自定义的方法来判断怎么执行(insert还是select?返回值的类型?...在这里会自动的判断出你要怎么来执行这条sql语句)
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
}
//再说Spring是怎么初始化MapperFactoryBean这个类的
//要想把一个类在Spring初始化的时候方法容器中,除了在这个类上添加类似于@Controller之类的注解,还有三种方式
//1.在Spring的AppCofig类中添加,@Configuration @Bean,这种方式相当于要每写一个Mapper接口,就要在配置文件中写一个这样的@Bean,显然Mybatis是不会这么做的。
//2.beanFactory.registerSingleton(直接放到工厂里),调用方式如下,在AnnotationConfigApplicationContext调用refresh()之前,手动将代理类放到Spring工厂中,显然这种方式也不行,理由和第一种方式一样
TDao tDao = (TDao)SqlSession.queryMapper(TDao.class);
ConfigurableListableBeanFactory beanFactory = ac.getBeanFactory();
beanFactory.registerSingleton("tDao", tDao);
//3.实现ImportBeanDefinitionRegistrar,创建一个BeanDefinition放到容器中。Mybatis使用的就是这种方法。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
...
}
//MapperScan中的MapperScannerRegistrar方法,实现了ImportBeanDefinitionRegistrar接口
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
//需要重写这个方法,这个方法会传入AnnotationMetadata(AppConfig上的注解信息,所以能拿到MapperScan中配置的路径,也就能拿到这个路径下的所有Mapper接口,就可以做代理了)和BeanDefinitionRegistry(生成一个BeanDefinitionBuilder,再通过registry将BeanDefinitionBuilder注入到spring中)
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
registerBeanDefinitions(mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
}
}
//注入到BeanDefinitionBuilder中的一个属性,就是这个MapperFactoryBean
Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
}
}
最后再说明一下在AppConfig中需要配置的两个类
//SqlSessionFactory的配置,在factoryBean中注入一个数据源就好,我们看下SqlSessionFactoryBean的getObject()方法
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
//数据源的配置
@Bean
public DataSource dataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManagerDataSource.setUrl("jdbc:mysql://10.70.6.34:3306/finance_mall");
driverManagerDataSource.setUsername("pay");
driverManagerDataSource.setPassword("pay123");
return driverManagerDataSource;
}
//也就是说,SqlSessionFactory在放到Spring中之前,要调用afterPropertiesSet()方法
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
//这个里面会初始化很多东西,比如要调用xml文件、解析我们配置的xml文件的地址、连接数据库的配置等
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
Mybatis就是按照上述的方法,让我们能够只通过一个接口就可以直接调用到数据库。
我们自己也可以模拟Mybatis的方式写一个类似于这样的框架。
1.创建一个FactoryBean,与MapperFactoryBean功能类似
public class BafanFactoryBean implements FactoryBean {
Class bafanMapperInterface;
public Object getObject() throws Exception {
Object o = BafanSession.queryMapper(bafanMapperInterface);
return o;
}
public Class<?> getObjectType() {
return bafanMapperInterface;
}
public boolean isSingleton() {
return true;
}
public void setBafanMapperInterface(Class bafanMapperInterface) {
this.bafanMapperInterface = bafanMapperInterface;
}
}
2.创建一个SqlSession,用来对Mapper接口进行代理
public class BafanSession {
public static Object queryMapper(Class clazz) {
Class[] classes = new Class[]{clazz};
//jdk代理
Object proxy = Proxy.newProxyInstance(BafanSession.class.getClassLoader(), classes, new BafanInvocationHandler());
return proxy;
}
}
3.创建代理类BafanInvocationHandler,用来模拟Mybatis执行操作数据库
public class BafanInvocationHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//模拟连接DB
System.out.println("conn db");
//获取到Select注解
Select annotation = method.getAnnotation(Select.class);
if (annotation != null) {
//打印Select注解里面的value值
System.out.println(annotation.value()[0]);
}
//重写toString方法
if (method.getName().equals("toString")) {
return proxy.getClass().getInterfaces()[0].getName();
}
return null;
}
}
4.将BafanFactoryBean放到Spring容器中
//实现ImportBeanDefinitionRegistrar
public class BafanBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//创建BeanDefinitionBuilder,传入BafanFactoryBean
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(BafanFactoryBean.class);
//将tDao生成的BeanDefinition放到Spring中
AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
beanDefinition.getPropertyValues().add("bafanMapperInterface", "com.bafan.spring.mybatis.mapper.TDao");
registry.registerBeanDefinition("tDao", beanDefinition);
}
}
5.BafanScan注解
@Retention(RetentionPolicy.RUNTIME)
//注入第4步中的BafanBeanDefinitionRegistrar
@Import(BafanBeanDefinitionRegistrar.class)
public @interface BafanScan {
String value();
}
6.配置及使用
@Configuration
@ComponentScan("com.bafan.spring.mybatis")
@BafanScan("com.bafan.spring.mybatis.mapper")
public class AppConfig {
}
/**
* 如何模拟,手写一个类似于mybatis的框架,mybatis是怎么做的
* 1. 创建一个SqlSession(为你的Mapper接口创建一个代理类)---------BafanSession
* 2. 代理类里面做了什么(BafanInvocationHandler)(连接DB、获取注解或者说要执行的sql的信息)
* 3. 如何把我们刚才返回的代理类放到放到spring中去?
* (1. beanFactory.registerSingleton(直接放到工厂里)
* 2. @Configuration @Bean
* 3. 实现ImportBeanDefinitionRegistrar,把注入到BeanDefinition里面)
* mybatis使用的是第三种方式
* 4. mybatis如何批量一次性把这些类都注入进去呢?
* FactoryBean(放在spring容器里面会产生两个bean,一个是他自己,另一个是经过处理的bean)
* 通过FactoryBean,传入一个原始的类,产生一个他的代理类。
* 5. 使用注解(BafanSacn)注入BafanBeanDefinitionRegistrar,可以拿到这个这个注解所配置的路径下的所有的类,再通过循环,把属性注入到FactoryBean中,
* 最后再放到BeanDefinition里面。
*/
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(ac.getBean("tDao"));
ac.getBean(TDao.class).list();
}