前言
利用Mybatis框架,我们只要提供一个
Mapper接口
,定义好相应的方法
,再利用XML文件
的配合。就可以实现利用调用Mapper接口的方法
来实现SQL语句的查询
,这其中是如何实现的呢?本文我们将带着这个问题,结合源码来解答
代码示例
分析源码之前我们首先贴一下代码示例,这样对于分析源码会起到画龙点睛的作用
UserOrderMapper.java
@Mapper
public interface UserOrderMapper {
// 根据用户手机号查询其下所有已完成订单列表
List<UserOrderDo> completedOrdersByUserPhone(@Param("phone") String phone);
}
UserOrderMapper.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.xxx.mapper.UserOrderMapper">
<!-- 根据用户手机号查询其下所有已完成订单列表 -->
<select id="completedOrdersByUserPhone" resultType="com.xxx.entity.UserOrderDo">
select
id as id,
order_amount as orderAmount,
create_time as createTime,
pay_time as PayTime
from
user_order
where
user_phone = #{phone} and order_status = '10'
</select>
</mapper>
UserOrderDo.java
public class UserOrderDo {
private Long id;
private BigDecimal orderAmount;
private Date createTime;
private Date PayTime;
// 省略其他字段以及相应的getter/setter方法...
}
Service层调用代码示例
@Service
public class UserOrderServiceImpl {
@Autowired
private UserOrderMapper userOrderMapper;
public List<UserOrderVo> getCompletedUserOrderByPhone(String phone) {
// 查询
List<UserOrderDo> completedOrders = userOrderMapper.completedOrdersByUserPhone(phone);
// 处理查询结果返回...
}
}
以上就是使用Mybatis框架的日常开发模式,我们带着本文需要解决的问题来看,为什么调用userOrderMapper.completedOrdersByUserPhone(phone)
就可以直接查询到数据库中用户已完成订单列表了呢?
进入源码
Configuration.java
代表着Mybatis配置的
Configuration对象
中持有一个MapperRegistry类型的mapperRegistry属性
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
// 注册Mapper接口
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
// 获取指定Mapper的代理
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
}
在Mybatis初始化阶段,会解析mybatis-config.xml文件,实例化出单例的Configuration对象,并调用其
addMappers方法将指定包下满足条件的所有Mapper接口都注册到MapperRegistry中(或者在解析mapper.xml文件阶段,以namaspace命名空间值,调用configuration.addMapper方法注册)
MapperRegistry.java
Mapper注册表,内部持有所有满足条件的Mapper接口的
Mapper代理工厂实例
集合,并以Mapper接口的Class名
作为key,MapperProxyFactory
作为value
public class MapperRegistry {
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
// 注册指定包下,满足指定超类的所有Mapper接口
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
// 遍历注册
addMapper(mapperClass);
}
}
// 注册Mapper接口
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 添加到knownMappers集合中
knownMappers.put(type, new MapperProxyFactory<T>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 1. 先获取以Mapper的Class类型作为key对应的MapperProxyFactory实例
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 2. 调用MapperProxyFactory的newInstance方法创建代理Mapper对象并返回
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
//省略其他属性方法...
从这里可以看出Mybatis初始化阶段,就已经将所有满足条件的Mapper以接口Class作为key,MapperProxyFactory对象作为value存入到Map集合中了
接下来就是程序调用configuration.getMapper(...)
方法获取代理Mapper
对象,并注入到spring的bean容器
中
我们继续向下看这个Mapper代理对象到底是何方神圣?
MapperProxyFactory
一个Mapper代理的工厂类,负责Mapper接口代理对象的创建工作
public class MapperProxyFactory<T> {
// 持有需要创建代理的Mapper接口的类型
private final Class<T> mapperInterface;
// 持有一个空的以Method为key,MapperMethod对象为value的Map集合
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
// 构造函数
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
// 省略相应属性的get方法...
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
// new了一个MapperProxy作为Mapper代理对象的handler
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
通过newInstance方法可以看到,实际上是利用JDK的动态代理创建了每个Mapper接口的一个代理类,其handler
相关的逻辑交给了MapperProxy
MapperProxy
一个标准的InvocationHandler,不仅兼任代理handler的职责,还持有方法缓存Map以及Mybatis中重要的sqlSession对象
public class MapperProxy<T> implements InvocationHandler, Serializable {
// 持有属性
// 重要的sqlSession
private final SqlSession sqlSession;
// Mapper接口的类型
private final Class<T> mapperInterface;
// Mapper接口的方法缓存集合,以Method为key,MapperMethod实例为value
private final Map<Method, MapperMethod> methodCache;
// 省略构造函数...
@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);
}
// 重要的invoke逻辑来了,这里解释下,当我们调用Mapper代理对象的方法,比如(completedOrdersByUserPhone(...))时,会执行到这里,会执行cachedMapperMethod方法以获取缓存的MapperMethod实例,再将具体的执行逻辑交给MapperMethod处理
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
// 尝试根据此次调用的方法作为key,获取已缓存的MapperMethod实例
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
// 没有就new一个出来
if (mapperMethod == null) {
// 重要的就是这个new MapperMehtod的方法
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
// new之后放入methodCache中缓存起来
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
代码分析到了这里,我们可以知道当我们在我们的service层利用@autowired
注解拿到Mapper代理对象
之后,第一次
调用它的某个(比如查询)方法时,会进入MapperProxy
的invoke
方法中,并调用new出来的MapperMethod
的execute
方法执行真正的SQL调用,之后再调用同一个查询方法时,就不会再new MapperMethod
实例了,而是从mehtodCache
这个map缓存中获取,以提高性能,接下来我们继续往下,看看MapperMethod
做了什么工作?
MapperMethod
每个Mapper接口中定义的查询/删除/新增/更新方法都对应一个MapperMethod实例,该实例持有两个重要的内部类SqlCommand和MehtodSignature属性,通过这两个内部类,就可以囊括所有查询SQL之前所需的各种基础信息
public class MapperMethod {
// 持有SQL命令相关,主要两个属性一个name一个type
private final SqlCommand command;
// Java方法签名相关
private final MethodSignature method;
// 构造函数, 就是实例化上面的两个实例
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
// 内部类SqlCommand
public static class SqlCommand {
// name属性并不是方法名哦,而是MappedStatement实例的id属性,至于怎么获取这个值,下面会分析
private final String name;
// 对应MappedStatement实例的sqlCommandType属性值,是一个枚举类型
private final SqlCommandType type;
// 构造函数,就是想办法初始化上面的两个属性
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
// 根据方法名以及方法所声明的class的类型以及Mapper代理类接口的类型去获取configuration中缓存的MappedStatement实例
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
// 若MappedStatement实例不存在,则判断方法是否含有@Flush注解
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
// 存在该注解,则将name置为null,type置为SqlCommandType.FLUSH
name = null;
type = SqlCommandType.FLUSH;
} else {
// 否则就抛出异常,找不到方法对应的statement实例
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
// 找到了对应的MappedStatement实例,则取其id属性作为name,sqlCommandType属性作为type
name = ms.getId();
type = ms.getSqlCommandType();
// 注意这里的sqlCommandType属性值若为UNKNOWN类型,则依赖会抛错
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
// 下面我们重点看其是如何根据Method就能找到对应的MappedStatement对象的
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 拼接Mapper接口名.方法名
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
// 从configuration中根据上面拼接后的作为statementId查询
return configuration.getMappedStatement(statementId);
// 当前方法申明的类型就是Mapper接口的类型,说明找不到,返回null
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
// 说明当前调用的方法可能是父类Mapper接口中定义的,那就递归调用直到找到对应的MappedStatement对象
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
源码分析到这里我们可以看出Mybatis中Mapper接口
是支持多层继承
关系的,至此整个SqlCommand对象
实例化完成,该有的属性也已经赋值完毕,接下来我们看MethodSignature
这个内部类的实例化过程
public static class MethodSignature {
// 是否返回Map,是否返回Void,是否返回Collection,是否返回Cursor以及如果指定了resultHandler,那么对应的参数index是多少,类似的还有rowBounds等
private final boolean returnsMany;
private final boolean returnsMap;
private final boolean returnsVoid;
private final boolean returnsCursor;
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex;
private final Integer rowBoundsIndex;
// 重要的属性来了,重点看这个属性的初始化过程
private final ParamNameResolver paramNameResolver;
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
MethodSignature内部类的实例化中有一个重要的属性ParamNameResolver对象,它会事先将Method的方法中的参数按照一定的规则先解析好,保存起来,将来执行SQL之前可以根据代理invoke方法中传入的Object[] args
参数,生成真正SQL语句执行时需要的(paranName,paramValue),以方便使用
ParamNameResolver.java
参数名称解析器,内部持有一个
SortedMap<Integer, String> names属性
,利用Java的反射
,事先将Mapper代理对象的Method方法的参数解析出来缓存
起来,以备将来SQL执行之前使用
public class ParamNameResolver {
private static final String GENERIC_NAME_PREFIX = "param";
// key为参数列表的index,value则为参数的name
// 参数name是从@Param注解获得的,当没有指定该注解时,则以String形式的index作为value,另外注意的是当参数中存在特殊参数(`RowBounds`和`ResultHandler`时,会跳过特殊参数)
// aMethod(@Param("M") int a, @Param("N") int b) --> {{0, "M"}, {1, "N"}}
// aMethod(int a, int b) --> {{0, "0"}, {1, "1"}}
// aMethod(int a, RowBounds rb, int b) --> {{0, "0"}, {2, "1"}}
private final SortedMap<Integer, String> names;
private boolean hasParamAnnotation;
// 构造函数
public ParamNameResolver(Configuration config, Method method) {
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
// 当调用SqlSession的执行方法之前会调用该方法,以将代理invoke方法得到的实际参数数组根据names属性中保存的参数名集合转为Sql执行需要的参数名对应参数值形式
// 注意:
// 1. 当参数列表为0个时,返回null
// 2. 当参数列表为1个且没有@Param注解时,则直接返回实际的参数值(非key,valueMap的形式)
// 3. 以上二者都不是的情况下,返回一个Map<String, Object>形式的以name作为key,实际传递的参数值作为value,需要注意的一点是多了一组默认参数以"param1, param2..."作为key的entry对象哦,也就是说我们可以用这些key在xml文件中直接写#{param1}, #{param2}来获取实际传递的value
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
这里简单举个例子,可以将ParamNameResolver
类的作用看的更清楚
还是以上面的List<UserOrderDo> completedOrdersByUserPhone(@Param("phone") String phone);
为例
当我们completedOrdersByUserPhone方法对应的MappedMehtod实例创建完成后,也即对应的MethodSignature.ParamNameResolver实例也创建完成,此时ParamNameResolver实例所持有的names属性值为:
names = {{0, "phone"}} // 0代表参数下标index,"phone"代表参数的name
当我们调用实际查询SQL前,假设传递给查询方法的参数phone的值为"15800000000", 经过Mapper代理
,调用invoke
方法执行查询,最终在调用Sqlsession
的查询方法前
会调用MethodSignature.convertArgsToSqlCommandParam(args)
方法进行参数的转换
(将args
转为SqlCommand可以使用的参数对象
), 会继而转到ParamNameResolver.getNamedParams方法处理,最终经过该方法处理后得到的Object对象如下:
{{"phone", "15800000000"}, {"param1", "15800000000"}}
这样在继续调用sqlSession的执行方法时就可以直接取出对应name的value值啦,也就是为什么我们在xml文件中可以写#{phone}就可以被替换成实际传递的手机号的原因
总结
为避免篇幅过于长,本文内容会直接在下文中继续分析,下文SqlSession的调用执行源码。
网友评论