文章目录
- mybatis初始化原理
- mybatis执行流程
- 1. 委托configuration获取mapper
- 2. configuration通过mapperRegistry(type,sqlSession)获取mapper
- 3. MapperRegistry.getMapper(type, sqlSession)
- 4. mapperMethod构造(mapper参数解析)
- 5. 实际执行通过SQLSession执行对应方法
- 6. 执行前解析方法参数
- 7. SQLSession里的执行又丢给executor
- 8. 构造sql对象(解析动态sql)
- 9. 先查询二级缓存
- 10. 尝试获取一级缓存
- 11. `statementHandler`执行底层方法
- 12. 设置Statement参数和typeHandler
- 13. 结果映射
- 缓存
- 插件
mybatis初始化原理
传统的查数据库方式,没有现在的方便;
我简单写一个以回顾一下这些东西。
private static final String URL = "jdbc:mysql://localhost:3306/ali?useSSl=false&zeroDateTimeBehavior=CONVERT_TO_NULL&characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
private static final String USERNAME = "root";
private static final String PAASWORD = "root";
private static final Logger logger = LoggerFactory.getLogger(TraditionConnection.class);
public static Connection connection() throws ClassNotFoundException, SQLException {
Class.forName(Driver.class.getName());
return DriverManager.getConnection(URL, USERNAME, PAASWORD);
}
public static List<Map<String, Object>> menuQuery(String sql, Object... args) throws SQLException, ClassNotFoundException {
logger.info("执行的sql: {}", sql);
logger.info("参数:{}", args);
Connection connection = connection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
preparedStatement.setObject(i + 1, args[i]);
}
List<Map<String, Object>> result = new ArrayList<>();
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
Map<String, Object> obj = new HashMap<>();
obj.put("name", resultSet.getString("name"));
obj.put("id", resultSet.getInt("id"));
result.add(obj);
}
close(connection, preparedStatement, resultSet);
logger.info("结果数据条数:{}", result.size());
return result;
}
public static void close(Connection connection, Statement statement, ResultSet resultSet) throws SQLException {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
}
public static void main(String[] args) throws SQLException, ClassNotFoundException {
String query = "select * from menu where name = ?";
List<Map<String, Object>> result = menuQuery(query, "菜单测试");
System.out.println("结果: " + JSON.toJSONString(result));
}
传统方式需要加载驱动,声明sql等操作,而现在的框架都像上面一样进行了各种封装,使得我们使用更加方便,下面来看看mybatis的方式。(只看主要方法)
mybatis初始化的入口是sqlSessionFactory.build();
InputStream is = new FileInputStream(ResourceUtils.getFile("classpath:Mybaties.xml"));
SqlSessionFactoryBuilder s = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = s.build(is);
// xml解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 通过parse解析xml配置
return build(parser.parse());
进入parser.parse()
方法,里面找到parseConfiguration
方法;
这个方法里的每一个调用对应配置文件下的每一个节点,然后再由每一个方法去解析对应的节点,先不说设计模式,这样的作法比较美观、简洁、易读,平常开发的时候,也应该像这种;比如写serviceImpl时,虽然功能逻辑,条理清晰,顺着写也没毛病,代码行数也差不多七八十至百行,但这样的代码可以更简介优化,看看有没有功能、逻辑,可以提取成一个方法,而在主方法里,只看到你哪一行代码做了什么,下面做了什么,整体逻辑更加清晰,在排错时也能更得心应手。
private void parseConfiguration(XNode root) {
try {
// 读取配置节点(连接数据库的配置)
propertiesElement(root.evalNode("properties"));
// 配置运行参数
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
// 配置类别名
typeAliasesElement(root.evalNode("typeAliases"));
// 配置插件
pluginElement(root.evalNode("plugins"));
// 查询到的结果映射到目标类可以由objectFactory执行
objectFactoryElement(root.evalNode("objectFactory"));
// 对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 反射工厂
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// 运行环境配置
environmentsElement(root.evalNode("environments"));
// 数据库提供者
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
// mapper类配置
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
这里主要讲解两个方法,其他比较简单
解析别名
mybatis可以配置别名,有两种方式:
- 一种是XML的方式,通过typeAliaes下的typeAlias标签,
<typeAliases>
<typeAlias alias="isAlias" type="com.lry.ma.dao.MenuDao"/>
</typeAliases>
标签还有package
,这个是配置mapper所在的包路径,会将包路径下的所有mapper注册,不会添加别名
<typeAliases>
<package name="com.lry.ma.dao"/>
</typeAliases>
- 一种是对应mapper类上使用
@Alias
注解配置就行,
@Alias("menuAlias")
public interface MenuDao {
}
源码中,它先是解析了typeAliases
typeAliasesElement(root.evalNode("typeAliases"));
然后传入方法,进行详细解析,里面会进行判断,如果不存在子标签就退出,如果有,就是上面说的两种情况,package
标签和typeAlias
标签,并且,解析方式也不一样
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 扫描package标签下的对象
if ("package".equals(child.getName())) {
// 获取到package标签的name,也就是mapper的包名
String typeAliasPackage = child.getStringAttribute("name");
// 通过别名注册器,注册别名;;
// 注意:里面注册前有一个判断,就是 !type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()
// 所以,实际上,package标签不会注册别名
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// typeAlias 标签解析,按配置的每个对象添加别名
// 它有两个属性,alias -》 别名,type -》 类全名
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
// 注册别名:key-》别名,value -》mapper class
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
先看下package
标签的解析:
这里并不会将package
标签下扫描到的class进行别名注册,因为,它有一个实质性的判断type.isInterface()
mapper都是接口,所以这里过不了;
然后再看typeAlias
标签的解析:
注册别名的两个方法:
方法一:以class注册,别名以类名,但如果类上有注解,则以注解为准
public void registerAlias(Class<?> type) {
// 以类名为别名
String alias = type.getSimpleName();
// 获取mapper类上是否有别名注解@Alias,有就以注解的为准
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
// 注册:其实就是一个HashMap,key -> alias ,value -> mapper class
// 这个方法也是最底层的注册方法
registerAlias(alias, type);
}
方法二:指定别名,与类名,都以指定为准,不做其他操作;
最底层的注册方法
public void registerAlias(String alias, Class<?> value) {
// 别名不可能为空
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// 别名统一化
String key = alias.toLowerCase(Locale.ENGLISH);
// 不存在该别名,并且别名对应的class也不相等才能被注册;
// 也就是说,如果出现别名一样的,那么就是后来者覆盖前者
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
}
// key -> alias ,value -> mapper class
TYPE_ALIASES.put(key, value);
}
解析mapper的方法
配置mappers
标签可以有4种方式:
// 配置包路径,将路径下的mapper全都注册
<package name="com.lry.mybatis.dao"/>
// 配置mapper.xml
<mapper resource="mapper/MenuMapper.xml"/>
// 配置mapper类全名,如果这样配置,那么走的是解析注解sql的方式解析,不会解析到resource下的mapperxml,也就是说,这样配置后,mapper.xml会失效
<mapper class="com.lry.mybatis.dao.MenuMapper"/>
// 配置mapper.xml绝对路径
<mapper url="file:E:\Program File\work\workspace\lry\pro\mybatis\target\classes\mapper\MenuMapper.xml"/>
使用addMapper
方法注册mapper时(这里的注册就是放到一个map中管理),都会经过一个方法hasMapper(type)
判断是否存在相同的的mapper,已经注册过的mapper在注册,会抛出异常;
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// mappers标签下,使用package标签的话,会扫描包路径下的文件,批量注册mapper,
String mapperPackage = child.getStringAttribute("name");
// 注册mapper,会判断是否当前mapper已经注册过
configuration.addMappers(mapperPackage);
} else {
// resource下的相对路径
String resource = child.getStringAttribute("resource");
// 绝对路径
String url = child.getStringAttribute("url");
// 类全名
String mapperClass = child.getStringAttribute("class");
// 上面三个个属性只能配置一个,如下:if else ,每个分支只有一个是不为空的,其他的都为空
if (resource != null && url == null && mapperClass == null) {
// 第一种:解析mapper.xml
// 这里是配置了resource ,相对路径解析mapper.xml文件
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
// 第二种:解析url
// 这里是配置了URL,远程连接解析
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 第三种:解析mapper class及sql注解,走这里不会解析resource下的mapper.xml
// 这里是配置了class,接口信息解析(这里的逻辑和上面package标签里的一样)
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
解析注解sql方式
这里先看注解方式的解析(习惯用注解方式的Mapper);
解析注解sql方式的情况有:1. package
标签注册mapper,2. mapper标签class属性不为空时
public <T> void addMapper(Class<T> type) {
// 不是接口,则退出
if (type.isInterface()) {
// 判断当前的mapper是否注册过
// 和别名不一样,出现重复的mapper.xml是不合理的,因为这可能导致业务逻辑的替换
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// key -> class, value -> mapper proxy factory(mapper代理工厂)
// 这个map是作为mapper注册的容器
// 之后我们getMapper,就是从这个knownMappers map中去的,取出后,再生成代理对象得到我们使用的mapper
knownMappers.put(type, new MapperProxyFactory<T>(type));
// 解析Mapper class
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
可以看到,它实例化了一个mapper解析类MapperAnnotationBuilder
,它默认这次了sql注解:
这里还不是最底层的东西,进入parser.parse()
方法里面,有这么一段,它会先判断是否加载过这个类,没有才进行解析,毕竟解析也是消耗资源的,从逻辑上来说也是必须的。
public void parse() {
String resource = type.toString();
// 判断是否加载过这个类
if (!configuration.isResourceLoaded(resource)) {
// 加载xml(加载mapper.xml)
// 找 包名/类名.xml,如:com.lry.mybatis.dao.MenuMapper.xml
loadXmlResource();
// 添加到已加载的容器
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
// 解析缓存注解
parseCache();
parseCacheRef();
// 获取到接口方法
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// 这个isBridge请百度
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
加载mapper.xml
type是我们定义的mapper类对象,type.getName()
拿到的就是类全名,如:com.lry.mybatis.MenuMapper
,那么这里的解析有可以理解为,当我们的mapper.xml是放在resources/mapper目录下的,那么这里的inputStream一定是null,因为它解析的路径是:resources/com/lry/mybatis/MenuMapper.xml
,这就是默认的解析路径,即使你在方法上使用@Select
配置了sql,在对应路径下也有xml配置,xml还是会被加载。
private void loadXmlResource() {
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 读取的路径是mapper接口对应的包名下的xml,也是默认读取路径。
// 如:com.lry.mybatis.dao.MenuMapper.xml
String xmlResource = type.getName().replace('.', '/') + ".xml";
InputStream inputStream = null;
try {
// 如果在resource下有对应包名的xml,就会加载到,inputStream就不会为null
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e) {
// ignore, resource is not required
}
if (inputStream != null) {
// xml 解析,如果是配置xml sql,不是注解sql就走parse
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
xmlParser.parse();
}
}
}
那么既配置类注解sql,也配置了xml sql,会执行哪一个?
答案是:报异常!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0c8UXKoT-1692525879066)(E:/BaiduNetdiskWorkspace/%E5%BE%85%E5%8F%91%E5%B8%83%E6%96%87%E7%AB%A0/Typora/typora/images/image-20230820114600932.png)]
解析xml后,进入这个方法解析注解sql,然后在assistant.addMappedStatement()
中注册方法时抛出异常;
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
// 省略...
//
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}
这有个问题,就是它都把xml解析了,注解也解析了,然后在注册sql声明的时候抛异常,感觉多此一举啊,既然让使用者指定了注解解析方式,自己还加一个默认解析,然后又不给注册上去,不理解设计者是怎样考虑的。
解析mapper.xml
这里有一部分和注解sql方式解析的方法loadXmlResource
里有部分是一样的:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
首先我们要知道,它怎么加载的mapper.xml
InputStream inputStream = Resources.getResourceAsStream(resource);
它和注解sql方式的不同,注解sql方式的因为没有指定mapper.xml的路径,所以以默认路径包名/类名.xml
为路径查找,所以是找不到的,但是这里是人为的指定了路径,如mapper/menuDao.xml
,那这是相对路径,但是这个相对路径它的默认基础路径(根目录)其实是resource
目录:
所以,在它通过XMLMapperBuilder
解析xml时,就已经将mapper.xml读取出来了,也就是下面的resource
属性:
public void parse() {
// 判断是否加载过
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper节点
configurationElement(parser.evalNode("/mapper"));
// 将已解析的xml文件添加记录
configuration.addLoadedResource(resource);
// 绑定命名空间
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
解析mapper节点
private void configurationElement(XNode context) {
try {
// 当前mapper.xml的命名空间,即对应的mapper类的类全名
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// builderAssistant就相当于当前读取的mapper节点的一个对象副本
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
// 这个很少写,参数映射
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 这个经常写,结果集映射
resultMapElements(context.evalNodes("/mapper/resultMap"));
// sql节点,通常会把一些通用的动态sql放到sql标签中,通过include标签静态引入
sqlElement(context.evalNodes("/mapper/sql"));
// 解析mapper方法
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
这个参数解析的不是很复杂,就解析了节点然后将解析后的结果放到parametermappings
中,这里有一个typeHandler
的东西,这个放后面讲,是个很有用的东西。
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
解析resultMap标签
resultMapNode.getStringAttribute
方法解析节点属性,默认值为null。该方法在解析时会被递归调用,所以它比较通用,其实标签属性也差不多。
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
// 异常上下文ErrorContext,用于记录mybatis的错误信息,然后打印
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 下面是解析resultmap属性,
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
String extend = resultMapNode.getStringAttribute("extends");
// 自动映射,当我们没有写resultMap这个标签映射字段时,mybatis会帮我们去做映射
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
Class<?> typeClass = resolveClass(type);
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
// 指定构造函数的标签节点
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
// 判别器,这个东西用法很有趣,但好像不怎么有用,我就没用到过
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
// 解析result、collection标签
List<ResultFlag> flags = new ArrayList<ResultFlag>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
// 解析子标签
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
上面的代码就是解析类似下面的xml:
<resultMap id="NodeDefStylePortMap" type="com.lry.mybatis.model.MenuBo" extends="MenuMap">
<collection property="menus" javaType="java.util.ArrayList" ofType="com.lry.mybatis.model.Menu">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
</collection>
<discriminator javaType="">
<case value=""></case>
</discriminator>
</resultMap>
然后是解析sql,其实这里也就是解析实例sql的标签属性,然后放到map里,没有其他解析。
private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
sqlFragments.put(id, context);
}
}
}
解析方法标签
之后是解析mapper方法对应的标签(select、update、insert、delete);
进入buildStatementFromContext
,往里走,就是下面方法,一连串的解析标签属性;
public void parseStatementNode() {
// 一连串的解析属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
// sql语句的类型;statementType 是声明类型,说明在下面
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// select 和其他语句不一样,所以这里会设定一个isSelect,分开解析
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 解析selectKey标签,并生成SelectKeyGenerator
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 解析sql
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// 这里判断有没有keyGenerator,如果是有selectKey标签,解析的时候会生成一个selectGenerator
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 没有selectKey标签,就解析useGeneratedKeys属性
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 添加映射语句
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
需要注意的几点:
- 上面有一个
statementType
这个是sql语句的类型,他有三个值:STATEMENT, PREPARED, CALLABLE;
STATEMENT:sql语句,字符串拼接,对应的是${}
PREPARED:预处理sql,preparedStatement 的默认类型,对应的是#{}
CALLABLE:存储过程,callableStatement
- insert返回id,有不同,mysql可以通过
useGeneratedKeys
可以返回自增id,也可以通过selectKey
标签,Oracle就只能通过selectKey
标签了,selectKey标签也和select标签一样,会添加到mapppedStatement里面(如下图);根据代码顺序,设置新增返回id的方式也有优先级,如果设置了selectKey
标签,那么设置的useGeneratedKeys
就会失效。
-
mapper的别名不区分大小写,在上面注册mapper时有提到,为了名称统一:
alias.toLowerCase(Locale.ENGLISH);
他会把mapper注册到一个别名库中。
解析sql是下面这句,它这里解析会将#{}
这部分替换成?
,然后在后面执行的时候通过这个sqlsource 做预编译处理。
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
if (isDynamic) {
// 动态sql解析
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 静态sql
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
这里它会区分是否是动态sql,即是否含有动态标签,如<if></if> 、<where></where> 、${}
这些,#{}
这个不是动态标签的标志;
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 这里进行区分动态sql
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
解析完的sql,会把变量替换成占位符的方式,比如:select * from menu where id = #{id} -> select * from menu where id = ?
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
在解析xml后,后面还有一句:bindMapperForNamespace();
这里的意思是字面意思,就是添加命名空间,之前解析的是xml,没有设计class,那么这里就是把class读出来,然后再解析一些mapper class,因为,Mybatis 支持xml 也支持注解sql,所以,在注册mapper时,都是需要解析mapper的。
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
// 命名空间为空,则跳过
if (namespace != null) {
Class<?> boundType = null;
try {
// 加载mapper class
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!configuration.hasMapper(boundType)) {
// 记录加载的mapper
configuration.addLoadedResource("namespace:" + namespace);
// 注册mapper class
configuration.addMapper(boundType);
}
}
}
}
然后addMapper
就和注解sql的解析方式是一样的方法
最后后面还有3句parse的代码,这个是在做补足解析,意思就是在正常解析过程中,失败了,在这里还会做一次,也就是,重试一次;
private void parsePendingResultMaps() {
Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
synchronized (incompleteResultMaps) {
Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
while (iter.hasNext()) {
try {
iter.next().resolve();
iter.remove();
} catch (IncompleteElementException e) {
// ResultMap is still missing a resource...
}
}
}
}
mybatis执行流程
回忆一下,在上面初始化过程中,Mybatis注册了mapper:key -> mapper class , value -> mapper proxy factory(mapper代理工厂),所以底层应该是获取这个factory,然后通过factory进行创建代理类;
我们从getMapper方法进行跟踪
MenuMapper ndao = getSqlSessionFactory().getMapper(MenuMapper.class);
1. 委托configuration获取mapper
注意,上面提过保存的是mapper class和factory,所以这个this (SQLSession)是做什么的,思考下?
return configuration.<T>getMapper(type, this);
2. configuration通过mapperRegistry(type,sqlSession)获取mapper
mapperRegistry.getMapper(type, sqlSession);
3. MapperRegistry.getMapper(type, sqlSession)
位置:org.apache.ibatis.binding.MapperRegistry#getMapper
这里也就是那个mapper注册类的缓存获取的逻辑;
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 拿到ProxyFactory,这个是在初始化时创建添加的
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
// 通过Mapper proxy factory(mapper代理工厂),利用传入SQLSession创建代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
这里,mapper代理工厂,通过JDK动态代理,利用mapper接口,创建了一个代理对象,而代理的对象是MapperProxy
,那也就是说,mapper接口执行的方法逻辑,是由MapperProxy
完成的。
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// jdk动态代理,接口就是Mapper接口
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
MapperProxy里的invoke方法(执行mapper方法的逻辑):
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 如果是默认的object的方法就直接执行,不去重写
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
// 如果是默认方法(接口可以定义default方法),就绑定到proxy对象上
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 这里生成一个method方法对象,这一步会解析方法的参数、类型、返回类型、方法执行的类型等;
// 生成的这些mapper方法对象,都会进行缓存
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 这里才是真正执行sql方法的方法
return mapperMethod.execute(sqlSession, args);
}
4. mapperMethod构造(mapper参数解析)
这里cachedMapperMethod(method);
这里将method封装为mybatis需要的对象,里面还有一个比较重要的东西,就是它new的MethodSignature
,里面它有一属性paramNameResolver
,通过它解析参数:
this.paramNameResolver = new ParamNameResolver(configuration, method);
方法参数有3中情况:
- @Param指定参数名
- 使用实际参数名:默认是使用实际参数名的,可以通过配置useActualParamName
- 以参数序号为参数名
三种情况下,是顺序判断的,只要其中有一种情况符合就行,第三种情况其实就是补漏,在所有情况都不行的情况下的一个替补;
public ParamNameResolver(Configuration config, Method method) {
// 获取参数类型
final Class<?>[] paramTypes = method.getParameterTypes();
// 参数注解
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
// 有序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++) {
// 这里就是出现特殊的参数,就直接退出了,就不会保存这类型的参数
// 特殊参数是指:参数是否是RowBounds ResultHandler 类的子类或接口,百度比较详细
if (isSpecialParameter(paramTypes[paramIndex])) {
continue;
}
String name = null;
// 第一种:@Param指定参数名
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
// 拿到@Param注解,并将@Param的value值给
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
// 第二种:使用实际参数名
// 默认是使用实际参数名的,可以通过配置useActualParamName
//
if (name == null) {
// 没有@Param,就取实际名称,这里的isUseActualParamName默认true
if (config.isUseActualParamName()) {
name = getActualParamName(method, paramIndex);
}
// 第三种:以参数序号为参数名
if (name == null) {
// 如果实际名称也取不到,就用map的大小,一般也不会出现这样的情况
name = String.valueOf(map.size());
}
}
// 这里用索引为key,后面的取值都是按参数顺序遍历的,所以这里以索引为键,然后再放到SortedMap中
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
5. 实际执行通过SQLSession执行对应方法
从execute
方法就可以看出,实际执行时,还是通过sqlSession
来做的
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;
}
6. 执行前解析方法参数
增删改查的方法执行前都会执行参数的解析:
解析参数的代码如下:
这里的names
就是上面解析参数的names
,也就是说,names
的值是:
- 如果有
@Param
那么,names就是注解的value; - 如果没有
@Param
,那么就是实际参数名; - 如果既没有
@Param
也没有实际参数名,那么就是序号(1,2,3…)
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()) {
// 这里将names里存的参数名,放入map中
// 1. 如果有`@Param`那么,names就是注解的value;
// 2. 如果没有`@Param`,那么就是实际参数名;
// 3. 如果既没有`@Param`也没有实际参数名,那么就是序号(1,2,3...)
param.put(entry.getValue(), args[entry.getKey()]);
// 这里是按照参数顺序,设置固定的参数名,param1开始递增:(param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// 这里是防止重复添加
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
所以这里的参数返回就是:
-
null,没有参数;
-
一个参数,返回参数值;
-
map对象,key -> @Param的值,或者是实际参数名,也或者是参数序号,value -> 参数对象,同时还要对应每一个参数再多添加
param
的参数,格式是(param1, param2,...)
;比如:// mapper方法定义如下; List<Menu> queryAllByLimit(@Param("o11") int offset, @Param("o22") int limit); // 参数 queryAllByLimit(1, 10); // 那么上面这段代码里的names就是[o11,o22] // 最终返回的map就是: [o11, 1] [o22, 10] [param1, 1] [param2, 10]
7. SQLSession里的执行又丢给executor
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 拿到对应的mapper方法声明
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
在这里它会对参数进行一个封装,先判断这个object(参数对象),上一步它对参数进行了解析,会返回3种值,只有第二种会出现符合这里的collection
,array
。
那么返回只有一个对象的,这里会再封装一遍,是list的话,就在包一层map,key为“list”,是array的话,key为“array”,最后结果就很统一了,参数集合都是一个map。
private Object wrapCollection(final Object object) {
if (object instanceof Collection) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
return map;
} else if (object != null && object.getClass().isArray()) {
StrictMap<Object> map = new StrictMap<Object>();
map.put("array", object);
return map;
}
return object;
}
8. 构造sql对象(解析动态sql)
这里 BoundSql boundSql = ms.getBoundSql(parameterObject);
会去拿绑定的sql对象。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 拿绑定的sql对象
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建缓存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果是静态sql,那么在加载mapper配置时就加载到configuration里了,而动态sql,它还会进行解析,${}
替换为变量值,如果sql是这样的:select * from menu where name = ? and id = ${id}
,那么这一步就会被转为成:select * from menu where name = ? and id = 1;
,之后在调用sqlSourceParser.parse()
处理,将sql包装成静态sql对象。
注意点:将#{}
变量替换为?
这个操作在初始化时已经完成了,它属于静态sql。
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 在这里会通过TextSqlNode 和 StaticTextSqlNode 进行处理sql语句,
// TextSqlNode是动态sql处理对象,它的处理方法和处理 #{} 一样,只是这个替换为了变量值
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 然后在通过sqlSourceParser 再处理一遍,那么放回静态sql对象
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
9. 先查询二级缓存
位置:org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
他在查询是会尝试从二级缓存中获取,如果没有就会直接查询;可以看到使用缓存和不使用缓存都调用了一个query的重载方法,使用缓存的方式比直接查询多了一些获取缓存的步骤。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
// 刷新缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 直接取缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 缓存是空的才会查表
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 没有使用缓存直接走这里
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
10. 尝试获取一级缓存
一级缓存得到的结果为空,才会调用queryFromDatabase()
方法;这里的queryStack
类似一个计数,因为可能会连续执行多个sql。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 尝试获取缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 这个方法针对的是statementType = callable的sql,就是执行存储过程语句的,再处理参数缓存
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
当一级缓存没有时执行这个方法;
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 一级缓存保存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
// 当sql语句的类型为CALLABLE时,就是存储过程语句执行
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
11. statementHandler
执行底层方法
doQuery
方法进去后,通过configuration
对象获取statementHandler
,通过它去执行连接数据库查询结果映射等操作。
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 根据不同的声明类型(3种:sql,预处理sql,存储过程 )创建声明处理器
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 预编译
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
12. 设置Statement参数和typeHandler
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
// 参数化,在这里进行参数的设置
handler.parameterize(stmt);
return stmt;
}
往handler.parameterize(stmt)
里面走就是setParameters
方法,是下面的方法,它有一个比较有用的地方是typeHandle
这个对象。
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
// 拿到xml里的参数映射对象
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 获取参数的类型处理器,
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
// 通过类型处理器转换参数值。如果是自定义的类型处理器,走的就是我们自定义的方法
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
typeHandler运用例子
这边我写了个例子:一个menu
对象,属性deleted
定义为枚举,然后再定义一个handle类来处理我们的这个属性,修改mapper.xml支持改属性。
我这里的例子不是很严谨,应该定义一个接口的,但这是个例子,就简单一点。
public class Menu implements Serializable {
private static final long serialVersionUID = 499536196155109971L;
private Long id;
private String name;
private DeleteEnum deleted;
// 省略setter getter
}
定义DeleteEnum
public enum DeleteEnum {
DELETED(1, "已删除"),
UN_DELETED(0, "未删除");
private Integer id;
private String desc;
// 省略构造器 和 setter getter
}
定义类型处理器
public class DeleteTypeHandle<E extends DeleteEnum> implements TypeHandler<E> {
private Class<E> tClass;
private E[] enums;
public DeleteTypeHandle(Class<E> tClass){
if (tClass == null) {
throw new RuntimeException("类型不能为空");
}
this.tClass = tClass;
this.enums = tClass.getEnumConstants();
if (this.enums == null) {
throw new RuntimeException("类型不能为空");
}
}
/**
在执行query时设置参数时执行这个方法
*/
@Override
public void setParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getId());
}
/**
结果映射时执行这个
*/
@Override
public E getResult(ResultSet rs, String columnName) throws SQLException {
if (!rs.wasNull()) {
return transform(rs.getInt(columnName));
}
return null;
}
/**
结果映射时执行这个
*/
@Override
public E getResult(ResultSet rs, int columnIndex) throws SQLException {
if (!rs.wasNull()) {
return transform(rs.getInt(columnIndex));
}
return null;
}
/**
结果映射时执行这个
*/
@Override
public E getResult(CallableStatement cs, int columnIndex) throws SQLException {
if (!cs.wasNull()) {
return transform(cs.getInt(columnIndex));
}
return null;
}
/**
遍历我们的枚举,找到对应的枚举对象
*/
private E transform(Integer id) {
for (E e : enums) {
if (e.getId().equals(id)) {
return e;
}
}
throw new RuntimeException("未知枚举类型");
}
}
mapper.xml里的查询语句修改:
注意这里需要写javaType、typeHandler
两个属性
<select id="querySelective2" resultMap="MenuMap">
select <include refid="base_column_list"></include>
from menu
where name = #{param1,jdbcType=VARCHAR}
and deleted = #{deleted, javaType=integer,javaType=com.lry.mybatis.config.DeleteEnum, typeHandler=com.lry.mybatis.config.DeleteTypeHandle}
</select>
结果映射修改:
<resultMap type="com.lry.mybatis.model.Menu" id="MenuMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="deleted" column="deleted" jdbcType="INTEGER" javaType="com.lry.mybatis.config.DeleteEnum" typeHandler="com.lry.mybatis.config.DeleteTypeHandle"/>
</resultMap>
13. 结果映射
位置:org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets
回到doQuery
方法中,通过StatementHandler执行查询方法
handler.<E>query(stmt, resultHandler)
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
// 包装查询项结果
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 从mapper声明中获取到结果映射,这个在初始化时就放入的了。
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
// 数据绑定在这里做
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
这里的包装,将一些相关的东西赋值到属性中,像类型处理器、结果集、查询列和列类型。
这里的columnNames
存放了sql查询出的列名,jdbcTypes 存放了对应的列类型。
public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
super();
this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.resultSet = rs;
final ResultSetMetaData metaData = rs.getMetaData();
final int columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
classNames.add(metaData.getColumnClassName(i));
}
}
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
// 这里是resultMap有嵌套是才会进入
if (parentMapping != null) {
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
// 处理查询结果
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
// 处理结果放到返回结果集合中
multipleResults.add(defaultResultHandler.getResultList());
} else {
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
handleRowValues往里走是下面这段;对读取的行数据,进行赋值,并存储到集合里。
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
skipRows(rsw.getResultSet(), rowBounds);
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
// 这里创建结果行对象并设置值
Object rowValue = getRowValue(rsw, discriminatedResultMap);
// 存储对象
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
// 自动映射设置,配置了resultType,在这里映射
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
// resultMap在这里映射
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
分别看看applyAutomaticMappings
和applyPropertyMappings
里是什么。
applyAutomaticMappings
这个方法没有设置自动映射都会走,所以resultMap和resultType的都走了这个方法。但是如果是设置了resultMap,不会走那个for循环,所以这个方法更像是为了resultType类型而做方法。
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
// 获取未映射的列;resultType会获取到实体对应的列,而如果是resultMap,这里是空的,这个列的名称也起的比较清楚“未自定映射的列”
List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
boolean foundValues = false;
if (!autoMapping.isEmpty()) {
for (UnMappedColumnAutoMapping mapping : autoMapping) {
final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
// gcode issue #377, call setter on nulls (value is not 'found')
metaObject.setValue(mapping.property, value);
}
}
}
return foundValues;
}
private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
final String mapKey = resultMap.getId() + ":" + columnPrefix;
// 到自动映射里拿
List<UnMappedColumnAutoMapping> autoMapping = autoMappingsCache.get(mapKey);
if (autoMapping == null) {
autoMapping = new ArrayList<UnMappedColumnAutoMapping>();
// 没有设置自动映射,就在这里获取sql查询出来的列,这个列的来源是: ResultSetWrapper rsw = getFirstResultSet(stmt);
final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
for (String columnName : unmappedColumnNames) {
String propertyName = columnName;
// ...
}
autoMappingsCache.put(mapKey, autoMapping);
}
return autoMapping;
}
applyPropertyMappings
这个方法是设置里resultMap类型的方法走的。这个方法比resultType的直接一点,拿到属性名和值,直接设置。
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
// 获取映射列
final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
if (propertyMapping.getNestedResultMapId() != null) {
// the user added a column attribute to a nested result map, ignore it
column = null;
}
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
// issue #541 make property optional
final String property = propertyMapping.getProperty();
if (property == null) {
continue;
} else if (value == DEFERED) {
foundValues = true;
continue;
}
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
// gcode issue #377, call setter on nulls (value is not 'found')
metaObject.setValue(property, value);
}
}
}
return foundValues;
}
缓存
流程里面说过,他会先查询二级缓存,然后在查询一级缓存,那么这些缓存具体是什么,怎么使用呢?
一级缓存:
本地缓存。它是SQLSession级别的缓存,不需要任何配置,默认开启。
缓存对象是BaseExecutor
对象的属性PerpetualCache
,在查询是会将结果放到缓存中,一次SQLSession的创建表示一个会话,只要会话不关闭,那么这个缓存就是生效的。
有几种情况会使缓存失效:
-
上面说过的,只要SQLSession不断,缓存就存在,所以调用了
close()
方法会使缓存失效; -
调用了SQLSession的方法
clearCache()
方法,会清空缓存; -
执行修改操作如update、delete、insert,会清空缓存;
对应的存储源码,在上面已经有了,这里就截个图
有一点,下面的查询会看到commit
,但是对于一级缓存来说,不用commit也可以进行缓存。
不同的SQLSession,打印两次sql
如下结果,两次查询,只有一次sql打印
调用clearCache()
方法后再查询,打印了两次sql
调用异常update,打印了两次查询sql
调用一次insert,打印了两次查询sql
调用一次delete,打印了两次查询sql
二级缓存:
与一级缓存不同,它作用的范围更广,它属于namespace级别的缓存,只要是相同的方法都可以共享。
该功能需要手动配置生效,配置方法:
-
在mybatis.xml(配置文件)中配置,默认是开启的(value = true),所以这一步可以跳过
<setting name="cacheEnabled" value="true" />
-
在mapper.xml中配置,针对单个mapper生效
说明:
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
eviction:清除策略
LRU:最近最少使用:移除最长时间不被使用的对象;
FIFO:先进先出:按对象进入缓存的顺序来移除它们
SOFT:软引用:基于垃圾回收器状态和软引用规则移除对象
WEAK:弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象
flushInterval:(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新;
readOnly:属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false;
size:可存储的结果引用个数
-
某个方法不需缓存,可以设置
useCache="false"
执行器executor有两个子类,BaseExecutor 和 CachingExecutor,在配置了二级缓存后,走的就是CachingExecutor 这个类的方法。
经过测试后,事务不提交,二级缓存也还存在;
执行insert、delete、update时,缓存也会被刷新;
插件
配置方式
-
实现
Interceptor
接口@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})) public class CustomInterceptor implements Interceptor { /** * 业务逻辑 * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { // Invocation对象进行了封装,自由度更高 System.out.println("自定义。。。"); Object proceed = invocation.proceed(); System.out.println("自定义结束。"); return proceed; } /** * 添加到连接器链 * @param target * @return */ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
-
在mybatis配置文件中添加插件配置
<plugins>
<plugin interceptor="com.lry.mybatis.config.CustomInterceptor">
</plugin>
-
SpringBoot配置方式,还有配置文件的方式,就不提了。
@Configuration @ConditionalOnBean(SqlSessionFactory.class) @AutoConfigureAfter(MybatisAutoConfiguration.class) public class MybatisConfig { @Autowired private List<SqlSessionFactory> sqlSessionFactoryList; @PostConstruct public void addPageInterceptor() { CustomInterceptor interceptor = new CustomInterceptor(); for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) { sqlSessionFactory.getConfiguration().addInterceptor(interceptor); } } }
官网还是比较详细的:https://mybatis.org/mybatis-3/zh/configuration.html#plugins
mybatis提供Interceptor
接口,只要实现并添加到拦截链中,就可以进行拦截,它可以拦截下面几个类:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
注解@Signature
里的值也是他们的参数类型,作为方法签名。
它的method
可以参考他们的接口方法,注意insert方法底层在executor里是update。
插件原理
而对于这个拦截器一开始看有些复杂,它的底层还是动态代理,捋一捋,mybatis提供了一个插件接口Interceptor
,方法对象Invocation
,拦截链InterceptorChain
,还有一个Plugin
的插件工具类相当于。
它的结构都是高度封装的比较复杂些,刚开始都把我绕进去了,我把它简化如下:
public interface IPlayGame {
void play();
}
public interface IPluginBusiness {
/**
* 业务实现
* @param target
* @param method
* @param args
*/
Object intercept(Object target, Method method, Object[] args);
}
public class PlayGame implements IPlayGame{
@Override
public void play() {
System.out.println("打游戏");
}
}
public class PlayGameHandler implements InvocationHandler {
private Object target;
private IPluginBusiness pluginBusiness;
public PlayGameHandler(Object target, IPluginBusiness pluginBusiness) {
this.target = target;
this.pluginBusiness = pluginBusiness;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return pluginBusiness.intercept(target, method, args);
}
}
public class PluginBusinessImpl implements IPluginBusiness {
@Override
public Object intercept(Object target, Method method, Object[] args) {
System.out.println("插件[1]开始。。。");
Object invoke = null;
try {
invoke = method.invoke(target, args);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
System.out.println("插件[1]结束。");
return invoke;
}
}
public class PluginChain {
private List<IPluginBusiness> pluginBusinesInterfaces = new ArrayList<>();
public void addPlugin(IPluginBusiness pluginBusinesInterface) {
pluginBusinesInterfaces.add(pluginBusinesInterface);
}
public List<IPluginBusiness> getPluginBusinesInterfaces() {
return pluginBusinesInterfaces;
}
}
public class PluginsIncationHandler implements InvocationHandler {
private Object target;
public PluginsIncationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("插件的动态代理。。。");
return method.invoke(target, args);
}
}
public static void main() {
// 1. 构建插件业务对象
PluginBusinessImpl plugin = new PluginBusinessImpl();
// 2. 将插件业务对象封装成代理对象
PluginsIncationHandler pluginHandler = new PluginsIncationHandler(plugin);
IPluginBusiness proxy = (IPluginBusiness)Proxy.newProxyInstance(plugin.getClass().getClassLoader(), new Class[]{IPluginBusiness.class}, pluginHandler);
// 3. 将代理对象放到插件链
PluginChain chain = new PluginChain();
chain.addPlugin(proxy);
// 1. 构建插件业务对象
PluginBusinessImpl2 plugin2 = new PluginBusinessImpl2();
// 2. 将插件业务对象封装成代理对象
PluginsIncationHandler pluginHandler2 = new PluginsIncationHandler(plugin2);
IPluginBusiness proxy2 = (IPluginBusiness)Proxy.newProxyInstance(plugin.getClass().getClassLoader(), new Class[]{IPluginBusiness.class}, pluginHandler2);
// 3. 将代理对象放到插件链
chain.addPlugin(proxy2);
// 4. 将所有的插件都封装到待执行的对象上
IPlayGame playGame = new PlayGame();
for (IPluginBusiness pluginBusiness : chain.getPluginBusinesInterfaces()) {
PlayGameHandler playGameHandler = new PlayGameHandler(playGame, pluginBusiness);
playGame = (IPlayGame)Proxy.newProxyInstance(PlayGame.class.getClassLoader(), new Class[]{IPlayGame.class}, playGameHandler);
}
playGame.play();
}
运行结果如上所示,我们可以随意的插入我们的插件逻辑,从main方法来看它的步骤就很清晰:
- 插件接口提供一个插件业务的接口,参数为
target, method, args
, - 实现插件,并生成代理对象
- 将插件代理对象放到插件链中
- 实现业务对象,
- 生成业务对象代理,插件对象作为参数,将invoke方法替换为插件的方法
mybatis的优化思路可能是这样的:
- 插件的功能独立,只要加入插件就能实现插件前后处理的效果,需要代理实际业务对象,但不能产生依赖,经理少的去关联业务对象的东西,那么就可以把
target, method, args
传入(intercept方法); - 同时,按功能划分,插件的功能可以提供一个实现对实际业务对象进行代理封装(warp方法);
- 作为管理插件的插件链类,必须有添加插件功能,可以额外的增加使插件生效的功能(pluginAll方法);
遵循了开发六原则,对功能划分细化,使得代码更加健壮。