自定义一个jdbc框架

一、数据库连接池: 

  在一般用JDBC 进行连接数据库进行CRUD操作时,每一次都会:

    通过:java.sql.Connection conn = DriverManager.getConnection(url,user,password); 重新获取一个数据库的链接再进行操作,这样用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。


      
        所以为了减少服务器的压力,便可用连接池的方法:在启动Web应用时,数据就创建好一定数量的Connection链接
  存放到一个容器中,然后当用户请求时,服务器则向容器中获取Connection链接来处理用户的请求,当用户的请求完成后,
  又将该Connection 链接放回到该容器中。这样的一个容器称为连接池。

    
  
  编写一个基本的连接池实现连接复用
       步骤:
       1、建立一个数据库连接池容器。(因为方便存取,则使用LinkedList集合)
       2、初始化一定数量的连接,放入到容器中。
       3、等待用户获取连接对象。(该部分要加锁)
          |---记得删除容器中对应的对象,放置别人同时获取到同一个对象。
       4、提供一个方法,回收用户用完的连接对象。
       5、要遵循先入先出的原则。

复制代码
 1 import java.io.InputStream;
 2 import java.sql.Connection;
 3 import java.sql.DriverManager;
 4 import java.sql.SQLException;
 5 import java.util.LinkedList;
 6 import java.util.Properties;
 7 
 8 
 9 /**
10  * 一个基本的数据连接池:  
11  * 1、初始化时就建立一个容器,来存储一定数量的Connection 对象
12  * 2、用户通过调用MyDataSource 的getConnection 来获取Connection 对象。
13  * 3、再通过release 方法来回收Connection 对象,而不是直接关闭连接。
14  * 4、遵守先进先出的原则。
15  *  
16  *     
17  * @author 贺佐安
18  *
19  */
20 public class MyDataSource {
21     private static String url = null;
22     private static String password = null;
23     private static String user = null ;
24     private static String DriverClass = null;
25     private static LinkedList<Connection> pool = new LinkedList<Connection>() ;
26 //    注册数据库驱动
27     static {
28         try {
29             InputStream in = MyDataSource.class.getClassLoader()
30                     .getResourceAsStream("db.properties");
31             Properties prop = new Properties(); 
32             prop.load(in);
33             user = prop.getProperty("user"); 
34             url = prop.getProperty("url") ;
35             password = prop.getProperty("password") ; 
36             DriverClass = prop.getProperty("DriverClass") ;  
37             Class.forName(DriverClass) ;  
38             
39         } catch (Exception e) {
40             throw new RuntimeException(e) ;
41         }  
42     }
43     //初始化建立数据连接池
44     public MyDataSource ()  {
45         for(int i = 0 ; i < 10 ; i ++) {
46             try {
47                 Connection conn = DriverManager.getConnection(url, user, password) ;
48                 pool.add(conn) ;
49             } catch (SQLException e) {
50                 e.printStackTrace();
51             }
52         }
53     }
54     //、从连接池获取连接
55     public Connection getConnection() throws SQLException {
56         return pool.remove() ;
57     } 
58     // 回收连接对象。
59     public void release(Connection conn) {
60         System.out.println(conn+"被回收");
61         pool.addLast(conn) ;
62     } 
63     public int getLength() {
64         return pool.size() ;
65     }
66 }
复制代码

  这样当我们要使用Connection 连接数据库时,则可以直接使用连接池中Connection 的对象。测试如下:

复制代码
 1 import java.sql.Connection;
 2 import java.sql.SQLException;
 3 
 4 import org.junit.Test;
 5 
 6 
 7 public class MyDataSourceTest {
 8     
 9     
10     /**
11      * 获取数据库连接池中的所有连接。
12      */
13     @Test
14     public void Test() {
15         MyDataSource mds = new MyDataSource() ; 
16         Connection conn = null ;
17         try {
18             
19             for (int i = 0 ; i < 20 ; i ++) {
20                 conn = mds.getConnection() ;
21                 System.out.println(conn+"被获取;连接池还有:"+mds.getLength()); 
22                 mds.release(conn) ;
23             } 
24         } catch (SQLException e) {
25             e.printStackTrace();
26         } 
27     }
28 }
复制代码

  再运行的时候,可以发现,循环10次后,又再一次获取到了第一次循环的得到的Connection对象。所以,这样可以大大的减轻数据库的压力。上面只是一个简单的数据库连接池,不完美的便是,回收需要调用数据池的release() 方法来进行回收,那么可以不可以直接调用Connection 实例的close 便完成Connection 对象的回收呢?

 


二、数据源:  

    > 编写连接池需实现javax.sql.DataSource接口。
      > 实现DataSource接口,并实现连接池功能的步骤:
        1、在DataSource构造函数中批量创建与数据库的连接,并把创建的连接加入LinkedList对象中。

      2、实现getConnection方法,让getConnection方法每次调用时,从LinkedList中取一个Connection返回给用户。当用户使用完Connection,调用Connection.close()方法时,Collection对象应保证将自己返回到LinkedList中,而不要把conn还给数据库。

    利用动态代理和包装设计模式来标准的数据源。

    1、包装设计模式实现标准数据源:

      这里的用包装设计模式,便是将Connection 接口进行包装。简单总结一下包装设计模式的步骤:

          a)定义一个类,实现与被包装类()相同的接口。
                |----可以先自己写一个适配器,然后后面继承这个适配器,改写需要改写的方法,提高编程效率。
             b)定义一个实例变量,记住被包装类的对象的引用。
             c)定义构造方法,转入被包装类的对象。

           e)对需要改写的方法,改写。
                    f)对不需要改写的方法,调用原来被包装类的对应方法。

      所以先编写一个类似适配器的类,将Connection 接口的方法都进行实现:

  View Code

      然后再对Connection 接口进行包装,将close 方法修改掉:

复制代码
 1 import java.sql.Connection;
 2 import java.sql.SQLException;
 3 import java.util.LinkedList;
 4 /**
 5  * 对MyConnectionAdapter 进行包装处理 
 6  * @author 贺佐安
 7  *
 8  */
 9 public class MyConnectionWrap extends MyConnectionAdapter {
10 
11     private LinkedList<Connection> pool = new LinkedList<Connection>() ;
12     public MyConnectionWrap(Connection conn ,LinkedList<Connection> pool ) {
13         super(conn); 
14         this.pool = pool ; 
15     }
16     
17     //改写要实现的方法
18     public void close() throws SQLException {
19         pool.addLast(conn) ;
20     }
21 }
复制代码

      编写标准数据源:

复制代码
  1 import java.io.PrintWriter;
  2 import java.sql.Connection;
  3 import java.sql.DriverManager;
  4 import java.sql.SQLException;
  5 import java.util.LinkedList;
  6 import java.util.ResourceBundle;
  7 
  8 import javax.sql.DataSource;
  9 
 10 
 11 /** 
 12  * 编写标准的数据源:
 13  * 1、实现DataSource 接口
 14  * 2、获取在实现类的构造方法中批量获取Connection 对象,并将这些Connection 存储
 15  * 在LinkedList 容器中。
 16  * 3、实现getConnection() 方法,调用时返回LinkedList容器的Connection对象给用户。
 17  * @author 贺佐安
 18  *
 19  */
 20 public class MyDataSource implements DataSource{
 21     private static String url = null;
 22     private static String password = null;
 23     private static String user = null ;
 24     private static String DriverClass = null;
 25     private static LinkedList<Connection> pool = new LinkedList<Connection>() ;
 26 
 27     //    注册数据库驱动
 28     static {
 29         try {  
 30             ResourceBundle rb = ResourceBundle.getBundle("db") ;
 31             url = rb.getString("url") ; 
 32             password = rb.getString("password") ; 
 33             user = rb.getString("user") ; 
 34             DriverClass = rb.getString("DriverClass") ;
 35             Class.forName(DriverClass) ;  
 36             
 37             //初始化建立数据连接池
 38             for(int i = 0 ; i < 10 ; i ++) {
 39                 Connection conn = DriverManager.getConnection(url, user, password) ;
 40                 pool.add(conn) ;
 41             }
 42         } catch (Exception e) {
 43             throw new RuntimeException(e) ;
 44         } 
 45         
 46     }
 47     public MyDataSource ()  {  
 48     }
 49     
 50     //、从连接池获取连接:通过包装模式
 51     public synchronized Connection getConnection() throws SQLException {
 52         if (pool.size() > 0) {
 53             MyConnectionWrap mcw = new MyConnectionWrap(pool.remove(), pool) ;
 54             return mcw ;
 55         }else {
 56             throw new RuntimeException("服务器繁忙!"); 
 57         }
 58     }
 59     
 60     // 回收连接对象。
 61     public void release(Connection conn) {
 62         System.out.println(conn+"被回收");
 63         pool.addLast(conn) ;
 64     }
 65     
 66     public int getLength() {
 67         return pool.size() ;
 68     }
 69     
 70     
 71     @Override
 72     public PrintWriter getLogWriter() throws SQLException {
 73         return null;
 74     }
 75     @Override
 76     public void setLogWriter(PrintWriter out) throws SQLException {
 77         
 78     }
 79     @Override
 80     public void setLoginTimeout(int seconds) throws SQLException {
 81         
 82     }
 83     @Override
 84     public int getLoginTimeout() throws SQLException {
 85         return 0;
 86     }
 87     @Override
 88     public <T> T unwrap(Class<T> iface) throws SQLException {
 89         return null;
 90     }
 91     @Override
 92     public boolean isWrapperFor(Class<?> iface) throws SQLException {
 93         return false;
 94     }
 95     @Override
 96     public Connection getConnection(String username, String password)
 97             throws SQLException {
 98         return null;
 99     }
100     
101 }
复制代码

 

 

  2、动态代理实现标准数据源:

    相对于用包装设计来完成标准数据源,用动态代理则方便许多:

  

复制代码
  1 import java.io.PrintWriter;
  2 import java.lang.reflect.InvocationHandler;
  3 import java.lang.reflect.Method;
  4 import java.lang.reflect.Proxy;
  5 import java.sql.Connection;
  6 import java.sql.DriverManager;
  7 import java.sql.SQLException;
  8 import java.util.LinkedList;
  9 import java.util.ResourceBundle;
 10 
 11 import javax.sql.DataSource;
 12 
 13 
 14 /** 
 15  * 编写标准的数据源:
 16  * 1、实现DataSource 接口
 17  * 2、获取在实现类的构造方法中批量获取Connection 对象,并将这些Connection 存储
 18  * 在LinkedList 容器中。
 19  * 3、实现getConnection() 方法,调用时返回LinkedList容器的Connection对象给用户。
 20  * @author 贺佐安
 21  *
 22  */
 23 public class MyDataSource implements DataSource{
 24     private static String url = null;
 25     private static String password = null;
 26     private static String user = null ;
 27     private static String DriverClass = null;
 28     private static LinkedList<Connection> pool = new LinkedList<Connection>() ;
 29 
 30     //    注册数据库驱动
 31     static {
 32         try {  
 33             ResourceBundle rb = ResourceBundle.getBundle("db") ;
 34             url = rb.getString("url") ; 
 35             password = rb.getString("password") ; 
 36             user = rb.getString("user") ; 
 37             DriverClass = rb.getString("DriverClass") ;
 38             Class.forName(DriverClass) ;  
 39             
 40             //初始化建立数据连接池
 41             for(int i = 0 ; i < 10 ; i ++) {
 42                 Connection conn = DriverManager.getConnection(url, user, password) ;
 43                 pool.add(conn) ;
 44             }
 45         } catch (Exception e) {
 46             throw new RuntimeException(e) ;
 47         }  
 48     }
 49     public MyDataSource ()  { 
 50         
 51     }
 52     
 53     //、从连接池获取连接:通过动态代理
 54     public Connection getConnection() throws SQLException {
 55         if (pool.size() > 0) {
 56             final Connection conn  = pool.remove() ; 
 57             Connection proxyCon = (Connection) Proxy.newProxyInstance(conn.getClass().getClassLoader(), conn.getClass().getInterfaces(), 
 58                     new InvocationHandler() {
 59                         //策略设计模式:
 60                         @Override
 61                         public Object invoke(Object proxy, Method method, Object[] args)
 62                                 throws Throwable {
 63                             if("close".equals(method.getName())){
 64                                 //谁调用,
 65                                 return pool.add(conn);//当调用close方法时,拦截了,把链接放回池中了
 66                             }else{
 67                                 return method.invoke(conn, args);
 68                             } 
 69                         }
 70                     });
 71           return proxyCon ;
 72         }else {
 73             throw new RuntimeException("服务器繁忙!"); 
 74         }
 75     } 
 76     
 77     public int getLength() {
 78         return pool.size() ;
 79     }
 80     
 81     
 82     @Override
 83     public PrintWriter getLogWriter() throws SQLException {
 84         return null;
 85     }
 86     @Override
 87     public void setLogWriter(PrintWriter out) throws SQLException {
 88         
 89     }
 90     @Override
 91     public void setLoginTimeout(int seconds) throws SQLException {
 92         
 93     }
 94     @Override
 95     public int getLoginTimeout() throws SQLException {
 96         return 0;
 97     }
 98     @Override
 99     public <T> T unwrap(Class<T> iface) throws SQLException {
100         return null;
101     }
102     @Override
103     public boolean isWrapperFor(Class<?> iface) throws SQLException {
104         return false;
105     }
106     @Override
107     public Connection getConnection(String username, String password)
108             throws SQLException {
109         return null;
110     } 
111 }
复制代码

    当然觉得麻烦的则可以直接使用一些开源的数据源如:DBCP、C3P0等。DBCP的原理是用包装设计模式开发的数据源,而C3P0则是动态代理的。

    1、DBCP的使用:

复制代码
 1 import java.io.InputStream;
 2 import java.sql.Connection;
 3 import java.sql.SQLException;
 4 import java.util.Properties;
 5 
 6 import javax.sql.DataSource;
 7 
 8 import org.apache.commons.dbcp.BasicDataSourceFactory;
 9 
10 /**
11  * 创建DBCP 工具类
12  * @author 贺佐安
13  *
14  */
15 public class DbcpUtil {
16     private static DataSource ds = null ;
17     static {
18         try {
19             //读取配置文件
20             InputStream in = DbcpUtil.class.getClassLoader().getResourceAsStream("dbcpconfig.properties") ;
21             Properties prop = new Properties() ; 
22             prop.load(in) ;
23             
24             //通过BasicDataSourceFactory 的creatDataSurce 方法创建 BasicDataSource 对象。
25             ds = BasicDataSourceFactory.createDataSource(prop) ;
26             
27         } catch (Exception e) {
28             e.printStackTrace();
29         } 
30     }
31     public static DataSource getDs() {
32         return ds ; 
33     }
34     public static Connection getConnection () {
35         try { 
36             return ds.getConnection() ;
37         } catch (SQLException e) {
38             throw new RuntimeException() ;
39         } 
40     }
41 }
复制代码

    2、C3P0 的使用:

复制代码
 1 import java.sql.Connection;
 2 import java.sql.SQLException;
 3 
 4 import com.mchange.v2.c3p0.ComboPooledDataSource;
 5 /**
 6  * C3P0 开源数据源的使用
 7  * @author 贺佐安
 8  *
 9  */
10 public class C3p0Util {
11     private static ComboPooledDataSource cpds  = null ;
12     static {
13         
14         cpds = new ComboPooledDataSource() ; 
15     }
16     public static Connection getConnection() {
17         try {
18             return cpds.getConnection() ;
19         } catch (SQLException e) {
20             throw new RuntimeException() ;
21         }
22     }
23 }
复制代码

  使用这两个数据源时,直接调用获取到的Connection 连接的close 方法,也是将连接放到pool中去。

    

 

三、元数据(DatabaseMetaData)信息的获取

  > 元数据:数据库、表、列的定义信息。    

  > 元数据信息的获取:为了编写JDBC框架使用。   

      1、数据库本身信息的获取:java.sql.DataBaseMateData java.sql.Connection.getMetaData() ;    

      DataBaseMateData 实现类的常用方法:    

        getURL():返回一个String类对象,代表数据库的URL。     

        getUserName():返回连接当前数据库管理系统的用户名。     

        getDatabaseProductName():返回数据库的产品名称。    

        getDatabaseProductVersion():返回数据库的版本号。     

        getDriverName():返回驱动驱动程序的名称。     

        getDriverVersion():返回驱动程序的版本号。    

        isReadOnly():返回一个boolean值,指示数据库是否只允许读操作。   

      2、ParameterMetaData: 代表PerparedStatment 中的SQL 参数元数据信息:    java.sql.ParameterMetaData java.sql.PerparedStatement.getParameterMetaData() ;          

      ParameterMetaData 实现类常用方法:     

        getParameterCount() :获得指定参数的个数    

        getParameterType(int param) :获得指定参数的sql类型(驱动可能不支持)

       3、ResultSetMetaData : 代表结果集的源数据信息:相当于SQL 中的 :DESC    java.sql.ResultSetMetaData java.sql.ResultSet.getMetaData() ;                

      java.sql.ResultSetMetaData 接口中常用的方法:     

        a) getColumnCount() : 获取查询方法有几列。     

        b) getColumnName(int index) : 获取列名:index从1开始。     

        c) getColumnType(int index) : 获取列的数据类型。返回的是TYPES  中的常量值。

 

 

四、编写自己的JDBC框架:

    JDBC框架的基本组成:  

    1、核心类:

      a、定义一个指定javax.sql.DataSource 实例的引用变量,通过构造函数获取指定的实例并给定义的变量。
        b、编写SQL运行框架。

         DML 语句的编写:
             1、通过获取的javax.sql.DataSource 实例,获取Connection 对象。
             2、通过ParamenterMeteData 获取数据库元数据。
   
           DQL 语句的编写:
             1、通过获取的DataSource 实例,获取Connection 对象。
             2、通过ParamenterMeteData、ResultSetMetaData 等获取数据库元数据。
             3、用抽象策略设计模式:设计一个ResultSetHandler 接口,作用:将查找出的数据封装到指定的JavaBean中。 
                    |————这里的JavaBean,由用户来指定。
                    抽象策略模式,用户可以更具具体的功能来扩展成具体策略设计模式。如:查找的一条信息、查找的所有信息。

复制代码
  1 import java.sql.Connection;
  2 import java.sql.ParameterMetaData;
  3 import java.sql.PreparedStatement;
  4 import java.sql.ResultSet;
  5 import java.sql.SQLException;
  6 import java.sql.Statement;
  7 
  8 import javax.sql.DataSource;
  9 
 10 /**
 11  * 实现JDBC 框架的核心类。
 12  * 在该类中定义了SQL语句完成的方法;
 13  * @author 贺佐安
 14  *
 15  */
 16 public class  MyJdbcFrame {
 17     /**
 18      * javax.sql.DataSource 实例的引用变量
 19      */
 20     private DataSource ds = null ;
 21     /**
 22      * 将用户指定的DataSource 指定给系统定义的DataSource 实例的引用变量
 23      * @param ds
 24      */
 25     public MyJdbcFrame(DataSource ds ) {
 26         this.ds = ds ; 
 27     }
 28     /**
 29      * 执行UPDATE、DELETE、INSERT 语句。
 30      * @param sql 
 31      * @param obj
 32      */
 33     public void update(String sql , Object[] obj) {
 34         Connection conn = null ; 
 35         PreparedStatement stmt = null ; 
 36         try {
 37             //获取Connection 对象
 38             conn = ds.getConnection() ;
 39             stmt = conn.prepareStatement(sql) ; 
 40             
 41             // 获取ParameterMetaData 元数据对象。
 42             ParameterMetaData pmd = stmt.getParameterMetaData() ;
 43             
 44             //获取SQL语句中需要设置的参数的个数
 45             int parameterCount = pmd.getParameterCount() ;
 46             if (parameterCount > 0) { 
 47                 if (obj == null || obj.length != parameterCount) {
 48                     throw new MyJdbcFrameException( "parameterCount is error!") ;
 49                 } 
 50                 //设置参数:
 51                 for ( int i = 0 ; i < obj.length ; i++) {
 52                     stmt.setObject(i+1, obj[i]) ;
 53                 }
 54             } 
 55             //执行语句:
 56             stmt.executeUpdate() ; 
 57             
 58         } catch(Exception e ) {
 59             throw new MyJdbcFrameException(e.getMessage()) ;
 60         } finally {
 61             release(stmt, null, conn) ;
 62         }
 63     }
 64     
 65     public Object query(String sql , Object[] obj , ResultSetHandler rsh) {
 66         Connection conn = null ; 
 67         PreparedStatement stmt = null ; 
 68         ResultSet rs = null ;
 69         try {
 70             //获取Connection 对象
 71             conn = ds.getConnection() ;
 72             stmt = conn.prepareStatement(sql) ; 
 73             
 74             // 获取ParameterMetaData 元数据对象。
 75             ParameterMetaData pmd = stmt.getParameterMetaData() ;
 76             
 77             //获取SQL语句中需要设置的参数的个数
 78             int parameterCount = pmd.getParameterCount() ;
 79             
 80             if (obj.length != parameterCount) {
 81                 throw new MyJdbcFrameException( "'" +sql +"' : parameterCount is error!") ;
 82             } 
 83             //设置参数:
 84             for ( int i = 0 ; i < obj.length ; i++) {
 85                 stmt.setObject(i+1, obj[i]) ;
 86             }
 87             //执行语句:
 88             rs = stmt.executeQuery(); 
 89             
 90             return rsh.handler(rs);
 91         } catch(Exception e ) {
 92             throw new MyJdbcFrameException(e.getMessage()) ;
 93         } finally {
 94             release(stmt, null, conn) ;
 95         } 
 96     } 
 97     /**
 98      * 释放资源
 99      * @param stmt
100      * @param rs
101      * @param conn
102      */
103     public static void release(Statement stmt 
104                              , ResultSet rs 
105                              , Connection conn) {
106         if(rs != null) {
107             try {
108                 rs.close() ;
109             } catch (SQLException e) {
110                 e.printStackTrace();
111             }
112             rs = null ;
113         }
114         if (stmt != null) { 
115             try {
116                 stmt.close();
117             } catch (SQLException e) {
118                 e.printStackTrace();
119             } 
120             stmt = null ;
121         }
122         if (conn != null) {
123             try {
124                 conn.close();
125             } catch (SQLException e) {
126                 e.printStackTrace();
127             }
128             conn = null ;
129         }
130     } 
131     
132 }
复制代码

    2、接口:策略模式的接口:ResultSetHandler 。

1 import java.sql.ResultSet;
2 
3 //抽象策略模式
4 public interface ResultSetHandler {
5     public Object handler(ResultSet rs) ;
6 }

    这里对ResultSetHandler 接口实现一个BeanHandler 实例 :

复制代码
 1 import java.lang.reflect.Field;
 2 import java.sql.ResultSet;
 3 import java.sql.ResultSetMetaData;
 4 
 5 /**
 6  * 该类获取ResultSet 结果集中的第一个值,封装到JavaBean中
 7  * @author 贺佐安
 8  *
 9  */
10 public class BeanHandler implements ResultSetHandler { 
11     //获取要封装的JavaBean的字节码
12     private Class clazz ;
13     public BeanHandler (Class clazz) {
14         this.clazz = clazz ;
15     }
16 
17     public Object handler(ResultSet rs) {
18         try {
19             if (rs.next()) {
20                 //1、获取结果集的元数据。
21                 ResultSetMetaData rsm = rs.getMetaData() ;
22                 //2、创建JavaBean的实例:
23                 Object obj = clazz.newInstance() ;
24                 //3、将数据封装到JavaBean中。  
25                 for (int i = 0 ; i < rsm.getColumnCount() ; i ++) {
26                     //获取属性名
27                     String columnName = rsm.getColumnName(i+1) ; 
28                     //获取属性值
29                     Object value = rs.getObject(i+1) ; 
30                     
31                     Field objField = obj.getClass().getDeclaredField(columnName) ;
32                     objField.setAccessible(true) ;
33                     objField.set(obj, value) ;
34                 }
35                 return obj ;
36             } else {
37                 return null ;
38             }
39         } catch (Exception e) {
40             throw new RuntimeException(e) ;
41         }   
42     } 
43 }
复制代码

    3、自定义异常类:继承RuntimeException。如:

复制代码
1 public class MyJdbcFrameException extends RuntimeException {
2     public MyJdbcFrameException() {
3         super() ; 
4     }
5     public MyJdbcFrameException(String e) {
6         super(e) ;
7     }
8 }
复制代码

    

  然后就可以将其打包发布,在以后写数据库操作时就可以用自己的JDBC框架了,如果要完成查询多条语句什么的,则要实现ResultSetHandler 接口。来完成更多的功能。

  当然,使用DBUtils 则更简单:Apache 组织提供的一个开源JDBC 工具类库。

转载于:https://my.oschina.net/lzhaoqiang/blog/547638

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值