MyBatis3源码深度解析(十五)SqlSession的创建与执行(二)Mapper接口和XML配置文件的注册与获取

本文主要是介绍MyBatis3源码深度解析(十五)SqlSession的创建与执行(二)Mapper接口和XML配置文件的注册与获取,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 前言
    • 5.5 Mapper接口与XML配置文件的注册过程
      • 5.5.1 Mapper接口的注册过程
      • 5.5.2 XML配置文件的注册过程
    • 5.6 MappedStatement对象的注册过程
    • 5.7 Mapper接口的动态代理对象的获取

前言

SqlSession对象创建后,接下来是执行Mapper。执行Mapper的过程可以拆解为三步:注册Mapper接口与XML配置文件;注册MappedStatement对象;调用Mapper方法。

5.5 Mapper接口与XML配置文件的注册过程

Mapper接口用于定义执行SQL语句相关的方法,方法名一般和Mapper XML配置文件中的<select|update|insert|delete>标签的id属性相同,Mapper接口的完全限定名一般和Mapper XML配置文件的命名空间相同。例如:

public interface UserMapper {List<User> selectAll();@Select("select * from user where id = #{id, jdbcType=INTEGER}")User selectById(@Param("id") Integer id);}
<!--UserMapper.xml-->
<mapper namespace="com.star.mybatis.mapper.UserMapper"><select id="selectAll" resultType="User">select * from user</select>
</mapper>

如上面的代码所示,UserMapper接口的selectAll()方法名与UserMapper.xml中的<select>标签的id属性相同;UserMapper接口的完全限定名与<mapper>标签的namespace属性相同。

如果要将Mapper接口或Mapper XML配置文件注册到Configuration组件中,需要在主配置文件mybatis-config.xml中配置<mappers>标签。

<!--mybatis-config.xml-->
<mappers><!--方式一:通过指定XML文件的类路径来注册--><mapper resource="mapper/UserMapper.xml"/><!--方式二:通过指定XML文件的完全限定资源定位符来注册--><mapper url="file:///C:\workspace\mybatis_demo2\src\main\resources\mapper\UserMapper.xml"/><!--方式三:通过Mapper接口的类路径来注册--><mapper class="com.star.mybatis.mapper.UserMapper"/><!--方式四:通过Mapper接口所在包路径类注册--><package name="com.star.mybatis.mapper"/>
</mappers>

如上面的代码所示,<mappers>标签支持四种配置方式,其中方式一和方式二指定XML配置文件的相对和绝对路径,方式三和方式四指定Mapper接口的类路径和所在包路径。实际开发中,选择其一即可。

在【MyBatis3源码深度解析(十四)Configuration与SqlSession的创建过程 5.2 Configuration实例创建过程】中已经研究过,在mappersElement()方法中,会对<mappers>标签的四种配置方式分别进行解析。

源码1org.apache.ibatis.builder.xml.XMLConfigBuilderprivate void mappersElement(XNode context) throws Exception {if (context == null) {return;}for (XNode child : context.getChildren()) {if ("package".equals(child.getName())) {// 方式四的处理String mapperPackage = child.getStringAttribute("name");configuration.addMappers(mapperPackage);} else {String resource = child.getStringAttribute("resource");String url = child.getStringAttribute("url");String mapperClass = child.getStringAttribute("class");if (resource != null && url == null && mapperClass == null) {// 方式一的处理 ...ErrorContext.instance().resource(resource);try (InputStream inputStream = Resources.getResourceAsStream(resource)) {XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,configuration.getSqlFragments());mapperParser.parse();}} else if (resource == null && url != null && mapperClass == null) {// 方式二的处理 ...ErrorContext.instance().resource(url);try (InputStream inputStream = Resources.getUrlAsStream(url)) {XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url,configuration.getSqlFragments());mapperParser.parse();}} else if (resource == null && url == null && mapperClass != null) {// 方式三的处理 ...Class<?> mapperInterface = Resources.classForName(mapperClass);configuration.addMapper(mapperInterface);} else {// throw ...}}
}

5.5.1 Mapper接口的注册过程

由 源码1 可知,如果是方式三或方式四这两种配置Mapper接口的方式,则会调用Configuration对象的addMappers()方法。

源码2org.apache.ibatis.session.Configurationprotected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 用于处理指定包路径下的所有Mapper接口的注册
public void addMappers(String packageName) {mapperRegistry.addMappers(packageName);
}
// 用于处理指定的Mapper接口的注册
public <T> void addMapper(Class<T> type) {mapperRegistry.addMapper(type);
}

由 源码2 可知,Configuration组件中组合了一个MapperRegistry对象,其addMappers(String)方法用于处理指定包路径下的所有Mapper接口的注册,其addMapper(Class<T>)方法用于处理指定的Mapper接口的注册。

先来看addMapper(Class<T>)方法:

源码3org.apache.ibatis.binding.MapperRegistryprivate final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();
public <T> void addMapper(Class<T> type) {// 判断是否是Mapper接口if (type.isInterface()) {// 判断是否已经注册过if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {// 注册Mapper接口到knownMappers属性中knownMappers.put(type, new MapperProxyFactory<>(type));MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);parser.parse();loadCompleted = true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}}
}

由 源码3 可知,MapperRegistry类中有一个knownMappers属性,用于注册Mapper接口对应的Class对象和MapperProxyFactory对象之间的关系。addMapper(Class<T>)方法经过type.isInterface()hasMapper(type)两个前置检查之后,然后为Mapper接口对应的Class对象创建一个MapperProxyFactory对象,并添加到knownMappers属性中。

再来看addMappers(String)方法:

源码4org.apache.ibatis.binding.MapperRegistrypublic void addMappers(String packageName, Class<?> superType) {// 借助ResolverUtil工具类获取指定包路径下的全部Mapper接口对应的Class对象ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();resolverUtil.find(new ResolverUtil.IsA(superType), packageName);Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();// 遍历Class对象集合,分别调用addMapper方法注册到MapperRegistry类的knownMappers属性中for (Class<?> mapperClass : mapperSet) {addMapper(mapperClass);}
}

由 源码4 可知,addMappers(String)方法借助ResolverUtil工具类获取指定包路径下的全部Mapper接口对应的Class对象,然后遍历Class对象集合,分别调用addMapper(Class<T>)方法将Class对象注册到MapperRegistry类的knownMappers属性中。

再深入resolverUtil.find()方法的源码可以发现,ResolverUtil是使用VFS工具扫描指定包路径下所有Class文件,然后再交给addMapper(Class<T>)方法进行筛选。

至此,Mapper接口的注册完毕,MapperRegistry类的knownMappers属性中保存了Mapper接口对应的Class对象与MapperProxyFactory对象之间的映射关系。

5.5.2 XML配置文件的注册过程

回到 源码1 可知,如果是方式一或方式二这两种配置Mapper XML配置文件的方式,则会先获取Mapper XML配置文件的输入流,再调用XMLMapperBuilder类的parse()方法来完成注册。

源码5org.apache.ibatis.builder.xml.XMLMapperBuilderprivate final XPathParser parser;
public void parse() {if (!configuration.isResourceLoaded(resource)) {// 调用XPathParser的evalNode方法获取mapper根节点对应的XNode对象// 调用configurationElement方法进一步解析configurationElement(parser.evalNode("/mapper"));// 将资源路径添加到Configuration对象中configuration.addLoadedResource(resource);bindMapperForNamespace();}// ......
}

由 源码5 可知,parse()方法首先调用XPathParser的evalNode()方法获取<mapper>根节点对应的XNode对象,然后调用configurationElement()方法进一步解析XNode对象。

源码6org.apache.ibatis.builder.xml.XMLMapperBuilderprivate void configurationElement(XNode context) {try {// 获取并设置命名空间String namespace = context.getStringAttribute("namespace");if (namespace == null || namespace.isEmpty()) {throw new BuilderException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);// 解析<cache-ref>标签cacheRefElement(context.evalNode("cache-ref"));// 解析<cache>标签cacheElement(context.evalNode("cache"));// 解析<parameterMap>标签parameterMapElement(context.evalNodes("/mapper/parameterMap"));// 解析<resultMap>标签resultMapElements(context.evalNodes("/mapper/resultMap"));// 解析<sql>标签sqlElement(context.evalNodes("/mapper/sql"));// 解析<select|insert|update|delete>标签buildStatementFromContext(context.evalNodes("select|insert|update|delete"));} // catch ......
}

由 源码6 可知,configurationElement()方法会对Mapper XML配置文件的所有标签进行解析,并封装成一个MappedStatement对象。

5.6 MappedStatement对象的注册过程

重点关注<select|insert|update|delete>标签的解析。由 源码6 可知,获取<select|insert|update|delete>标签节点对应的XNode对象后,调用buildStatementFromContext()方法做进一步的解析。

源码7org.apache.ibatis.builder.xml.XMLMapperBuilderprivate void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {for (XNode context : list) {// 创建XMLStatementBuilder对象final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context,requiredDatabaseId);try {// 真正解析标签statementParser.parseStatementNode();} catch (IncompleteElementException e) {configuration.addIncompleteStatement(statementParser);}}
}

由 源码7 可知,buildStatementFromContext()方法会对所有的XNode对象进行遍历,为每一个XNode对象创建一个XMLStatementBuilder对象。然后调用该对象的parseStatementNode()方法真正进行解析标签。

源码8org.apache.ibatis.builder.xml.XMLStatementBuilderpublic void parseStatementNode() {// 获取id、databaseId属性并进行前置判断String id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}// 根据标签名字得到SQL语句的类型String nodeName = context.getNode().getNodeName();SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));// 获取flushCache属性,如果是SELECT语句,则默认为falseboolean isSelect = sqlCommandType == SqlCommandType.SELECT;boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);// 获取useCache属性,如果是SELECT语句,则默认为trueboolean useCache = context.getBooleanAttribute("useCache", isSelect);// 获取resultOrdered属性,默认为falseboolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);// 将<include>标签内容替换为<sql>标签定义的SQL片段XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());// 获取parameterType属性,并转换成对应的Class对象String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);// 获取lang属性,并转换成对应的LanguageDriver对象String lang = context.getStringAttribute("lang");LanguageDriver langDriver = getLanguageDriver(lang);// 解析<selectKey>标签processSelectKeyNodes(id, parameterTypeClass, langDriver);// 获取主键生成策略KeyGenerator keyGenerator;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);if (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}// 通过LanguageDriver解析SQL内容,生成SqlSource对象SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);// 获取statementType属性,默认为PREPARED类型StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));// 获取fetchSize、timeout、parameterMap属性Integer fetchSize = context.getIntAttribute("fetchSize");Integer timeout = context.getIntAttribute("timeout");String parameterMap = context.getStringAttribute("parameterMap");// 获取resultType属性,并转换为对应的Class对象String resultType = context.getStringAttribute("resultType");Class<?> resultTypeClass = resolveClass(resultType);String resultMap = context.getStringAttribute("resultMap");if (resultTypeClass == null && resultMap == null) {resultTypeClass = MapperAnnotationBuilder.getMethodReturnType(builderAssistant.getCurrentNamespace(), id);}// 获取resultSetType属性,并转换为对应的ResultSetType对象String resultSetType = context.getStringAttribute("resultSetType");ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);if (resultSetTypeEnum == null) {resultSetTypeEnum = configuration.getDefaultResultSetType();}// 获取keyProperty、keyColumn、resultSets、affectData属性String keyProperty = context.getStringAttribute("keyProperty");String keyColumn = context.getStringAttribute("keyColumn");String resultSets = context.getStringAttribute("resultSets");boolean dirtySelect = context.getBooleanAttribute("affectData", Boolean.FALSE);// 使用上面获取的所有属性构造一个MappedStatement对象builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}

如 源码8 所示,parseStatementNode()方法篇幅很长,但逻辑很清晰,主要做了以下几件事情:

(1)获取<select|insert|update|delete>标签的所有属性信息。
(2)将<include>标签引用的SQL片段替换为对应的<sql>标签中定义的内容。
(3)获取lang属性指定的LanguageDriver对象,通过该对象创建代表SQL资源的SqlSource对象。
(4)获取KeyGenerator对象。KeyGenerator的不同实例代表不同的主键生成策略。
(5)所有解析工作完成后,使用MapperBuilderAssistant对象的addMappedStatement()方法创建一个MappedStatement对象。

源码9org.apache.ibatis.builder.MapperBuilderAssistantpublic MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,LanguageDriver lang, String resultSets, boolean dirtySelect) {if (unresolvedCacheRef) {throw new IncompleteElementException("Cache-ref not yet resolved");}id = applyCurrentNamespace(id, false);// 创建MappedStatement.Builder对象MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType).resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType).keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang).resultOrdered(resultOrdered).resultSets(resultSets).resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType).flushCacheRequired(flushCache).useCache(useCache).cache(currentCache).dirtySelect(dirtySelect);ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);if (statementParameterMap != null) {statementBuilder.parameterMap(statementParameterMap);}// 创建MappedStatement对象MappedStatement statement = statementBuilder.build();// 注册到Configuration组件中configuration.addMappedStatement(statement);return statement;
}

由 源码9 可知,addMappedStatement()方法会通过MappedStatement.Builder对象的build()方法创建一个MappedStatement对象,最后注册到Configuration组件中。

至此,Mapper接口和Mapper XML配置文件的解析全部完成,保存这些信息的位置是Configuration组件中的mappedStatements属性和mapperRegistry属性。

借助Debug,可以查看到这两个属性保存的信息:

5.7 Mapper接口的动态代理对象的获取

Mapper接口和Mapper XML配置文件解析注册完成后,接下来是执行Mapper接口中定义的方法。

为了执行Mapper接口中定义的方法,首先需要调用SqlSession对象的getMapper()方法获取一个Mapper接口的动态代理对象,然后通过代理对象调用Mapper方法。

代码如下:

SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.selectAll();

SqlSession对象的getMapper()方法的源码如下:

源码10org.apache.ibatis.session.defaults.DefaultSqlSession@Override
public <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);
}
源码11org.apache.ibatis.session.Configurationpublic <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);
}
源码12org.apache.ibatis.binding.MapperRegistryprivate final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();
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);}
}

由 源码10-12 可知,SqlSession对象的getMapper()方法最终会从MapperRegistry对象的knownMappers属性中,根据传入的Class对象取出一个MapperProxyFactory对象,并调用其newInstance()方法创建一个实例并返回。

由类名可知,MapperProxyFactory是一个代理工厂类,其newInstance()方法会创建一个代理对象实例。

源码13org.apache.ibatis.binding.MapperProxyFactorypublic T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);
}protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

由 源码13 可知,MapperProxyFactory对象的newInstance()方法会创建一个MapperProxy对象,并调用Proxy类的newProxyInstance()方法创建一个代理对象实例。

源码14org.apache.ibatis.binding.MapperProxypublic class MapperProxy<T> implements InvocationHandler, Serializable {public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> 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);}return cachedInvoker(method).invoke(proxy, method, args, sqlSession);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}
}

由 源码14 可知,MapperProxy类实现了InvocationHandler接口,使用的是JDK内置的动态代理

通过以上分析可以知道,SqlSession对象的getMapper()方法返回的是一个MapperProxy动态代理对象。


本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

这篇关于MyBatis3源码深度解析(十五)SqlSession的创建与执行(二)Mapper接口和XML配置文件的注册与获取的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/825319

相关文章

PostgreSQL的扩展dict_int应用案例解析

《PostgreSQL的扩展dict_int应用案例解析》dict_int扩展为PostgreSQL提供了专业的整数文本处理能力,特别适合需要精确处理数字内容的搜索场景,本文给大家介绍PostgreS... 目录PostgreSQL的扩展dict_int一、扩展概述二、核心功能三、安装与启用四、字典配置方法

深度解析Java DTO(最新推荐)

《深度解析JavaDTO(最新推荐)》DTO(DataTransferObject)是一种用于在不同层(如Controller层、Service层)之间传输数据的对象设计模式,其核心目的是封装数据,... 目录一、什么是DTO?DTO的核心特点:二、为什么需要DTO?(对比Entity)三、实际应用场景解析

深度解析Java项目中包和包之间的联系

《深度解析Java项目中包和包之间的联系》文章浏览阅读850次,点赞13次,收藏8次。本文详细介绍了Java分层架构中的几个关键包:DTO、Controller、Service和Mapper。_jav... 目录前言一、各大包1.DTO1.1、DTO的核心用途1.2. DTO与实体类(Entity)的区别1

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

python删除xml中的w:ascii属性的步骤

《python删除xml中的w:ascii属性的步骤》使用xml.etree.ElementTree删除WordXML中w:ascii属性,需注册命名空间并定位rFonts元素,通过del操作删除属... 可以使用python的XML.etree.ElementTree模块通过以下步骤删除XML中的w:as

浏览器插件cursor实现自动注册、续杯的详细过程

《浏览器插件cursor实现自动注册、续杯的详细过程》Cursor简易注册助手脚本通过自动化邮箱填写和验证码获取流程,大大简化了Cursor的注册过程,它不仅提高了注册效率,还通过友好的用户界面和详细... 目录前言功能概述使用方法安装脚本使用流程邮箱输入页面验证码页面实战演示技术实现核心功能实现1. 随机

Golang如何对cron进行二次封装实现指定时间执行定时任务

《Golang如何对cron进行二次封装实现指定时间执行定时任务》:本文主要介绍Golang如何对cron进行二次封装实现指定时间执行定时任务问题,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录背景cron库下载代码示例【1】结构体定义【2】定时任务开启【3】使用示例【4】控制台输出总结背景

使用Python绘制3D堆叠条形图全解析

《使用Python绘制3D堆叠条形图全解析》在数据可视化的工具箱里,3D图表总能带来眼前一亮的效果,本文就来和大家聊聊如何使用Python实现绘制3D堆叠条形图,感兴趣的小伙伴可以了解下... 目录为什么选择 3D 堆叠条形图代码实现:从数据到 3D 世界的搭建核心代码逐行解析细节优化应用场景:3D 堆叠图

深度解析Python装饰器常见用法与进阶技巧

《深度解析Python装饰器常见用法与进阶技巧》Python装饰器(Decorator)是提升代码可读性与复用性的强大工具,本文将深入解析Python装饰器的原理,常见用法,进阶技巧与最佳实践,希望可... 目录装饰器的基本原理函数装饰器的常见用法带参数的装饰器类装饰器与方法装饰器装饰器的嵌套与组合进阶技巧

MySQL 获取字符串长度及注意事项

《MySQL获取字符串长度及注意事项》本文通过实例代码给大家介绍MySQL获取字符串长度及注意事项,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录mysql 获取字符串长度详解 核心长度函数对比⚠️ 六大关键注意事项1. 字符编码决定字节长度2