Mybatisc學習筆記

Mybatis參數處理方式

通常方式

  1. 單個參數,mybatis不會做特殊處理
    #{參數名}就可以取出參數值

  2. 多個參數,mybatis會做特殊處理
    多個參數會封裝成一個map
    key: param1,param2,param3…或者其他參數索引
    value: 傳入的值
    #{} 就是從map找那個獲取指定的key對應的value
    在mapper.xml文件中默認使用#{param1},#{param2}這樣的方式來獲取傳入參數

  3. 命名參數:明確的指定封裝參數時map的key
    使用@Param(key)註解
    例如 Employee findByIdAndName(@Param(“id”) Integer id, @Param(“name”) String name);

通過POJO傳遞

如果多個參數是業務邏輯的數據類型,可以直接傳入POJO
使用#{屬性字段}來取出POJO的屬性值

通過Map容器傳遞

如果多個參數沒有對應的POJO,則可以直接使用Map
使用#{key}來取出對應的值

public interface EmployeeMapper{
    /*...*/
    // EmployeeMapper中定義方法
    Employee getEmpByMap(Map<String, Object> map);
}

然後在EmployeeMapper.xml文件中如下配置

<select id="getEmpByMap" resultType="com.crzmy.entity.Employee">
    SELECT * FROM tb_employee WHERE emp_id = #{id} AND emp_name = #{name}
</select>

測試方法代碼


public class AppTest{
	@Test
	public void test(){
		/*
		...
		*/
		// 測試方法中主要代碼
		EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
		Map<String, Object> map = new HashMap<>();
		map.put("id",1);
		map.put("name", "jack");
		Employee emp = mapper.getEmpByMap(map);
		System.out.println(emp);
		/*
		...
		*/
    }
}

通過DTO傳遞

如果多個參數不是業務模型中的數據,但是經常使用,可以使用一個DTO即數據傳輸對象

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class EmployeeDTO{
	private Integer empId;
	private String empName;
}

其他情況

public class EmployeeMapper{
	/**
	* 通過id和名字查找僱員
	* 
	* 這種情況僱員id取值方式有1種
	* #{empId}或者#{param1}
	* 而僱員名字則只能通過1個方式
	* #{param2}
	* 
	* @param empId 僱員id
	* @param empName 僱員名字
	* @return 僱員對象
	*/
	Employee getEmp(@Param("empId") Integer empId, String empName);
    
	/**
	* 通過id和僱員信息更新對象
	* 
	* 這種情況僱員id取值方式只有1種
	* #{param1}
	* 而僱員對象中的屬性有2種方式,以僱員名字爲例
	* #{param2.empName} 或者 #{emp.empName}
	* 
	* @param empId  待更新的僱員id
	* @param emp    新的僱員對象
	* @return  是否更改成功
	*/
	boolean updateEmp(Integer empId, @Param("emp") Employee emp);
    
	/**
	* 通過一個僱員id列表批量查詢僱員信息
	* 
	* mybatis對Collection或者數組也會特殊處理
	* 把集合或者數組封裝在Map中
	* 
	* 如果是List集合也可以直接使用"list"這個key來獲取
	* 比如取出list中的第一個id
	* #{list[0]}
	* 
	* @param ids    僱員id列表
	* @return   僱員對象列表
	*/
	List<Employee> getEmpsByIds(List<Integer> ids);
}

源碼分析Mybatis的參數處理過程

Mapper接口

public class EmployeeMapper{
	/**
	* 通過id,名字和性別查詢僱員
	* @param id 僱員id
	* @param name   僱員名字
	* @param gender 僱員性別
	* @return   Employee對象
	*/
	Employee findByIdAndNameWithGender(@Param("empId") Integer id, @Param("empName") String name, String gender);
}

分析入口


public class AppTest{
	@Test
	public void test(){
		/*
		...
		*/
		EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee = mapper.findByIdAndNameWithGender(1,"cruii","1");
		/*
		...
		*/
	}
}

第一行代碼,mybatis會使用MapperProxyFactory類中的newInstance(MapperProxy mapperProxy)方法來使用JDK動態代理生成EmployeeMapper代理對象
通過代理對象來與數據庫進行會話.

public class MapperProxyFactory{
	/*
	...
	*/
	protected T newInstance(MapperProxy<T> mapperProxy) {
	    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
	}
	    
	public T newInstance(SqlSession sqlSession) {
	    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
	    return newInstance(mapperProxy);
	}
}

Employee employee = mapper.findByIdAndNameWithGender(1,“cruii”,“1”);
該行代碼會調用代理對象的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);
	}
	
	/*
	...
	*/
}

如果是Object類裏的方法,如toString(),hashCode()等方法,則直接通過反射執行

MapperProxy是一個InvocationHandler,在使用JDK動態代理生成對象時使用,
會根據該接口生成動態代理對象,然後利用反射調用實際對象的目標方法.
然而動態代理對象裏面的方法是有接口(interface)聲明的.
但是動態代理對象也能調用toString(),hashCode()等方法,而這些方法就是從Object類繼承過來的.
所以if (Object.class.equals(method.getDeclaringClass()))這行代碼的作用就是:
如果利用動態代理對象調用的是toString(),hashCode()等從Object類繼承的方法,則直接反射調用.
如果是接口聲明的方法,則通過下面的MapperMethod執行.

此時,傳入的方法是com.crzmy.mapper.EmployeeMapper.findByIdAndNameWithGender
通過method.getDeclaringClass()得到的結果是interface com.crzmy.mapper.EmployeeMapper
所以直接通過MapperProxy類的cachedMapperMethod方法生成一個MapperMethod對象.

public class MapperProxy {
	/*
	...
	 */
	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;
	}
	/*
	...
	 */
}

該方法首先在緩存中查找是否存在目標方法,如果不存在,則創建一個新的MapperMethod對象並且緩存,每一個MapperMethod對象都代表了SQL映射文件mapper.xml裏的一個SQL語句或者FLUSH配置,對應的SQL語句通過全類名和方法名從Configuration對象中獲得.
這樣當以後再次調用同一個mapper方法時直接返回緩存中的對象,不必再次創建,節省內存.
當獲取了MapperMethod對象後,則通過該對象的execute方法執行目標方法.
MapperMethod類中有兩個成員變量,SqlCommand對象和MethodSignature對象.
在創建MapperMethod對象時,會同時初始化這兩個對象.

  • SqlCommand類

該類負責封裝SQL語句的標籤類型(如:SELECT,UPDATE,DELETE,INSERT)和目標方法名

SqlCommand類部分源碼如下

public static class SqlCommand {
	/*
	name 負責存放調用的目標方法名  
	type 負責存放SQL語句的類型
	*/
	private final String name;
	private final SqlCommandType type;
	
	public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
		final String methodName = method.getName();
		final Class<?> declaringClass = method.getDeclaringClass();
		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和type
			*/
			name = ms.getId();
			type = ms.getSqlCommandType();
			if (type == SqlCommandType.UNKNOWN) {
			  throw new BindingException("Unknown execution method for: " + name);
			}
		}
	}
	    
	/*
	...
	*/
}

MapperMethod中的resolveMappedStatement方法

public class MapperMethod{

	/*
	...
	*/
	 
	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的過程中,在構造方法裏首先獲取到目標方法的方法名以及目標方法聲明的類對應的Class對象.
進入resolveMappedStatement方法,首先通過傳入的EmployeeMapper接口的Class對象,獲取全類名,然後拼接目標方法名組成statementId.再判斷Configuration的mappedStatements中是否有對應的key,若true,則返回mappedStatements中對應key的MappedStatement對象.
回到SqlCommand的構造器中,此時MappedStatement ms已被賦值爲對應的目標方法的MappedStatement對象.直接通過ms.getId()和ms.getSqlCommandType()方法獲取目標方法名和SQL類型.
在本例中,即:
name: com.crzmy.mapper.EmployeeMapper.findByIdAndNameWithGender
type: SELECT

至此SqlCommand對象初始化完畢.

  • MethodSignature類

該類負責封裝方法的參數和返回值類型等信息

MethodSignature部分源碼

public static class MethodSignature {
	
	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);
		  
		/*
		根據不同的返回值類型進行處理,並賦值給returnType
		*/
		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類的構造方法會首先調用TypeParameterResolver類的resolveReturnType方法來獲取目標方法的返回值類型,傳入的參數就是目標方法對應的Method對象和EmployeeMapper類對象

TypeParameterResolver類部分源碼

public class TypeParameterResolver {
	/*
	...
	*/
	  
	public static Type resolveReturnType(Method method, Type srcType) {
		Type returnType = method.getGenericReturnType();
		Class<?> declaringClass = method.getDeclaringClass();
		return resolveType(returnType, srcType, declaringClass);
	}
	  
	/*
	...
	*/
	  
	private static Type resolveType(Type type, Type srcType, Class<?> declaringClass) {
		if (type instanceof TypeVariable) {
			return resolveTypeVar((TypeVariable<?>) type, srcType, declaringClass);
		} else if (type instanceof ParameterizedType) {
			return resolveParameterizedType((ParameterizedType) type, srcType, declaringClass);
		} else if (type instanceof GenericArrayType) {
			return resolveGenericArrayType((GenericArrayType) type, srcType, declaringClass);
		} else {
			return type;
		}
	}
	  
	/*
	...
	*/
}

resolveReturnType直接獲取到目標方法的返回值類型,在該例子中即class com.crzmy.entity.Employee
然後獲取方法所屬的類對象,即interface com.crzmy.mapper.EmployeeMapper
再進入resolveType方法,該方法判斷返回值類型是否是TypeVariable(類型變量), ParameterizedType(參數化類型)或者GenericArrayType(泛型數組).

void method(E e){}中的E就是類型變量
Map<String, Integer> map; map的Type就是ParameterizedType
List[] list; list的Type就是GenericArrayType

很顯然,本例子中以上都不是,則直接返回class com.crzmy.entity.Employee.
然後在MethodSignature類的構造方法的最後一句

this.paramNameResolver = new ParamNameResolver(configuration, method);

該條語句實例化了一個ParamNameResolver類對象,該類主要的作用就是解析參數.

ParamNameResolver部分源碼

public class ParamNameResolver {

	private static final String GENERIC_NAME_PREFIX = "param";
	
	private final SortedMap<Integer, String> names;
	
	private boolean hasParamAnnotation;
	
	public ParamNameResolver(Configuration config, Method method) {
		/*
		存儲目標方法的參數對應的Class對象
		*/
		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);
	}
	  
	private String getActualParamName(Method method, int paramIndex) {
		if (Jdk.parameterExists) {
		  return ParamNameUtil.getParamNames(method).get(paramIndex);
		}
		return null;
	}
	  
	/*
	...
	*/
 
}

ParamNameUtil工具類源碼

@UsesJava8
public class ParamNameUtil {
	public static List<String> getParamNames(Method method) {
		return getParameterNames(method);
	}
	
	public static List<String> getParamNames(Constructor<?> constructor) {
		return getParameterNames(constructor);
	}
	
	private static List<String> getParameterNames(Executable executable) {
		final List<String> names = new ArrayList<String>();
		final Parameter[] params = executable.getParameters();
		for (Parameter param : params) {
			names.add(param.getName());
		}
		return names;
	}
	
	private ParamNameUtil() {
		super();
	}
}

通過isSpecialParameter(paramTypes[paramIndex])判斷是否是RowBounds和ResultHandler特殊類型,如果true,則跳過.
緊接着判斷每一個註解是否是Param註解,如果true,則hasParamAnnotation賦值爲true表示該方法有@Param註解,然後直接把Param註解的value值賦值給name,在本例子中即empId和empName.
如果沒有使用@Param註解,則判斷是否開啓了useActualParamName,

  • 如果爲true,則調用getActualParamName方法,並通過ParamNameUtil工具類獲取目標方法的參數名,再把參數名存儲到List中,接着根據傳入的索引獲取對應的參數名.然後把參數索引和參數名存放到map中.

  • 如果爲false,那麼就會使用參數索引作爲name.

當所有參數都判斷之後,通過Collections.unmodifiableSortedMap(map)返回一個只讀的Map容器賦值給names,同樣存放着參數索引和參數名的映射關係,即:
當useActualParamName()爲true時:
0 -> “id”
1 -> “name”
2 -> “gender”

當useActualParamName()爲false時:
0 -> “id”
1 -> “name”
2 -> 2

補充
從JDK8開始,可以通過打開javac -parameters,然後通過method.getParameters()獲取到參數的名稱.
上面代碼中的Executable對象就是Java的方法Method類和構造器Constructor類的父類,擁有getParameters()方法.
在本例中即:id,name,gender
如果是JDK7及以下,則獲取到的是arg0,arg1,arg2等無意義的參數名.

本例默認是useActualParamName()爲false,所以names的存儲情況爲第二種情況

至此ParamNameResolver對象實例化完成,然後賦值給MethodSignature的paramNameResolver變量.緊接着MethodSignature對象也實例化完成,同時MapperMethod也初始化完成,最後通過methodCache.put(method, mapperMethod);將mapperMethod對象緩存,存儲的是目標方法的Method對象和mapperMethod的映射.
現在就正式開始進入MapperMethod的execute方法,該方法執行對應的SQL語句並且根據返回值類型返回值.

MapperMethod類execute方法

public class MapperMethod {
	private final SqlCommand command;
	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);
	}
	
	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);
				}
				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;
	}
	  
	/*
	...
	*/
}

首先通過SqlCommand的實例化對象command獲取SQL類型,在本例中即SELECT,進入對應的case語句塊.
其次根據MethodSignature的實例化對象method存儲的目標方法的返回值類型判斷,在本例中返回值爲一個Employee對象,所以直接到達Object param = method.convertArgsToSqlCommandParam(args);
然後進入method.convertArgsToSqlCommandParam方法

public static class MethodSignature {
	/*
	...
	*/
		
	public Object convertArgsToSqlCommandParam(Object[] args) {
		return paramNameResolver.getNamedParams(args);
	}
		
	/*
	...
	*/
}

該方法又調用了ParamNameResolver的getNamedParams方法,該方法是解析參數的核心方法,進入該方法

public class ParamNameResolver {
	private static final String GENERIC_NAME_PREFIX = "param";
	
	private final SortedMap<Integer, String> names;
	
	private boolean hasParamAnnotation;
		
	/*
	...
	*/
		
	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;
		}
	}
}

調用的目標方法

Employee findByIdAndNameWithGender(@Param("empId") Integer id, @Param("empName") String name, String gender);

此時傳入的參數args的內容是

1,"cruii","1"

而names存儲的內容是

0 -> "id"  
1 -> "name"  
2 -> 2 

先來看只有一個參數的情況
假設現在傳入的參數args數組裏只有一個Integer類型的1,沒有使用@Param註解,並且names存儲的只有一個"0"->"id"映射.

return args[names.firstKey()];

/* 
則相當於  
return args[0];返回 1 
*/

如果使用了@Param註解,則遍歷Map容器names,以value作爲key和args數組對應索引的值作爲value存儲到Map容器param中
此時相當於

param.put("id", 1);

然後返回param.
再回到原來的例子,多個參數情況
同樣遍歷Map容器names,以value作爲key和args數組對應索引的值作爲value存儲到Map容器param中.並且會根據GENERIC_NAME_PREFIX常量即"param"和當前的索引拼裝成新的字符串,即param1,param2,…,paramN.然後和args數組裏對應索引的值存儲到Map容器param中,最後遍歷完成後param的存儲情況是:

"id" -> 1
"param1" -> 1  
"name" -> "cruii"
"param2" -> "cruii"  
2 -> "1"  
"param3" -> "1"

所以可以在SQL映射文件中如下兩種方式配置都可以獲取到參數的值

第一種方式:

<select id="findByIdAndNameWithGender" resultType="employee">
    SELECT
        *
    FROM
        tb_employee
    WHERE
        emp_id = #{empId}
    AND
        emp_name = #{empName}
    AND
        emp_gender = #{param3}
</select>

第二種方式:

<select id="findByIdAndNameWithGender" resultType="employee">
    SELECT
        *
    FROM
        tb_employee
    WHERE
        emp_id = #{param1}
    AND
        emp_name = #{param2}
    AND
        emp_gender = #{param3}
</select>

至此,Mybatis的參數處理過程源碼分析完畢.

發佈了73 篇原創文章 · 獲贊 67 · 訪問量 4514
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章