在看binding模块之前首先要想好下面三点:
1、binding模块存在的目的是什么?解决了什么问题?
2、binding模块有哪些必须要的步骤(关键点)?
3、binding模块与原ibatis(SqlSession)是怎么衔接的?
概述
我们先来看下使用ibatis也就是没有binding模块的情况:
// 开启一个SqlSession
SqlSession sqlSession = factory.openSession();
int arr[]={4,5,6};
// 通过SqlSession的selectList方法执行查询操作
List<User> users = sqlSession.selectList("UserMapper.findUserById", arr);
// 关闭SqlSession
sqlSession.close();
这样操作有几个缺点。
1、首先,就是SqlSession的执行方法需要用户手动传入一个statement参数,用来映射MapperStatement。这个statement极易写错。而且这个错误只有在运行时才会暴露出来。这一点是安全性。
2、用户需要去决定到底该使用哪个SqlSession的方法来实现具体的功能,其实,我们所定义的所有的statementId到最后都对应着SqlSession中不同的数据库操作inset|update|select|delete,而这个具体的操作其实是有规律的,比如通过标签来判断执行的方法类型,通过返回值来判断用selectOne或者selectMany。这一点是不透明性。
3、我觉得之前的statement只能是一个标识,用来查找statement的标识,并不具备可读性,这个可读性需要人为的标识出来。
4、我们在手动调用SqlSession的时候传入的parameter实际上是一个映射的参数,它可以是一个对象,也可以是一个map,当接口的参数是一个的时候可以是一个对象,当接口中的参数是多个的时候,就变成了一个Map,而在解析的时候会使用ognl表达式来解析。后面我会详细说明。
基于上面几点,Mybatis引入了binding模块。我们先来看下binding模块是如何使用的。
public void mybatis() {
// 1、加载资源
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
// 2、构建SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader);
// 3、通过SqlSessionFactory构建SqlSession
SqlSession sqlSession = factory.openSession();
// 4、通过SqlSession获取用户接口代理类
UserDao userDao = sqlSession.getMapper(UserDao.class);
// 5、执行方法
userDao.addUser(user);
// 6、提交
sqlSession.commit();
// 7、关闭
sqlSession.close();
}
可以看到,这里需要我们定义一个UserDao的接口类,这个接口类中定义了我们需要执行的一些方法。这些类名+方法名决定了一个唯一的Statement。如果在实例化Dao对象的时候找不到对应的Statement那么会抛出异常。
所以总结下,其实mybatis的binding模块存在的最重要的意义是实现了通过Dao接口和xml映射文件的绑定,自动生成接口的具体实现,这样做的好处就是尽可能的屏蔽实现细节,同时可以将实现模板化。
binding模块的核心内容
所以,目标有了,想要实现接口和配置文件的绑定,自动实现接口,该怎么实现呢?
首先,我们先看自动实现接口和配置文件的绑定。
自动实现接口和配置文件绑定,必要的功能:
1、确定一个唯一的MappedStatement。
2、自动确定需要执行的SqlSession的方法,确定方法需要两点,一个是方法名,一个是返回类型。
3、需要确定SqlSession用户入参。
为了实现上面三点。Mybatis引入了一个叫MapperMethod的类,该类持有了两个类的引用,分别是SqlCommand和MethodSignature。
我们先来看SqlCommand的实现
public static class SqlCommand {
// 名称空间+id
private final String name;
// SQL类型UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 获取方法名
final String methodName = method.getName();
// 获取类
final Class<?> declaringClass = method.getDeclaringClass();
// 通过类和方法名来获取MappedStatement,注意这里获取不到就会直接抛出异常,不用等到执行才抛。
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) != null) {
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
name = ms.getId();
// 获取方法执行的类型。之前解析Mapper的时候会自动解析对应的TYPE。
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
这里我们看到其实SqlCommand做了两件事,1、获取唯一ID,2、获取方法执行类型。
我们知道唯一定位一个MappedStatement是通过一个名称空间(namespace)和一个SQL的id。其中namespace可以是用户自定义接口的全路径,那么SQL的id可以是一个方法名。
也就是说我们实现接口和配置文件的绑定是通过namespace和id来的。
我们先看Configuration中定义如下:
// 定义了名称空间和MappedStatement的映射
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
我们再来看下MethodSignature
public static class MethodSignature {
// 是否返回多个
private final boolean returnsMany;
// 是否返回是Map
private final boolean returnsMap;
// 是否返回是void
private final boolean returnsVoid;
// 是否返回Cursor
private final boolean returnsCursor;
// 返回类型
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex;
private final Integer rowBoundsIndex;
// 参数名解析器,确定SqlSession用户入参
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);
}
public Object convertArgsToSqlCommandParam(Object[] args) {
return paramNameResolver.getNamedParams(args);
}
private Integer getUniqueParamIndex(Method method, Class<?> paramType) {
Integer index = null;
final Class<?>[] argTypes = method.getParameterTypes();
for (int i = 0; i < argTypes.length; i++) {
if (paramType.isAssignableFrom(argTypes[i])) {
if (index == null) {
index = i;
} else {
throw new BindingException(method.getName() + " cannot have multiple " + paramType.getSimpleName() + " parameters");
}
}
}
return index;
}
private String getMapKey(Method method) {
String mapKey = null;
if (Map.class.isAssignableFrom(method.getReturnType())) {
final MapKey mapKeyAnnotation = method.getAnnotation(MapKey.class);
if (mapKeyAnnotation != null) {
mapKey = mapKeyAnnotation.value();
}
}
return mapKey;
}
}
这里我们看到MethodSignature也是做了两件事,1、确定返回类型,2、确定用户调用SqlSession的实际入参对象。
扩展内容:我们把ParamNameResolver代码贴出来,看一下参数是如何解析和获取的。
// 参数解析:主要的逻辑是按照参数的顺序进行排序组成一个Map,key是顺序,value是字段名,
// 如果没有就按照"0"、"1"补充。 然后解析的时候也是按照顺序从数组中挨个取出数据,
// 需要注意,获取参数需要按照顺序来
public ParamNameResolver(Configuration config, Method method) {
// 解析参数类型
final Class<?>[] paramTypes = method.getParameterTypes();
// 解析参数的注解
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
// 最后的结果,注意是排序的,从0开始依次有多少个参数,map里面就有多少的键值对。
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]) {
// 如果存在Param注解,就解析Param的值最为value存起来。
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
// 如果没有解析到,则尝试直接解析参数的字段名,也不一定解析到,看JDK配置
if (name == null) {
// @Param was not specified.
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
// 如果实在没有了,就补充0,1这种数字字符串
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的真实的对象,这个对象有可能是Map,有可能是个单独对象
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
// 如果没有解析到,那就返回空
if (args == null || paramCount == 0) {
return null;
// 如果参数只有1个对象,并且没有Param标记,就直接把对象取出,返回,
// 后续会使用ognl表达式解析这个对象
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
// 接下来就是存在多个参数或者存在Param标记的参数的情况,这种情况需要组成一个map
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 这个map存放的有可能是"0","1",有可能是Param的参数值,有可能是参数的名字
// value则存放的就是参数的值。
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;
}
}
这时候我们可以确定一个SqlSession中唯一的方法了。MapperMethod的代码如下
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
// 实际上是调用了ParamNameResolver
Object param = method.convertArgsToSqlCommandParam(args);
// 执行sqlSession.insert
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
// 实际上是调用了ParamNameResolver
Object param = method.convertArgsToSqlCommandParam(args);
// 执行sqlSession.update
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
// 实际上是调用了ParamNameResolver
Object param = method.convertArgsToSqlCommandParam(args);
// 执行sqlSession.delete
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 {
// 实际上是调用了ParamNameResolver
Object param = method.convertArgsToSqlCommandParam(args);
// 执行sqlSession.selectOne
result = sqlSession.selectOne(command.getName(), param);
}
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;
}
至此,实现接口和配置文件的绑定我们已经分析完了。我们总结下:
1、Mybatis使用SqlCommand和MethodSignature实现了接口的定位。
2、Mybatis使用了MapperMethod实现了到底层SqlSession操作数据库方法的调用。
最后我们再来看一下,Mybatis是如何实现接口自动代理的。这部分就简单了。
首先,Mybatis先定义了一个代理类MapperProxy。
代理类一般只增强方法功能。而获取代理对象是通过ProxyFactory来实现的。
现在我们来看Mybatis的实现。
public class MapperProxy<T> implements InvocationHandler, Serializable {
// 持有一个SqlSession的对象,代理模式
private final SqlSession sqlSession;
// 需要代理的类
private final Class<T> mapperInterface;
// 缓存下MapperMethod,不用再重复解析SqlCommand和MethodSignature
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = 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);
}
// 获取MapperMethod,并调用执行方法,上面已经分析过了。
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
// 缓存
private MapperMethod cachedMapperMethod(Method method) {
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
}
我们再来看下ProxyFactory,Mybatis中的ProxyFactory是MapperProxyFactory
public class MapperProxyFactory<T> {
// 代理类
private final Class<T> mapperInterface;
// 缓存,这里只是定义了一个对象
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public Class<T> getMapperInterface() {
return mapperInterface;
}
public Map<Method, MapperMethod> getMethodCache() {
return methodCache;
}
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
// 手动创建一个Proxy,并且创建代理
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
最后,Mybatis定义了一个MapperRegistry用来存放MapperProxy。
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
public <T> boolean hasMapper(Class<T> type) {
return knownMappers.containsKey(type);
}
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.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);
}
}
}
}
然后,将getMapper暴露给SqlSession供用户调用。