@

一起学 mybatis

你想不想来学习 mybatis? 学习其使用和源码呢?那么, 在博客园关注我吧!!

我自己打算把这个源码系列更新完毕, 同时会更新相应的注释。快去 star 吧!!

mybatis最新源码和注释


在 mybatis 中, 对应 CRUD 的是四种节点: <select>, <insert>, <delete>, <update>。

在解析 Mapper.xml 文件中, 会调用 XMLStatementBuilder 来 进行这几个节点的解析。 解析完成后使用 MappedStatement 来表示一条条 SQL 语句。 完成的是这样这个过程

0 <sql> 节点解析

在此之前, 需要先了解一下 <sql>。

<sql> 节点不仅仅是代码生成器生成时, 代表一些字段而已, 其定义可重用的 SQL 语句的片段。 类似于我们在写代码时, 抽象出一个方法。

  1. /**
  2. * 解析 <sql> 节点
  3. *
  4. * @param list
  5. * @param requiredDatabaseId
  6. * @throws Exception
  7. */
  8. private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
  9. // 遍历 <sql> 节点
  10. for (XNode context : list) {
  11. // 获取 databaseId 属性
  12. String databaseId = context.getStringAttribute("databaseId");
  13. // 获取 id 属性
  14. String id = context.getStringAttribute("id");
  15. // 为 id 添加命名空间
  16. id = builderAssistant.applyCurrentNamespace(id, false);
  17. // 检查 sql 节点的 databaseId 与当前 Configuration 中的是否一致
  18. if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
  19. // 记录到 XMLMapperBuider.sqlFragments(Map<String, XNode>)中保存
  20. // 其最终是指向了 Configuration.sqlFragments(configuration.getSqlFragments) 集合
  21. sqlFragments.put(id, context);
  22. }
  23. }
  24. }

整体的过程就是获取所有节点, 然后逐个解析。 然后以 id-> context 键值对的方式存放在 XMLMapperBuilder.sqlFragments 对象中, 后续会用到。

注意, 此时的 context 还是 XNode 对象, 其最终的解析还是在解析 include 时进行解析。

注意, id 使用了 MapperBuilderAssistant.applyCurrentNamespace 进行了处理。 其是按照一定的规则在前面添加 namespace, 以便 id 在全局具有唯一性。

1 解析流程

其整体的代码是这样子的

  1. public void parseStatementNode() {
  2. // 获取 id 属性
  3. String id = context.getStringAttribute("id");
  4. // 获取 databaseid
  5. String databaseId = context.getStringAttribute("databaseId");
  6. //验证databaseId是否匹配
  7. if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
  8. return;
  9. }
  10. // 获取各个属性
  11. Integer fetchSize = context.getIntAttribute("fetchSize");
  12. Integer timeout = context.getIntAttribute("timeout");
  13. String parameterMap = context.getStringAttribute("parameterMap");
  14. String parameterType = context.getStringAttribute("parameterType");
  15. Class<?> parameterTypeClass = resolveClass(parameterType);
  16. String resultMap = context.getStringAttribute("resultMap");
  17. String resultType = context.getStringAttribute("resultType");
  18. String lang = context.getStringAttribute("lang");
  19. LanguageDriver langDriver = getLanguageDriver(lang);
  20. Class<?> resultTypeClass = resolveClass(resultType);
  21. String resultSetType = context.getStringAttribute("resultSetType");
  22. StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  23. ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  24. // 获取节点的类型
  25. String nodeName = context.getNode().getNodeName();
  26. SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  27. boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  28. boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  29. boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  30. boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
  31. // Include Fragments before parsing
  32. // 引入include 解析出的 sql 节点内容
  33. XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  34. includeParser.applyIncludes(context.getNode());
  35. // Parse selectKey after includes and remove them.
  36. // 处理 selectKey
  37. processSelectKeyNodes(id, parameterTypeClass, langDriver);
  38. // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  39. SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  40. String resultSets = context.getStringAttribute("resultSets");
  41. String keyProperty = context.getStringAttribute("keyProperty");
  42. String keyColumn = context.getStringAttribute("keyColumn");
  43. KeyGenerator keyGenerator;
  44. // 设置主键自增的方式
  45. String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  46. keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  47. if (configuration.hasKeyGenerator(keyStatementId)) {
  48. keyGenerator = configuration.getKeyGenerator(keyStatementId);
  49. } else {
  50. keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
  51. configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
  52. ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  53. }
  54. builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
  55. fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
  56. resultSetTypeEnum, flushCache, useCache, resultOrdered,
  57. keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  58. }

但去除一些获取节点属性的代码, 去除一些反射的代码。 其流程可以用下图表示

2 节点解析

在其他的内容解析之前, 会先解析 <incliude>节点, 用对应 id 的重用 SQL 语句将该节点替换掉。

先看看约束的定义

  1. <!ELEMENT include (property+)?>
  2. <!ATTLIST include
  3. refid CDATA #REQUIRED
  4. >

可以看出, <incliude> 节点中可以包含有 property 一个或多个, 必须包含有 refid。 refid 是对应 <sql> 节点的 id。

2.1 解析流程

解析时, 通过 XMLIncludeTransformer.applyIncludes 方法进行解析。

  1. /**
  2. * 从 parseStatementNode 方法进入时, Node 还是 (select|insert|update|delete) 节点
  3. */
  4. public void applyIncludes(Node source) {
  5. Properties variablesContext = new Properties();
  6. // 获取的是 mybatis-config.xml 所定义的属性
  7. Properties configurationVariables = configuration.getVariables();
  8. if (configurationVariables != null) {
  9. variablesContext.putAll(configurationVariables);
  10. }
  11. // 处理 <include> 子节点
  12. applyIncludes(source, variablesContext, false);
  13. }

获取 Coniguration.variables 中的所有属性, 这些属性后续在将 ${XXX} 替换成真实的参数时会用到。 然后递归解析所有的 include 节点。 具体的实现过程如下:

  1. /**
  2. * Recursively apply includes through all SQL fragments.
  3. * 递归的包含所有的 SQL 节点
  4. *
  5. * @param source Include node in DOM tree
  6. * @param variablesContext Current context for static variables with values
  7. */
  8. private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
  9. // 下面是处理 include 子节点
  10. if (source.getNodeName().equals("include")) {
  11. // 查找 refid 属性指向 <sql> 节点
  12. Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
  13. // 解析 <include> 节点下的 <property> 节点, 将得到的键值对添加到 variablesContext 中
  14. // 并形成 Properties 对象返回, 用于替换占位符
  15. Properties toIncludeContext = getVariablesContext(source, variablesContext);
  16. // 递归处理 <include> 节点, 在 <sql> 节点中可能会 <include> 其他 SQL 片段
  17. applyIncludes(toInclude, toIncludeContext, true);
  18. if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
  19. toInclude = source.getOwnerDocument().importNode(toInclude, true);
  20. }
  21. // 将 <include> 节点替换成 <sql>
  22. source.getParentNode().replaceChild(toInclude, source);
  23. while (toInclude.hasChildNodes()) {
  24. toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
  25. }
  26. toInclude.getParentNode().removeChild(toInclude);
  27. } else if (source.getNodeType() == Node.ELEMENT_NODE) {
  28. if (included && !variablesContext.isEmpty()) {
  29. // replace variables in attribute values
  30. // 获取所有的属性值, 并使用 variablesContext 进行占位符的解析
  31. NamedNodeMap attributes = source.getAttributes();
  32. for (int i = 0; i < attributes.getLength(); i++) {
  33. Node attr = attributes.item(i);
  34. attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
  35. }
  36. }
  37. // 获取所有的子类, 并递归解析
  38. NodeList children = source.getChildNodes();
  39. for (int i = 0; i < children.getLength(); i++) {
  40. applyIncludes(children.item(i), variablesContext, included);
  41. }
  42. } else if (included && source.getNodeType() == Node.TEXT_NODE
  43. && !variablesContext.isEmpty()) {
  44. // replace variables in text node
  45. // 使用 variablesContext 进行占位符的解析
  46. source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
  47. }
  48. }

它分三种节点进行解析

  • include
  • Node.ELEMENT_NODE
  • Node.TEXT_NODE

2.2 <include> 节点的解析

这个是节点为 <include> 时才进行解析的, 其解析的流程大体如下

2.3 Node.ELEMENT_NODE 类型解析

什么时候回出现这种情况呢? 节点是非 <include> 的 Node.ELEMENT_NODE 类型的节点时, 如 sql 节点, (select | insert | update | delete) 节点的时候。 这些节点的特点就是都有可能含有 <include> 节点。

这个的流程很简单, 就是递归调用解析所有的 <include> 子节点。

  1. // 获取所有的子类, 并递归解析
  2. NodeList children = source.getChildNodes();
  3. for (int i = 0; i < children.getLength(); i++) {
  4. applyIncludes(children.item(i), variablesContext, included);
  5. }

2.4 Node.TEXT_NODE

Node.TEXT_NODE 就是文本节点, 当时该类型的节点时, 就会使用 PropertyParser.parse 方法来进行解析。 其大体就是将 ${xxx} 替换成相应的值。

由于有 included 条件的现在, 其只有是在 include 所包含的子节点时才会如此。

举例

该过程中涉及到了多层递归, 同时还有多种节点类型, 还需要进行占位符的处理, 理解上还是比较费劲的, 举个栗子吧

  1. <!--全部字段-->
  2. <sql id="Base_Column_List">
  3. student_id, name, phone, email, sex, locked, gmt_created, gmt_modified
  4. </sql>
  5. <!--表名-->
  6. <sql id="sometable">
  7. ${table}
  8. </sql>
  9. <!--refid可以使用${}-->
  10. <sql id="someinclude">
  11. from
  12. <include refid="${include_target}"/>
  13. </sql>
  14. <!--SQL-->
  15. <select id="selectById" resultMap="BaseResultMap">
  16. select
  17. <include refid="Base_Column_List" />
  18. <include refid="someinclude">
  19. <property name="table" value="student"/>
  20. <property name="include_target" value="sometable"/>
  21. </include>
  22. where student_id=#{studentId, jdbcType=INTEGER}
  23. </select>

其流程大体如下

看的时候, 请对照代码来看, 详细讲解了前面三个节点的解析过程。 后面的类似, 可能有的递归层次加深了, 并大体的思路并没有改变。

3 节点

<insert>、<update>可以定义<selectKey>节点来获取主键。

  1. /**
  2. * 真正解析 selectKey 的函数
  3. */
  4. private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
  5. // 开始时获取各个属性
  6. String resultType = nodeToHandle.getStringAttribute("resultType");
  7. Class<?> resultTypeClass = resolveClass(resultType);
  8. StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  9. String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
  10. String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
  11. boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
  12. //defaults
  13. boolean useCache = false;
  14. boolean resultOrdered = false;
  15. KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
  16. Integer fetchSize = null;
  17. Integer timeout = null;
  18. boolean flushCache = false;
  19. String parameterMap = null;
  20. String resultMap = null;
  21. ResultSetType resultSetTypeEnum = null;
  22. // 生成对应的 SqlSource
  23. SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
  24. SqlCommandType sqlCommandType = SqlCommandType.SELECT;
  25. // 使用 SqlSource 创建 MappedStatement 对象
  26. builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
  27. fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
  28. resultSetTypeEnum, flushCache, useCache, resultOrdered,
  29. keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
  30. id = builderAssistant.applyCurrentNamespace(id, false);
  31. MappedStatement keyStatement = configuration.getMappedStatement(id, false);
  32. // 添加到 Configuration 中, 并通过 executeBefore 还觉得是在sql之前执行还是之后执行
  33. configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
  34. }

其中涉及到

  1. SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);

这个过程。

LanguageDriver 类有两个实现类

默认是 XMLLanguageDriver。 可以通过 Configuration 的构造函数得出。

  1. languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);

langDriver.createSqlSource 函数中, 会调用 parseScriptNode 函数

  1. /**
  2. * 解析动态节点
  3. * @return
  4. */
  5. public SqlSource parseScriptNode() {
  6. // 首先判断是不是动态节点
  7. MixedSqlNode rootSqlNode = parseDynamicTags(context);
  8. SqlSource sqlSource = null;
  9. if (isDynamic) {
  10. sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  11. } else {
  12. sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  13. }
  14. return sqlSource;
  15. }

而其中, 需要判定是否为动态SQL, 其中, 有 $ 和动态 sql 的节点, 都会认为是动态SQL。

  1. /**
  2. * 解析动态节点
  3. * @param node
  4. * @return
  5. */
  6. protected MixedSqlNode parseDynamicTags(XNode node) {
  7. List<SqlNode> contents = new ArrayList<>();
  8. // 获取节点下的所有子节点
  9. NodeList children = node.getNode().getChildNodes();
  10. for (int i = 0; i < children.getLength(); i++) {
  11. // 获取节点
  12. XNode child = node.newXNode(children.item(i));
  13. if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
  14. // 如果有 $ , 则为动态sql节点
  15. String data = child.getStringBody("");
  16. TextSqlNode textSqlNode = new TextSqlNode(data);
  17. if (textSqlNode.isDynamic()) {
  18. contents.add(textSqlNode);
  19. isDynamic = true;// 标记为动态节点
  20. } else {
  21. contents.add(new StaticTextSqlNode(data));
  22. }
  23. } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
  24. // 子节点是标签, 则一定是动态sql节点。 根据nodeName, 生产不同的 NodeHandler
  25. String nodeName = child.getNode().getNodeName();
  26. NodeHandler handler = nodeHandlerMap.get(nodeName);
  27. if (handler == null) {
  28. throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
  29. }
  30. handler.handleNode(child, contents);
  31. isDynamic = true;
  32. }
  33. }
  34. return new MixedSqlNode(contents);
  35. }

NodeHandler 有以下几个实现类

是不是似曾相识? 就是动态 SQL 的几个节点所对应的。

在该过程之后, selectById 就变成了:

4 创建 SqlSource

该过程与上面的过程相似, 经过 include 节点的解析之后, 会创建对应的 SqlSourceNode 对象。

关于 SqlSource, 会在后续的文章中详细展开讲解。

在该过程之后, selectById 变成了

对应参数及其类型被保存起来, 同时参数的占位符 #{xxx, JdbcType=yyy} 变成了问号。 在调用 RawSqlSource 构造函数时, 会完成该过程

  1. public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
  2. SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  3. Class<?> clazz = parameterType == null ? Object.class : parameterType;
  4. sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  5. }
  6. public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
  7. // 占位符处理器
  8. ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
  9. GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
  10. String sql = parser.parse(originalSql);
  11. return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  12. }
  13. // SQL 中的占位符处理。
  14. @Override
  15. public String handleToken(String content) {
  16. parameterMappings.add(buildParameterMapping(content));
  17. return "?";
  18. }

5 获取对应的 KeyGenerator

KeyGenerator为键生成器。 在我们使用主键自动生成时, 会生成一个对应的主键生成器实例。

该接口主要定义了生成器的在 SQL 在查询前执行还是之后执行。 其有如下的实现类

  • Jdbc3KeyGenerator:用于处理数据库支持自增主键的情况,如MySQL的auto_increment。
  • NoKeyGenerator:空实现,不需要处理主键。没有主键生成器, 如不是 INSERT, 也没有使用主键生成器的时候, 就是该类型。
  • SelectKeyGenerator:配置了 <selectKey> 之后, 就是该类型。 用于处理数据库不支持自增主键的情况,比如Oracle,postgres的sequence序列。

6 创建并添加 MappedStatement

在完成以上步骤的处理之后, 通过

  1. builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
  2. fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
  3. resultSetTypeEnum, flushCache, useCache, resultOrdered,
  4. keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

进行 MappedStatement 对象的生成, 并添加到 Configuration 中。

以上的 selectById 最后再存在 Configuration中:

mybatis源码-Mapper解析之SQL 语句节点解析(一条语句对应一个MappedStatement)的更多相关文章

  1. mybatis源码分析(二)------------配置文件的解析

    这篇文章中,我们将讲解配置文件中 properties,typeAliases,settings和environments这些节点的解析过程. 一 properties的解析 private void ...

  2. 初看Mybatis 源码 (三) SQL是怎么执行的

    前面说到Java动态代理,Mybatis通过这种方式实现了我们通过getMapper方式得到的Dao接口,可以直接通过接口的没有实现的方法来执行sql. AuthUserDao mapper = se ...

  3. MyBatis 源码分析 - 映射文件解析过程

    1.简介 在上一篇文章中,我详细分析了 MyBatis 配置文件的解析过程.由于上一篇文章的篇幅比较大,加之映射文件解析过程也比较复杂的原因.所以我将映射文件解析过程的分析内容从上一篇文章中抽取出来, ...

  4. Mybatis源码解析优秀博文

    最近阅读了许久的mybatis源码,小有所悟.同时也发现网上有许多优秀的mybatis源码讲解博文.本人打算把自己阅读过的.觉得不错的一些博文列出来.以此进一步加深对mybatis框架的理解.其实还有 ...

  5. Mybatis源码解析-MapperRegistry注册mapper接口

    知识储备 SqlsessionFactory-mybatis持久层操作数据的根本,具体的解析是通过SqlSessionFactoryBean生成的,具体的形成可见>>>Spring ...

  6. Spring mybatis源码篇章-sql mapper配置文件绑定mapper class类

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-MybatisDAO文件解析(二) 背景知识 MappedStatement是mybatis操作sql ...

  7. Spring mybatis源码篇章-NodeHandler实现类具体解析保存Dynamic sql节点信息

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-XMLLanguageDriver解析sql包装为SqlSource SqlNode接口类 publi ...

  8. Spring mybatis源码篇章-XMLLanguageDriver解析sql包装为SqlSource

    前言:通过阅读源码对实现机制进行了解有利于陶冶情操,承接前文Spring mybatis源码篇章-MybatisDAO文件解析(二) 首先了解下sql mapper的动态sql语法 具体的动态sql的 ...

  9. Mybatis源码解析-MapperRegistry代理注册mapper接口

    知识储备 SqlsessionFactory-mybatis持久层操作数据的前提,具体的解析是通过SqlSessionFactoryBean生成的,可见>>>Spring mybat ...

随机推荐

  1. coTurn 运行在Windows平台的方法及服务与客户端运行交互流程和原理

    coTurn是一个开源的STUN和TURN及ICE服务项目,只是不支持Windows.为了在window平台上使用coTurn源码,需要在windows平台下安装Cygwin环境,并编译coTurn源 ...

  2. 使用html+css+js实现简易计算器

    使用html+css+js实现简易计算器, 效果图如下: html代码如下: <!DOCTYPE html> <html lang="en"> <he ...

  3. 分组统计SQL

    Itpub上遇到一个求助写SQL的帖子,感觉很有意思,于是写出来看看,要求如下: 有个计划表1, 记录物料的年度计划量 有个实际使用情况表2,记录实际使用情况. 最后要出个统计表,把计划和实际的数据结 ...

  4. Elasticsearch深入搜索之结构化搜索及JavaAPI的使用

    一.Es中创建索引 1.创建索引: 在之前的Es插件的安装和使用中说到创建索引自定义分词器和创建type,当时是分开写的,其实创建索引时也可以创建type,并指定分词器. PUT /my_index ...

  5. Java中的生产消费者问题

    package day190109; import java.util.LinkedList; import java.util.Queue; import java.util.Random; pub ...

  6. JavaSE: Java 5 新特性

    Java5新特性 1.Java 语言 1.1 Generics 1.2 foreach 1.3 自动拆箱装箱 1.4 enum 1.5 可变参数 varargs 1.6 static import 1 ...

  7. windows平台下实现高可用性和可扩展性-ARR和HLB

    本文档提供了关于如何将应用程序请求路由(ARR)与硬件负载均衡器一起使用以实现高可用性和可伸缩性的说明性指导.本文采用F5大IP负载均衡器来说明ARR与硬件负载平衡器之间的工作关系. IIS7.0及以 ...

  8. JDK动态代理和cglib代理详解

    JDK动态代理 先做一下简单的描述,通过代理之后返回的对象已并非原类所new出来的对象,而是代理对象.JDK的动态代理是基于接口的,也就是说,被代理类必须实现一个或多个接口.主要原因是JDK的代理原理 ...

  9. Windows10反安装报错error code 2502 2503

    先找系统TEMP目录,一般为C:\windows\temp,打开这个目录的权限,为这个目录中的User用户添加权限为完全控制,现在再反安装就不会报错了. 注:原因就是因为系统运行时需要用到临时文件的目 ...

  10. 在win7下python的xlrd和xlwt的安装于应用

    1. http://pypi.python.org/pypi/xlwt 和http://pypi.python.org/pypi/xlrd下载xlwt-0.7.4.tar.gz和xlrd-0.7.7. ...