创建JDBC模板简化代码、JDBC应用的事务管理以及连接池的作用

一、创建JDBC模板简化代码

一个简单的查询,要做这么一大堆事情,而且还要处理异常,我们不防来梳理一下: 

1、获取connection 
2、获取statement 
3、获取resultset 
4、遍历resultset并封装成集合 
5、依次关闭connection,statement,resultset,而且还要考虑各种异常 
6、..... 
在面向对象编程的年代里,这样的代码简直不能上人容忍。一堆重复的代码: 

这时候,使用模板模式的时机到了!!! 

通过观察我们发现上面步骤中大多数都是重复的,可复用的,只有在遍历ResultSet并封装成集合的这一步骤是可定制的,因为每张表都映射不同的java bean。这部分代码是没有办法复用的,只能定制。可以用一个抽象的父类把它们封装一下:

public abstract class JdbcTemplate {

	//template method
	public final Object execute(String sql) throws SQLException{
		
		Connection con = HsqldbUtil.getConnection();
		Statement stmt = null;
		try {
 
			stmt = con.createStatement();
			ResultSet rs = stmt.executeQuery(sql);
			Object result = doInStatement(rs);//abstract method 
			return result;
		}
		catch (SQLException ex) {
			 ex.printStackTrace();
			 throw ex;
		}
		finally {
 
			try {
				stmt.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
			try {
				if(!con.isClosed()){
					try {
						con.close();
					} catch (SQLException e) {
						e.printStackTrace();
					}
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
			
		}
	}
	
	//implements in subclass
	protected abstract Object doInStatement(ResultSet rs);
}

在上面这个抽象类中,封装了SUN JDBC API的主要流程,而遍历ResultSet这一步骤则放到抽象方法doInStatement()中,由子类负责实现。 
好,我们来定义一个子类,并继承上面的父类: 

public class JdbcTemplateUserImpl extends JdbcTemplate {

	@Override
	protected Object doInStatement(ResultSet rs) {
		List<User> userList = new ArrayList<User>();
		
		try {
			User user = null;
			while (rs.next()) {

				user = new User();
				user.setId(rs.getInt("id"));
				user.setUserName(rs.getString("user_name"));
				user.setBirth(rs.getDate("birth"));
				user.setCreateDate(rs.getDate("create_date"));
				userList.add(user);
			}
			return userList;
		} catch (SQLException e) {
			e.printStackTrace();
			return null;
		}
	}

}

由代码可见,我们在doInStatement()方法中,对ResultSet进行了遍历,最后并返回。 
有人可能要问:我如何获取ResultSet 并传给doInStatement()方法啊??问这个问题的大多是新手。因为此方法不是由子类调用的,而是由父类调用,并把ResultSet传递给子类的。我们来看一下测试代码: 

		String sql = "select * from User";
		JdbcTemplate jt = new JdbcTemplateUserImpl();
		List<User> userList = (List<User>) jt.execute(sql);

如果我每次用jdbcTemplate时,都要继承一下上面的父类,是不是有些不方面呢? 
那就让我们甩掉abstract这顶帽子吧,这时,就该 callback(回调) 上场了 。

所谓回调,就是方法参数中传递一个接口,父类在调用此方法时,必须调用方法中传递的接口的实现类。 

以上摘自网友。


二、JDBC应用的事务管理

在介绍查询模板(回调)之前,我们先来看看JDBC应用的事务管理。

JDBC 应用的事务管理——Service层和Dao层事务的传递。 
方式一:跨层传递方法参数——在Service层创建开启事务的连接,并传递到Dao层,最后在Service层提交事务。
方式二:ThreadLocal 绑定连接——使用ThreadLocal进行事务管理——ThreadLocal可以实现在线程范围内实现数据共享。 
方式三:使用Spring进行事务管理。

1、JDBC应用的事务管理——采用跨层传递方法参数 
思想:在Service层创建开启事务的连接,并传递到Dao层,最后在Service层提交事务;
Demo样例1:Service层(Dao层中只要在方法中参数中接收该 连接参数 就好了)

public class BusinessService {  
  /*  
  create table account(
    id int primary key auto_increment,
    name varchar(40),
    money float
  )character set utf8 collate utf8_general_ci;
  
  insert into account(name,money) values('aaa',1000);
  insert into account(name,money) values('bbb',1000);
  insert into account(name,money) values('ccc',1000); 
  */  

  public void transfer1(int sourceid,int targetid,double money) throws SQLException{
    Connection conn = null;
    try{
      // 获取连接并开启事务。
      conn = JdbcUtils.getConnection();
      conn.setAutoCommit(false);
      // 将开启事务的连接传递到各层。
      AccountDao dao = new AccountDao(conn);
      Account a = dao.find(sourceid);   //select
      Account b = dao.find(targetid);   //select
      a.setMoney(a.getMoney()-money); 
      b.setMoney(b.getMoney()+money);
      dao.update(a); //update     
      dao.update(b);//update
      // 提交事务。
      conn.commit();
    }finally{
      // 关闭连接。
      if(conn!=null) conn.close();
    }
  }
}

2、JDBC应用的事务管理—— ThreadLocal 绑定连接 
思想:在Service层将开启事务的连接绑定到ThreadLocal中,在当前线程所途径的其他各层从ThreadLocal中获取连接并进行操作,最后线程返回至Service层时,再提交事务,移除绑定的链接.

package dhp.com.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSourceFactory;

public class JdbcUtils {
	private static DataSource ds;
	// 为保证各层的类所使用的ThreadLocal是同一个,建议将其设定成静态的。
	// 但是一定要记得使用后要移出绑定在上面的对象。
	// 其实就是一个Map集合
	private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

	static {
		try {
			Properties prop = new Properties();
			InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
			prop.load(in);
			BasicDataSourceFactory factory = new BasicDataSourceFactory();
			ds = factory.createDataSource(prop);
		} catch (Exception e) {
			throw new ExceptionInInitializerError(e);
		}
	}

	// 返回数据源
	public static DataSource getDataSource() {
		return ds;
	}
	// 备注:该获取连接的方法,仅当使用ThreadLocal来管理事务连接的情况,因为向静态对象ThreadLocal中绑定了对象,所以当我们不需要管理事务的普通获取连接的方法,就不要用此方法。应该用普通的获取连接的方法。

	public static Connection getConnection() throws SQLException {
		try {
			// 得到当前线程上绑定的连接
			Connection conn = tl.get();
			if (conn == null) { // 代表线程上没有绑定连接
				conn = ds.getConnection();
				tl.set(conn);
			}
			return conn;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	

	public static void startTransaction() {
		try {
			// 得到当前线程上绑定连接开启事务
			Connection conn = tl.get();
			if (conn == null) { // 代表线程上没有绑定连接
				conn = ds.getConnection();
				tl.set(conn);
			}
			conn.setAutoCommit(false);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// 提交当前线程上的连接
	public static void commitTransaction() {
		try {
			Connection conn = tl.get();
			if (conn != null) {
				conn.commit();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// 回滚当前线程上的连接
	public static void rollback() {
		try {
			Connection conn = tl.get();
			if (conn != null) {
				conn.rollback();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// 释放当前线程上的连接
	public static void closeConnection() {
		try {
			Connection conn = tl.get();
			if (conn != null) {
				conn.close();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			// 千万注意,解除当前线程上绑定的链接(从threadlocal容器中移除对应当前线程的链接)
			tl.remove();
		}
	}

	// 释放资源
	public static void release(Connection conn, Statement st, PreparedStatement pst, ResultSet rs) {
		if (rs != null) {
			try {
				rs.close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			} finally {
				rs = null;
			}
		}
		if (st != null) {
			try {
				st.close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			} finally {
				st = null;
			}
		}
		if (pst != null) {
			try {
				pst.close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			} finally {
				pst = null;
			}
		}
		if (conn != null) {
			try {
				conn.close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			} finally {
				conn = null;
			}
		}
	}
}

Demo样例2: 采用 ThreadLocal 绑定连接 来管理事务的 Service层的代码。

package dhp.com.service;

import java.sql.SQLException;


import dhp.com.dao.AccountDao;
import dhp.com.dao.impl.AccountDaoImpl;
import dhp.com.model.Account;
import dhp.com.util.JdbcUtils;

public class BusinessService {
	// 用上ThreadLocal的事务管理
	public void transfer(int sourceid, int targetid, float money) {
		try {
			JdbcUtils.startTransaction();
			AccountDao dao = new AccountDaoImpl();
			Account a = (Account) dao.find(sourceid); // select
			Account b = (Account) dao.find(targetid); // select
			a.setMoney(a.getMoney() - money);
			b.setMoney(b.getMoney() + money);
			dao.update(a); // update
			dao.update(b); // update
			JdbcUtils.commitTransaction();
		} catch(SQLException e) {
			JdbcUtils.rollback();
			throw new RuntimeException(e);
        } finally {
			JdbcUtils.closeConnection();
		}
	}
}

JDBC的数据库操作中,一项事务是由一条或是多条表达式所组成的一个不可分割的工作单元。我们通过提交commit()或是回退rollback()来结束事务的操作。关于事务操作的方法都位于接口java.sql.Connection中。
首先我们要注意,在JDBC中,事务操作默认是自动提交。也就是说,一条对数据库的更新表达式代表一项事务操作。操作成功后,系统将自动调用commit()来提交,否则将调用rollback()来回退。
其次,在JDBC中,可以通过调用setAutoCommit(false)来禁止自动提交。之后就可以把多个数据库操作的表达式作为一个事务,在操作完成后调用commit()来进行整体提交。倘若其中一个表达式操作失败,都不会执行到commit(),并且将产生响应的异常。此时就可以在异常捕获时调rollback()进行回退。这样做可以保持多次更新操作后,相关数据的一致性。

ThreadLocal作用:

由于请求中的一个事务涉及多个 DAO 操作,而这些 DAO 中的 Connection  不能从连接池中获得,如果是从连接池获得的话,两个 DAO 就用到了两个Connection,这样的话是没有办法完成一个事务的。DAO 中的 Connection 如果是从 ThreadLocal 中获得 Connection 的话那么这些 DAO 就会被纳入到同一个 Connection 之下。当然了,这样的话,DAO 中就不能把 Connection 给关了,关掉的话,下一个使用者就不能用了。ThreadLocal保证了每个线程都有自己的连接。

三、连接池的作用(就是为了提高性能)
       连接池的作用:连接池是将已经创建好的连接保存在池中,当有请求来时,直接使用已经创建好的连接对数据库进行访问。这样省略了创建连接和销毁连接的过程。这样性能上得到了提高。
基本原理是这样的:
(1)建立数据库连接池对象(服务器启动)。
(2)按照事先指定的参数创建初始数量的数据库连接(即:空闲连接数)。
(3)对于一个数据库访问请求,直接从连接池中得到一个连接。如果数据库连接池对象中没有空闲的连接,且连接数没有达到最大(即:最大活跃连接数),创建一个新的数据库连接。
(4)存取数据库。
(5)关闭数据库,释放所有数据库连接(此时的关闭数据库连接,并非真正关闭,而是将其放入空闲队列中。如实际空闲连接数大于初始空闲连接数则释放连接)。
(6)释放数据库连接池对象(服务器停止、维护期间,释放数据库连接池对象,并释放所有连接)。
1 .连接池的概念和为什么要使用连接池?
    连接池放了N个Connection对象,本质上放在内存当中,在内存中划出一块缓存对象,应用程序每次从池里获得Connection对象,而不是直接从数据里获得,这样不占用服务器的内存资源。
2 .如果不使用连接池会出现的情况:
a.占用服务器的内存资源
b.导致服务器的速度非常慢
3 .应用连接池的三种方式:
a.自定义连接池
b.使用第三方连接池
c.使用服务器自带的连接池
       连接池一般比直接连接更有优越性,因为它提高了性能的同时还保存了宝贵的资源。在整个应用程序的使用过程,当中重复的打开直接连接将导致性能的下降。而池连接只在服务器启动时打开一次,从而消除了这种性能问题。
        连接池主要考虑的是性能,每次获取连接和释放连接都有很大的工作量,会对性能有很大影响;而对资源来说起的是反作用,因为保存一定数量的连接是要消耗内存的。应用程序每次从池里获得Connection对象,而不是直接从数据里获得,这样不占用服务器的内存资源。所以一般要建立连接池,而连接的数量要适当,不能太大,太大会过多消耗资源。(所以,考虑2个方面,一个是内存,另一个是资源)。
       连接池就是为了避免重复多次的打开数据库连接而造成的性能的下降和系统资源的浪费。

详情请看建立连接池的三种方式:http://doc.okbase.net/u011225629/archive/174436.html


好了,一下是我的项目视图和完整代码:



model类:

package dhp.com.model;

public class Account {
	private int id;
	private String name;
	private float 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 float getMoney() {
		return money;
	}

	public void setMoney(float money) {
		this.money = money;
	}

}

数据库配置文件

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3308/test
username=root
password=123456

initialSize=10
maxActive=50
maxIdle=20
minIdle=5
maxWait=60000
connectionProperties=useUnicode=true;characterEncoding=utf8
defaultAutoCommit=true

工具类util:

package dhp.com.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSourceFactory;

public class JdbcUtils {
	private static DataSource ds;
	// 为保证各层的类所使用的ThreadLocal是同一个,建议将其设定成静态的。
	// 但是一定要记得使用后要移出绑定在上面的对象。
	// 其实就是一个Map集合
	private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

	static {
		try {
			Properties prop = new Properties();
			InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream("dbcpconfig.properties");
			prop.load(in);
			BasicDataSourceFactory factory = new BasicDataSourceFactory();
			ds = factory.createDataSource(prop);
		} catch (Exception e) {
			throw new ExceptionInInitializerError(e);
		}
	}

	// 返回数据源
	public static DataSource getDataSource() {
		return ds;
	}
	// 备注:该获取连接的方法,仅当使用ThreadLocal来管理事务连接的情况,因为向静态对象ThreadLocal中绑定了对象,所以当我们不需要管理事务的普通获取连接的方法,就不要用此方法。应该用普通的获取连接的方法。

	public static Connection getConnection() throws SQLException {
		try {
			// 得到当前线程上绑定的连接
			Connection conn = tl.get();
			if (conn == null) { // 代表线程上没有绑定连接
				conn = ds.getConnection();
				tl.set(conn);
			}
			return conn;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	

	public static void startTransaction() {
		try {
			// 得到当前线程上绑定连接开启事务
			Connection conn = tl.get();
			if (conn == null) { // 代表线程上没有绑定连接
				conn = ds.getConnection();
				tl.set(conn);
			}
			conn.setAutoCommit(false);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// 提交当前线程上的连接
	public static void commitTransaction() {
		try {
			Connection conn = tl.get();
			if (conn != null) {
				conn.commit();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// 回滚当前线程上的连接
	public static void rollback() {
		try {
			Connection conn = tl.get();
			if (conn != null) {
				conn.rollback();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	// 释放当前线程上的连接
	public static void closeConnection() {
		try {
			Connection conn = tl.get();
			if (conn != null) {
				conn.close();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		} finally {
			// 千万注意,解除当前线程上绑定的链接(从threadlocal容器中移除对应当前线程的链接)
			tl.remove();
		}
	}

	// 释放资源
	public static void release(Connection conn, Statement st, PreparedStatement pst, ResultSet rs) {
		if (rs != null) {
			try {
				rs.close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			} finally {
				rs = null;
			}
		}
		if (st != null) {
			try {
				st.close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			} finally {
				st = null;
			}
		}
		if (pst != null) {
			try {
				pst.close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			} finally {
				pst = null;
			}
		}
		if (conn != null) {
			try {
				conn.close();
			} catch (SQLException e) {
				throw new RuntimeException(e);
			} finally {
				conn = null;
			}
		}
	}
}

回调接口:

package dhp.com.util;

import java.sql.ResultSet;
import java.sql.SQLException;

public interface RowMapper {
	Object mapRow(ResultSet rs) throws SQLException;
}


增删改查的工具模板:
package dhp.com.util;

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

public class JDBCTemplate {
	public static void update(String sql, Object params[]) throws SQLException {
		Connection conn = null;
		PreparedStatement pst = null;
		conn = JdbcUtils.getConnection();
		pst = conn.prepareStatement(sql);
		for (int i = 0; i < params.length; i++) {
			pst.setObject(i + 1, params[i]);
		}
		pst.executeUpdate();
	}
	
	public static Object queryForObject(String sql, Object params[], RowMapper rowMapper) throws SQLException {
		Connection conn = null;
		PreparedStatement pst = null;
		ResultSet rs = null;
		Object obj = null;  
		conn = JdbcUtils.getConnection();
		pst = conn.prepareStatement(sql);
		for (int i = 0; i < params.length; i++) {
			pst.setObject(i + 1, params[i]);
		}
		rs = pst.executeQuery();
		if(rs.next()){
			obj=rowMapper.mapRow(rs);
		}
		return obj;
		
	}
}


dao的接口层:
package dhp.com.dao;

import java.sql.SQLException;

import dhp.com.model.Account;

public interface AccountDao {
	void add(Account a) throws SQLException;
	void update(Account a) throws SQLException;
	void delete(int id) throws SQLException;
	Object find(int id) throws SQLException;
}

接口实现层:

package dhp.com.dao.impl;

import java.sql.ResultSet;
import java.sql.SQLException;

import dhp.com.dao.AccountDao;
import dhp.com.model.Account;
import dhp.com.util.JDBCTemplate;
import dhp.com.util.RowMapper;

public class AccountDaoImpl implements AccountDao {

	@Override
	public void add(Account a) throws SQLException {
		String sql = "insert into account(id,name,money) values(?,?,?)";
		Object params[] = { a.getId(), a.getName(), a.getMoney() };
		JDBCTemplate.update(sql, params);
	}

	@Override
	public void update(Account a) throws SQLException {
		String sql = "update account set money=? where id=?";
		Object params[] = { a.getMoney(), a.getId() };
		JDBCTemplate.update(sql, params);
	}
	
	@Override
	public void delete(int id) throws SQLException{
		String sql = "delete from account where id=?";
        Object params[] = {id};
        JDBCTemplate.update(sql, params);
  }

	@Override
	public Object find(int id) throws SQLException {
		String sql = "select * from account where id=?";
        Object params[] = {id};
        Object obj = JDBCTemplate.queryForObject(sql, params, new RowMapper() {
			
			@Override
			public Object mapRow(ResultSet rs) throws SQLException {
				Account account = new Account();
				account.setId(rs.getInt("id"));
				account.setName(rs.getString("name"));
				account.setMoney(rs.getFloat("money"));
				return account;
			}
		});
		return (Account)obj;
		
	} 

}

service层:

package dhp.com.service;

import java.sql.SQLException;


import dhp.com.dao.AccountDao;
import dhp.com.dao.impl.AccountDaoImpl;
import dhp.com.model.Account;
import dhp.com.util.JdbcUtils;

public class BusinessService {
	// 用上ThreadLocal的事务管理
	public void transfer(int sourceid, int targetid, float money) {
		try {
			JdbcUtils.startTransaction();
			AccountDao dao = new AccountDaoImpl();
			Account a = (Account) dao.find(sourceid); // select
			Account b = (Account) dao.find(targetid); // select
			a.setMoney(a.getMoney() - money);
			b.setMoney(b.getMoney() + money);
			dao.update(a); // update
			dao.update(b); // update
			JdbcUtils.commitTransaction();
		} catch(SQLException e) {
			JdbcUtils.rollback();// 如果中间事务发生错误,进行回滚,保证要么执行成功,要么执行失败
			throw new RuntimeException(e);
        } finally {
			JdbcUtils.closeConnection();
		}
	}
}

测试类:

package dhp.com.test;


import dhp.com.service.BusinessService;

public class Test {
	public static void main(String[] args) {
		BusinessService service = new BusinessService();
		service.transfer(1, 2, 100);
	}
}

最后提一点:

关于关闭Connection是否会自动关闭Statement,ResultSet问题 ?

做程序离不开连接数据库,所以一些打开,关闭数据库是经常要执行的操作,打开数据库后,在程序用完后要及时关闭数据库连接资源,以释放内存,避免资源耗尽.但现在有一个问题,即当我们关闭了Connection对象后,Statement,ResultSet对象是否会自动关闭问题,对于这个问题,之前我在网上也找了相关资料,说会自动关闭,所以一段时间以来,我都是只关闭Connection对象,而没有关闭Statement,ResultSet对象,但程序也能正常运行,程序也没有因为资源耗尽而崩溃,对于这一点,其实是有原因的:

1)首先,关闭了Connection对象后,是不会自动关闭Statement,ResultSet对象的。

try {
Connection con = null;
Statement st = null;
ResultSet rs = null;
con = getConnection();
st = con.createStatement();
rs = st.executeQuery(sql);
}
catch(Exception e) {
 System.out.println("ocurr error");
}
finally {
          con.close();con=null;
 try {
    con.close();
 }
 catch(SQLException se) {
    System.out.println("ocurr close error");
 } 
}

System.out.println("statement object:"+st);
System.out.println("resultset object:"+rs);

上面的代码先获取了连接,然后只关闭了Connection对象,而没有关闭Statement,ResultSet对象,最后两行代码输出Statement,ResultSet对象,是有结果的,表明关闭了Connection对象,而没有关闭Statement,ResultSet对象。
2)Statement对象将由Java垃圾收集程序自动关闭,而作为一种好的编程风格,应在不需要Statement对象时显式地关闭它们,这将立即释放DBMS资源,有助于避免潜在的内存问题。
3)ResultSet维护指向其当前数据行的光标.每调用一次next方法,光标向下移动一行.最初它位于第一行之前,因此第一次调用next将把光标置于第一行上,使它成为当前行.随着每次调用next导致光标向下移动一行.按照从上至下的次序获取ResultSet行,在ResultSet对象或其父辈Statement对象关闭之前,光标一直保持有效。
     所以在打开数据库资源后,尽量手工关闭Connection对象和Statement,ResultSet对象,要养成一种良好的编程风格.
注:他们三者之间关闭没有任何关联,即先关闭谁没有任何先后顺序,可以先关闭他们中的任何一个,且关闭其中的任何一个对象都不会关闭其他其他对象,但一般养成按关闭ResultSet,Statement,Connection的顺序关闭资源。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值