解决Spring导致iBatis缓存失效问题

 

版本:
  Spring 3.0.4(2.x版本中也存在类似问题)
  iBatis 2.3.4.726(2.3.x版本都适用)

起因:
  使用Spring管理iBatis实例,标准方式采用SqlMapClientFactoryBean创建SqlMapClient

 

Java代码    收藏代码
  1. <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">  
  2.     <property name="configLocation"  
  3.         value="classpath:/com/foo/xxx/sqlMapConfig.xml" />  
  4.     <property name="dataSource" ref="dataSource" />  
  5.     <property name="mappingLocations" value="classpath*:/**/sqlmap/*SqlMap.xml" />  
  6. </bean>  



使用mappingLocations方式定义可以让sqlmap不必添加到sqlMapConfig.xml
即避免了如下形式

 

 

Java代码    收藏代码
  1. <sqlMapConfig>  
  2. ...  
  3.   <sqlMap url="someSqlMap" resource="com/foo/xxx/someSqlMap.xml"/>  
  4. ...  
  5. </sqlMapConfig>  

 

随之而来的问题是在sqlMap中的cache无法在执行预订方法时自动flush!

Java代码    收藏代码
  1. <cacheModel id="mycache" type ="LRU" readOnly="false" serialize="false">  
  2.   <flushInterval hours="24"/>  
  3.   <flushOnExecute statement="myobj.insert"/>  
  4.   <property name="cache-size" value="50" />  
  5. </cacheModel>  

 

在myobj.insert被执行时cache不会flush,即无报错也无提醒,彻头彻尾的坑爹,不清楚Spring开发组为什么会让这问题一直存在。

产生问题的原因网上有不少分析贴(不再重复,需要的可搜索相关帖子),原因是Spring在处理mappingLocations之前,iBatis已经完成对sqlMapConfig里注册的sqlMap的cache设置(真绕口=.="),非亲生的sqlMap被无情的抛弃了。解决办法大都建议放弃mappingLocations的方式,把sqlMap写到sqlMapConfig里——还真是够简单——或者说够偷懒。

找到原因就想办法解决吧,从SqlMapClientFactoryBean下手,扩展一份

Java代码    收藏代码
  1. /** 
  2.  
  3. * @author Foxswily 
  4. */  
  5.   
  6. @SuppressWarnings("deprecation")  
  7. public class MySqlMapClientFactoryBean implements FactoryBean<SqlMapClient>,  
  8.         InitializingBean {  
  9.   
  10.     private static final ThreadLocal<LobHandler> configTimeLobHandlerHolder = new ThreadLocal<LobHandler>();  
  11.   
  12.     /** 
  13.      * Return the LobHandler for the currently configured iBATIS SqlMapClient, 
  14.      * to be used by TypeHandler implementations like ClobStringTypeHandler. 
  15.      * <p> 
  16.      * This instance will be set before initialization of the corresponding 
  17.      * SqlMapClient, and reset immediately afterwards. It is thus only available 
  18.      * during configuration. 
  19.      *  
  20.      * @see #setLobHandler 
  21.      * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler 
  22.      * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler 
  23.      * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler 
  24.      */  
  25.     public static LobHandler getConfigTimeLobHandler() {  
  26.         return configTimeLobHandlerHolder.get();  
  27.     }  
  28.   
  29.     private Resource[] configLocations;  
  30.   
  31.     private Resource[] mappingLocations;  
  32.   
  33.     private Properties sqlMapClientProperties;  
  34.   
  35.     private DataSource dataSource;  
  36.   
  37.     private boolean useTransactionAwareDataSource = true;  
  38.   
  39.     @SuppressWarnings("rawtypes")  
  40.     private Class transactionConfigClass = ExternalTransactionConfig.class;  
  41.   
  42.     private Properties transactionConfigProperties;  
  43.   
  44.     private LobHandler lobHandler;  
  45.   
  46.     private SqlMapClient sqlMapClient;  
  47.   
  48.     public MySqlMapClientFactoryBean() {  
  49.         this.transactionConfigProperties = new Properties();  
  50.         this.transactionConfigProperties.setProperty("SetAutoCommitAllowed""false");  
  51.     }  
  52.   
  53.     /** 
  54.      * Set the location of the iBATIS SqlMapClient config file. A typical value 
  55.      * is "WEB-INF/sql-map-config.xml". 
  56.      *  
  57.      * @see #setConfigLocations 
  58.      */  
  59.     public void setConfigLocation(Resource configLocation) {  
  60.         this.configLocations = (configLocation != null ? new Resource[] { configLocation }  
  61.                 : null);  
  62.     }  
  63.   
  64.     /** 
  65.      * Set multiple locations of iBATIS SqlMapClient config files that are going 
  66.      * to be merged into one unified configuration at runtime. 
  67.      */  
  68.     public void setConfigLocations(Resource[] configLocations) {  
  69.         this.configLocations = configLocations;  
  70.     }  
  71.   
  72.     /** 
  73.      * Set locations of iBATIS sql-map mapping files that are going to be merged 
  74.      * into the SqlMapClient configuration at runtime. 
  75.      * <p> 
  76.      * This is an alternative to specifying "&lt;sqlMap&gt;" entries in a 
  77.      * sql-map-client config file. This property being based on Spring's 
  78.      * resource abstraction also allows for specifying resource patterns here: 
  79.      * e.g. "/myApp/*-map.xml". 
  80.      * <p> 
  81.      * Note that this feature requires iBATIS 2.3.2; it will not work with any 
  82.      * previous iBATIS version. 
  83.      */  
  84.     public void setMappingLocations(Resource[] mappingLocations) {  
  85.         this.mappingLocations = mappingLocations;  
  86.     }  
  87.   
  88.     /** 
  89.      * Set optional properties to be passed into the SqlMapClientBuilder, as 
  90.      * alternative to a <code>&lt;properties&gt;</code> tag in the 
  91.      * sql-map-config.xml file. Will be used to resolve placeholders in the 
  92.      * config file. 
  93.      *  
  94.      * @see #setConfigLocation 
  95.      * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient(java.io.InputStream, 
  96.      *      java.util.Properties) 
  97.      */  
  98.     public void setSqlMapClientProperties(Properties sqlMapClientProperties) {  
  99.         this.sqlMapClientProperties = sqlMapClientProperties;  
  100.     }  
  101.   
  102.     /** 
  103.      * Set the DataSource to be used by iBATIS SQL Maps. This will be passed to 
  104.      * the SqlMapClient as part of a TransactionConfig instance. 
  105.      * <p> 
  106.      * If specified, this will override corresponding settings in the 
  107.      * SqlMapClient properties. Usually, you will specify DataSource and 
  108.      * transaction configuration <i>either</i> here <i>or</i> in SqlMapClient 
  109.      * properties. 
  110.      * <p> 
  111.      * Specifying a DataSource for the SqlMapClient rather than for each 
  112.      * individual DAO allows for lazy loading, for example when using 
  113.      * PaginatedList results. 
  114.      * <p> 
  115.      * With a DataSource passed in here, you don't need to specify one for each 
  116.      * DAO. Passing the SqlMapClient to the DAOs is enough, as it already 
  117.      * carries a DataSource. Thus, it's recommended to specify the DataSource at 
  118.      * this central location only. 
  119.      * <p> 
  120.      * Thanks to Brandon Goodin from the iBATIS team for the hint on how to make 
  121.      * this work with Spring's integration strategy! 
  122.      *  
  123.      * @see #setTransactionConfigClass 
  124.      * @see #setTransactionConfigProperties 
  125.      * @see com.ibatis.sqlmap.client.SqlMapClient#getDataSource 
  126.      * @see SqlMapClientTemplate#setDataSource 
  127.      * @see SqlMapClientTemplate#queryForPaginatedList 
  128.      */  
  129.     public void setDataSource(DataSource dataSource) {  
  130.         this.dataSource = dataSource;  
  131.     }  
  132.   
  133.     /** 
  134.      * Set whether to use a transaction-aware DataSource for the SqlMapClient, 
  135.      * i.e. whether to automatically wrap the passed-in DataSource with Spring's 
  136.      * TransactionAwareDataSourceProxy. 
  137.      * <p> 
  138.      * Default is "true": When the SqlMapClient performs direct database 
  139.      * operations outside of Spring's SqlMapClientTemplate (for example, lazy 
  140.      * loading or direct SqlMapClient access), it will still participate in 
  141.      * active Spring-managed transactions. 
  142.      * <p> 
  143.      * As a further effect, using a transaction-aware DataSource will apply 
  144.      * remaining transaction timeouts to all created JDBC Statements. This means 
  145.      * that all operations performed by the SqlMapClient will automatically 
  146.      * participate in Spring-managed transaction timeouts. 
  147.      * <p> 
  148.      * Turn this flag off to get raw DataSource handling, without Spring 
  149.      * transaction checks. Operations on Spring's SqlMapClientTemplate will 
  150.      * still detect Spring-managed transactions, but lazy loading or direct 
  151.      * SqlMapClient access won't. 
  152.      *  
  153.      * @see #setDataSource 
  154.      * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy 
  155.      * @see org.springframework.jdbc.datasource.DataSourceTransactionManager 
  156.      * @see SqlMapClientTemplate 
  157.      * @see com.ibatis.sqlmap.client.SqlMapClient 
  158.      */  
  159.     public void setUseTransactionAwareDataSource(boolean useTransactionAwareDataSource) {  
  160.         this.useTransactionAwareDataSource = useTransactionAwareDataSource;  
  161.     }  
  162.   
  163.     /** 
  164.      * Set the iBATIS TransactionConfig class to use. Default is 
  165.      * <code>com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig</code> 
  166.      * . 
  167.      * <p> 
  168.      * Will only get applied when using a Spring-managed DataSource. An instance 
  169.      * of this class will get populated with the given DataSource and 
  170.      * initialized with the given properties. 
  171.      * <p> 
  172.      * The default ExternalTransactionConfig is appropriate if there is external 
  173.      * transaction management that the SqlMapClient should participate in: be it 
  174.      * Spring transaction management, EJB CMT or plain JTA. This should be the 
  175.      * typical scenario. If there is no active transaction, SqlMapClient 
  176.      * operations will execute SQL statements non-transactionally. 
  177.      * <p> 
  178.      * JdbcTransactionConfig or JtaTransactionConfig is only necessary when 
  179.      * using the iBATIS SqlMapTransactionManager API instead of external 
  180.      * transactions. If there is no explicit transaction, SqlMapClient 
  181.      * operations will automatically start a transaction for their own scope (in 
  182.      * contrast to the external transaction mode, see above). 
  183.      * <p> 
  184.      * <b>It is strongly recommended to use iBATIS SQL Maps with Spring 
  185.      * transaction management (or EJB CMT).</b> In this case, the default 
  186.      * ExternalTransactionConfig is fine. Lazy loading and SQL Maps operations 
  187.      * without explicit transaction demarcation will execute 
  188.      * non-transactionally. 
  189.      * <p> 
  190.      * Even with Spring transaction management, it might be desirable to specify 
  191.      * JdbcTransactionConfig: This will still participate in existing 
  192.      * Spring-managed transactions, but lazy loading and operations without 
  193.      * explicit transaction demaration will execute in their own auto-started 
  194.      * transactions. However, this is usually not necessary. 
  195.      *  
  196.      * @see #setDataSource 
  197.      * @see #setTransactionConfigProperties 
  198.      * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig 
  199.      * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig 
  200.      * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig 
  201.      * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig 
  202.      * @see com.ibatis.sqlmap.client.SqlMapTransactionManager 
  203.      */  
  204.     @SuppressWarnings("rawtypes")  
  205.     public void setTransactionConfigClass(Class transactionConfigClass) {  
  206.         if (transactionConfigClass == null  
  207.                 || !TransactionConfig.class.isAssignableFrom(transactionConfigClass)) {  
  208.             throw new IllegalArgumentException(  
  209.                     "Invalid transactionConfigClass: does not implement "  
  210.                             + "com.ibatis.sqlmap.engine.transaction.TransactionConfig");  
  211.         }  
  212.         this.transactionConfigClass = transactionConfigClass;  
  213.     }  
  214.   
  215.     /** 
  216.      * Set properties to be passed to the TransactionConfig instance used by 
  217.      * this SqlMapClient. Supported properties depend on the concrete 
  218.      * TransactionConfig implementation used: 
  219.      * <p> 
  220.      * <ul> 
  221.      * <li><b>ExternalTransactionConfig</b> supports "DefaultAutoCommit" 
  222.      * (default: false) and "SetAutoCommitAllowed" (default: true). Note that 
  223.      * Spring uses SetAutoCommitAllowed = false as default, in contrast to the 
  224.      * iBATIS default, to always keep the original autoCommit value as provided 
  225.      * by the connection pool. 
  226.      * <li><b>JdbcTransactionConfig</b> does not supported any properties. 
  227.      * <li><b>JtaTransactionConfig</b> supports "UserTransaction" (no default), 
  228.      * specifying the JNDI location of the JTA UserTransaction (usually 
  229.      * "java:comp/UserTransaction"). 
  230.      * </ul> 
  231.      *  
  232.      * @see com.ibatis.sqlmap.engine.transaction.TransactionConfig#initialize 
  233.      * @see com.ibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig 
  234.      * @see com.ibatis.sqlmap.engine.transaction.jdbc.JdbcTransactionConfig 
  235.      * @see com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig 
  236.      */  
  237.     public void setTransactionConfigProperties(Properties transactionConfigProperties) {  
  238.         this.transactionConfigProperties = transactionConfigProperties;  
  239.     }  
  240.   
  241.     /** 
  242.      * Set the LobHandler to be used by the SqlMapClient. Will be exposed at 
  243.      * config time for TypeHandler implementations. 
  244.      *  
  245.      * @see #getConfigTimeLobHandler 
  246.      * @see com.ibatis.sqlmap.engine.type.TypeHandler 
  247.      * @see org.springframework.orm.ibatis.support.ClobStringTypeHandler 
  248.      * @see org.springframework.orm.ibatis.support.BlobByteArrayTypeHandler 
  249.      * @see org.springframework.orm.ibatis.support.BlobSerializableTypeHandler 
  250.      */  
  251.     public void setLobHandler(LobHandler lobHandler) {  
  252.         this.lobHandler = lobHandler;  
  253.     }  
  254.   
  255.     public void afterPropertiesSet() throws Exception {  
  256.         if (this.lobHandler != null) {  
  257.             // Make given LobHandler available for SqlMapClient configuration.  
  258.             // Do early because because mapping resource might refer to custom  
  259.             // types.  
  260.             configTimeLobHandlerHolder.set(this.lobHandler);  
  261.         }  
  262.   
  263.         try {  
  264.             this.sqlMapClient = buildSqlMapClient(this.configLocations,  
  265.                     this.mappingLocations, this.sqlMapClientProperties);  
  266.   
  267.             // Tell the SqlMapClient to use the given DataSource, if any.  
  268.             if (this.dataSource != null) {  
  269.                 TransactionConfig transactionConfig = (TransactionConfig) this.transactionConfigClass  
  270.                         .newInstance();  
  271.                 DataSource dataSourceToUse = this.dataSource;  
  272.                 if (this.useTransactionAwareDataSource  
  273.                         && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) {  
  274.                     dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource);  
  275.                 }  
  276.                 transactionConfig.setDataSource(dataSourceToUse);  
  277.                 transactionConfig.initialize(this.transactionConfigProperties);  
  278.                 applyTransactionConfig(this.sqlMapClient, transactionConfig);  
  279.             }  
  280.         }  
  281.   
  282.         finally {  
  283.             if (this.lobHandler != null) {  
  284.                 // Reset LobHandler holder.  
  285.                 configTimeLobHandlerHolder.set(null);  
  286.             }  
  287.         }  
  288.     }  
  289.   
  290.     /** 
  291.      * Build a SqlMapClient instance based on the given standard configuration. 
  292.      * <p> 
  293.      * The default implementation uses the standard iBATIS 
  294.      * {@link SqlMapClientBuilder} API to build a SqlMapClient instance based on 
  295.      * an InputStream (if possible, on iBATIS 2.3 and higher) or on a Reader (on 
  296.      * iBATIS up to version 2.2). 
  297.      *  
  298.      * @param configLocations 
  299.      *            the config files to load from 
  300.      * @param properties 
  301.      *            the SqlMapClient properties (if any) 
  302.      * @return the SqlMapClient instance (never <code>null</code>) 
  303.      * @throws IOException 
  304.      *             if loading the config file failed 
  305.      * @throws NoSuchFieldException 
  306.      * @throws SecurityException 
  307.      * @throws IllegalAccessException 
  308.      * @throws IllegalArgumentException 
  309.      * @throws NoSuchMethodException 
  310.      * @throws InvocationTargetException 
  311.      * @see com.ibatis.sqlmap.client.SqlMapClientBuilder#buildSqlMapClient 
  312.      */  
  313.     protected SqlMapClient buildSqlMapClient(Resource[] configLocations,  
  314.             Resource[] mappingLocations, Properties properties) throws IOException,  
  315.             SecurityException, NoSuchFieldException, IllegalArgumentException,  
  316.             IllegalAccessException, NoSuchMethodException, InvocationTargetException {  
  317.   
  318.         if (ObjectUtils.isEmpty(configLocations)) {  
  319.             throw new IllegalArgumentException(  
  320.                     "At least 1 'configLocation' entry is required");  
  321.         }  
  322.   
  323.         SqlMapClient client = null;  
  324.         SqlMapConfigParser configParser = new SqlMapConfigParser();  
  325.         for (Resource configLocation : configLocations) {  
  326.             InputStream is = configLocation.getInputStream();  
  327.             try {  
  328.                 client = configParser.parse(is, properties);  
  329.             } catch (RuntimeException ex) {  
  330.                 throw new NestedIOException("Failed to parse config resource: "  
  331.                         + configLocation, ex.getCause());  
  332.             }  
  333.         }  
  334.   
  335.         if (mappingLocations != null) {  
  336.             SqlMapParser mapParser = SqlMapParserFactory.createSqlMapParser(configParser);  
  337.             for (Resource mappingLocation : mappingLocations) {  
  338.                 try {  
  339.                     mapParser.parse(mappingLocation.getInputStream());  
  340.                 } catch (NodeletException ex) {  
  341.                     throw new NestedIOException("Failed to parse mapping resource: "  
  342.                             + mappingLocation, ex);  
  343.                 }  
  344.             }  
  345.         }  
  346.         //*************其实只改这一点而已,为了方便他人,全source贴出**************  
  347.         //为了取sqlMapConfig,反射private的field  
  348.         Field stateField = configParser.getClass().getDeclaredField("state");  
  349.         stateField.setAccessible(true);  
  350.         XmlParserState state = (XmlParserState) stateField.get(configParser);  
  351.         SqlMapConfiguration sqlMapConfig = state.getConfig();  
  352.         //反射取设置cache的方法,执行  
  353.         Method wireUpCacheModels = sqlMapConfig.getClass().getDeclaredMethod(  
  354.                 "wireUpCacheModels");  
  355.         wireUpCacheModels.setAccessible(true);  
  356.         wireUpCacheModels.invoke(sqlMapConfig);  
  357.         //*************************************************************************  
  358.         return client;  
  359.     }  
  360.   
  361.     /**  
  362.      * Apply the given iBATIS TransactionConfig to the SqlMapClient.  
  363.      * <p>  
  364.      * The default implementation casts to ExtendedSqlMapClient, retrieves the  
  365.      * maximum number of concurrent transactions from the  
  366.      * SqlMapExecutorDelegate, and sets an iBATIS TransactionManager with the  
  367.      * given TransactionConfig.  
  368.      *   
  369.      * @param sqlMapClient  
  370.      *            the SqlMapClient to apply the TransactionConfig to  
  371.      * @param transactionConfig  
  372.      *            the iBATIS TransactionConfig to apply  
  373.      * @see com.ibatis.sqlmap.engine.impl.ExtendedSqlMapClient  
  374.      * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#getMaxTransactions  
  375.      * @see com.ibatis.sqlmap.engine.impl.SqlMapExecutorDelegate#setTxManager  
  376.      */  
  377.     protected void applyTransactionConfig(SqlMapClient sqlMapClient,  
  378.             TransactionConfig transactionConfig) {  
  379.         if (!(sqlMapClient instanceof ExtendedSqlMapClient)) {  
  380.             throw new IllegalArgumentException(  
  381.                     "Cannot set TransactionConfig with DataSource for SqlMapClient if not of type "  
  382.                             + "ExtendedSqlMapClient: " + sqlMapClient);  
  383.         }  
  384.         ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient) sqlMapClient;  
  385.         transactionConfig.setMaximumConcurrentTransactions(extendedClient.getDelegate()  
  386.                 .getMaxTransactions());  
  387.         extendedClient.getDelegate().setTxManager(  
  388.                 new TransactionManager(transactionConfig));  
  389.     }  
  390.   
  391.     public SqlMapClient getObject() {  
  392.         return this.sqlMapClient;  
  393.     }  
  394.   
  395.     public Class<? extends SqlMapClient> getObjectType() {  
  396.         return (this.sqlMapClient != null ? this.sqlMapClient.getClass()  
  397.                 : SqlMapClient.class);  
  398.     }  
  399.   
  400.     public boolean isSingleton() {  
  401.         return true;  
  402.     }  
  403.   
  404.     /** 
  405.      * Inner class to avoid hard-coded iBATIS 2.3.2 dependency (XmlParserState 
  406.      * class). 
  407.      */  
  408.     private static class SqlMapParserFactory {  
  409.   
  410.         public static SqlMapParser createSqlMapParser(SqlMapConfigParser configParser) {  
  411.             // Ideally: XmlParserState state = configParser.getState();  
  412.             // Should raise an enhancement request with iBATIS...  
  413.             XmlParserState state = null;  
  414.             try {  
  415.                 Field stateField = SqlMapConfigParser.class.getDeclaredField("state");  
  416.                 stateField.setAccessible(true);  
  417.                 state = (XmlParserState) stateField.get(configParser);  
  418.             } catch (Exception ex) {  
  419.                 throw new IllegalStateException(  
  420.                         "iBATIS 2.3.2 'state' field not found in SqlMapConfigParser class - "  
  421.                                 + "please upgrade to IBATIS 2.3.2 or higher in order to use the new 'mappingLocations' feature. "  
  422.                                 + ex);  
  423.             }  
  424.             return new SqlMapParser(state);  
  425.         }  
  426.     }  
  427.   
  428. }  

 

修改Spring配置文件

Java代码    收藏代码
  1. <bean id="sqlMapClient" class="com.foo.xxx.MySqlMapClientFactoryBean">  
  2.   <property name="configLocation"  
  3. value="classpath:/com/foo/xxx/sqlMapConfig.xml" />  
  4.   <property name="dataSource" ref="dataSource" />  
  5.   <property name="mappingLocations" value="classpath*:/**/sqlmap/*SqlMap.xml" />  
  6. </bean>  

 
至此,cache又工作如初了。
编后:
·反射会消耗,但仅仅在初始化时一次性消耗,还可以接受。
·iBatis的cache能力比较弱,但没太多要求的情况下是个省事的方案,聊胜于无。
·Mybatis3.0的cache暂时不用为好,EhCache选项本身还有bug,实在很无语。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值