【AOP】通过AOP实现MyBatis多数据源的动态切换

         以前转载过一个关于多数据库的文章,写的很好,也很不多,是在方法上自动切换数据库。方便快捷。

        在后面的工作中,有一个项目也需要用到多数据库,但是是在一个方法中,里面涉及到两个查询,可能还要和线程进行绑定。这就涉及到在查询的时候切换数据库。这个文章写的也很不错。现在分享给大家。

        【环境参数】
1.开发框架:Spring + SpringMVC + MyBatis
2.数据库A的URL:jdbc.url=jdbc:mysql://172.16.17.164:3306/ test?characterEncoding=UTF-8&useUnicode=TRUE&autoReconnect=true&failOverReadOnly=false
3.数据库B的URL:bakdb.jdbc.url=jdbc:mysql://172.16.17.68:3306/bakDB?characterEncoding=UTF-8&useUnicode=TRUE&autoReconnect=true&failOverReadOnly=false


【需求描述】
(1)当用户调用X方法“之前”,系统会首先切换当前数据源为A数据源(bakDb数据库),之后再去调用方法X。
(2)当用户调用Y方法“之前”,系统会首先切换当前的数据源为B数据源(testDb数据库),之后再去调用方法Y。
(3)X方法和Y方法所在的包名
    3.1) X方法:该方法位于com.zjrodger.bakdata.service包下其子包下。
    3.2) Y方法:该方法位于com.zjrodger.datatobank.service或者com.zjrodger.zxtobank.service包及其子包下。

【具体步骤】
1、编写动态数据源相关代码。
(1) 编写DynamicDataSource类。
DynamicDataSource的主要作用是以Map的形式,来存储多个数据源。
因为该类继承了父类AbstractRoutingDataSource,在父类中,多数据源的实例是被存放在一个名为“targetDataSource”的Map类型的成员变量中。

点击(此处)折叠或打开

  1. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

  2. public class DynamicDataSource extends AbstractRoutingDataSource {

  3.     @Override
  4.     protected Object determineCurrentLookupKey() {
  5.         return DatabaseContextHolder.getDbType();
  6.     }
  7. }
(2) 编写DatabaseContextHolder类。

点击(此处)折叠或打开

  1. public class DatabaseContextHolder {

  2.     private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

  3.     public static void setDbType(String dataSourceType) {
  4.         contextHolder.set(dataSourceType);
  5.     }

  6.     public static String getDbType() {
  7.         return contextHolder.get();
  8.     }

  9.     public static void clearDbType() {
  10.         contextHolder.remove();
  11.     }
  12. }

2、编写切换数据源的拦截器。

点击(此处)折叠或打开

  1. public class DataSourceInterceptor {
  2.     
  3.     /** 数据源切换常量 */
  4.     public static final String DATASOURCE_TEST_DB="dataSourceKey4TestDb";
  5.     public static final String DATASOURCE_BAK_DB="dataSourceKey4BakDb";

  6.     /**
  7.      * 设置数据源为test数据库所对应的数据源。
  8.      * @param jp
  9.      */
  10.     public void setdataSourceTestDb(JoinPoint jp) {
  11.         DatabaseContextHolder.setDbType(DATASOURCE_TEST_DB);
  12.     }
  13.     
  14.     /**
  15.      * 设置数据源为bak数据库所对应的数据源。
  16.      * @param jp
  17.      */
  18.     public void setdataSourceBakDb(JoinPoint jp) {
  19.         DatabaseContextHolder.setDbType(DATASOURCE_BAK_DB);
  20.     }
  21. }
3、在Spring配置文件中进行相关配置。
(1)配置两个数据源
A.第一个数据源:

点击(此处)折叠或打开

  1. <bean id="c3p0DataSource4BakDb" class="com.mchange.v2.c3p0.ComboPooledDataSource"
  2.     destroy-method="close" depends-on="propertyConfigurer">
  3.     <property name="driverClass" value="${bakdb.jdbc.driverclass}" />
  4.     <property name="jdbcUrl" value="${bakdb.jdbc.url}" />
  5.     <property name="user" value="${bakdb.jdbc.username}" />
  6.     <property name="password" value="${bakdb.jdbc.password}" />

  7.     <!-- 初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
  8.     <property name="initialPoolSize" value="10" />
  9.     <!-- 连接池中保留的最小连接数。 -->
  10.     <property name="minPoolSize" value="5" />
  11.     <!-- 连接池中保留的最大连接数。Default: 15 -->
  12.     <property name="maxPoolSize" value="100" />
  13.     <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
  14.     <property name="acquireIncrement" value="5" />
  15.     <!-- 最大空闲时间,10秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
  16.     <property name="maxIdleTime" value="10" />
  17.     <!-- JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 
  18.         如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0 -->
  19.     <property name="maxStatements" value="0" />
  20.     <!-- 连接池用完时客户调用getConnection()后等待获取连接的时间,单位:毫秒。超时后会抛出 SQLEXCEPTION,如果设置0,则无限等待。Default:0 -->
  21.     <property name="checkoutTimeout" value="30000" />
  22. </bean>
B.第二个数据源:

点击(此处)折叠或打开

  1. <bean id="c3p0DataSource4TestDb" class="com.mchange.v2.c3p0.ComboPooledDataSource"
  2.     destroy-method="close" depends-on="propertyConfigurer">
  3.     <property name="driverClass" value="${jdbc.driverclass}" />
  4.     <property name="jdbcUrl" value="${jdbc.url}" />
  5.     <property name="user" value="${jdbc.username}" />
  6.     <property name="password" value="${jdbc.password}" />

  7.     <!-- 初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
  8.     <property name="initialPoolSize" value="10" />
  9.     <!-- 连接池中保留的最小连接数。 -->
  10.     <property name="minPoolSize" value="5" />
  11.     <!-- 连接池中保留的最大连接数。Default: 15 -->
  12.     <property name="maxPoolSize" value="100" />
  13.     <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
  14.     <property name="acquireIncrement" value="5" />
  15.     <!-- 最大空闲时间,10秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
  16.     <property name="maxIdleTime" value="10" />
  17.     <!-- JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 
  18.         如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0 -->
  19.     <property name="maxStatements" value="0" />
  20.     <!-- 连接池用完时客户调用getConnection()后等待获取连接的时间,单位:毫秒。超时后会抛出 SQLEXCEPTION,如果设置0,则无限等待。Default:0 -->
  21.     <property name="checkoutTimeout" value="30000" />
  22. </bean>

(2)两个数据源所对应的properties属性文件

点击(此处)折叠或打开

  1. =========== Test数据库相关信息 ============
  2. jdbc.url=jdbc:mysql://172.16.17.164:3306/ test?characterEncoding=UTF-8&amp;useUnicode=TRUE&amp;autoReconnect=true&amp;failOverReadOnly=false
  3. jdbc.username=root
  4. jdbc.password=123456
  5. jdbc.driverclass=com.mysql.jdbc.Driver
  6. jdbc.ip=172.16.5.64
  7. jdbc.dbname=test


  8. =========== BakDB数据库相关信息 ============
  9. bakdb.jdbc.url=jdbc:mysql://172.16.17.68:3306/bakDB?characterEncoding=UTF-8&amp;useUnicode=TRUE&amp;autoReconnect=true&amp;failOverReadOnly=false
  10. bakdb.jdbc.username=root
  11. bakdb.jdbc.password=123456
  12. bakdb.jdbc.driverclass=com.mysql.jdbc.Driver
  13. bakdb.jdbc.ip=172.16.17.68
  14. bakdb.jdbc.dbname=bakDB

(3)配置DynamicDataSource这个Bean(关键)。
该DynamicDataSource的主要作用是以Map的形式,来存储多个数据源。

点击(此处)折叠或打开

  1. <!-- 配置可以存储多个数据源的Bean -->
  2. <bean id="dataSource" class="com.beebank.pub.datasource.DynamicDataSource">
  3.     <property name="targetDataSources">
  4.         <map key-type="java.lang.String">
  5.             <entry key="dataSourceKey4TestDb" value-ref="c3p0DataSource4TestDb" />
  6.             <entry key="dataSourceKey4BakDb" value-ref="c3p0DataSource4BakDb" />
  7.         </map>
  8.     </property>
  9.     <property name="defaultTargetDataSource" ref="c3p0DataSource4HuihangDb" />
  10. </bean> 

  11. 配置dataSource这个Bean

(4)配置DataSourceInterceptor这个Bean(关键)。

点击(此处)折叠或打开

  1. <!-- 配置切换数据源Key的拦截器 -->
  2. <bean id="dataSourceInterceptor" class="com.zjrodger.pub.datasource.DataSourceInterceptor"/>

(5)利用AOP,配置控制数据源在特定条件下切换的切面(
关键,重要 )。
注意要添加aop名字空间。
配置Spring事务切面和自定义切面类,动态切换数据源,注意两切面的执行顺序。

点击(此处)折叠或打开

  1. <!-- 1.配置Spring框架自身提供的切面类 -->
  2. <tx:advice id="userTxAdvice" transaction-manager="transactionManager">
  3.     <tx:attributes>
  4.         <tx:method name="delete*" propagation="REQUIRED" read-only="false"
  5.             rollback-for="java.lang.Exception" no-rollback-for="java.lang.RuntimeException" />
  6.         <tx:method name="insert*" propagation="REQUIRED" read-only="false"
  7.             rollback-for="java.lang.Exception" />
  8.         <tx:method name="update*" propagation="REQUIRED" read-only="false"
  9.             rollback-for="java.lang.Exception" />
  10.         <tx:method name="find*" propagation="SUPPORTS" />
  11.         <tx:method name="get*" propagation="SUPPORTS" />
  12.         <tx:method name="select*" propagation="SUPPORTS" />
  13.     </tx:attributes>
  14. </tx:advice>

  15. <!-- 2.配置用户自定义的切面,用于切换数据源Key -->
  16. <bean id="dataSourceInterceptor" class="com.zjrodger.pub.datasource.DataSourceInterceptor"></bean> 

  17. <!-- 3.(重要)配置Spring事务切面和自定义切面类,动态切换数据源,注意两切面的执行顺序 -->
  18. <aop:config> 
  19.     <!-- (1) Spring框架自身提供的切面 -->
  20.     <aop:advisor advice-ref="userTxAdvice" pointcut="execution(public * com.zjrodger.*.service..*.*(..))" order="2"/> 
  21.      
  22.     <!-- (2) 用户自定义的切面,根据切入点,动态切换数据源。 --> 
  23.     <aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor" order="1"> 
  24.         <aop:before method="setdataSourceBakDb" pointcut="execution(* com.zjrodger.bakdata.service..*.*(..))"/> 
  25.         <aop:before method="setdataSourceTestDb" pointcut="execution(* com.zjrodger.datatobank.service..*.*(..))"/>
  26.     </aop:aspect> 
  27. </aop:config>
5.1)   注意:
A.注意上述两个切面中的order属性的配置。
B.自定义切面类和Spring自带事务切面类(即元素)的执行的先后顺序要配置正确,否则就会导致导致数据源不能动态切换。
    在AOP中,当执行同一个切入点时,不同切面的执行先后顺序是由“每个切面的order属性”而定的,order越小,则该该切面中的通知越先被执行。
上述元素中,引用了两个切面类:“userTxAdvice类”和“dataSourceAspect类”,其中是Spring框架自定义的切面标签。
根据两个切面类order属性的定义,当程序执行时并且触发切入点后(即调用com.zjrodger.bakdata.service包及其子包下的方法),dataSourceAspect切面类中的setdatasourceBakDb()方通知法首先执行,之后才会执行userTxAdvice事务类中的相关通知方。

5.2) 配置文件作用说明
切面类“DataSourceInterceptor”中有两个方法:setdataSourceTestDb()方法和setdataSourceBakDb()。
1)当用户调用“com.zjrodger.bakdata.service”包及其子包下的方法X“之前”,系统会首先去调用setdataSourceBakDb()方法,设置当前数据源为bakDb的数据源,之后再去调用方法X。
2)当用户调用“com.zjrodger.datatobank.service”或者“com.zjrodger.zxtobank.service”包及其子包下的方法Y之前,系统会首先去调调用setdataSourceTestDb()方法,设置当前的数据源为testDb数据库的数据源,之后再去调用方法Y。

(6)完整的Spring配置文档

点击(此处)折叠或打开

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
  4.     xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
  5.     xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc"
  6.     xmlns:task="http://www.springframework.org/schema/task"
  7.     xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
  8.         http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd
  9.         http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  10.         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
  11.         http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
  12.         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

  13.     <!-- 该配置为自动扫描配置的包下所有使用@Controller注解的类 -->
  14.     <context:component-scan base-package="com.zjrodger" />

  15.     <bean id="propertyConfigurer"
  16.         class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  17.         <property name="location">
  18.             <value>classpath:properties/dbconfig.properties</value>
  19.         </property>
  20.         <property name="fileEncoding" value="utf-8" />
  21.     </bean>
  22.     
  23.     <!-- 备份库数据库数据源 -->
  24.     <bean id="c3p0DataSource4BakDb" class="com.mchange.v2.c3p0.ComboPooledDataSource"
  25.         destroy-method="close" depends-on="propertyConfigurer">
  26.         <property name="driverClass" value="${bakdb.jdbc.driverclass}" />
  27.         <property name="jdbcUrl" value="${bakdb.jdbc.url}" />
  28.         <property name="user" value="${bakdb.jdbc.username}" />
  29.         <property name="password" value="${bakdb.jdbc.password}" />

  30.         <!-- 初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
  31.         <property name="initialPoolSize" value="10" />
  32.         <!-- 连接池中保留的最小连接数。 -->
  33.         <property name="minPoolSize" value="5" />
  34.         <!-- 连接池中保留的最大连接数。Default: 15 -->
  35.         <property name="maxPoolSize" value="100" />
  36.         <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
  37.         <property name="acquireIncrement" value="5" />
  38.         <!-- 最大空闲时间,10秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
  39.         <property name="maxIdleTime" value="10" />
  40.         <!-- JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 
  41.             如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0 -->
  42.         <property name="maxStatements" value="0" />
  43.         <!-- 连接池用完时客户调用getConnection()后等待获取连接的时间,单位:毫秒。超时后会抛出 SQLEXCEPTION,如果设置0,则无限等待。Default:0 -->
  44.         <property name="checkoutTimeout" value="30000" />
  45.     </bean>
  46.     

  47.     <!--Test数据库数据源 -->
  48.     <bean id="c3p0DataSource4TestDb" class="com.mchange.v2.c3p0.ComboPooledDataSource"
  49.         destroy-method="close" depends-on="propertyConfigurer">
  50.         <property name="driverClass" value="${jdbc.driverclass}" />
  51.         <property name="jdbcUrl" value="${jdbc.url}" />
  52.         <property name="user" value="${jdbc.username}" />
  53.         <property name="password" value="${jdbc.password}" />

  54.         <!-- 初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
  55.         <property name="initialPoolSize" value="10" />
  56.         <!-- 连接池中保留的最小连接数。 -->
  57.         <property name="minPoolSize" value="5" />
  58.         <!-- 连接池中保留的最大连接数。Default: 15 -->
  59.         <property name="maxPoolSize" value="100" />
  60.         <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
  61.         <property name="acquireIncrement" value="5" />
  62.         <!-- 最大空闲时间,10秒内未使用则连接被丢弃。若为0则永不丢弃。Default: 0 -->
  63.         <property name="maxIdleTime" value="10" />
  64.         <!-- JDBC的标准参数,用以控制数据源内加载的PreparedStatements数量。但由于预缓存的statements 属于单个connection而不是整个连接池。所以设置这个参数需要考虑到多方面的因素。 
  65.             如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default: 0 -->
  66.         <property name="maxStatements" value="0" />
  67.         <!-- 连接池用完时客户调用getConnection()后等待获取连接的时间,单位:毫秒。超时后会抛出 SQLEXCEPTION,如果设置0,则无限等待。Default:0 -->
  68.         <property name="checkoutTimeout" value="30000" />
  69.     </bean>
  70.     
  71.     <!-- 配置可以存储多个数据源的Bean -->
  72.     <bean id="dataSource" class="com.zjrodger.pub.datasource.DynamicDataSource">
  73.         <property name="targetDataSources">
  74.             <map key-type="java.lang.String">
  75.                 <entry key="dataSourceKey4TestDb" value-ref="c3p0DataSource4TestDb" />
  76.                 <entry key="dataSourceKey4BakDb" value-ref="c3p0DataSource4BakDb" />
  77.             </map>
  78.         </property>
  79.         <property name="defaultTargetDataSource" ref="c3p0DataSource4TestDb" />
  80.     </bean> 

  81.     <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  82.         <!-- <property name="dataSource" ref="c3p0DataSource" /> -->
  83.         <property name="dataSource" ref="dataSource" />

  84.         <property name="mapperLocations" value="classpath*:com/zjrodger/**/dao/xml/*.xml" />
  85.         <!-- 添加分页插件 -->
  86.         <property name="plugins">
  87.             <list>
  88.                 <bean class="com.github.pagehelper.PageHelper">
  89.                     <property name="properties">
  90.                         <props>
  91.                             <prop key="dialect">mysql</prop>
  92.                             <prop key="offsetAsPageNum">true</prop>
  93.                             <prop key="rowBoundsWithCount">true</prop>
  94.                             <prop key="pageSizeZero">true</prop>
  95.                             <prop key="reasonable">true</prop>
  96.                         </props>
  97.                     </property>
  98.                 </bean>
  99.             </list>
  100.         </property>

  101.     </bean>

  102.     <bean id="transactionManager"
  103.         class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  104.         <!-- <property name="dataSource" ref="c3p0DataSource" /> -->
  105.         <property name="dataSource" ref="dataSource" />
  106.     </bean>

  107.     <!-- 注解驱动,使spring的controller全部生效 -->
  108.     <mvc:annotation-driven />
  109.     <!-- 注解驱动,是spring的task全部生效 -->
  110.     <task:annotation-driven />


  111.     <aop:aspectj-autoproxy expose-proxy="true" /> 
  112.     <tx:annotation-driven transaction-manager="transactionManager"/> 

  113.     <!-- Spring声明式事务切面 -->
  114.     <tx:advice id="userTxAdvice" transaction-manager="transactionManager">
  115.         <tx:attributes>
  116.             <tx:method name="delete*" propagation="REQUIRED" read-only="false"
  117.                 rollback-for="java.lang.Exception" no-rollback-for="java.lang.RuntimeException"/>
  118.             <tx:method name="insert*" propagation="REQUIRED" read-only="false"
  119.                 rollback-for="java.lang.Exception" />
  120.             <tx:method name="update*" propagation="REQUIRED" read-only="false"
  121.                 rollback-for="java.lang.Exception" />
  122.             <tx:method name="find*" propagation="SUPPORTS" />
  123.             <tx:method name="get*" propagation="SUPPORTS" />
  124.             <tx:method name="select*" propagation="SUPPORTS" />
  125.         </tx:attributes>
  126.     </tx:advice>

  127.     <aop:config> 
  128.         <!-- Spring框架自身提供的切面 -->
  129.         <aop:advisor advice-ref="userTxAdvice" pointcut="execution(public * com.zjrodger.*.service..*.*(..))" order="2"/> 
  130.     
  131.         <!-- 用户自定义的切面,根据切入点,动态切换数据源。 --> 
  132.         <aop:aspect id="dataSourceAspect" ref="dataSourceInterceptor" order="1"> 
  133.             <aop:before method="setdataSourceBakDb" pointcut="execution(* com.zjrodger.bakdata.service..*.*(..))"/> 
  134.             <aop:before method="setdataSourceTestDb" pointcut="execution(* com.zjrodger.datatobank.service..*.*(..))"/>
  135.             <aop:before method="setdataSourceTestDb" pointcut="execution(* com.zjrodger.zxtobank.service..*.*(..))"/> 
  136.         </aop:aspect> 
  137.     </aop:config>
  138.     
  139.     <!-- 配置切换数据源Key的拦截器 -->
  140.     <bean id="dataSourceInterceptor" class="com.zjrodger.pub.datasource.DataSourceInterceptor"></bean> 
  141.     
  142.     <!-- mybatis配置 -->
  143.     <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  144.         <property name="basePackage" value="com.zjrodger.pub.dao,com.zjrodger.zxtobank.dao,com.zjrodger.bakdata.dao" />
  145.         <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
  146.     </bean> 
  147. </beans>
至此,MyBatis多数据源的配置完毕,之后在自己的环境下进行测试,结果测试通过。
要特别注意自定义AOP切面与Spring自带的事务切面的执行顺序 ,即注意中的配置部分,否则,很容易会出现动态切换数据源失败的现象。

【参考连接】
1、《Spring中事务与aop的先后顺序问题》http://my.oschina.net/HuifengWang/blog/304188
2、Order属性决定了不同切面类中通知执行的先后顺序
http://www.cnblogs.com/zjrodger/p/5633922.html
3、不定义Order属性,通过切面类的定义顺序来决定通知执行的先后顺序
http://www.cnblogs.com/zjrodger/p/5633951.html


【其他注意事项】
读者如要转载,请标明出处和作者名,谢谢。
本文地址:http://www.cnblogs.com/zjrodger/p/5627878.html
作者名:zjrodger
作者博客地址01:http://space.itpub.net/25851087
作者博客地址02:http://www.cnblogs.com/zjrodger


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值