DOM解析XML之mybatis逆向工程框架解读

XML

概念

Xml可扩展的标记语言

XML与html区别

1、  Html的所有标记是固定的,不允许程序员新增,而xml语言可允许程序员任意的新增自己的标签,没有自己固定的标签;

2、  Html可以将数据和格式综合编写,然而xml语言则只能放入一定逻辑规则的数据;

Java 解析XML文档方案

应用程序

API

Xml解析器

Xml文档

Xml解析流程

 

 


DOM和SAX 这两套API

应用程序

JAXP的接口与抽象类

Xerces的JAXP实现

Xerces的DOM或SAX解析器

 

 

 


查找解析器工厂类

入口方法:

public static DocumentBuilderFactory newInstance() {

        return FactoryFinder.find(

                /* The default property name according tothe JAXP spec */

                DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory"

                /* The fallback implementation class name*/

                "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");

}

第一次通过系统属性获取:

// Use thesystem property first

        try {

            String systemProp = ss.getSystemProperty(factoryId);

            if (systemProp != null) {

                dPrint("found system property, value=" + systemProp);

                return newInstance(type, systemProp, null, true);

            }

        }

        catch (SecurityException se) {

            if (debug) se.printStackTrace();

        }

为什么要使用“AccessController.doPrivileged”,一探究竟...

String getSystemProperty(final String propName) {

        return (String)

            AccessController.doPrivileged(new PrivilegedAction() {

                public Object run() {

                    return System.getProperty(propName);

                }

            });

}

当我们的API去访问某些资源时,由于对资源访问做了保护处理,需要检查通过才可,使用此方法(AccessController.doPrivileged)可以忽略检查,能合理访问资源。具体资料说明:http://huangyunbin.iteye.com/blog/1942509。数据的最终来源于Properties类(继承hashmap)

第二次通过jaxp.properties获取:

String configFile = ss.getSystemProperty("java.home") + File.separator +

                            "lib" + File.separator + "jaxp.properties";

                        File f = new File(configFile);

                        firstTime = false;

                        if (ss.doesFileExist(f)) {

                            dPrint("Read properties file "+f);

                            cacheProps.load(ss.getFileInputStream(f));

                        }

将jaxp.properties文件中的键值对装载到cacheProps对象中。

第三次通过ServiceLoader获取:

通过查找META-INF/services目录中配置文件配置的服务

final ServiceLoader<T> serviceLoader = ServiceLoader.load(type);

                    final Iterator<T> iterator = serviceLoader.iterator();

                    if (iterator.hasNext()) {

                        return iterator.next();

                    } else {

                        return null;

                    }

如果按照以上三种方法都没找到对应的xml解析器,系统有默认的解析器

newInstance(type, fallbackClassName, null, true);

如果fallbackClassName的值以“com.sun.org.apache.xerces.internal”开头,fallbackClassName的默认值为com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl

DOM具体解析API不再详细论述,后面将用两个案例来分别说明。第一个案例以跟读mybatis逆向工程框架来学习优秀的设计,第二个通过编写配置文件来自我重新开发demo。

解读mybatis逆向生成工程框架源码

所需核心的jar文件

         mybatis-generator-core-1.3.2.jar

源码文件

         mybatis-generator-core-1.3.2-sources.jar

1、传入xml解析的数据源

//指定 逆向工程配置文件

  File configFile = new File("src/com/huawei/genrator/generatorConfig.xml");

   ConfigurationParser cp = newConfigurationParser(warnings);

   Configuration config =cp.parseConfiguration(configFile);//解析xml文件后,将配置数据保存在Configuration实例中

2、解析xml文件

parseErrors.clear();//清楚解析过程中错误脏数据

   DocumentBuilderFactory factory =DocumentBuilderFactory.newInstance();//xml解析抽象工厂类

   factory.setValidating(true);

   try {

       DocumentBuilder builder =factory.newDocumentBuilder();//实例化一个解析器

       builder.setEntityResolver(newParserEntityResolver());

       ParserErrorHandler handler = newParserErrorHandler(warnings,

       parseErrors);

       builder.setErrorHandler(handler);

       Document document = null;

       try {

            document =builder.parse(inputSource);//调用解析方法,在内存中以document实例形式保存

       } catch (SAXParseException e) {

            throw newXMLParserException(parseErrors);

       } catch (SAXException e) {

       if (e.getException() == null) {

            parseErrors.add(e.getMessage());

       } else {

            parseErrors.add(e.getException().getMessage());

           }

       }

       if (parseErrors.size() > 0) {

           throw newXMLParserException(parseErrors);

       }

        Configuration config;//所有的解析结果将被保存在这个实例中

        Element rootNode = document.getDocumentElement();

        DocumentType docType =document.getDoctype();

        if (rootNode.getNodeType() ==Node.ELEMENT_NODE

            &&docType.getPublicId().equals(

             XmlConstants.IBATOR_CONFIG_PUBLIC_ID)) {

              config =parseIbatorConfiguration(rootNode);

        } else if (rootNode.getNodeType() ==Node.ELEMENT_NODE

                    &&docType.getPublicId().equals(

            XmlConstants.MYBATIS_GENERATOR_CONFIG_PUBLIC_ID)) {

             config =parseMyBatisGeneratorConfiguration(rootNode);//获取对应的解析xml实例

        } else {

            throw newXMLParserException(getString("RuntimeError.5")); //$NON-NLS-1$

        }

        if (parseErrors.size() > 0) {

            throw new XMLParserException(parseErrors);

        }

         return config;

        } catch (ParserConfigurationExceptione) {

            parseErrors.add(e.getMessage());

            throw newXMLParserException(parseErrors);

        }

3、  通过xml文件的根节点,进行下一层节点的解析

Configuration configuration = new Configuration();

      NodeList nodeList =rootNode.getChildNodes();//通过根元素获取其子元素,有效子元素为context节点

      for(int i = 0; i < nodeList.getLength(); i++) {

         Node childNode = nodeList.item(i);

         if (childNode.getNodeType() !=Node.ELEMENT_NODE) {

              continue;

         }

         if("properties".equals(childNode.getNodeName())) { //$NON-NLS-1$

              parseProperties(configuration, childNode);

        } else if ("classPathEntry".equals(childNode.getNodeName())){ //$NON-NLS-1$

              parseClassPathEntry(configuration,childNode);

        } else if("context".equals(childNode.getNodeName())) { //$NON-NLS-1$获取节点的名字,此案例中名字为context

              parseContext(configuration, childNode);//解析context节点

          }

     }

     return configuration;

4、  解析context节点

Properties attributes = parseAttributes(node);//解析context节点的属性,并保存到properties对象中

String defaultModelType = attributes.getProperty("defaultModelType");//获取defaultModelType对应的值

String targetRuntime =attributes.getProperty("targetRuntime"); //获取targetRuntime对应的值

String introspectedColumnImpl = attributes

.getProperty("introspectedColumnImpl"); //获取introspectedColumnImpl对应的值

String id = attributes.getProperty("id"); //获取id对应的值

ModelType mt = defaultModelType == null ? null :ModelType

.getModelType(defaultModelType);

Context context = new Context(mt);

context.setId(id);//id值设置为context实例的ID属性

if (stringHasValue(introspectedColumnImpl)) {

context.setIntrospectedColumnImpl(introspectedColumnImpl);

}

if (stringHasValue(targetRuntime)) {

context.setTargetRuntime(targetRuntime);

}

configuration.addContext(context);//context实例保存在configuration实例的list

NodeList nodeList = node.getChildNodes();

for (int i = 0; i < nodeList.getLength(); i++) {

Node childNode = nodeList.item(i);

if (childNode.getNodeType() != Node.ELEMENT_NODE) {

continue;

}//解析context节点的子节点

if ("property".equals(childNode.getNodeName())){ //子节点名字为property

parseProperty(context, childNode);

} else if("plugin".equals(childNode.getNodeName())) { //子节点名字为plugin

parsePlugin(context, childNode);

} else if("commentGenerator".equals(childNode.getNodeName())) { //子节点名字为commentGenerator

parseCommentGenerator(context, childNode);

} else if ("jdbcConnection".equals(childNode.getNodeName())){ //子节点名字为jdbcConnection

parseJdbcConnection(context, childNode);

} else if("javaModelGenerator".equals(childNode.getNodeName())) { //子节点名字为javaModelGenerator

parseJavaModelGenerator(context, childNode);

} else if ("javaTypeResolver".equals(childNode.getNodeName())){ //子节点名字为javaTypeResolver

parseJavaTypeResolver(context, childNode);

} else if("sqlMapGenerator".equals(childNode.getNodeName())) { //子节点名字为sqlMapGenerator

parseSqlMapGenerator(context, childNode);

} else if("javaClientGenerator".equals(childNode.getNodeName())) { //子节点名字为javaClientGenerator

parseJavaClientGenerator(context, childNode);

} else if("table".equals(childNode.getNodeName())) { //$NON-NLS-1$

parseTable(context, childNode);

}

}

5、解析节点的属性

Properties attributes = new Properties();

NamedNodeMap nnm = node.getAttributes();//获取节点的属性,储存到NamedNodeMap实例中

for (int i = 0; i < nnm.getLength(); i++) {

Node attribute = nnm.item(i);//属性键值对解析为node属性

String value = parsePropertyTokens(attribute.getNodeValue());//对节点属性对应的值进行解析

attributes.put(attribute.getNodeName(), value);//将属性中对应的键值对转化为map对应的key-value

}

return attributes;

6、解析节点属性的值

final String OPEN = "${"; //$NON-NLS-1$如果属性对应的值以${}开头和结尾,说明值配置在其他的properties文件中

final String CLOSE = "}"; //$NON-NLS-1$

String newString = string;

if (newString != null) {

int start = newString.indexOf(OPEN);

int end = newString.indexOf(CLOSE);

while (start > -1 && end > start) {

String prepend = newString.substring(0, start);

String append = newString.substring(end +CLOSE.length());

String propName = newString.substring(start +OPEN.length(),

end);

String propValue = properties.getProperty(propName);//获取系统中属性文件对应的值

if (propValue != null) {

newString = prepend + propValue + append;

}

start = newString.indexOf(OPEN, end);

end = newString.indexOf(CLOSE, end);

}

}

return newString;

7、解析commentGenerator节点

CommentGeneratorConfigurationcommentGeneratorConfiguration = new CommentGeneratorConfiguration();

context.setCommentGeneratorConfiguration(commentGeneratorConfiguration);

Properties attributes = parseAttributes(node);//解析节点的属性

String type = attributes.getProperty("type");//$NON-NLS-1$

if (stringHasValue(type)) {

commentGeneratorConfiguration.setConfigurationType(type);

}

NodeList nodeList = node.getChildNodes();

for (int i = 0; i < nodeList.getLength(); i++) {

Node childNode = nodeList.item(i);

if (childNode.getNodeType() != Node.ELEMENT_NODE) {

continue;

}

if ("property".equals(childNode.getNodeName())){ //解析commentGenerator节点子节点

parseProperty(commentGeneratorConfiguration, childNode);

}

}

8、解析property节点

Properties attributes = parseAttributes(node);//解析节点

String name = attributes.getProperty("name");//获取name,不支持配置在其他的配置文件中

String value = attributes.getProperty("value");//获取value,不支持配置在其他的配置文件中

   propertyHolder.addProperty(name, value);

9、解析jdbcConnection节点

JDBCConnectionConfiguration jdbcConnectionConfiguration =new JDBCConnectionConfiguration();

context.setJdbcConnectionConfiguration(jdbcConnectionConfiguration);

 

Properties attributes = parseAttributes(node);//解析jdbcConnection节点属性

String driverClass =attributes.getProperty("driverClass"); //获取driverClass属性值

String connectionURL = attributes.getProperty("connectionURL");//获取connectionURL属性值

String userId =attributes.getProperty("userId"); //获取userId属性值

String password =attributes.getProperty("password"); //获取password属性值

jdbcConnectionConfiguration.setDriverClass(driverClass);

jdbcConnectionConfiguration.setConnectionURL(connectionURL);

if (stringHasValue(userId)) {

jdbcConnectionConfiguration.setUserId(userId);

}

if (stringHasValue(password)) {

jdbcConnectionConfiguration.setPassword(password);

}

NodeList nodeList = node.getChildNodes();

for (int i = 0; i < nodeList.getLength(); i++) {

Node childNode = nodeList.item(i);

if (childNode.getNodeType() != Node.ELEMENT_NODE) {

continue;

}

if ("property".equals(childNode.getNodeName())){ //解析property子节点

parseProperty(jdbcConnectionConfiguration, childNode);

}

}

10、解析javaTypeResolver节点

JavaTypeResolverConfigurationjavaTypeResolverConfiguration = new JavaTypeResolverConfiguration();

context.setJavaTypeResolverConfiguration(javaTypeResolverConfiguration);

Properties attributes = parseAttributes(node);//解析属性

String type = attributes.getProperty("type");//获取type属性的值

if (stringHasValue(type)) {

javaTypeResolverConfiguration.setConfigurationType(type);

}

NodeList nodeList = node.getChildNodes();

for (int i = 0; i < nodeList.getLength(); i++) {

Node childNode = nodeList.item(i);

if (childNode.getNodeType() != Node.ELEMENT_NODE) {

continue;

}

if ("property".equals(childNode.getNodeName())){ //解析子元素property

parseProperty(javaTypeResolverConfiguration, childNode);

}

}

}

11、解析javaModelGenerator节点

JavaModelGeneratorConfigurationjavaModelGeneratorConfiguration = new JavaModelGeneratorConfiguration();

context

.setJavaModelGeneratorConfiguration(javaModelGeneratorConfiguration);

Properties attributes = parseAttributes(node);//解析属性

String targetPackage =attributes.getProperty("targetPackage"); //获取生成Java pojo的包名

String targetProject =attributes.getProperty("targetProject"); //获取生成Java pojo的根路径

javaModelGeneratorConfiguration.setTargetPackage(targetPackage);

javaModelGeneratorConfiguration.setTargetProject(targetProject);

NodeList nodeList = node.getChildNodes();

for (int i = 0; i < nodeList.getLength(); i++) {

Node childNode = nodeList.item(i);

if (childNode.getNodeType() != Node.ELEMENT_NODE) {

continue;

}

if ("property".equals(childNode.getNodeName())){ //$NON-NLS-1$

parseProperty(javaModelGeneratorConfiguration,childNode);//解析子节点property

}

}

12、解析sqlMapGenerator节点

SqlMapGeneratorConfiguration sqlMapGeneratorConfiguration= new SqlMapGeneratorConfiguration();

context.setSqlMapGeneratorConfiguration(sqlMapGeneratorConfiguration);

Properties attributes = parseAttributes(node);

String targetPackage =attributes.getProperty("targetPackage"); //获取生成mapper文件的包名

String targetProject = attributes.getProperty("targetProject");//获取生成mapper文件的根路径

sqlMapGeneratorConfiguration.setTargetPackage(targetPackage);

sqlMapGeneratorConfiguration.setTargetProject(targetProject);

NodeList nodeList = node.getChildNodes();

for (int i = 0; i < nodeList.getLength(); i++) {

Node childNode = nodeList.item(i);

if (childNode.getNodeType() != Node.ELEMENT_NODE) {

continue;

}

if ("property".equals(childNode.getNodeName())){ //$NON-NLS-1$

parseProperty(sqlMapGeneratorConfiguration, childNode);//解析property子节点

}

}

13、解析javaClientGenerator节点

JavaClientGeneratorConfigurationjavaClientGeneratorConfiguration = new JavaClientGeneratorConfiguration();

context.setJavaClientGeneratorConfiguration(javaClientGeneratorConfiguration);

Properties attributes = parseAttributes(node);//解析属性

String type = attributes.getProperty("type");//获取type属性

String targetPackage =attributes.getProperty("targetPackage"); //获取dao接口生成的包名

String targetProject = attributes.getProperty("targetProject");//获取dao接口生成的根目录

String implementationPackage = attributes

.getProperty("implementationPackage");

javaClientGeneratorConfiguration.setConfigurationType(type);

javaClientGeneratorConfiguration.setTargetPackage(targetPackage);

javaClientGeneratorConfiguration.setTargetProject(targetProject);

javaClientGeneratorConfiguration

.setImplementationPackage(implementationPackage);

NodeList nodeList = node.getChildNodes();

for (int i = 0; i < nodeList.getLength(); i++) {

Node childNode = nodeList.item(i);

if (childNode.getNodeType() != Node.ELEMENT_NODE) {

continue;

}

if ("property".equals(childNode.getNodeName())){  //解析property子节点

parseProperty(javaClientGeneratorConfiguration,childNode);

}

}

14、解析table节点

TableConfiguration tc = new TableConfiguration(context);

context.addTableConfiguration(tc);

Properties attributes = parseAttributes(node);

String catalog =attributes.getProperty("catalog"); //$NON-NLS-1$

String schema =attributes.getProperty("schema"); //$NON-NLS-1$

String tableName =attributes.getProperty("tableName"); //获取表名

String domainObjectName =attributes.getProperty("domainObjectName"); //$NON-NLS-1$

String alias = attributes.getProperty("alias");//$NON-NLS-1$

String enableInsert =attributes.getProperty("enableInsert"); //$NON-NLS-1$

String enableSelectByPrimaryKey = attributes

.getProperty("enableSelectByPrimaryKey");//$NON-NLS-1$

String enableSelectByExample = attributes

.getProperty("enableSelectByExample");//$NON-NLS-1$

String enableUpdateByPrimaryKey = attributes

.getProperty("enableUpdateByPrimaryKey");//$NON-NLS-1$

String enableDeleteByPrimaryKey = attributes

.getProperty("enableDeleteByPrimaryKey");//$NON-NLS-1$

String enableDeleteByExample = attributes

.getProperty("enableDeleteByExample");//$NON-NLS-1$

String enableCountByExample = attributes

.getProperty("enableCountByExample");//$NON-NLS-1$

String enableUpdateByExample = attributes

.getProperty("enableUpdateByExample");//$NON-NLS-1$

String selectByPrimaryKeyQueryId = attributes

.getProperty("selectByPrimaryKeyQueryId");//$NON-NLS-1$

String selectByExampleQueryId = attributes

.getProperty("selectByExampleQueryId");//$NON-NLS-1$

String modelType =attributes.getProperty("modelType"); //$NON-NLS-1$

String escapeWildcards =attributes.getProperty("escapeWildcards"); //$NON-NLS-1$

String delimitIdentifiers = attributes

.getProperty("delimitIdentifiers");//$NON-NLS-1$

String delimitAllColumns =attributes.getProperty("delimitAllColumns"); //$NON-NLS-1$

if (stringHasValue(catalog)) {

tc.setCatalog(catalog);

}

if (stringHasValue(schema)) {

tc.setSchema(schema);

}

if (stringHasValue(tableName)) {

tc.setTableName(tableName);

}

if (stringHasValue(domainObjectName)) {

tc.setDomainObjectName(domainObjectName);

}

if (stringHasValue(alias)) {

tc.setAlias(alias);

}

if (stringHasValue(enableInsert)) {

tc.setInsertStatementEnabled(isTrue(enableInsert));

}

if (stringHasValue(enableSelectByPrimaryKey)) {

tc.setSelectByPrimaryKeyStatementEnabled(

isTrue(enableSelectByPrimaryKey));

}

if (stringHasValue(enableSelectByExample)) {

tc.setSelectByExampleStatementEnabled(

isTrue(enableSelectByExample));

}

if (stringHasValue(enableUpdateByPrimaryKey)) {

tc.setUpdateByPrimaryKeyStatementEnabled(

isTrue(enableUpdateByPrimaryKey));

}

if (stringHasValue(enableDeleteByPrimaryKey)) {

tc.setDeleteByPrimaryKeyStatementEnabled(

isTrue(enableDeleteByPrimaryKey));

}

if (stringHasValue(enableDeleteByExample)) {

tc.setDeleteByExampleStatementEnabled(

isTrue(enableDeleteByExample));

}

if (stringHasValue(enableCountByExample)) {

tc.setCountByExampleStatementEnabled(

isTrue(enableCountByExample));

}

if (stringHasValue(enableUpdateByExample)) {

tc.setUpdateByExampleStatementEnabled(

isTrue(enableUpdateByExample));

}

if (stringHasValue(selectByPrimaryKeyQueryId)) {

tc.setSelectByPrimaryKeyQueryId(selectByPrimaryKeyQueryId);

}

if (stringHasValue(selectByExampleQueryId)) {

tc.setSelectByExampleQueryId(selectByExampleQueryId);

}

if (stringHasValue(modelType)) {

tc.setConfiguredModelType(modelType);

}

if (stringHasValue(escapeWildcards)) {

tc.setWildcardEscapingEnabled(isTrue(escapeWildcards));

}

if (stringHasValue(delimitIdentifiers)) {

tc.setDelimitIdentifiers(isTrue(delimitIdentifiers));

}

if (stringHasValue(delimitAllColumns)) {

tc.setAllColumnDelimitingEnabled(isTrue(delimitAllColumns));

}

NodeList nodeList = node.getChildNodes();

for (int i = 0; i < nodeList.getLength(); i++) {

Node childNode = nodeList.item(i);

if (childNode.getNodeType() != Node.ELEMENT_NODE) {

continue;

}

if ("property".equals(childNode.getNodeName())){ //$NON-NLS-1$

parseProperty(tc, childNode);

} else if("columnOverride".equals(childNode.getNodeName())) { //$NON-NLS-1$

parseColumnOverride(tc, childNode);

} else if("ignoreColumn".equals(childNode.getNodeName())) { //$NON-NLS-1$

parseIgnoreColumn(tc, childNode);

} else if("generatedKey".equals(childNode.getNodeName())) { //$NON-NLS-1$

parseGeneratedKey(tc, childNode);

} else if("columnRenamingRule".equals(childNode.getNodeName())) {//$NON-NLS-1$

parseColumnRenamingRule(tc, childNode);

}

}

Xml中所有节点解析完毕后,存储得到config对象后,MyBatisGenerator对象调用generate()方法生成对应的*.Java*mapper.xml文件,可以判断出后面所有的工作将是从config对象中获取所需数据。

解析表配置

introspectedTables = newArrayList<IntrospectedTable>();

JavaTypeResolver javaTypeResolver = ObjectFactory.createJavaTypeResolver(this,warnings);

Connection connection = null;

try {

     callback.startTask(getString("Progress.0"));//链接数据库

     connection =getConnection();//获取数据库链接

     DatabaseIntrospectordatabaseIntrospector = new DatabaseIntrospector(

                   this, connection.getMetaData(), javaTypeResolver, warnings);

     for(TableConfiguration tc : tableConfigurations) {

          String tableName =composeFullyQualifiedTableName(tc.getCatalog(), tc.getSchema(), tc.getTableName(),'.');//获取完整表名

     if(fullyQualifiedTableNames != null&& fullyQualifiedTableNames.size()> 0) {

         if(!fullyQualifiedTableNames.contains(tableName)) {

             continue;

         }

     }

     if(!tc.areAnyStatementsEnabled()) {

       warnings.add(getString("Warning.0",tableName)); //$NON-NLS-1$

            continue;

     }

     callback.startTask(getString("Progress.1",tableName)); //解析表配置信息

     List<IntrospectedTable>tables = databaseIntrospector.introspectTables(tc);//整理表信息(列信息)

    if (tables !=null) {

        introspectedTables.addAll(tables);

    }

        callback.checkCancel();

    }

    } finally {

       closeConnection(connection);

}

1、  获取配置中的jdbc链接

Connection connection = ConnectionFactory.getInstance().getConnection(jdbcConnectionConfiguration);

获取链接的具体实现

Driver driver = getDriver(config);//获取jdbc驱动

Properties props = new Properties();//创建零时数据载体

if (stringHasValue(config.getUserId())) {

 props.setProperty("user",config.getUserId()); //存放数据源用户名

}

if (stringHasValue(config.getPassword())) {

 props.setProperty("password",config.getPassword()); //存放数据源密码

}

props.putAll(config.getProperties());

Connection conn =driver.connect(config.getConnectionURL(), props);//根据url和登录验证获取数据库链接

if (conn == null) {

throw newSQLException(getString("RuntimeError.7")); //$NON-NLS-1$

}

return conn;

2、  加载jdbc驱动

String driverClass =connectionInformation.getDriverClass();

    Driver driver;

    try {

         Class<?>clazz = ObjectFactory.externalClassForName(driverClass);

         driver =(Driver) clazz.newInstance();

    } catch(Exception e) {

         throw newRuntimeException(getString("RuntimeError.8"), e); //$NON-NLS-1$

    }

 returndriver;

3、  遍历表配置

// get the raw columns from the DB

   Map<ActualTableName,List<IntrospectedColumn>> columns = getColumns(tc);//获取表的列数据

   if(columns.isEmpty()) {

         warnings.add(getString("Warning.19",tc.getCatalog(), //$NON-NLS-1$

          tc.getSchema(),tc.getTableName()));

       return null;

   }

   removeIgnoredColumns(tc,columns);//去掉可忽略的列信息

   calculateExtraColumnInformation(tc,columns);//确定列信息的Java类型

   applyColumnOverrides(tc,columns);//重写列信息

   calculateIdentityColumns(tc,columns);

   List<IntrospectedTable> introspectedTables= calculateIntrospectedTables(tc, columns);//解析表信息

   // nowintrospectedTables has all the columns from all the

  // tables in theconfiguration. Do some validation...

  Iterator<IntrospectedTable>iter = introspectedTables.iterator();

  while(iter.hasNext()) {

      IntrospectedTable introspectedTable =iter.next();

      if (!introspectedTable.hasAnyColumns()) {

          // addwarning that the table has no columns, remove from the

         // list

         Stringwarning = getString("Warning.1",introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$

         warnings.add(warning);

         iter.remove();

    } else if(!introspectedTable.hasPrimaryKeyColumns()&&!introspectedTable.hasBaseColumns()) {

        // addwarning that the table has only BLOB columns, remove from

       // the list

       Stringwarning = getString("Warning.18",introspectedTable.getFullyQualifiedTable().toString()); //$NON-NLS-1$

    warnings.add(warning);

    iter.remove();

    } else {

          // nowmake sure that all columns called out in the

          //configuration

         //actually exist

         reportIntrospectionWarnings(introspectedTable,tc,   introspectedTable.getFullyQualifiedTable());

            }

        }

return introspectedTables;

4、  去除可忽略的列信息

for (Map.Entry<ActualTableName,List<IntrospectedColumn>> entry : columns.entrySet()) {

    Iterator<IntrospectedColumn>tableColumns = (entry.getValue()).iterator();

    while(tableColumns.hasNext()) {

          IntrospectedColumnintrospectedColumn = tableColumns.next();

          if(tc.isColumnIgnored(introspectedColumn.getActualColumnName())) {//根据ignoredColumns判断列信息是否有效

                  tableColumns.remove();

                  if(logger.isDebugEnabled()) {

                     logger.debug(getString("Tracing.3", //$NON-NLS-1$

                             introspectedColumn.getActualColumnName(),entry.getKey().toString()));

                   }

                }

            }

       }

5、  解析表信息

Map<ActualTableName,List<IntrospectedColumn>> columns) {

        booleandelimitIdentifiers = tc.isDelimitIdentifiers()

                ||stringContainsSpace(tc.getCatalog())

                ||stringContainsSpace(tc.getSchema())

                ||stringContainsSpace(tc.getTableName());

List<IntrospectedTable> answer = newArrayList<IntrospectedTable>();

for (Map.Entry<ActualTableName,List<IntrospectedColumn>> entry : columns.entrySet()) {

           ActualTableName atn = entry.getKey();

FullyQualifiedTable table = newFullyQualifiedTable(stringHasValue(tc.getCatalog()) ? atn

                            .getCatalog() :null,stringHasValue(tc.getSchema()) ? atn.getSchema() : null,

                   atn.getTableName(),tc.getDomainObjectName(),tc.getAlias(),

                   isTrue(tc.getProperty(PropertyRegistry.TABLE_IGNORE_QUALIFIERS_AT_RUNTIME)),

                   tc.getProperty(PropertyRegistry.TABLE_RUNTIME_CATALOG),

                   tc.getProperty(PropertyRegistry.TABLE_RUNTIME_SCHEMA),

                    tc.getProperty(PropertyRegistry.TABLE_RUNTIME_TABLE_NAME),

                   delimitIdentifiers, context);

IntrospectedTable introspectedTable =ObjectFactory.createIntrospectedTable(tc, table, context);//根据context具体配置的targetRuntime="MyBatis3"实例IntrospectedTable对象

for (IntrospectedColumn introspectedColumn :entry.getValue()) {

    introspectedTable.addColumn(introspectedColumn);

 }

 calculatePrimaryKey(table, introspectedTable);//解析索引

 answer.add(introspectedTable);

 }

     returnanswer;

生成文件

pluginAggregator = new PluginAggregator();//插件配置

for (PluginConfiguration pluginConfiguration :pluginConfigurations) {

Plugin plugin =ObjectFactory.createPlugin(this,pluginConfiguration);

if (plugin.validate(warnings)) {

     pluginAggregator.addPlugin(plugin);

} else {

    warnings.add(getString("Warning.24", //$NON-NLS-1$

     pluginConfiguration.getConfigurationType(),id));

}

}

if (introspectedTables != null) {

   for(IntrospectedTable introspectedTable : introspectedTables) {

      callback.checkCancel();

      introspectedTable.initialize();//初始化工作

      introspectedTable.calculateGenerators(warnings, callback);//实例化JavaMapperGeneratorjavaModelGenerators解析类型

      generatedJavaFiles.addAll(introspectedTable .getGeneratedJavaFiles());

      generatedXmlFiles.addAll(introspectedTable.getGeneratedXmlFiles());

      generatedJavaFiles.addAll(pluginAggregator.contextGenerateAdditionalJavaFiles(introspectedTable));

     generatedXmlFiles.addAll(pluginAggregator.contextGenerateAdditionalXmlFiles(introspectedTable));

        }

    }

generatedJavaFiles.addAll(pluginAggregator.contextGenerateAdditionalJavaFiles());

generatedXmlFiles.addAll(pluginAggregator.contextGenerateAdditionalXmlFiles());

1、  初始化工作

calculateJavaClientAttributes();//初始化接口、mapper文件存储路径信息

calculateModelAttributes();//初始化实体类存放路径信息

calculateXmlAttributes();//初始化mapper.xml文件的信息

if (tableConfiguration.getModelType() ==ModelType.HIERARCHICAL) {

       rules = newHierarchicalModelRules(this);

} else if (tableConfiguration.getModelType() ==ModelType.FLAT) {

       rules = newFlatModelRules(this);

} else {

      rules = newConditionalModelRules(this);

        }

    context.getPlugins().initialized(this);

2、  生成Java文件GeneratedFile

List<GeneratedJavaFile> answer = newArrayList<GeneratedJavaFile>();

for (AbstractJavaGenerator javaGenerator :javaModelGenerators) {

   List<CompilationUnit>compilationUnits = javaGenerator.getCompilationUnits();//获取实体类编译单元(源文件)

   for(CompilationUnit compilationUnit : compilationUnits) {

                 GeneratedJavaFile gjf = newGeneratedJavaFile(compilationUnit,                    context.getJavaModelGeneratorConfiguration().getTargetProject(),

        context.getProperty(PropertyRegistry.CONTEXT_JAVA_FILE_ENCODING),

        context.getJavaFormatter());

        answer.add(gjf);

    }

}

for (AbstractJavaGenerator javaGenerator :clientGenerators) {

    List<CompilationUnit>compilationUnits = javaGenerator.getCompilationUnits();

    for(CompilationUnit compilationUnit : compilationUnits) {

          GeneratedJavaFilegjf = new GeneratedJavaFile(compilationUnit,

          context.getJavaClientGeneratorConfiguration().getTargetProject(),

          context.getProperty(PropertyRegistry.CONTEXT_JAVA_FILE_ENCODING),

          context.getJavaFormatter());

          answer.add(gjf);

  }

}

       return answer;

3、  生成编译单元,由于类属性较多,仅举例说明(代码不完整)

向类中添加一个方法

TopLevelClass topLevelClass = new TopLevelClass(type);//实例顶层类

topLevelClass.setVisibility(JavaVisibility.PUBLIC);//类的访问权限设置为共有

commentGenerator.addJavaFileComment(topLevelClass);

Method method = new Method();//实例一个方法

method.setVisibility(JavaVisibility.PUBLIC);//方法权限为共有

  method.setConstructor(true);//设置为构造器

method.setName(type.getShortName());//为方法设置名字

    method.addBodyLine("oredCriteria= new ArrayList<Criteria>();"); //编写方法体

commentGenerator.addGeneralMethodComment(method,introspectedTable);

    topLevelClass.addMethod(method);

向类中添加一个属性

   Field field = newField();//实例一个类中属性

   field.setVisibility(JavaVisibility.PROTECTED);//将该属性设置为受保护

   field.setType(FullyQualifiedJavaType.getStringInstance());//设置属性类型为Java.lang.String

   field.setName("orderByClause");//设置属性名字

   commentGenerator.addFieldComment(field,introspectedTable);

   topLevelClass.addField(field);//将属性添加到类

   method = newMethod();//实例一个方法

   method.setVisibility(JavaVisibility.PUBLIC);//设置方法访问权限为公有

   method.setName("setOrderByClause");//方法名字为setOrderByClause

   method.addParameter(newParameter(FullyQualifiedJavaType.getStringInstance(),"orderByClause")); //方法参数为orderByClause,类型为Java.lang.String

   method.addBodyLine("this.orderByClause= orderByClause;"); //向方法体中添加代码

   commentGenerator.addGeneralMethodComment(method,introspectedTable);

   topLevelClass.addMethod(method);//将方法添加到类

   method = newMethod();

   method.setVisibility(JavaVisibility.PUBLIC);//设置为公有访问权限

   method.setReturnType(FullyQualifiedJavaType.getStringInstance());//设置返回值类型为Java.lang.string

   method.setName("getOrderByClause");//设置方法名为getOrderByClause

   method.addBodyLine("returnorderByClause;"); //向方法体中添加代码

   commentGenerator.addGeneralMethodComment(method,introspectedTable);

      topLevelClass.addMethod(method);//将方法添加到类

4、  编译内部类

InnerClass answer = newInnerClass(FullyQualifiedJavaType.getGeneratedCriteriaInstance());//实例内部类

answer.setVisibility(JavaVisibility.PROTECTED);//设置为受保护的权限

answer.setStatic(true);//设置为静态属性

answer.setAbstract(true);//设置为抽象类

context.getCommentGenerator().addClassComment(answer,introspectedTable);

method = new Method();//添加一个方法

method.setVisibility(JavaVisibility.PROTECTED);//设置方法访问权限为受保护

method.setName("GeneratedCriteria"); //设置方法名字

 method.setConstructor(true);//设置为构造方法

method.addBodyLine("super();"); //向方法体中添加代码

method.addBodyLine("criteria = newArrayList<Criterion>();"); //添加代码

answer.addMethod(method);

List<String> criteriaLists = newArrayList<String>();

    criteriaLists.add("criteria");//$NON-NLS-1$

生成daomapper)接口的源码不再详细解析,最终将生成的Java文件保存在GeneratedJavaFile数据结构中,生成的mapper.xml文件保存在GeneratedXmlFile数据结构中。

5、  生成xml配置文件

List<GeneratedXmlFile> answer = newArrayList<GeneratedXmlFile>();

if (xmlMapperGenerator != null) {

      Document document =xmlMapperGenerator.getDocument();

                 GeneratedXmlFile gxf = newGeneratedXmlFile(document,getMyBatis3XmlMapperFileName(),getMyBatis3XmlMapperPackage(),context.getSqlMapGeneratorConfiguration().getTargetProject(),

      true,context.getXmlFormatter());

      if(context.getPlugins().sqlMapGenerated(gxf, this)) {

            answer.add(gxf);

        }

    }

    return answer;

数据是根据stringbuffer拼接而成的

StringBuilder sb = new StringBuilder();

sb.append("<?xml version=\"1.0\"encoding=\"UTF-8\" ?>"); //xml申明数据拼接

if (publicId != null && systemId != null) {

     OutputUtilities.newLine(sb);

     sb.append("<!DOCTYPE"); //$NON-NLS-1$

     sb.append(rootElement.getName());

     sb.append(" PUBLIC\""); //$NON-NLS-1$

     sb.append(publicId);

     sb.append("\"\""); //$NON-NLS-1$

     sb.append(systemId);

     sb.append("\">"); //$NON-NLS-1$

 }

 OutputUtilities.newLine(sb);//换行

 sb.append(rootElement.getFormattedContent(0));//获取子节点下面的数据

 return sb.toString();

6、  持久化mapper.xmlJava文件

持久化xml文件

for (GeneratedXmlFile gxf : generatedXmlFiles) {

projects.add(gxf.getTargetProject());

File targetFile;

String source;

try {

     File directory= shellCallback.getDirectory(gxf.getTargetProject(), gxf.getTargetPackage());

     targetFile =new File(directory, gxf.getFileName());

     if(targetFile.exists()) {

        if(gxf.isMergeable()) {

            source= XmlFileMergerJaxp.getMergedSource(gxf,targetFile);

         } else if(shellCallback.isOverwriteEnabled()) {

            source= gxf.getFormattedContent();

            warnings.add(getString("Warning.11",//$NON-NLS-1$

            targetFile.getAbsolutePath()));

         } else {

             source = gxf.getFormattedContent();

             targetFile= getUniqueFileName(directory, gxf.getFileName());

             warnings.add(getString("Warning.2",targetFile.getAbsolutePath())); //$NON-NLS-1$

         }

    } else {

             source= gxf.getFormattedContent();

    }

} catch (ShellException e) {

   warnings.add(e.getMessage());

   continue;

}

   callback.checkCancel();

   callback.startTask(getString("Progress.15",targetFile.getName())); //$NON-NLS-1$

   writeFile(targetFile,source, "UTF-8"); //$NON-NLS-1$

}

持久化java文件

for (GeneratedJavaFilegjf : generatedJavaFiles) {

projects.add(gjf.getTargetProject());

File targetFile;

String source;

try {//拼接出文件存放全路径

File directory = shellCallback.getDirectory(gjf.getTargetProject(),gjf.getTargetPackage());

targetFile = new File(directory, gjf.getFileName());

if (targetFile.exists()) {

 if(shellCallback.isMergeSupported()) {

      source =shellCallback.mergeJavaFile(gjf.getFormattedContent(),targetFile.getAbsolutePath(),

                MergeConstants.OLD_ELEMENT_TAGS,gjf.getFileEncoding());

  } else if(shellCallback.isOverwriteEnabled()) {

      source =gjf.getFormattedContent();

      warnings.add(getString("Warning.11",//$NON-NLS-1$

      targetFile.getAbsolutePath()));

   } else {

       source =gjf.getFormattedContent();

       targetFile =getUniqueFileName(directory, gjf.getFileName());

       warnings.add(getString("Warning.2",targetFile.getAbsolutePath())); //$NON-NLS-1$

    }

} else {

source = gjf.getFormattedContent();//对象字符串化,方便写入文件

}

callback.checkCancel();

callback.startTask(getString("Progress.15",targetFile.getName())); //$NON-NLS-1$

writeFile(targetFile, source, gjf.getFileEncoding());//将字符串内容写入文件

} catch(ShellException e) {

warnings.add(e.getMessage());

}

}

此处大家想必和我一样有一些疑惑:

1、  如果对象内容比较多,没有办法一次性转化为字符串怎么办?

2、  其中writeFile方法内部使用BufferedWriterwrite方法,对写入的字符串长度是否有限制?

基于以上两个问题,我准备网上寻找答案,和大家一起解惑!!!

 

String对象的最大长度:看了String源码发现value其实就是一个char数组,String对象的长度及时char数组的长度,数组的长度而是一个整数,整数的最大值是2^31-12,147,483647),由此可见如此庞大的字符串已经够我们使用了。

框架中的writeFile()方法实现如下:

FileOutputStream fos = new FileOutputStream(file, false);//文件字节输出流

OutputStreamWriter osw;

if (fileEncoding == null) {

     osw = newOutputStreamWriter(fos);//字节流转化为字符流

} else {

     osw = newOutputStreamWriter(fos, fileEncoding);

}

BufferedWriter bw = new BufferedWriter(osw);//为了获得较高效率,将其转化为带缓冲的输出字符流中,以避免频繁调用缓冲器

bw.write(content);//我们的最大疑问就是此处如果字符串长度十分大,超过了缓冲区的最大长度怎么办?

    bw.close();

下面我们需要再跟读BufferedWriterwrite方法源码

public void write(String str, int off, int len) throwsIOException {

       synchronized (lock) {

            charcbuf[];

            if (len<= WRITE_BUFFER_SIZE) {//如果字符串长度不大于1024

                if(writeBuffer == null) {

                   writeBuffer = new char[WRITE_BUFFER_SIZE];

                }

               cbuf = writeBuffer;

            } else{    // Don't permanently allocate verylarge buffers.长度大于1024

               cbuf = new char[len];

            }

           str.getChars(off, (off + len), cbuf, 0);

           write(cbuf, 0, len);

        }

    }

到此我们基本已经分析完毕,从xml配置文件的解析,通过解析的数据建立起与数据库的链接,获取对应的表信息,并按照配置根据系统支持的编译单元生成对应的Javaxml文件,最后持久化到硬盘。其中用到了dom解析,Java反射技术,数据库jdbc链接,文件io读写,线程安全等Java核心技术,其中线程安全框架出现较少,主要出现位置在io源码中。

 

框架中关键技术说明

n  通过一个类名,获取class对象

Class<?> clazz = null;

try {

ClassLoader cl =Thread.currentThread().getContextClassLoader();//获取当前线程的类加载器

clazz = Class.forName(type, true, cl);//通过类名和加载器生成对应的Class对象

} catch (Exception e) {

// ignore - failsafe below

}

if (clazz == null) {

clazz = Class.forName(type, true,ObjectFactory.class.getClassLoader());

}

return clazz;

n  通过class实例创建一个对应类的实例

Object answer;

try {

Class<?> clazz = internalClassForName(type);

answer = clazz.newInstance();//通过class对象实例一个对象

} catch (Exception e) {

throw new RuntimeException(getString(

"RuntimeError.6", type), e); //$NON-NLS-1$

}

return answer;

n  框架中循环整理

框架中由于有许多解析xml节点的编码,所以循环处理出现地方较多,此处单独用区别的方式说明一下它们。

传统for循环:for (int i = 0; i < tableConfigurations.size(); i++)

有循环体可看出传统循环依赖下标,多见于数组或则以数组实现的数据结构如list

加强for循环:如for (TableConfiguration tc : tableConfigurations)

此种方法,不需要数组下标,不需要知道数组长度,写法十分简洁;

迭代器循环:Iterator it =list.iterator();

迭代器的实现具体根据数据结构底层的数据载体,list底层则是依赖数组,map则是依赖链式实现。

详细资料可参见:http://www.cnblogs.com/leskang/p/6031282.html

框架整体设计研读

异常设计

异常记录采用list记录字符串

日志设计

日志采用jdk自带的日志记录器和log4j,本框架默认使用log4j日志记录器

模式设计

1、工厂模式中的饱汉模式

Connection connection= ConnectionFactory.getInstance().getConnection(jdbcConnectionConfiguration);

private staticConnectionFactory instance = new ConnectionFactory();

public staticConnectionFactory getInstance() {

     return instance;

}

 

类关系设计

n  将配置文件的解析存放到Configuration,可理解为对Context的一个包装类

n  将配置文件中的Context节点信息解析成Context对象,包装了jdbc链接配置信息、mapper.xml配置信息、数据库表对应Java实体和mapper接口配置等信息

n   

 

资源国际化

private static finalString BUNDLE_NAME ="org.mybatis.generator.internal.util.messages.messages"; //指定资源文件路径

private static finalResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME);//加载本地资源文件的对象

try {

     return RESOURCE_BUNDLE.getString(key);//获取资源文件对应key的值

} catch(MissingResourceException e) {

     return '!' + key + '!';

}

本框架中用到了静态引入的方式,以前自己没用过,也很少见别人这么使用,特在此说明

import staticorg.mybatis.generator.internal.util.messages.Messages.getString;

通过此种引用,可以在类中直接使用方法名而达到类名.静态方法的效果,具体细节可参考下面帖子http://blog.csdn.net/u012843873/article/details/51741675

l   加载资源绑定对象

if (locale == null || control == null) {

throw new NullPointerException();

}

CacheKey cacheKey = new CacheKey(baseName, locale,loader);//通过资源文件路径、本地国际化属性对象和类加载器创建一个唯一的缓存key

ResourceBundle bundle = null;

BundleReference bundleRef = cacheList.get(cacheKey);//在缓存数据载体中查找该缓存key是否已经加载到缓存中

if (bundleRef != null) {

    bundle =bundleRef.get();

    bundleRef =null;

}

if (isValidBundle(bundle) &&hasValidParentChain(bundle)) {

    return bundle;

}

此处比较好奇,为什么需要有一个CacheKey对象,因为cacheList是一个map对象,CacheKey作为他的key存在,那么是否可以只使用baseName按照某种规则生成一个简略的key呢?我觉得是可行的,那么我们看看,框架如此设计的好处。

hashCodeCache =name.hashCode() << 3;

hashCodeCache ^=locale.hashCode();

ClassLoader loader =getLoader();

if (loader != null) {

    hashCodeCache ^= loader.hashCode();

}

将资源名称和本地国际化对象类加载器分别计算hash值,然后在异或

说明:

1、  DOM解析XML文件,首先将xml文件以document实例的形式载入内存中;

2、  Configuration类,其中核心的数据为Contextlist

3、  Context类,解析多个配置后对应的Java pojo封装类,比如jdbc链接配置、mapper文件生成配置、Java pojo生成配置和数据表配置等;

4、  获取xml文件头遵循的dtd规则,案例中以此为准则“-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN”;

5、  获取节点的时候会有空白节点的出现,所有有效节点之间往往会有空白节点出现;

6、  结点属性内容保存在Properties实例中,此实例底层数据载体为map

7、  Dom解析节点属性类NamedNodeMap

8、  一个逆向xml配置文件对应一个Configuration实例,一个context节点对应一个Context实例

框架的缺点

1、  框架中硬编码出现大的位置较多,不好维护扩展;

2、  框架封装方法不够优雅,类定义的方法不太合理;

Mybatis逆向工程配置文件

<?xml version="1.0"encoding="UTF-8"?>

  <!DOCTYPE generatorConfiguration

    PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration1.0//EN"

    "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

    <generatorConfiguration>//根元素

      <context id="testTables"targetRuntime="MyBatis3">

          <commentGenerator>

              <!-- 是否去除自动生成的注释 true:是 false: -->

             <property name="suppressAllComments"value="true" />

         </commentGenerator>

         <!--数据库连接的信息:驱动类、连接地址、用户名、密码,这里配置的是mysql的,当然也可以配置oracle等数据库 -->

         <jdbcConnection driverClass="com.mysql.jdbc.Driver"

             connectionURL="jdbc:mysql://localhost:3306/test_wangjian"userId="root"

             password="jane@123456">

         </jdbcConnection>

          <!-- 默认false,把JDBC DECIMAL NUMERIC 类型解析为 Integer,为 true时把JDBC DECIMAL

             NUMERIC 类型解析为java.math.BigDecimal -->

         <javaTypeResolver>

             <property name="forceBigDecimals"value="false" />

         </javaTypeResolver>

          <!-- targetProject:生成PO类的位置 -->

         <javaModelGenerator targetPackage="com.mybatis.entity"

             targetProject=".\src">

             <!-- enableSubPackages:是否让schema作为包的后缀 -->

             <property name="enableSubPackages"value="false" />

             <!-- 从数据库返回的值被清理前后的空格 -->

             <property name="trimStrings"value="true" />

         </javaModelGenerator>

         <!-- targetProject:mapper映射文件生成的位置 -->

         <sqlMapGenerator targetPackage="com.mybatis.mapper"

             targetProject=".\src">

             <!-- enableSubPackages:是否让schema作为包的后缀 -->

             <property name="enableSubPackages"value="false" />

         </sqlMapGenerator>

         <!-- targetPackagemapper接口生成的位置 -->

         <javaClientGenerator type="XMLMAPPER"

             targetPackage="com.mybatis.mapper"targetProject=".\src">

             <!-- enableSubPackages:是否让schema作为包的后缀 -->

             <property name="enableSubPackages"value="false" />

         </javaClientGenerator>

         <!-- 指定数据库表 -->

         <table tableName="goods"></table>

         <table tableName="users"></table>

         <!-- <table tableName="orderdetail"></table>

         <tabletableName="t_user"></table> -->

     </context>

</generatorConfiguration>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值