相信大家在刚开始学习mybatis注解方式,或者
spring
+mybatis注解方式的时候,一定会有一个疑问,为什么mybatis的dao接口只需要一个接口,不需要实现类,就可以正常使用,笔者最开始的时候也会有这种疑问,当时在网上查了很多资料,也问过公司比较年长的同事,但是并没有得到答案,后来通过自己看mybatis的源码的方式才明白其中道理,接下来我就对大家分享,为什么dao接口不需要实现类的原理,这篇文章的讲解主要分为两部分:
1.mybatis注解方式是怎样通过没有实现类的dao接口进行
数据库
操作
2.spring+mybatis注解方式是怎样在没有实现类的dao接口的情况下结合的
文章的结构是通过 总结+详细讲解 的方式来进行说明的,希望大家能和我一同进步,例子程序放在github上了,
mybatis-demo
。
环境:
mybatis 3.2.7
mybatis-spring 1.2.2
spring 4.1.6
总结:
1.mybatis注解方式通过没有实现类的dao接口进行数据库操作的原理,一句话概括,就是jdk proxy,就是jdk代理
2.spring+mybatis注解方式,也是没有实现类的,但是spring会默认返回MapperFactoryBean对象作为实现类的替换,但是这个只是被spring使用的,mybatis本身还是通过jdk代理来运行的。
详细讲解:
1.mybatis注解方式是怎样通过没有实现类的dao接口进行数据库操作
-
/**
-
*
-
* 类UserMapper.java的实现描述:TODO 类实现描述
-
* @author yuezhihua 2015年7月9日 上午11:18:30
-
*/
-
public
interface
UserMapper {
-
-
-
/**
-
* 根据用户id查询用户角色
-
* @param userId
-
* @return
-
*/
-
@Select
(
“select * from role_main a INNER JOIN user_role b ON a.id = b.role_id WHERE b.user_id=#{userId}”
)
-
public
List<RolePO> getRolesByUserId(
@Param
(
“userId”
)Integer userId);
-
-
-
/**
-
* 根据用户id查询用户角色名
-
* @param userId
-
* @return
-
*/
-
@Select
(
“select a.role_name from role_main a INNER JOIN user_role b ON a.id = b.role_id WHERE b.user_id=#{userId}”
)
-
public
Set<String> getRoleNamesByUserId(
@Param
(
“userId”
)Integer userId);
-
-
-
/**
-
* 根据userid查询用户的所有权限
-
* @param userId
-
* @return
-
*/
-
@Select
(
“SELECT a.permission_name FROM permission_main a INNER JOIN role_permission b ON a.id=b.permission_id WHERE b.role_id IN (SELECT d.role_id from user_main c INNER JOIN user_role d ON c.id = d.user_id WHERE c.id=#{userId})”
)
-
public
Set<String> getPermissionsByUserId(
@Param
(
“userId”
)Integer userId);
-
-
/**
-
* 通过用户名查询用户信息
-
* @param username
-
* @return
-
*/
-
@Select
(
“select * from user_main where username=#{username}”
)
-
@Results
({
-
@Result
(property =
“roleNames”
, column =
“id”
, many =
@Many
(fetchType=FetchType.LAZY,select =
“getRoleNamesByUserId”
)),
-
@Result
(property =
“permissionNames”
, column =
“id”
, many =
@Many
(fetchType=FetchType.LAZY,select =
“getPermissionsByUserId”
))
-
})
-
public
UserPO getUserByUsername(
@Param
(
“username”
)String username);
-
-
-
@Select
(
“select username from user_main”
)
-
public
List<String> getRoleMain();
-
-
}
测试
用例:
-
/**
-
*
-
* 类SqlTemplateTest.java的实现描述:TODO 类实现描述
-
* @author yuezhihua 2015年7月29日 下午2:07:44
-
*/
-
@RunWith
(SpringJUnit4ClassRunner.
class
)
-
@ContextConfiguration
(locations = {
-
“classpath*:spring/demo-locator.xml”
-
})
-
public
class
SqlTemplateTest {
-
-
@Autowired
-
private
SqlSessionTemplate sqlSessionTemplate;
-
-
@Autowired
-
private
SqlSessionFactory sqlSessionFactory;
-
-
/**
-
* 初始化datasource
-
*/
-
@Before
-
public
void
init(){
-
DataSource ds =
null
;
-
try
{
-
ds = BaseDataTest.createUnpooledDataSource(BaseDataTest.DERBY_PROPERTIES);
-
BaseDataTest.runScript(ds,
“com/mybatis/demo/databases/lazyloader/lazyloader-schema.sql”
);
-
BaseDataTest.runScript(ds,
“com/mybatis/demo/databases/lazyloader/lazyloader-dataload.sql”
);
-
}
catch
(IOException e) {
-
e.printStackTrace();
-
}
catch
(SQLException e) {
-
e.printStackTrace();
-
}
-
}
-
-
-
/**
-
* 测试mybatis自身的查询
-
*/
-
@Test
-
public
void
testMybatisSelect(){
-
SqlSession session = sqlSessionFactory.openSession();
-
UserMapper mapper = session.getMapper(UserMapper.
class
);
-
UserPO userPo = mapper.getUserByUsername(
“zhangsan”
);
-
System.out.println(
“—–testMybatisSelect:”
+userPo.getUsername());
-
}
-
-
-
-
/**
-
* mybatis-spring : sqlSessionTemplate测试查询
-
* java.lang.UnsupportedOperationException: Manual close is not allowed over a Spring managed SqlSession不要在意
-
*/
-
@Test
-
public
void
testSelect(){
-
UserPO result = sqlSessionTemplate.selectOne(
“com.mybatis.demo.lazyload.mapper.UserMapper.getUserByUsername”
,
“zhangsan”
);
-
System.out.println(result);
-
}
-
-
-
}
笔者这里不是纯使用mybatis,而是使用mybatis+spring,讲解第一部分的时候,我还是会用带有spring的方式来给大家讲解,大家注重看原理就好
第一部分的时候会用到测试用例;testMybatisSelect
大家可以看到,测试用例里边获取dao接口的方法时session.getMapper(UserMapper.class);那咱们就看看sqlsession是怎么样获取usermapper接口的,返回的这个usermaper接口又有什么改变
获取usermapper接口代理对象的时序图
返回的Usermapper编程了jdk代理对象,org.apache.ibatis.binding.MapperProxy@7e276594
虽然这里打印信息显示貌似mapperproxy是usermapper的实现类,但是笔者认为,mapperproxy不能算是usermapper的实现类,因为笔者觉得实现类的概念是应该实现usermapper接口的,但是mapperproxy不是,mapperproxy只是usermapper执行方法之前的一个拦截器
所以session.getMapper(UserMapper.class)返回的其实是usermapper的代理对象,而且usermapper中定义的方法执行都是通过mapperproxy的invoke方法代理执行的,
接下来我们看看mapper.getUserByUsername(“zhangsan”);这行代码的执行过程,通过usermapper一个方法的执行来讲解mybatis是怎么通过dao接口执行数据库操作的
mapperproxy.invoke(相当于一个拦截器):
-
public
Object invoke(Object proxy, Method method, Object[] args)
throws
Throwable {
-
if
(Object.
class
.equals(method.getDeclaringClass())) {
-
try
{
-
return
method.invoke(
this
, args);
-
}
catch
(Throwable t) {
-
throw
ExceptionUtil.unwrapThrowable(t);
-
}
-
}
-
final
MapperMethod mapperMethod = cachedMapperMethod(method);
-
return
mapperMethod.execute(sqlSession, args);
-
}
这个方法中,首先会过滤object中的通用方法,遇到object方法会直接执行
但是如果是非通用方法,就会调用mappermethod.execute来代理执行方法,
mappermethod.execute
-
public
Object execute(SqlSession sqlSession, Object[] args) {
-
Object result;
-
if
(SqlCommandType.INSERT == command.getType()) {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = rowCountResult(sqlSession.insert(command.getName(), param));
-
}
else
if
(SqlCommandType.UPDATE == command.getType()) {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = rowCountResult(sqlSession.update(command.getName(), param));
-
}
else
if
(SqlCommandType.DELETE == command.getType()) {
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = rowCountResult(sqlSession.delete(command.getName(), param));
-
}
else
if
(SqlCommandType.SELECT == command.getType()) {
-
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
{
-
Object param = method.convertArgsToSqlCommandParam(args);
-
result = sqlSession.selectOne(command.getName(), param);
-
}
-
}
else
{
-
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;
-
}
这个execute方法会根据不同的注解@select,@update,@delete,@insert来分配不同的执行sql环境,进行操作数据库,其实这四个操作可以分为两类,一类是更新类型,返回更新的行数;一类是查询类型,返回查询的结果,这两部分的内部原理我会在其他的文章中进行详细解释。
所以,可以总结说,mybatis执行jdk代理的dao接口方法,跳转到mappermethod,execute方法来执行具体的数据库操作,并且返回结果;而且通过jdk代理的方法返回的代理对象,让人感觉和原接口对象一样,造成使用没有实现类的接口来执行的感觉
第二部分:
spring+mybatis注解方式是怎样在没有实现类的dao接口的情况下结合的
咱们先看一下spring是怎么管理mybatis的dao接口的吧。
我画了一个流程图
配置文件扫描所有mybatis的dao接口:
-
<
bean
class
=
“org.mybatis.spring.mapper.MapperScannerConfigurer”
>
-
<
property
name
=
“basePackage”
value
=
“com.mybatis.demo.*.mapper”
/>
-
<!– 这里要用传beanName,不能传bean的ref,否则,会提前加载,用不到PropertyPlaceholder,切记 –>
-
<
property
name
=
“sqlSessionFactoryBeanName”
value
=
“demo_sqlSessionFactory”
/>
-
</
bean
>
ClasspathMapperScanner.doScan
-
/**
-
* Calls the parent search that will search and register all the candidates.
-
* Then the registered objects are post processed to set them as
-
* MapperFactoryBeans
-
*/
-
@Override
-
public
Set<BeanDefinitionHolder> doScan(String… basePackages) {
-
Set<BeanDefinitionHolder> beanDefinitions =
super
.doScan(basePackages);
-
-
if
(beanDefinitions.isEmpty()) {
-
logger.warn(
“No MyBatis mapper was found in ‘”
+ Arrays.toString(basePackages) +
“‘ package. Please check your configuration.”
);
-
}
else
{
-
for
(BeanDefinitionHolder holder : beanDefinitions) {
-
GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
-
-
if
(logger.isDebugEnabled()) {
-
logger.debug(
“Creating MapperFactoryBean with name ‘”
+ holder.getBeanName()
-
+
“‘ and ‘”
+ definition.getBeanClassName() +
“‘ mapperInterface”
);
-
}
-
-
// the mapper interface is the original class of the bean
-
// but, the actual class of the bean is MapperFactoryBean
-
<span style=
“color:#ff6666;”
>definition.getPropertyValues().add(
“mapperInterface”
, definition.getBeanClassName());
-
definition.setBeanClass(MapperFactoryBean.
class
);</span>
-
-
definition.getPropertyValues().add(
“addToConfig”
,
this
.addToConfig);
-
-
boolean
explicitFactoryUsed =
false
;
-
if
(StringUtils.hasText(
this
.sqlSessionFactoryBeanName)) {
-
definition.getPropertyValues().add(
“sqlSessionFactory”
,
new
RuntimeBeanReference(
this
.sqlSessionFactoryBeanName));
-
explicitFactoryUsed =
true
;
-
}
else
if
(
this
.sqlSessionFactory !=
null
) {
-
definition.getPropertyValues().add(
“sqlSessionFactory”
,
this
.sqlSessionFactory);
-
explicitFactoryUsed =
true
;
-
}
-
-
if
(StringUtils.hasText(
this
.sqlSessionTemplateBeanName)) {
-
if
(explicitFactoryUsed) {
-
logger.warn(
“Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.”
);
-
}
-
definition.getPropertyValues().add(
“sqlSessionTemplate”
,
new
RuntimeBeanReference(
this
.sqlSessionTemplateBeanName));
-
explicitFactoryUsed =
true
;
-
}
else
if
(
this
.sqlSessionTemplate !=
null
) {
-
if
(explicitFactoryUsed) {
-
logger.warn(
“Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.”
);
-
}
-
definition.getPropertyValues().add(
“sqlSessionTemplate”
,
this
.sqlSessionTemplate);
-
explicitFactoryUsed =
true
;
-
}
-
-
if
(!explicitFactoryUsed) {
-
if
(logger.isDebugEnabled()) {
-
logger.debug(
“Enabling autowire by type for MapperFactoryBean with name ‘”
+ holder.getBeanName() +
“‘.”
);
-
}
-
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
-
}
-
}
-
}
-
-
return
beanDefinitions;
-
}
大家注意看红色部分,红色部分的意思就是对于mybatis的dao接口,spring是以MapperFactoryBean的方式来管理的,举个例子说
@autowired
private UserMapper userMapper;
这个userMapper返回的实例对象会是MapperFactoryBean,这个过程是由spring控制的,因为笔者对于spring原理没有深入研究过,笔者在这里不做说明。
可能大家好奇,为什么这里不能直接像第一部分一样,通过sqlsession.getMapper(…)的方式来获取dao接口对象呢,笔者在这里觉得,之所以出现MapperFactoryBean
这个中间对象,是因为SqlSessionTemplate,sqlsessionTemplate是mybatis-spring封装的用于方法执行mybatis方法的工具类,但是大家平时可能很少用到这个,
笔者在这里做了一个小测试利用,简单的看一下它的用法:
-
/**
-
*
-
* 类SqlTemplateTest.java的实现描述:TODO 类实现描述
-
* @author yuezhihua 2015年7月29日 下午2:07:44
-
*/
-
@RunWith
(SpringJUnit4ClassRunner.
class
)
-
@ContextConfiguration
(locations = {
-
“classpath*:spring/demo-locator.xml”
-
})
-
public
class
SqlTemplateTest {
-
-
@Autowired
-
private
SqlSessionTemplate sqlSessionTemplate;
-
-
@Autowired
-
private
SqlSessionFactory sqlSessionFactory;
-
-
/**
-
* 初始化datasource
-
*/
-
@Before
-
public
void
init(){
-
DataSource ds =
null
;
-
try
{
-
ds = BaseDataTest.createUnpooledDataSource(BaseDataTest.DERBY_PROPERTIES);
-
BaseDataTest.runScript(ds,
“com/mybatis/demo/databases/lazyloader/lazyloader-schema.sql”
);
-
BaseDataTest.runScript(ds,
“com/mybatis/demo/databases/lazyloader/lazyloader-dataload.sql”
);
-
}
catch
(IOException e) {
-
e.printStackTrace();
-
}
catch
(SQLException e) {
-
e.printStackTrace();
-
}
-
}
-
-
-
/**
-
* 测试mybatis自身的查询
-
*/
-
@Test
-
public
void
testMybatisSelect(){
-
SqlSession session = sqlSessionFactory.openSession();
-
UserMapper mapper = session.getMapper(UserMapper.
class
);
-
System.out.println(
“jdk proxy mapper : ”
+mapper);
-
UserPO userPo = mapper.getUserByUsername(
“zhangsan”
);
-
System.out.println(
“—–testMybatisSelect:”
+userPo.getUsername());
-
}
-
-
-
-
/**
-
* mybatis-spring : sqlSessionTemplate测试查询
-
* java.lang.UnsupportedOperationException: Manual close is not allowed over a Spring managed SqlSession不要在意
-
*/
-
@Test
-
public
void
testSelect(){
-
UserPO result = sqlSessionTemplate.selectOne(
“com.mybatis.demo.lazyload.mapper.UserMapper.getUserByUsername”
,
“zhangsan”
);
-
System.out.println(result);
-
}
-
-
-
}
大家看上面testSelect这个测试用例,可以看到sqlsessiontemplate的基本使用方法
spring+mybatis注解方式获取dao接口对象的方法;:
MapperFactoryBean.getObject
-
/**
-
* {@inheritDoc}
-
*/
-
public
T getObject()
throws
Exception {
-
return
getSqlSession().getMapper(
this
.mapperInterface);
-
}
这里边的getSqlSession其实就是sqlsessiontemplate
总结:对于第一部分可以说是返回了mybatis的dao接口的jdk代理对象,通过mapperproxy这个类似于拦截器一样的类跳转执行sql的,可以说是原生dao接口的一层代理对象;
那么对于第二部分来说,确实有三层代理对象:
所以,咱们在spring中使用
@autowired
private UserMapper userMapper;
来注入对象的时候,其实是经历了 cglib –> mapperfactorybean –> sqlsessiontemplate –> mapperproxy –> 原生dao接口 的包装过程,才获取的
所以咱们在使用spring来调用没有实现类的mybatis的dao接口的时候,并不是像看起来那么简单,而是经过多层代理包装的一个代理对象,对方法的执行也跳转到mybatis框架中的mappermethod中了