自定义持久层框架和mybatis源码分析

本文详细解读了自定义持久层框架的设计与实现,包括配置文件的优化、数据库连接管理、以及使用XML映射文件提高SQL操作效率。重点介绍了如何通过Druid连接池、动态代理和XMLConfigBuild解析配置,提升开发效率和数据库性能。
摘要由CSDN通过智能技术生成

自定义持久层框架和mybatis源码分析

JDBC问题分析:

1.数据库配置信息存在硬编码(修改源代码再打包部署)
2.频繁创建释放数据库连接
3.手动封装结果集 繁琐

自定义持久层框架:

1.创建Persistence IPersistence两个模块

Persistence是自定义持久层框架的具体实现。
IPersistence是用户端存放sqlMapConfig.xml和mapper.xml端。

2.IPersistence编写

1.sqlMapConfig.xml配置

<configuration>

    <dataSource>
        <property name="driverClass" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/test" />
        <property name="username" value="root" />
        <property name="password" value="root" />
    </dataSource>

    <mapper resoure="UserMapper.xml"></mapper>

</configuration>

2.mapper.xml配置

<mapper namespace="com.fq.dao.UserDao">


    <select id="selectOne" resultType="com.fq.pojo.User" paramType="com.fq.pojo.User">
        select * from user where username = #{username}
    </select>

    <select id="selectAll" resultType="com.fq.pojo.User">
        select * from user
    </select>

    <update id="update" paramType="com.fq.pojo.User" resultType="java.lang.Integer">
        update `user` set `username` = #{username} where `id` = #{id}
    </update>

    <insert id="insert" paramType="com.fq.pojo.User" resultType="java.lang.Integer">
        insert into `user` (`username`) values (#{username})
    </insert>

    <delete id="delete" paramType="com.fq.pojo.User" resultType="java.lang.Integer">
        delete from `user` where id = #{id}
    </delete>

</mapper>
3.Persistence编写

1.首先编写Resource类,获取sqlMapConfig.xml文件后将该文件加载成内存输入流

public class Resource {

    //使用类加载器动态加载文件 把文件保存在内存中
    public static InputStream getResourceAsStream(String path){
       return Resource.class.getClassLoader().getResourceAsStream(path);
    }

}

2.编写sqlSessionBulid类生产sqlSessionFactory并解析sqlMapConfig.xml和mapper.xml文件,把解析后的文件放在Configrution类中,并将Configrution向
sqlSessionFactory传递
sqlSessionBulid:

public class SqlSessionFactoryBuild {

    public SqlSessionFactory build (InputStream in) throws DocumentException {
        //创建XMLConfigBuild对象解析内存中的sqlMapConfig.xml
        XMLConfigBuild xmlConfigBuild = new XMLConfigBuild();
        Configuration configuration = xmlConfigBuild.parseConfig(in);

        //创建SqlSessionFactory
        SqlSessionFactory defaultSqlSessionFactory = new DefaultSqlSessionFactory(configuration);


        return defaultSqlSessionFactory;
    }


}

XMLConfigBuild解析sqlMapConfig.xml 并获取mapper属性使用XMLMapperBuild 类解析 :

public class XMLConfigBuild {

    private Configuration configuration;

    public XMLConfigBuild(){
        this.configuration = new Configuration();
    }

    /**
     *解析sqlMapConfig.xml
     */
    public Configuration parseConfig (InputStream inputStream) throws DocumentException {
        //使用dom4j对内存的xml解析
        Document document = new SAXReader().read(inputStream);
        //获取根节点属性<configuration>
        Element rootElement = document.getRootElement();
        //找到节点下的property属性//表示只要是子节点都可以找到
        List<Element> elementList = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        for (Element element : elementList) {
            //获取property的name value 并使用Properties保存key value
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.setProperty(name,value);
        }
         getConfiguration(properties);


        //创建XMLMapperBuild对象解析内存中的mapper.xml
        XMLMapperBuild xmLmapperBuild = new XMLMapperBuild(configuration);
        //获取<mapper>节点
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String resource = element.attributeValue("resoure");
            InputStream in = Resource.getResourceAsStream(resource);
            xmLmapperBuild.parse(in);
        }


        return configuration;
    }


    /**
     * 添加连接池 并给Configuration赋值
     */
    public void getConfiguration(Properties properties){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(properties.getProperty("driverClass"));
        druidDataSource.setUrl(properties.getProperty("url"));
        druidDataSource.setUsername(properties.getProperty("username"));
        druidDataSource.setPassword(properties.getProperty("password"));
        configuration.setDataSource(druidDataSource);
    }

}

3.XMLMapperBuild 解析mapper.xml文件

public class XMLMapperBuild {

    private Configuration configuration;

    public XMLMapperBuild(Configuration configuration){
        this.configuration = configuration;
    }

    /**
     * 解析mapper.xml 代码注释请参照 XMLConfigBuild
     * @param inputStream mapper.xml
     */
    public void parse (InputStream inputStream) throws DocumentException {
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        //获取<mapper>的namespace属性
        String namespace = rootElement.attributeValue("namespace");

        //查找
        //获取select标签
        List<Element> selectList = rootElement.selectNodes("//select");
        //存入mapperStatementMap中
        setConfigurationMapperStatement(selectList,namespace);

        //更新
        List<Element> updateList = rootElement.selectNodes("//update");
        setConfigurationMapperStatement(updateList,namespace);

        //插入
        List<Element> insertList = rootElement.selectNodes("//insert");
        setConfigurationMapperStatement(insertList,namespace);

        //删除
        List<Element> deleteList = rootElement.selectNodes("//delete");
        setConfigurationMapperStatement(deleteList,namespace);
    }


    /**
     * 把<select>标签中的属性获取到并添加到Configuration 中的mapperStatementMap属性里
     * @param elements 获取标签属性
     * @param namespace 命名空间
     */
    public void setConfigurationMapperStatement(List<Element> elements,String namespace){
        for (Element element : elements) {
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String paramType = element.attributeValue("paramType");
            String sql = element.getTextTrim();
            String key = namespace + "." + id;
            MapperStatement mapperStatement = new MapperStatement();
            mapperStatement.setId(id);
            mapperStatement.setParamType(paramType);
            mapperStatement.setResultType(resultType);
            mapperStatement.setSql(sql);
            configuration.getMapperStatementMap().put(key , mapperStatement);
        }
    }

}

4.创建sqlSessionFactory工厂类 生产具体的执行SqlSession 类和封装好的Configruation

public class DefaultSqlSessionFactory implements SqlSessionFactory{

    private Configuration configuration;

    public DefaultSqlSessionFactory(Configuration configuration) {
        this.configuration = configuration;
    }

    @Override
    public SqlSession openSession(){
       return new DefaultSqlSession(configuration);
    }

}

5.具体实现类SqlSession 该类封装一些定义好的select,insert等方法(传统开发模式),并使用动态代理实现不用实现类对接口的代理方式执行增删改查。selectOne实际调用的都是selectList,而增删改因为低层执行jdbc都是调用的是excuteUpdate();所以逻辑是一样的。

public class DefaultSqlSession implements SqlSession{

    private Configuration configuration;

    private SimpleExecutor simpleExecutor = new SimpleExecutor();

    public DefaultSqlSession(Configuration configuration){
        this.configuration = configuration;
    }
    @Override
    public <E> List<E> selectAll(String statementId, Object... obj) throws Exception {
        MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
        List<Object> list = simpleExecutor.query(configuration, mapperStatement, obj);
        return (List<E>) list;
    }

    @Override
    public <T> T selectOne(String statementId, Object... obj) throws Exception {
        List<Object> objects = selectAll(statementId, obj);
        if(objects.size()==1){
            return (T) objects.get(0);
        }else if(objects.size()==0){
            throw new SQLSyntaxErrorException("数据库没有你要查找的数据");
        }else {
            throw new RuntimeException("返回结果过多");
        }
    }



    @Override
    public Integer update(String statementId, Object... obj) throws Exception {
        MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
        Integer state = simpleExecutor.update(configuration,mapperStatement,obj);
        return state;
    }

    @Override
    public Integer insert(String statementId, Object... obj) throws Exception {
        MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
        Integer state = simpleExecutor.insert(configuration,mapperStatement,obj);
        return state;
    }

    @Override
    public Integer delete(String statementId, Object... obj) throws Exception {
        MapperStatement mapperStatement = configuration.getMapperStatementMap().get(statementId);
        Integer state = simpleExecutor.delete(configuration,mapperStatement,obj);
        return state;
    }

    /**
     *
     * @param mapperClass 传过来的接口
     * @param <T>
     * @return
     */
    @Override
    public <T> T getMapper(Class<?> mapperClass) {
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 底层都还是去执行JDBC代码 //根据不同情况,来调用selctList或者selectOne
                // 准备参数 1:statmentid :sql语句的唯一标识:namespace.id= 接口全限定名.方法名
                //获取方法名
                String methodName = method.getName();
                //获取类名
                String className = method.getDeclaringClass().getName();
                String statementId = className + "." + methodName;

                if(methodName.contains("update")){
                    Object o = update(statementId,args);
                    return o;
                }
                else if(methodName.contains("select")){
                    //判断方法返回的类型是否带泛型
                    Type genericReturnType = method.getGenericReturnType();
                    if (genericReturnType instanceof ParameterizedType) {
                        List<Object> objects = selectAll(statementId,args);
                        return objects;
                    }
                    Object o = selectOne(statementId, args);
                    return o;
                }else if(methodName.contains("insert")){
                   Object o =  insert(statementId,args);
                   return o;
                }else if(methodName.contains("delete")){
                    Object o =  delete(statementId,args);
                    return o;
                }
                return null;
            }
        });
        return (T) proxyInstance;
    }

}

6.具体执行sql类(Excutor),对结果集的封装,参数的设置,以及执行sql和解析标签中的占位符#{}

public class SimpleExecutor implements Executor {
    @Override
    public <E> List<E> query(Configuration configuration, MapperStatement mapperStatement, Object... obj) throws Exception {
         //获取连接
        Connection connection = getConnection(configuration);
        //2.获取MapperStatement对象存的sql信息
        String sql = mapperStatement.getSql();
        BoundSql parseSql = getParseSql(sql);
        //获取预处理对象
        PreparedStatement preparedStatement = connection.prepareStatement(parseSql.getSql());
        //4.获取参数类型
            //类全限定名
        String paramType = mapperStatement.getParamType();
            //获取类的字节码
        Class<?> paramClass = getParamClass(paramType);

        List<ParameterMapping> params = parseSql.getParams();
        for (int i = 0; i < params.size() ; i++) {
            ParameterMapping parameterMapping = params.get(i);
            String content = parameterMapping.getContent();
            Field declaredField = paramClass.getDeclaredField(content);
            declaredField.setAccessible(true);
            Object o = declaredField.get(obj[0]);
            //设置参数
            preparedStatement.setObject(i+1,o);
        }
        //5.执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        //封装结果集
            //获取返回值类型
        String resultType = mapperStatement.getResultType();
        Class<?> resultClass = getParamClass(resultType);
        ArrayList<Object> objects = new ArrayList<>();
        while (resultSet.next()){
            Object o = resultClass.newInstance();
            //获取元数据
            ResultSetMetaData metaData = resultSet.getMetaData();
            for (int i = 1; i <= metaData.getColumnCount(); i++) {
                String columnName = metaData.getColumnName(i);
                Object value = resultSet.getObject(columnName);
                //内省
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultClass);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                writeMethod.invoke(o,value);
            }
            objects.add(o);
        }
        close(preparedStatement,connection,resultSet);
        return (List<E>) objects;
    }

    @Override
    public Integer update(Configuration configuration, MapperStatement mapperStatement, Object[] obj) throws Exception {
        return common(configuration,mapperStatement,obj);
    }

    @Override
    public Integer insert(Configuration configuration, MapperStatement mapperStatement, Object[] obj) throws Exception {
        return common(configuration,mapperStatement,obj);
    }

    @Override
    public Integer delete(Configuration configuration, MapperStatement mapperStatement, Object[] obj) throws Exception {
        return common(configuration,mapperStatement,obj);
    }

    private Class<?> getParamClass(String paramType) throws ClassNotFoundException {
        //判断是否为null
        if(paramType != null){
           return Class.forName(paramType);
        }
        return null;
    }

    //解析占位符#{}
    private BoundSql getParseSql(String sql) {
        //创建解析器
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        GenericTokenParser genericTokenParser = new GenericTokenParser("#{", "}", parameterMappingTokenHandler);
        //把占位符转换为?
        String parseSql = genericTokenParser.parse(sql);
        //保存解析后的占位符里的内容
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        //用类保存sql信息
        BoundSql boundSql = new BoundSql(parseSql, parameterMappings);
        return boundSql;
    }

    /**
     * 获取connection
     */
    public Connection getConnection(Configuration configuration) throws SQLException {
        //注册驱动
        DataSource dataSource = configuration.getDataSource();
        //创建连接
        return dataSource.getConnection();

    }


    /**
     * 关流
     */
    public void close(Statement statement,Connection connection,ResultSet resultSet) throws SQLException {
        statement.close();
        connection.close();
        if(resultSet != null){
            resultSet.close();
        }
    }


    /**
     * update insert 共同方法
     */

    public Integer common(Configuration configuration, MapperStatement mapperStatement, Object... obj) throws Exception {
        Connection connection = getConnection(configuration);
        String sql = mapperStatement.getSql();
        BoundSql parseSql = getParseSql(sql);
        PreparedStatement preparedStatement = connection.prepareStatement(parseSql.getSql());
        String paramType = mapperStatement.getParamType();
        Class<?> paramClass = getParamClass(paramType);
        List<ParameterMapping> params = parseSql.getParams();
        for (int i = 0; i < params.size() ; i++) {
            ParameterMapping parameterMapping = params.get(i);
            String content = parameterMapping.getContent();
            Field declaredField = paramClass.getDeclaredField(content);
            declaredField.setAccessible(true);
            Object o = declaredField.get(obj[0]);
            //设置参数
            preparedStatement.setObject(i+1,o);
        }
        //5.执行sql
        Integer state = preparedStatement.executeUpdate();
        if(state != 1){
            throw new SQLSyntaxErrorException("你的sql有误");
        }
        close(preparedStatement,connection,null);
        return state;
    }
}

6.测试类 创建sqlSessionBulid,加载sqlMapConfig.xml配置文件,创建DefultSqlSessionFactory,创建SqlSession,执行getMapper方法。

    public static void main(String[] args) throws Exception {
        InputStream resourceAsStream = Resource.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactoryBuild sqlSessionFactoryBuild = new SqlSessionFactoryBuild();
        SqlSessionFactory factory = sqlSessionFactoryBuild.build(resourceAsStream);
        SqlSession sqlSession = factory.openSession();
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        userDao.selectAll();
        User user = new User();
        user.setId(1);
        userDao.delete(user);



//        User object = sqlSession.selectOne("com.fq.pojo.User.selectByUsername",user);
//        List<Object>  objects = sqlSession.selectAll("com.fq.pojo.User.selectAll");
//        System.out.println(objects);
    }
}

mybatis源码分析

1.SqlSessionFactoryBuilder

构建者模式 生产Configruation这个复杂对象,并创建DefaultSqlSessionFactory

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

使用dom4j解析(XMLConfigBuilder )

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        inputStream.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }
2.Configruation

将sqlMapConfigruation.xml文件解析到Configruation类中。mapper标签是保存mapper.xml中的文件解析到MappedStatement类中。而整个Configruation不仅保存配置的配置信息(数据库连接,别名,插件等)还保存一个protected final Map<String, MappedStatement> mappedStatements = new StrictMap(“Mapped Statements collection”);很多个mapper文件解析的标签(SqlCommandType(标签名select,update),参数类型paramType,返回类型resultType,sql信息,一二级缓存信息等)

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
3.DefaultSqlSessionFactory

openSession执行的方法如下(第一个参数是具体执行的Executor的类型,默认是SimpleExecutor,其他两个暂时不介绍,第二个参数是事务隔离级别,第三个参数是是否自动提交事务)

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
4.DefaultSqlSession的getMapper()方法

newProxyInstance方法第一个参数是当前执行类的类加载器,第二个参数是获取到的接口数组,第三个是InvocationHandler(动态代理JDK)接口并需要实现该接口的invoke方法。invoke方法的第一个参数是代理对象(很少用),第二个参数是被代理对象执行的方法,第三个参数是被执行代理对象参入的参数。返回的是该代理对象

    public <T> T getMapper(Class<?> mapperClass) {
        Object proxyInstance = Proxy.newProxyInstance(DefaultSqlSession.class.getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                return null;
            }
        });
        return (T) proxyInstance;
    }
5.Executor

Excetor是最后执行sql的类该类分为CacheExcetor和BaseExcetor两个实现类,和三个具体执行类(默认是SimpleExcetor)。CacheExcetor是一二级缓存对象类,若开启了一二级缓存先执行该类,但最终都是执行BaseExcetor类的SimpleExcetor。该类的select方法最终都是执行selectList方法。而增删改执行的都是update方法,因为jdbc底层增删改都是执行ExcuteUpdate方法。所以在update标签中写insert方法也可以

 @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
6.缓存

⼆级缓存构建在⼀级缓存之上,在收到查询请求时,MyBatis ⾸先会查询⼆级缓存,若⼆级缓存未命中,再去查询⼀级缓存,⼀级缓存没有,再查询数据库。⼆级缓存------》 ⼀级缓存------》数据库与⼀级缓存不同,⼆级缓存和具体的命名空间绑定,⼀个Mapper中有⼀个Cache,相同Mapper中的MappedStatement共⽤⼀个Cache,⼀级缓存则是和 SqlSession 绑定。
⼆级缓存是从 MappedStatement 中获取的。由于 MappedStatement 存在于全局配置中,可以多个 CachingExecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共⽤⼀个缓存实例,会导致脏读问题。⼆级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别
⼆级缓存具有丰富的缓存策略。⼆级缓存可由多个装饰器,与基础缓存组合⽽成⼆级缓存⼯作由 ⼀个缓存装饰执⾏器CachingExecutor和 ⼀个事务型预缓存TransactionalCache完成。

@Override
public void commit(boolean force) {
 try {
 // 主要是这句
 executor.commit(isCommitOrRollbackRequired(force));
⼆级缓存的刷新
我们来看看SqlSession的更新操作
 dirty = false;
 } catch (Exception e) {
 throw ExceptionFactory.wrapException("Error committing transaction. 
Cause: " + e, e);
 } finally {
 ErrorContext.instance().reset();
 }
}
// CachingExecutor.commit()
@Override
public void commit(boolean required) throws SQLException {
 delegate.commit(required);
 tcm.commit();// 在这⾥
}
// TransactionalCacheManager.commit()
public void commit() {
 for (TransactionalCache txCache : transactionalCaches.values()) {
 txCache.commit();// 在这⾥
 }
}
// TransactionalCache.commit()
public void commit() {
 if (clearOnCommit) {
 delegate.clear();
 }
 flushPendingEntries();//这⼀句
 reset();
}
// TransactionalCache.flushPendingEntries()
private void flushPendingEntries() {
 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
 // 在这⾥真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,⼆
级缓存才真正的⽣效
 delegate.putObject(entry.getKey(), entry.getValue());
 }
 for (Object entry : entriesMissedInCache) {
 if (!entriesToAddOnCommit.containsKey(entry)) {
 delegate.putObject(entry, null);
 }
 }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值