一、问题引入
mybatis在写dao层的时候只是写了个接口,并没有具体实现,如何正常工作的?
其实最初开发web的时候是需要写dao接口的实现,只是后面mybatis简化了我们的开发模式,将“dao层的实现”这部分重复代码给我们自动生成了,不需要手动写了。
我们先回顾下“需要写dao实现的传统开发模式” 和 “不需要写dao实现的代理开发模式”,再看看mybatis是如何做到这点的。
二、传统开发 VS 代理开发
为了方便比价,这里将“带dao实现”的开发方式称作“传统开发”模式,将“不带dao实现”的开发模式称作“代理开发”模式。
2.1 传统开发模式
step1: 定义UserDao接口
public interface UserDao {
public User findUserById(int id) throws IOException;
?
public User findByUserIdAndName(User user) throws IOException;
public List<User> getUserList(User user) throws IOException;
}
step2: 定义 UserDao 接口的实现类 UserDaoImpl
public class UserDaoImpl implements UserDao {
private SqlSessionFactory sqlSessionFactory;
public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public User findUserById(int id) throws IOException {
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("userMapper.findByUserId", id);
System.out.println(user);
sqlSession.close();
return user;
}
@Override
public User findByUserIdAndName(User user) throws IOException {
SqlSession sqlSession = sqlSessionFactory.openSession();
User result = sqlSession.selectOne("userMapper.findByIdAndName", user);
System.out.println(result);
sqlSession.close();
return result;
}
@Override
public List<User> getUserList(User user) throws IOException {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> userList= sqlSession.selectList("userMapper.getUserList", user);
System.out.println(user);
sqlSession.close();
return userList;
}
}
step3: 对应的 UserMapper.xml 文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.demo.dao.impl.UserDaoImpl" >
<select id="findUserById" parameterType="int" resultType="User">
select *from user
where id = #{id}
</select>
<select id="findByUserIdAndName" parameterType="User" resultType="User">
select *from user
where id = #{id} and name = #{name}
</select>
<select id="getUserList" parameterType="User" resultType="User">
select *from user
where age = #{age}
</select>
</mapper>
step4: 模拟业务层调用
public static void main(String [] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
UserDao dao = new UserDaoImpl(sqlSessionFactory);
User user = dao.findUserById(1);
System.out.println(user);
}
2.2 代理开发模式
为了简化开发,Mybatis 提供了一种代理开发的方式,这种方式是项目开发中主流,由Mybatis 实现 Dao 层的接口。
代理开发方式只需要程序员编写Dao 接口 和 对应的Mapper.xml文件,然后由 Mybatis 框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。 **也就是说,不用写dao层的实现类了。**相应的开发流程如下:
step1: 定义UserDao接口
内容同2.1节
step2: 对应的 UserMapper.xml 文件
内容同2.1节
将mapper标签中的 nameSpace值由 UserDaoImpl 改为 UserDao 即可
step3:模拟业务层调用
public static void main(String [] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserDao dao = sqlSession.getMapper(UserDao.class);
User user = dao.findByUserId(1);
System.out.println(user);
}
代理开发方式使用的是动态代理的 JDK 代码实现的,使用该代理方式需要遵循下面4个规范:
规范1. 映射文件mapper.xml中的mapper标签的namespace属性与 dao 接口的全限定名相同 规范2. 映射文件mapper.xml中的每条映射语句中id的属性值与 dao 接口中方法名相同 规范3. 映射文件mapper.xml中的每条映射语句的parameterType属性与 dao 接口中方法的形参相同 规范4. 映射文件mapper.xml中的每条映射语句的resultType属性与 dao 接口中方法的返回值类型相同 如果 Mapper 接口已经遵循上述规范,那么不需要创建 Dao 层的实现类了,可以直接进行使用。
三、Dao接口和XML文件里的SQL是如何建立关系的?
3.1 初始化SqlSessionFactoryBean
3.1.1 SqlSessionFactoryBean简介
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的,但SqlSessionFactory是一个接口,它里面其实就两个方法:
- openSession方法 是为了获取一个SqlSession对象,完成必要数据库增删改查功能;
- getConfiguration方法 可以返回一个Configuration类,可来配置mapper映射文件、SQL参数、返回值类型、缓存等属性;Configuration类可以看成是一个配置管家,MyBatis所有的配置信息都维持在Configuration对象之中,基本每个对象都会持有它的引用。
public interface SqlSessionFactory {
SqlSession openSession();
SqlSession openSession(boolean autoCommit);
SqlSession openSession(Connection connection);
SqlSession openSession(TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType);
SqlSession openSession(ExecutorType execType, boolean autoCommit);
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
SqlSession openSession(ExecutorType execType, Connection connection);
Configuration getConfiguration();
}
日常开发中都是将Mybatis与Spring一起使用的,所以把实例化工作交给Spring处理。通常用org.mybatis.spring.SqlSessionFactoryBean来获取SqlSessionFactory实例。
3.1.2 SqlSessionFactoryBean
SqlSessionFactoryBean的类图如下: 它实现了InitializingBean接口,该接口内只有一个afterPropertiesSet()方法。这说明,SqlSessionFactoryBean类被实例化之后会调用到该方法。
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
SqlSessionFactoryBean类对应的实现:
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
······
@Override
public void afterPropertiesSet() throws Exception {
······
this.sqlSessionFactory = buildSqlSessionFactory();
}
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
1、从配置文件的property属性中加载各种组件,解析配置到configuration中
2、加载mapper文件,解析SQL语句,封装成MappedStatement对象,配置到configuration中。
}
}
SqlSessionFactoryBean.afterPropertiesSet()方法只有一个动作,就是buildSqlSessionFactory,这个动作的作用有两个:
- 从配置文件的property属性中加载各种组件,解析配置到configuration中
- 加载mapper.xml文件,解析SQL语句,封装成MappedStatement对象,配置到configuration中。
3.1.3 解析mapper.xml 文件的过程
从mapperLocations路径去解析里面所有的XML文件,获取SQL内容,可以分位两部分。
3.1.3.1 创建SqlSource
Mybatis会把每个SQL标签封装成SqlSource对象,然后根据SQL语句的不同,又分为动态SQL和静态SQL。其中,静态SQL包含一段String类型的sql语句;而动态SQL则是由一个个SqlNode组成。 假如我们有这样一个SQL:
<select id="getUserById" resultType="user">
select * from user
<where>
<if test="uid!=null">
and uid=#{uid}
</if>
</where>
</select>
它对应的SqlSource对象看起来应该是这样的:
3.1.3.2 创建MappedStatement
XML文件中的每一个SQL标签就对应一个MappedStatement对象,这里面有两个属性很重要:
- id:全限定类名+方法名组成的ID。
- sqlSource:当前SQL标签对应的SqlSource对象。
创建完MappedStatement对象,将它缓存到Configuration#mappedStatements中。
Configuration对象就是Mybatis中的大管家,基本所有的配置信息都维护在这里。把所有的XML都解析完成之后,Configuration就包含了所有的SQL信息。 到目前为止,XML就解析完成了。当我们执行Mybatis方法的时候,就通过全限定类名+方法名找到MappedStatement对象,然后解析里面的SQL内容,执行即可。
3.2 Dao接口代理
3.2.1 配置需要扫描的基本包路径
通过注解的方式配置:
@MapperScan({"com.xxxx.UserDao"})
或者xml的方式配置:
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xxx.UserDao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
3.2.2 扫描并注册bean
它们的作用是一样的。这个过程是通过 org.mybatis.spring.mapper.MapperScannerConfigurer这个类发挥作用的。 可以看到它实现了几个接口。其中的重点是BeanDefinitionRegistryPostProcessor。它可以动态的注册Bean信息,方法为postProcessBeanDefinitionRegistry()。
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
······
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
······
}
ClassPathMapperScanner中的scan方法最终会调用本类重写的doScan方法:
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
……
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
this.logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
this.processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
……
}
经过上面这些步骤,此时已经扫描到了所有的Mapper接口,并将其注册为BeanDefinition对象。
最终的效果是:将包路径下的所有dao类注册到Spring Bean中,并且将它们的beanClass设置为MapperFactoryBean。
这就相当于使用UserDao注册bean时:当前的dao接口在Spring容器中,beanName是userDao,beanClass是MapperFactoryBean.class。故在Spring的IOC初始化的时候,实例化的对象就是MapperFactoryBean对象。
3.3 使用bean
MapperFactoryBean实现了FactoryBean接口,俗称工厂Bean。那么,当我们通过@Autowired注入这个Dao接口的时候,返回的对象就是MapperFactoryBean这个工厂Bean中的getObject()方法对象。
这个getObject()方法干了些什么呢? 它就是通过JDK动态代理,返回了一个Dao接口的代理对象,这个代理对象的处理器是MapperProxy对象。所有,我们通过@Autowired注入Dao接口的时候,注入的就是这个代理对象,我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke方法。
使用bean:
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserDao userDao1;
public List<User> getUserList(Map<String,Object> map) {
return userDao1.getUserList(map);
}
}
我们调用Dao接口方法的时候,实际调用到代理对象的invoke方法。
public class MapperProxy<T> implements InvocationHandler, Serializable {
……
@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);
return mapperMethod.execute(sqlSession, args);
}
……
}
在这里,实际上调用的就是SqlSession里面的东西了。
public class DefaultSqlSession implements SqlSession {
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms,
wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
}
}
通过statement全限定类型+方法名拿到MappedStatement 对象,然后通过执行器Executor去执行具体SQL并返回:
参考文档
ref1. MyBatis之Dao层实现 ref2. Mybatis中的Dao接口和XML文件里的SQL是如何建立关系的? ref3. mybatis 的 dao 接口跟 xml 文件里面的 sql 是如何建立关系的?一步步解析 ref4. SqlSession与SqlSessionFactory到底是什么关系? ref5.MyBatis常用对象SqlSessionFactory和SqlSession介绍和运用 ref6.全网最通俗易懂理清mybatis中SqlSession、SqlSessionTemplate、SessionFactory和SqlSessionFactoryBean之间的关系
|