JDBC

今天我们来说一下关于JDBC的相关知识,关于JDBC我想大家都不陌生了,而且我记得早就开始使用它了,记得那是大二的时候做课程设计,但是那时候是为了完成任务,所以遇到问题就google,那时候也没有时间去整理,所以这次就来详细说一下关于JDBC的知识
摘要:

JDBC(Java Data Base Connectivity,java数据库连接),由一些接口和类构成的API。
J2SE的一部分,由java.sql,javax.sql包组成。

应用程序、JDBC API、数据库驱动及数据库之间的关系

JDBC的使用步骤
1.注册驱动 (只做一次)

方式一:Class.forName(“com.mysql.jdbc.Driver”);
推荐这种方式,不会对具体的驱动类产生依赖。
方式二:DriverManager.registerDriver(com.mysql.jdbc.Driver);
会造成DriverManager中产生两个一样的驱动,并会对具体的驱动类产生依赖。
方式三:System.setProperty(“jdbc.drivers”, “driver1:driver2”);
虽然不会对具体的驱动类产生依赖;但注册不太方便,所以很少使用。
驱动类型(四种类型)

2.建立连接(Connection)

[java] view plain copy
在CODE上查看代码片派生到我的代码片

Connection conn = DriverManager.getConnection(url, user, password);  

url格式:
JDBC:子协议:子名称//主机名:端口/数据库名?属性名=属性值&…
User,password可以用“属性名=属性值”方式告诉数据库;
其他参数如:useUnicode=true&characterEncoding=GBK。

3.创建执行SQL的语句(Statement)

[java] view plain copy
在CODE上查看代码片派生到我的代码片

Statement  
Statement st = conn.createStatement();  
st.executeQuery(sql);  
PreparedStatement  
String sql = “select * from table_name where col_name=?”;  
PreparedStatement ps = conn.preparedStatement(sql);  
ps.setString(1, “col_value”);  
ps.executeQuery();  

4.处理执行结果(ResultSet)

[java] view plain copy
在CODE上查看代码片派生到我的代码片

ResultSet rs = statement.executeQuery(sql);  
While(rs.next()){  
rs.getString(“col_name”);  
rs.getInt(“col_name”);  
//…  
}  

5.释放资源
释放ResultSet, Statement,Connection.
数据库连接(Connection)是非常稀有的资源,用完后必须马上释放,如果Connection不能及时正确的关闭将导致系统宕机。Connection的使用原则是尽量晚创建,尽量早的释放。

下面来看一下完整的Demo:

工具类:JdbcUtils

[java] view plain copy
在CODE上查看代码片派生到我的代码片

package com.weijia.firstdemo;  

import java.sql.Connection;  
import java.sql.DriverManager;  
import java.sql.ResultSet;  
import java.sql.SQLException;  
import java.sql.Statement;  

import javax.sql.DataSource;  

public class JdbcUtils {  

    private static String user = "root";  
    private static String password = "123456";  
    private static String dbName = "test";  
    private static  String url = "jdbc:mysql://localhost:3306/"+dbName+"?user="+user+"&password="+password+"&useUnicode=true&characterEncoding=gb2312";  

    private static DataSource dataSource = null;  

    /** 
     * 加载驱动 
     */  
    static{  
        try{  
            Class.forName("com.mysql.jdbc.Driver");  
        }catch(Exception e){  
            System.out.println("Exception:"+e.getMessage()+"");  
            throw new ExceptionInInitializerError(e);  
        }  
    }  

    private JdbcUtils(){  
    }  

    /** 
     * 获取连接 
     * @return 
     * @throws SQLException 
     */  
    public static Connection getConnection() throws SQLException{  
        return DriverManager.getConnection(url);  
    }  

    public static DataSource getDataSource(){  
        return dataSource;  
    }  

    /** 
     * 释放资源 
     * @param rs 
     * @param st 
     * @param conn 
     */  
    public static void free(ResultSet rs,Statement st,Connection conn){  
        try{  
            if(rs != null){  
                rs.close();  
            }  
        }catch(SQLException e){  
            e.printStackTrace();  
        }finally{  
            try{  
                if(st != null){  
                    st.close();  
                }  
            }catch(SQLException e){  
                e.printStackTrace();  
            }finally{  
                try{  
                    if(conn != null){  
                        conn.close();  
                    }  
                }catch(SQLException e){  
                    e.printStackTrace();  
                }  
            }  
        }  

    }  

}  

测试类:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

package com.weijia.firstdemo;  

import java.sql.Connection;  
import java.sql.DriverManager;  
import java.sql.ResultSet;  
import java.sql.SQLException;  
import java.sql.Statement;  

public class Demo {  

    public static void main(String[] args) throws Exception{  
        //测试代码:  
        test();  
        //标准规范代码:  
        template();  
    }  

    //模板代码  
    public static void template(){  
        Connection conn = null;  
        Statement st = null;  
        ResultSet rs = null;  
        try {  
            conn = JdbcUtils.getConnection();  
            //创建语句  
            st = conn.createStatement();  
            //执行语句  
            rs = st.executeQuery("select * from user");  
            //处理结果  
            while(rs.next()){  
                System.out.println(rs.getObject(1) + "\t" + rs.getObject(2) + "\t" + rs.getObject(3) + "\t");  
            }  
        }catch(SQLException e){  
            e.printStackTrace();  
        }catch(Exception e){  
            e.printStackTrace();  
        }finally{  
            JdbcUtils.free(rs, st, conn);  
        }  
    }  

    //测试  
    static void test() throws Exception{  
        //注册驱动  
        DriverManager.registerDriver(new com.mysql.jdbc.Driver());  
        //通过系统属性来注册驱动  
        System.setProperty("jdbc.drivers","");  
        //静态加载驱动  
        Class.forName("com.mysql.jdbc.Driver");  

        //建立连接  
        String url = "jdbc:mysql://localhost:3306";  
        String userName = "root";  
        String password = "";  
        Connection conn = DriverManager.getConnection(url,userName,password);  

        //创建语句  
        Statement st = conn.createStatement();  

        //执行语句  
        ResultSet rs = st.executeQuery("select * from user");  

        //处理结果  
        while(rs.next()){  
            System.out.println(rs.getObject(1) + "\t" + rs.getObject(2) + "\t" + rs.getObject(3) + "\t");  
        }  

        //释放资源  
        rs.close();  
        st.close();  
        conn.close();  
    }  

}  

注意:这里还要记住引入额外的jar.这个网上很多的,这里使用的是MySql,搜一下MySql驱动的jar就行了。这里我们将一些操作都放到一个工具类中,这种方式是很优雅的。

使用JDBC来实现CRUD的操作

我们这里就采用分层操作:Dao层,Service层

首先看一下domain域中的User实体

[java] view plain copy
在CODE上查看代码片派生到我的代码片

package com.weijia.domain;  

import java.util.Date;  

public class User {  

    private int id;  
    private String name;  
    private Date birthday;  
    private float money;  

    public User(){  

    }  

    public User(int id,String name,Date birthday,float money){  
        this.id = id;  
        this.name = name;  
        this.birthday = birthday;  
        this.money = money;  
    }  

    public int getId() {  
        return id;  
    }  
    public void setId(int id) {  
        this.id = id;  
    }  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public Date getBirthday() {  
        return birthday;  
    }  
    public void setBirthday(Date birthday) {  
        this.birthday = birthday;  
    }  
    public float getMoney() {  
        return money;  
    }  
    public void setMoney(float money) {  
        this.money = money;  
    }  

    @Override  
    public String toString(){  
        return "[id="+id+",name="+name+",birthday="+birthday+",money="+money+"]";  
    }  
}  

再来看一下Dao层结构:

接口:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

package com.weijia.domain;  

public interface UserDao {  

    //添加用户  
    public void addUser(User user);  
    //通过userid查询用户,id是唯一的,所以返回的是一个user  
    public User getUserById(int userId);  
    //更新用户信息  
    public int update(User user);  
    //删除用户信息  
    public int delete(User user);  

}  

实现类:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

package com.weijia.domain;  

import java.sql.Connection;  
import java.sql.Date;  
import java.sql.PreparedStatement;  
import java.sql.ResultSet;  

import com.weijia.firstdemo.JdbcUtils;  

public class UserDaoImpl implements UserDao{  

    /** 
     * 添加用户 
     */  
    public void addUser(User user) {  
        Connection conn = null;  
        PreparedStatement st = null;  
        try{  
            conn = JdbcUtils.getConnection();  
            String sql = "insert into user(id,name,birthday,money) values(?,?,?,?)";  
            st = conn.prepareStatement(sql);  
            st.setInt(1,user.getId());  
            st.setString(2,user.getName());  
            //日期格式的转换(utils.date转化成sql.date)  
            st.setDate(3,new Date(user.getBirthday().getTime()));  
            st.setFloat(4, user.getMoney());  
            int count = st.executeUpdate();  
            System.out.println("添加记录条数:"+count);  
        }catch(Exception e){  
            throw new DaoException(e.getMessage(),e);  
        }finally{  
            JdbcUtils.free(null, st, conn);  
        }  
    }  

    /** 
     * 删除用户 
     */  
    public int delete(User user) {  
        Connection conn = null;  
        PreparedStatement st = null;  
        try{  
            conn = JdbcUtils.getConnection();  
            String sql = "delete from user where id=?";  
            st = conn.prepareStatement(sql);  
            st.setInt(1,user.getId());  
            int count = -1;  
            count = st.executeUpdate();  
            System.out.println("删除记录条数:"+count);  
            return count;  
        }catch(Exception e){  
            throw new DaoException(e.getMessage(),e);  
        }finally{  
            JdbcUtils.free(null, st, conn);  
        }  
    }  

    /** 
     * 通过userId获取用户信息 
     */  
    public User getUserById(int userId) {  
        Connection conn = null;  
        PreparedStatement st = null;  
        ResultSet rs = null;  
        try{  
            conn = JdbcUtils.getConnection();  
            String sql = "select * from user where id=?";  
            st = conn.prepareStatement(sql);  
            st.setInt(1,userId);  
            rs = st.executeQuery();  
            if(rs.next()){  
                User user = new User();  
                user.setId(userId);  
                user.setName(rs.getString("name"));  
                user.setBirthday(rs.getDate("birthday"));  
                user.setMoney(rs.getFloat("money"));  
                return user;  
            }  
        }catch(Exception e){  
            throw new DaoException(e.getMessage(),e);  
        }finally{  
            JdbcUtils.free(rs, st, conn);  
        }  
        return null;  
    }  

    /** 
     * 更新用户信息 
     */  
    public int update(User user){  
        Connection conn = null;  
        PreparedStatement st = null;  
        try{  
            conn = JdbcUtils.getConnection();  
            String sql = "update user set name=?,birthday=?,money=? where id=?";  
            st = conn.prepareStatement(sql);  
            st.setString(1,user.getName());  
            st.setDate(2,new Date(user.getBirthday().getTime()));  
            st.setFloat(3,user.getMoney());  
            st.setInt(3,user.getId());  
            int count = 0;  
            count = st.executeUpdate();  
            System.out.println("更新的记录数:"+count);  
            return count;  
        }catch(Exception e){  
            throw new DaoException(e.getMessage(),e);  
        }finally{  
            JdbcUtils.free(null, st, conn);  
        }  
    }  

}  

然后是Servic层:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

package com.weijia.domain;  

public class UserService {  

    private UserDao userDao;  

    public UserService(){  
        //通过工厂实例化UserDao对象  
        userDao = DaoFactory.getInstance().createUserDao();  
        System.out.println("userDao:"+userDao);  
    }  

    /** 
     * 注册用户 
     * @param user 
     */  
    public void regist(User user){  
        if(user == null){  
            System.out.println("注册信息无效!!");  
        }else{  
            userDao.addUser(user);  
        }  

    }  

    /** 
     * 查询用户 
     * @param userId 
     * @return 
     */  
    public User query(int userId){  
        User user = userDao.getUserById(userId);  
        if(user == null){  
            System.out.println("查询结果为空!!");  
        }else{  
            System.out.println(user.getId()+"\t"+user.getName()+"\t"+user.getBirthday()+"\t"+user.getMoney());  
        }  
        return userDao.getUserById(userId);  
    }  

    /** 
     * 更新用户 
     * @param user 
     */  
    public void update(User user){  
        if(user.getId()<=0){  
            System.out.println("用户id无效,无法更新");  
        }else{  
            userDao.update(user);  
        }  
    }  

    /** 
     * 删除用户 
     * @param user 
     */  
    public void delete(User user){  
        if(user.getId()<=0){  
            System.out.println("用户id无效,无法删除!!");  
        }else{  
            userDao.delete(user);  
        }  
    }  

}  

这里我们还需要额外的两个类:

一个是异常类,因为我们需要自定义我们自己的一个异常,这样方便进行捕获:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

package com.weijia.domain;  

public class DaoException extends RuntimeException{  

    private static final long serialVersionUID = 1L;  

    public DaoException(){  

    }  

    public DaoException(Exception e){  
        super(e);  
    }  

    public DaoException(String msg){  
        super(msg);  
    }  

    public DaoException(String msg,Exception e){  
        super(msg,e);  
    }  

}  

同时,我们这里面采用工厂模式进行实例化UserDao对象:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

package com.weijia.domain;  

import java.io.FileInputStream;  
import java.util.Properties;  

public class DaoFactory {  
    /** 
     * 单例模式 
     */  
    private static UserDao userDao = null;  
    private static DaoFactory instance = new DaoFactory();  

    private DaoFactory(){  
        /** 
         * 通过读取属性文件来动态的加载Dao层类 
         */  
        Properties prop = new Properties();  
        try{  
            FileInputStream fis = new FileInputStream("src/com/weijia/domain/daoconfig.properties");  
            prop.load(fis);  
            String className = prop.getProperty("userDaoClass");  
            Class<?> clazz = Class.forName(className);  
            userDao = (UserDao)clazz.newInstance();  
            fis.close();  
        }catch(Throwable e){  
            throw new ExceptionInInitializerError(e);  
        }  
    }  

    public static DaoFactory getInstance(){  
        return instance;  
    }  

    public UserDao createUserDao(){  
        return userDao;  
    }  

}  

这里面是读取properties文件,然后去读取类名进行加载,这种方式是很灵活的

测试:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

package com.weijia.domain;  

import java.util.Date;  

public class TestDemo {  

    public static void main(String[] args) throws Exception{  
        UserService userService = new UserService();  
        System.out.println("添加用户:");  
        userService.regist(new User(1,"jiangwei",new Date(System.currentTimeMillis()),300));  
    }  

}  

这里我们看到其实这些操作真的很简单,就是按照那样的几个步骤来操作即可,同时我们还需要将结构进行分层,以便管理,我们这里面测试的时候,撇开了创建数据库的一个环节,至于那个环节,也是不难的,可以从网上搜索一下即可。

Statement中的sql依赖注入的问题

接着来看一下关于我们上面的例子中使用了Statement进行操作的,其实这里面是存在一个问题的,就是会有sql注入的问题,我们先来看一下这个问题:

查询学生信息:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

/** 
     * 使用Statement读取数据 
     * @param name 
     * @throws SQLException 
     */  
    static void read(String name) throws SQLException{  
        Connection conn = null;  
        Statement st = null;  
        ResultSet rs = null;  
        try {  
            conn = JdbcUtils.getConnection();  
            //创建语句  
            st = conn.createStatement();  
            //执行语句(不建议使用*)  
            String sql = "select id,name from user where name='"+name+"'";  
            rs = st.executeQuery(sql);  
            //根据列名取数据  
            while(rs.next()){  
                System.out.println(rs.getObject("id") + "\t" + rs.getObject("name") + "\t");  
            }  
        }catch(SQLException e){  
            e.printStackTrace();  
        }catch(Exception e){  
            e.printStackTrace();  
        }finally{  
            JdbcUtils.free(rs, st, conn);  
        }  
    }  

我们使用代码测试一下:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

read("'or 1 or'");  

我们运行会发现,将查询出所有的学生的记录,这个是什么原因呢?我们不妨将sql打印一下会发现:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

select id,name from user where name=''or 1 or''  

擦,因为sql语句中把1认为是true,又因为是或的关系,所以将所有的学生的信息查询出来了,这个就是sql注入,因为Statement会把传递进来的参数进行一下转化操作,用引号包含一下,所以会出现这个问题,那么我们该怎么解决呢?有的同学说我们可以添加一句过滤的代码,将传递的参数取出单引号,这个方法是可行的的,但是这个只能解决那些使用单引号的数据库,可能有的数据库使用的是双引号包含内容,那就不行了,所以应该想一个全套的方法,那么这里我们就是用一个叫做:PreparedStatement类,这个类是Statement类的子类,关于这两个类的区别可以查看我的另外一片文章:

http://blog.csdn.net/jiangwei0910410003/article/details/26143977

我们这里只看这个sql注入的问题:

我们将上面读取用户信息的代码改写成PreparedStatement:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

/** 
     * 使用PreparedStatement 
     * @param name 
     * @throws SQLException 
     */  
    static void readPrepared(String name) throws SQLException{  
        Connection conn = null;  
        PreparedStatement st = null;  
        ResultSet rs = null;  
        try{  
            conn = JdbcUtils.getConnection();  
            //执行语句(不建议使用*)  
            String sql = "select id,name from user where name=?";  
            //创建语句  
            st = conn.prepareStatement(sql);  
            st.setString(1, name);  
            rs = st.executeQuery();  
            //根据列名取数据  
            while(rs.next()){  
                System.out.println(rs.getObject("id") + "\t" + rs.getObject("name") + "\t");  
            }  
        }catch(Exception e){  

        }  
    }  

之后我们在执行:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

readPrepared("'or 1 or'");  

就不会全部查出来了,只会查询空结果,因为表中没有一个学生的名字叫做 ‘or 1 or’。

JDBC中特殊数据类型的操作问题
第一个是日期问题:

我们在操作日期问题的时候会发现,使用PreparedStatement进行参数赋值的时候,有一个方法是:setDate(…),但是这个方法接收的参数是sql中的Date类,而不是我们平常使用的util中的Date类,所以我们要做一次转化,通常我们是这样做的,就是在定义实体类的时候将其日期型的属性定义成util中的Date类型,在进行数据库操作的时候.

进行一次转换:setDate(x,new Date(birthday.getTime());,这里birthday就是一个util.Date类型的一个属性,而new Date是sql.Date类型的,这样转化就可以了,同样我们在读取数据的时候将转化操作反过来即可。

第二个问题就是大文本数据的问题

因为有时候我们会存入一些文本内容,因为varchar的大小在mysql中也是有上线的,所以我们这里要使用blob类型了,我们这里来看一下实例:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

/** 
     * 插入大文本 
     */  
    static void insert(){  
        Connection conn = null;  
        PreparedStatement ps = null;  
        ResultSet rs = null;  
        try{  
            conn = JdbcUtils.getConnection();  
            String sql = "insert into clob_test(bit_text) values(?)";  
            ps = conn.prepareStatement(sql);  
            File file = new File("src/com/weijia/type/ClubDemo.java");  
            Reader reader = new BufferedReader(new FileReader(file));  
            //ps.setAsciiStream(1, new FileInputStream(file), (int)file.length());//英文的文档  
            ps.setCharacterStream(1, reader, (int)file.length());  
            ps.executeUpdate();  
            reader.close();  
        }catch(Exception e){  
            e.printStackTrace();  
        }finally{  
            JdbcUtils.free(rs,ps,conn);  
        }  
    }  

我们将一个Java代码文件插入到数据库中

我们查询一下clob_test表:

我们看到文件内容存入到库中了。同样我们也可以从表中读取一段文本出来,使用

[java] view plain copy
在CODE上查看代码片派生到我的代码片

Clob clob = rs.getClob(1);  
InputStream is = clob.getAsciiStream();  

或者读取一个Reader也是可以的,这里的InputStream和Reader是针对不同流,一个字节流,这个不需要关心编码问题的,Reader是字符流需要关心编码问题。

JDBC中事务的概念

我们当初在学习数据库的时候就了解事务的概念了,事务在数据库中的地位是很重要的。在JDBC中默认情况事务是自动提交的,所以我们在进行CRUD操作的时候不需要关心开启事务,提交事务,事务回滚的一些操作,那么下面我们就来看一下怎么手动的操作一些事务:

下载我们假定这样的一个场景:

有来两个用户1和2,现在

将用户1中的账户的钱减少10
查询用户2中的账户的钱,如果钱少于300,就增加10,否则抛出异常

看一下代码:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

static void test() throws Exception{  
        Connection conn = null;  
        Statement st = null;  
        ResultSet rs = null;  
        try{  
            conn = JdbcUtils.getConnection();  
            /**************事务START********************/  
            conn.setAutoCommit(false);  
            st = conn.createStatement();  

            String sql = "update user set money=money-10 where id=1";  
            st.executeUpdate(sql);  

            sql = "select money from user where id=2";  
            rs = st.executeQuery(sql);  
            float money = 0.0f;  
            if(rs.next()){  
                money = rs.getFloat("money");  
            }  
            if(money>300){  
                throw new RuntimeException("已经超过最大值");  
            }  
            sql = "update user set money=money+10 where id=2";  
            st.executeUpdate(sql);  
            conn.commit();  
            /*******************事务END*********************/  
        }catch(RuntimeException e){  

        }finally{  
            JdbcUtils.free(rs, st, conn);  
        }  
    }  

我们运行测试一下,因为我们这里想让它抛出异常,所以我们将用户2中的钱改成大于300的,运行一下,结果抛出异常了,但是我们发现了用户1中的钱少了10,但是由于抛出异常,所以后面的代码不执行了,用户2中的钱没有变化,那么这样的操作明显不对的,所以我们这时候要解决这个问题,使用事务的回滚操作,在捕获到异常的时候需要做回滚操作:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

if(conn != null){  
   conn.rollback();  
}  

这样即使抛出了异常,这些操作也会进行回滚的,那么用户1中的钱就不会少10了。

同时上面我们看到,我们是在开始的时候手动的关闭事务的自动提交,然后再手动的提交事务,下面再来看一下事务的保存点的问题。

场景:在上面的基础上,我们添加一个用户3,同时对用户1和用户3中的钱进行减少10,用户2的操作不变,但是当抛出异常的时候,我们希望用户1的操作还是有效的,用户3的操作还原,这时候我们需要将事务回滚到用户3的那个点就可以了,这就是事务的保存点的概念,看一下代码:

[java] view plain copy
在CODE上查看代码片派生到我的代码片

static void test() throws Exception{  
        Connection conn = null;  
        Statement st = null;  
        ResultSet rs = null;  
        Savepoint sp = null;  
        try{  
            conn = JdbcUtils.getConnection();  
            /**************事务START********************/  
            conn.setAutoCommit(false);  
            st = conn.createStatement();  

            String sql = "update user set money=money-10 where id=1";  
            st.executeUpdate(sql);  
            sp = conn.setSavepoint();//设置回滚点  

            sql = "update user set money=money-10 where id=3";  
            st.executeUpdate(sql);  

            sql = "select money from user where id=2";  
            rs = st.executeQuery(sql);  
            float money = 0.0f;  
            if(rs.next()){  
                money = rs.getFloat("money");  
            }  
            System.out.println("money:"+money);  
            if(money>300){  
                throw new RuntimeException("已经超过最大值");  
            }  
            sql = "update user set money=money+10 where id=2";  
            st.executeUpdate(sql);  
            conn.commit();  
            /*******************事务END*********************/  
        }catch(SQLException e){  
            if(conn != null && sp != null){  
                conn.rollback(sp);  
                conn.commit();  
            }  
        }finally{  
            JdbcUtils.free(rs, st, conn);  
        }  
    }  

我们在用户1之后设置一个保存点,在异常中只需要回滚到保存点就可以了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值