一、 定义

1. 什么是JDBC?

jdbc是一种由一组用Java语言编写的标准类和标准接口组成,用于执行SQL语句的Java API。JDBC提供了面向多种关系型数据库连接的统一访问,同时统一规范了标准接口和工具,使数据库开发人员能够编写数据库应用程序,实现了所有这些面向标准的目标并且具有简单,严格类型定义且高性能实现的接口。

2. 具体实现

如下图,我们很明显的看出:java应用程序是不能直接访问关系型数据库的,必须要通过相应的数据库驱动程序,通过驱动程序去和数据库打交道。而所谓的数据库驱动,实际上是java中抽象工厂的一个具体实例。其实也就是通过JDBC API接口中的抽象工厂来生产各种关系数据库的抽象接口数据库驱动厂商JDBC DriverManager(对Connection等接口的实现类的jar文件),来分别具体生产实现各种关系型数据库的驱动接口,从而使得程序员可向相应数据库发送SQL调用。同时,将Java语言和JDBC结合起来使程序员不必为不同的平台编写不同的应用程序,只须写一遍程序就可以让它在任何平台上运行,这也是Java语言“编写一次,处处运行”的优势

二、流程原理四大接口

1. Driver接口

在Java程序中,连接数据库,必须先装载特定厂商的数据库驱动程序,不同的数据库有不同的装载方法。我们直接使用Driver接口(由数据库厂家提供),可以通过 “Class.forName(“指定数据库的驱动程序”)” 方式来加载添加到开发环境中的驱动程序,例如:
装载MySql驱动:Class.forName(“com.mysql.jdbc.Driver”);
装载Oracle驱动:Class.forName(“oracle.jdbc.driver.OracleDriver”);

实际上,JDBC 源码中第一句 的 class.forName(),做了很多的事情, 在jdk 中,只有Driver 的一个接口,没有一个具体实现实例,就是说mysql-connector-java.jar 这种类似的 jar 需要第三方去实现,具体的Driver 实现类是在第三方的jar里面的。那么它是怎么实现的呢?在 class.forName 里面,指定加载 第三方的 Driver接口,那么在类加载的时候,可以完成 jdk 中 Driver 接口, 在 第三方jar 中具体实现了class(第三方 的jar里面,存在一个 配置文件,指向了在 第三方 jar 中具体实现了 jdk 中 sql 包下的 Driver 接口的 类),同时Driver 接口中使用了泛型,这样就可以通过反射的方式获取到 第三方中具体实现类,同时解析SQL语句不同类型的结果集,映射为不同的实体类型。
本文最后,本人写了一个小型配置文件的BaseDao,来封装实现了mysql的SQL语句在java中的实现细节,仅供参考,来理解JDBC具体实现的原理。

2. Connection接口
  • Connection是java与特定关系数据库建立的连接(会话),是连接执行SQL语句返回结果集的前提。DriverManager.getConnection(url, user, password)方法建立在JDBC URL中定义的数据库Connection连接上。
      连接MySql数据库:Connection con = DriverManager.getConnection(“jdbc:mysql://host:port/database”, “user”, “password”);
      连接Oracle数据库:Connection con = DriverManager.getConnection(“jdbc:oracle:thin:@host:port:database”, “user”, “password”);
  • 常用方法:
    createStatement():创建向数据库发送sql的statement对象。
    prepareStatement(sql) :创建向数据库发送预编译sql的PrepareSatement对象。
    prepareCall(sql):创建数据库发送执行存储过程的callableStatement对象。
    setAutoCommit(boolean autoCommit):设置事务是否自动提交。
    commit() :在链接上提交事务。
    rollback() :在此链接上回滚事务。
3. Statement接口
  • Statement接口是用于执行静态SQL语句并返回它所生成结果的对象,默认情况下,每个SQL语句同时只有一个ResultSet对象。它有以下三大接口,这三大接口也是JDBC的核心所在。

    • Statement:由createStatement创建,用于发送简单的SQL语句(不带参数)。存在sql注入的危险(String sql = “delete from table where id=” + id;如果用户传入的id为“5 or 1=1”,那么将删除表中的所有记录),不适用于多条相同的SQL语句。
    • PreparedStatement:继承自Statement接口,由preparedStatement创建,用于发送含有一个或多个参数的SQL语句。PreparedStatement对象比Statement对象的效率更高,并且可以防止SQL注入,是一种预编译SQL语句执行对象的接口,是一种类似于JQuery模板的方式,所以我们一般都使用PreparedStatement
    • CallableStatement:继承自PreparedStatement接口,由方法prepareCall创建,用于调用存储过程和自定义函数。
  • 常用Statement方法:
    execute(String sql):运行语句,返回是否有结果集
    executeQuery(String sql):运行select语句(DQL语句),返回ResultSet结果集。
    executeUpdate(String sql):运行insert/update/delete(DML语句),返回更新的行数(执行成功的话,大于0)。当然也可以用于运行create/drop/alter(DDL语句),返回更新的行数(执行成功的话,结果等于0)。

    addBatch(String sql) :把多条sql语句放到一个批处理中。
    executeBatch():向数据库发送一批sql语句执行。
4. ResultSet接口
  • ResultSet提供操作结果集结构的方法,常用的有
    getMetaData():返回结果集的列(域)的数量、类型和参数的对象
    getRow():返回当前的实体行数
  • ResultSet提供检索不同类型字段的方法,常用的有:
    getString(int index)、getString(String columnName):获得在数据库里是varchar、char等类型的数据对象。
    getObject(int index)、getObject(String columnName):获取在数据库里任意类型的数据。
    mysql以下所有数据类型都有上述两种get方法。mysql数据类型和java数据类型对照如下:
     char/varchar/text               java.lang.Stringtinyint/smallint/int/bigint     java.lang.Byte/Short/Integer/Longdecimal                            java.math.BigDecimalbit                             java.lang.Booleandatetime/timestamp/date            java.sql.Date
  • ResultSet还提供了对结果集进行滚动的方法:
    next():移动到下一行,相当于java集合中的iterator的hasNext()和next()的结合体
    Previous():移动到前一行
    absolute(int row):移动到指定行
    beforeFirst():移动resultSet的最前面。
    afterLast() :移动到resultSet的最后面。
    使用后依次关闭对象及连接:ResultSet → Statement → Connection

三、使用JDBC的步骤

加载JDBC驱动程序 → 建立数据库连接Connection → 创建执行SQL的语句Statement → 处理执行结果ResultSet → 释放资源
下面以java连接mysql数据库为例:

1. 以反射的方式装载驱动
 Class.forName("com.mysql.jdbc.Driver");

原则上必须只装载一次。

2. 建立连接对象Connection
final String URL = "jdbc:mysql://192.168.40.103:3306/school?useUnicode=true&characterEncoding=utf-8&useSSL=true",USERNAME = "root",PASSWORD = "kb08";Connection con = DriverManager.getConnection(URL,USERNAME,PASSWORD);

建立连接对象Connection的传入参数必须是常量,事实上,真正的连接对象过程都是写在配置文件里的,所以,当然是常量。
关于URL的结构如下:
协议:子协议://主机IP:端口号/数据库?【参数名:参数值】【其他参数如:统一编码&字符编码格式&加密套接字协议】
备注:【】为可选项,不写,就是默认值。其他为必选项,且顺序必须一致。

3. 创建执行SQL的语句+处理执行结果ResultSet

这里使用的是PreparedStatement预编译SQL语句执行,关于Statement执行SQL语句执行(不实用,且有SQL注入风险)和prepareCall执行自定义函数和存储过程(不常用),这里不做举例了。

     //创建执行对象Statement sta = con.createStatement();//执行sql命令/*非查询操作*/final String SQL1 = "insert into deptinfo(deptName) value('市场部')";final String SQL2 = "create table deptinfo";int rst1 = sta.executeUpdate(SQL1);int rst2 = sta.executeUpdate(SQL2);System.out.println(rst1);//结果1,DML语句成功执行,返回1System.out.println(rst2);//结果0,DDL语句成功执行,返回0/*查询操作*///返回结果集final String SQL3 = "select * from deptinfo";ResultSet rst3 = sta.executeQuery(SQL);while(rst3.next()){System.out.print(rst3.getInt(1)+"\t");System.out.println(rst3.getString("deptName"));}//占位符的使用final String SQL = "select * from empinfo where deptId=?";PreparedStatement pst = con.prepareStatement(SQL);pst.setObject(1,3);ResultSet rst = pst.executeQuery();ResultSetMetaData struct = rst.getMetaData();String[] arr = new String[struct.getColumnCount()];while (rst.next()) {for (int i = 1; i <= arr.length; i++) {arr[i - 1] = rst.getObject(i).toString();}System.out.println(MessageFormat.format("{0}\t{1}\t{2}\t{3}", arr));}

占位符,其实非常像JQuery中的模板字符串的形式,同时也可以理解为java中String的Message.format()的填坑操作。

4. 释放资源
         PoolUtil.close(rst);PoolUtil.close(pst);PoolUtil.close(con);

这里的释放资源我调用了我自己封装的关闭资源的工具类PoolUtil进行释放资源。

四、综合运用(BaseDao搭建)

BaseDao,就是数据库访问对象的工具类,它广泛应用于mysql通过JDBC在java平台中SQL语句的执行,它对java执行SQL语句的过程进行了封装,方便多个数据库连接对象同时多并发地去调用该工具类去执行多条SQL语句(包含基本的增删改查、自定义函数和存储过程的执行),是一款功能强大的工具类。本人搭建了一个简单的BaseDao,供大家参考。

1. 设计难点

1、查询操作:通过自定义类型SelRtn反馈【是否存在异常】和查询结果
2、BaseDao对象的管理缺失(不停的创建):单例工厂的模式
3、连接字符串管理:通过properties小型配置文件
4、并发管理:通过弹性线程池ConPool对多并发和Connection对象进行管理

2. 具体实现
  1. properties小型配置文件(config/sys.properties)
    properties小型配置文件实际存储的是键值对形式的配置内容,下面会详说。mysql和连接池对象poolCon的配置如下:
#mysql配置
#mysql 驱动装载配置
mysql01.driver=com.mysql.jdbc.Driver
#mysql url配置(数据库地址)
mysql01.url=jdbc:mysql://192.168.40.103:3306/school?useUnicode=true&characterEncoding=utf-8&userSSL=true
#mysql 连接对象用户名
mysql01.username=root
#mysql 连接对象用户密码
mysql01.password=kb08#连接池配置
#核心连接池数
pool.coreCount=3
#最大连接池数
pool.maxCount=10
#最大等待时间(s)
pool.maxWait=5
#连接池最大空闲时间(s)
pool.maxIdle=30
#重新尝试连接间隔时间(ms)
pool.retryInterval=50
#重新尝试连接最大次数
pool.maxRetryCount=8
#连接池异常信息    0:无异常   1:异常
pool.exitOnErr=0
  1. 系统级常量PoolConstant(使用枚举也行)
    PoolConstant类将 properties小型配置文件的每个配置信息进行了封装,此外也封装了POOL和MYSQL两大类的配置信息,方便之后调用。
package cn.kgc.kb08.jdbc.dao3.impl;public interface PoolConstant {String POOL_CORE_COUNT = "coreCount";String POOL_MAX_COUNT = "maxCount";String POOL_MAX_WAIT = "maxWait";String POOL_MAX_IDLE = "maxIdle";String POOL_RETRY_INTERVAL = "retryInterval";String POOL_MAX_RETRY_COUNT = "maxRetryCount";String POOL_EXIT_ON_ERR = "exitOnErr";String[] POOL = {POOL_CORE_COUNT,POOL_MAX_COUNT,POOL_MAX_WAIT,POOL_MAX_IDLE,POOL_RETRY_INTERVAL,POOL_MAX_RETRY_COUNT,POOL_EXIT_ON_ERR};String MYSQL_DRI = "driver";String MYSQL_URL = "url";String MYSQL_USER = "username";String MYSQL_PASS = "password";String[] MYSQL = {MYSQL_DRI,MYSQL_URL,MYSQL_USER,MYSQL_PASS};
}
  1. 连接池Pool公共接口
    规定两个方法:
    void destroy():销毁连接池对象
    Dao newDao():创建唯一的Dao对象
package cn.kgc.kb08.jdbc.dao3;public interface Pool {void destroy();Dao newDao();
}
  1. 连接池Pool的抽象工厂PoolFactory
    该类规定了连接池的单例模式的核心两点:
    Pool 构造方法私有不可见
    同步锁初始化获得连接池Pool对象(第一次创建,其他获得同一个对象Pool)。
    这两点保证了Pool对象的单例模式,而Pool对象实际上封装了BaseDao对象(下面会讲到)的创建,由于Pool对象是唯一的,自然而然其封装的内部类BaseDao对象也是唯一的,这样就解决了BaseDao对象的管理缺失。
package cn.kgc.kb08.jdbc.dao3;import cn.kgc.kb08.jdbc.dao3.impl.ConPool;/*** 连接池的单例模式*/
public abstract class PoolFactory {private static Pool pool;private static synchronized void init(){if (null==pool){pool = new ConPool();}}public static Pool get(){if (null==pool){init();}return pool;}
}
  1. 连接池对象管理类ConPool
    这是BaseDao的核心所在,通过弹性线程池ConPool对多并发和Connection对象进行管理,内部封装了BaseDao的内部类,下面简要分析一下原理:

    实际上,连接池对象管理类ConPool就相当于一个分段锁,意思是说,ConPool整体是一个分段锁的外部,是一个读锁(共享锁),每一个连接对象都能同时访问,上面说过连接对象内部封装了BaseDao对象,就意味着,每个连接对象都能通过唯一的BaseDao,去访问数据库mysql。ConPool其内部又有一定数量的读锁(核心连接池数,当然当核心池连接长时间繁忙时,就是说超过配置信息的最大尝试时间*最大尝试次数,系统会额外的建立一个非核心连接池对象,解决临时问题,当然这个非核心连接池对象在其空闲时间超过配置的最大空闲时间时,就会被系统销毁)每一个核心连接池对象,他们之间又是写锁(排它锁),而我们知道读锁是支持并发查询数据的,而写锁又能保证在增删改数据的同时只能保证一个连接池对象能够拿到那个唯一的ConPool对象,进而拿到唯一的BaseDao对象,进行操作,操作完成后释放锁,保证了并发地增删改查操作。具体代码如下:
package cn.kgc.kb08.jdbc.dao3.impl;import cn.kgc.kb08.jdbc.dao3.Pool;
import cn.kgc.kb08.jdbc.dao3.SelRtn;
import cn.kgc.kb08.jdbc.dao3.Dao;import java.lang.reflect.Method;
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;import static cn.kgc.kb08.jdbc.dao3.impl.PoolUtil.close;/*** Base:通用* Dao:DataBase Access Object 数据库访问对象* @version kb08-3.0* @since kb08-1.0* 已解决缺陷:* 1、查询操作:通过自定义类型SelRtn反馈【是否存在异常】和查询结果* 2、BaseDao对象的管理缺失(不停的创建):单例工厂的模式* 3、连接字符串管理:通过properties小型配置文件* 4、并发管理:通过弹性线程池PoolUtil对多并发和Connection对象进行管理*/
public final class ConPool implements Pool {/*** 自定义类型:池中连接*/private class PoolCon{//池连接对象是否空闲boolean free = true;//池连接对象是否为核心连接对象boolean core;//连接数据库对象Connection con;//临时池连接对象空闲开始时间Long idleBegin;public PoolCon(boolean core,Connection con){this.core = core;this.con = con;resetIdle();}public void resetIdle(){if (!core){this.idleBegin = System.currentTimeMillis();}}}//池连接键值对private ConcurrentMap<Integer,PoolCon> pool;//池连接配置信息键值对private Map<String,Integer> cnfPool;//数据库连接配置信息键值对private Map<String,String> cnfCon;/*** 执行定期清理线程池* 1、检查核心连接对象的有效性:无效则创建新核心连接对象覆盖之* 2、检查临时连接对象是否超时,超时则并关闭并移除*/private ScheduledExecutorService schedule;//主线程池(后面定义为配置信息的2倍数量)private ExecutorService service;private Lock lock;private Condition cond;//临时池连接对象是否正在清理private boolean clearing;//数据库访问对象private Dao dao;public  ConPool(){initCnf();initPool();startClear();}/*** 初始化池和数据配置信息*/private void initCnf(){cnfPool = PoolUtil.parse(Integer.class,"pool",Arrays.asList(PoolConstant.POOL));cnfCon = PoolUtil.parse(String.class,"mysql01",Arrays.asList(PoolConstant.MYSQL));}/*** 初始化连接池(核心连接对象)*/private void initPool(){final int MAX_COUNT = cnfPool.get(PoolConstant.POOL_MAX_COUNT);//主线程池数量为配置信息最大数量的2倍service = Executors.newFixedThreadPool(MAX_COUNT*2);schedule = Executors.newSingleThreadScheduledExecutor();lock = new ReentrantLock(true);cond = lock.newCondition();//支持并发操作的Mappool = new ConcurrentHashMap<>(MAX_COUNT);PoolCon pc;final int CORE_COUNT = cnfPool.get(PoolConstant.POOL_CORE_COUNT);//初始化核心池连接对象//变量j的作用是保证核心连接池对象创建成功(数量与配置信息一致),即使线程中途中断,也能保证成功for (int i = 1,j = 1; i <= CORE_COUNT; i++) {pc = mkPoolCon(true);if (null!=pc){System.out.println(j+"池连接对象创建成功");pool.put(j++,pc);}}if (pool.size()==0){System.out.println("连接池初始化失败,系统强制退出");System.exit(-1);}//规则:连接对象不小于最大连接对象的一半,或者出现异常(配置信息的异常为1)if (cnfPool.get(PoolConstant.POOL_EXIT_ON_ERR)==1 && pool.size()<=CORE_COUNT/2){System.out.println("连接池初始化有效核心连接数为未过半异常,系统强制退出");System.exit(-1);}System.out.println("连接池初始化成功");}/*** 创建一个池对象* @param core 池对象类型:true:核心对象  false:临时对象* @return PoolCon*/private PoolCon mkPoolCon(boolean core){PoolCon pc = null;for (int i = 1; i <=cnfPool.get(PoolConstant.POOL_MAX_RETRY_COUNT); i++) {try {//建立与数据库的连接Connection con = DriverManager.getConnection(cnfCon.get(PoolConstant.MYSQL_URL),cnfCon.get(PoolConstant.MYSQL_USER),cnfCon.get(PoolConstant.MYSQL_PASS));pc = new PoolCon(core,con);break;} catch (SQLException e) {try {//尝试配置信息的间隔时间进行请求建立池连接对象TimeUnit.SECONDS.sleep(cnfPool.get(PoolConstant.POOL_RETRY_INTERVAL));continue;} catch (InterruptedException ex) {System.err.println(ex.getMessage());}}}return pc;}/*** 验证连接对象是否有效* @param pc 池连接对象* @return true :有效 false:无效*/private boolean isPCValid(PoolCon pc){try {//执行一条无意义的SQL语句pc.con.createStatement().executeQuery("select 1");return true;} catch (SQLException e) {return false;}}/*** 验证临时连接对象是否过期* @param pc 池连接对象* @return true: 过期     false:未过期*/private boolean isExpired(PoolCon pc){//临时连接对象空闲时间超过配置信息的最大空闲时间return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()-pc.idleBegin)>=cnfPool.get(PoolConstant.POOL_MAX_IDLE);}/*** 验证用户等待是否超时(配置最大时限)* @param waitBegin 计算参考起点时间* @return true:等待已超时   false:等待未超时*/private boolean isWaitExpried(long waitBegin){//连接对象请求未获得批准时间超过配置信息的最大请求等待时间return TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()-waitBegin)>=cnfPool.get(PoolConstant.POOL_MAX_WAIT);}/*** 开启定期清理任务*/private void startClear(){System.out.println("池清理线程启动成功。。。。");//定时时间为配置信息的最大空闲时间int delay = cnfPool.get(PoolConstant.POOL_MAX_IDLE);schedule.scheduleWithFixedDelay(new Runnable() {@Overridepublic void run() {//定时清理时获得锁,正在清理lock.lock();clearing = true;//非空闲的连接池对象不清理for (Integer key : pool.keySet()) {PoolCon pc = pool.get(key);if (!pc.free){continue;}//核心连接池对象无效,重新建立覆盖之if (pc.core){if (!isPCValid(pc)){pool.put(key,mkPoolCon(true));}}else{//临时连接池对象无效或者已超时,进行清理if (isExpired(pc) || !isPCValid(pc)){pool.remove(key);}}}//定时清理后释放锁,通知其他线程,已完成清理clearing = false;cond.signalAll();lock.unlock();}},delay,delay,TimeUnit.SECONDS);}/*** 连接池销毁*/@Overridepublic void destroy(){//只要有连接池对象就有间隔时间的判断是否销毁while (pool.size()>0){for (Integer key : pool.keySet()) {PoolCon pc = pool.get(key);//连接池对象空闲方能清理,释放资源,并移除if (pc.free){pc.free = false;close(pc.con);pool.remove(key);System.out.println("释放池连接"+key+"对象成功");}}try {TimeUnit.MILLISECONDS.sleep(cnfPool.get(PoolConstant.POOL_RETRY_INTERVAL));} catch (InterruptedException e) {e.printStackTrace();}}}/*** 从池中获取一个有效连接对象* @return PoolCon*/private PoolCon fetch(){long waitBegin = System.currentTimeMillis();for (int i = 1; i <=cnfPool.get(PoolConstant.POOL_MAX_RETRY_COUNT) ; i++) {try {//上锁,正在清理就等待lock.lock();if (clearing) {cond.await();}for (Integer key : pool.keySet()) {PoolCon pc = pool.get(key);//连接池对象空闲且有效方能取出if (pc.free && isPCValid(pc)){pc.free = false;System.out.println("成功取出池连接对象");return pc;}}//没有超时无法获得,一定时间间隔后再次请求if (isWaitExpried(waitBegin)){return null;}TimeUnit.MILLISECONDS.sleep(cnfPool.get(PoolConstant.POOL_RETRY_INTERVAL));}catch (Exception ex){ex.printStackTrace();}finally {lock.unlock();}}//取出临时连接对象(并非是第一个临时连接池对象)if (pool.size()<cnfPool.get(PoolConstant.POOL_MAX_COUNT)){PoolCon pc = mkPoolCon(false);if (null!=pc){pc.free = false;pool.put(pool.size()+1,pc);System.out.println("成功取出池连接对象");return pc;}}return null;}/*** 归还一个有效连接对象给池中* @param pc*/private void giveBack(PoolCon pc){//没有连接池对象,返回if (null==pc){return;}//非核心连接池对象重置空闲时间if (!pc.core){pc.resetIdle();}pc.free = true;}/*** 初始化Dao对象*/private synchronized void init(){if (null==dao){dao = new Dao() {/*** 自定义方法:获取预编译执行对象* @param con 连接对象* @param SQL 数据库操作命令* @param params 动态参数:预编译参数* @return 预编译执行对象* @throws SQLException*/private PreparedStatement getPst(Connection con,final String SQL,Object...params) throws SQLException {PreparedStatement pst = con.prepareStatement(SQL);if (null!=params && params.length>0){for (int i = 0; i < params.length; i++) {pst.setObject(i+1,params[i]);}}return pst;}/*** 自定义方法:执行增删改操作* @param pst 执行对象* @return int* @throws SQLException*/private int update(PreparedStatement pst) throws SQLException {return pst.executeUpdate();}/*** 自定义方法:执行查询操作* @param pst 执行对象* @return 结果集* @throws SQLException*/private ResultSet query(PreparedStatement pst) throws SQLException {return pst.executeQuery();}/*** 自定义方法:反射解析实体参数类型的set方法,并以属性名为键,以方法对象为值进行存储* @param c 实体类的实际类型* @return HashMap<String, Method>*/private HashMap<String, Method> parseMethod(Class c) throws Exception{HashMap<String, Method> mapMethod = new HashMap<>();final String PREFIX = "set";for (Method method : c.getDeclaredMethods()) {String name = method.getName();if (!name.startsWith(PREFIX)){continue;}name = name.substring(3);name = name.substring(0,1).toLowerCase()+name.substring(1);mapMethod.put(name,method);}return mapMethod;}/*** 自定义方法:解析结果集携带的表结构(表字段名称)* @param md 结构对象* @return Stirng[]* @throws SQLException*/private String[] parseStruct(ResultSetMetaData md) throws SQLException {String[] names = new String[md.getColumnCount()];for (int i = 0; i < names.length; i++) {names[i] = md.getColumnLabel(i+1);}return names;}/*** 自定义方法:封装数据库的增删改操作* @param SQL 数据库命令* @param params 动态参数: 预编译命令参数* @return int: -1:异常   0:失败    >0表示成功(多条操作)*/@Overridepublic int exeUpd(final String SQL,final Object...params){try {return service.submit(new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int rst = 0;PoolCon pc = null;PreparedStatement pst = null;try {pc = fetch();if (null!=pc){pst = getPst(pc.con,SQL,params);rst = update(pst);}} catch (SQLException e) {rst = -1;}finally {close(pst);giveBack(pc);}return rst;}}).get();} catch (Exception e) {return -1;}}/*** 自定义方法:封装查询单个值* @param <T> 泛型* @param c 需要单个值的实际类型:基本类型的包装类型* @param SQL 数据库命令* @param params 动态参数:预编译参数值* @return SelRtn:  isErr=true:异常   isErr=false(rtn=null:失败,否则:成功)*/@Overridepublic <T> SelRtn exeSingle(final Class<T> c, final String SQL, final Object...params){try {return service.submit(new Callable<SelRtn>() {@Overridepublic SelRtn call() throws Exception {PoolCon pc = null;PreparedStatement pst = null;ResultSet rst = null;try {pc = fetch();pst = getPst(pc.con,SQL,params);rst = query(pst);if (null!=rst && rst.next()){return SelRtn.succeed(c.getConstructor(String.class).newInstance(rst.getObject(1).toString()));}else{return SelRtn.succeed(null);}} catch (Exception e) {e.printStackTrace();}finally {PoolUtil.close(rst,pst);giveBack(pc);}return SelRtn.fail();}}).get();} catch (Exception e) {return SelRtn.fail();}}/*** 自定义方法:封装查询多行多列* @param <T> 泛型* @param c 需要实际类型(实体自定义类型)* @param SQL 数据库命令* @param params 动态参数:预编译参数值* @return SelRtn:  isErr=true:异常   isErr=false(rtn=null:失败,否则:成功)*/@Overridepublic <T> cn.kgc.kb08.jdbc.dao3.SelRtn exeQuery(final Class<T> c, final String SQL, final Object...params){try {return service.submit(new Callable<SelRtn>() {@Overridepublic SelRtn call() throws Exception {PoolCon pc = null;PreparedStatement pst = null;ResultSet rst = null;try {pc = fetch();pst = getPst(pc.con,SQL,params);rst = query(pst);if (null!=rst && rst.next()){List<T> list = new ArrayList<>();Map<String, Method> map = parseMethod(c);String[] names = parseStruct(rst.getMetaData());do{T t = c.newInstance();for (String name : names) {map.get(name).invoke(t,rst.getObject(name));}list.add(t);}while (rst.next());return SelRtn.succeed(list);}else{return SelRtn.succeed(null);}} catch (Exception e) {e.printStackTrace();}finally {PoolUtil.close(rst,pst);giveBack(pc);}return SelRtn.fail();}}).get();} catch (Exception e) {return SelRtn.fail();}}};}}/*** 实例化Dao对象* @return Dao*/@Overridepublic Dao newDao(){if (null==dao){init();}return dao;}
}
  1. PoolUtil连接池对象工具类
    封装了解析数据源配置信息和释放系统资源这两个行为
package cn.kgc.kb08.jdbc.dao3.impl;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;public class PoolUtil {/*** 自定义方法:解析数据源配置信息* @param dataSource 数据源名称* @return Map<String,String>*/protected static <T> Map<String,T> parse(Class<T> c,String dataSource, List<String> items){File config = new File("config/sys.properties");Properties pro = new Properties();try {pro.load(new FileInputStream(config));Map<String,T> map = new HashMap(items.size());for (String item : items) {String key = dataSource+"."+item;if (!pro.containsKey(key)){throw new IOException("缺失配置项:"+item);}map.put(item,c.getConstructor(String.class).newInstance(pro.getProperty(key)));}return map;} catch (Exception e) {System.out.println(dataSource+"数据源配置信息异常,系统强制退出:"+e.getMessage());System.exit(-1);}finally {if (null!=pro){//清空键值对,并清空引用pro.clear();pro = null;}}return null;}/*** 数据库资源释放* @param acs 动态参数:待释放资源*/protected static void close(AutoCloseable...acs){for (AutoCloseable ac : acs) {if (null!=ac){try {ac.close();} catch (Exception e) {e.printStackTrace();}}}}
}
  1. BaseDao的公共方法Dao接口
ackage cn.kgc.kb08.jdbc.dao3;public interface Dao {int exeUpd(final String SQL, Object... params);<T> SelRtn exeSingle(final Class<T> c, final String SQL, final Object... params);<T> cn.kgc.kb08.jdbc.dao3.SelRtn exeQuery(final Class<T> c, final String SQL, final Object... params);}
  1. 自定义类型SelRtn
    通过自定义类型SelRtn反馈【是否存在异常】和查询结果:
    异常:返回SelRtn fail(),其err属性为true
    结果空:返回SelRtn succeed(),其rtn属性为null
    结果非空:返回SelRtn succeed(),其rtn属性为Object对象
package cn.kgc.kb08.jdbc.dao3;/*** 完善查询操作返回类型,对于异常的缺失*/
public final class SelRtn {private boolean err = false;private Object rtn;public static SelRtn succeed(Object rtn){return new SelRtn(rtn);}public static SelRtn fail(){return new SelRtn();}private SelRtn(Object rtn){this.rtn = rtn;}private SelRtn(){this.err = true;}public boolean isErr(){return this.err;}public <T> T getRtn(){return (T)rtn;}
}
  1. 实体类
    与数据库中表字段一一对应
package cn.kgc.kb08.jdbc.entity;public class Student {private Integer id;private Integer classId;private String stuName;private String stuGender;private String province;private String city;private String district;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public Integer getClassId() {return classId;}public void setClassId(Integer classId) {this.classId = classId;}public String getStuName() {return stuName;}public void setStuName(String stuName) {this.stuName = stuName;}public String getStuGender() {return stuGender;}public void setStuGender(Boolean stuGender) {this.stuGender = stuGender?"男":"女";}public String getProvince() {return province;}public void setProvince(String province) {this.province = province;}public String getCity() {return city;}public void setCity(String city) {this.city = city;}public String getDistrict() {return district;}public void setDistrict(String district) {this.district = district;}@Overridepublic String toString() {return "Student{" +"id=" + id +", classId=" + classId +", stuName='" + stuName + '\'' +", stuGender='" + stuGender + '\'' +", province='" + province + '\'' +", city='" + city + '\'' +", district='" + district + '\'' +'}';}
}
  1. 测试类
package cn.kgc.kb08.jdbc.daotest;import cn.kgc.kb08.jdbc.dao3.Pool;
import cn.kgc.kb08.jdbc.dao3.SelRtn;
import cn.kgc.kb08.jdbc.dao3.Dao;
import cn.kgc.kb08.jdbc.dao3.PoolFactory;
import cn.kgc.kb08.jdbc.entity.Student;import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Test {public static void main(String[] args) {Pool pool = PoolFactory.get();final Dao dao = pool.newDao();final int PAGE_SIZE = 10;String sql = "select ceil(count(1)/?) from studentinfo";final int TOTAL = dao.exeSingle(Integer.class,sql,PAGE_SIZE).getRtn();ExecutorService service = Executors.newFixedThreadPool(TOTAL);final String SQL = "select * from studentinfo limit ?,?";for (int i = 1; i <= TOTAL; i++) {final int I = i;service.submit(new Runnable() {@Overridepublic void run() {SelRtn sr = dao.exeQuery(Student.class, SQL,(I-1)*PAGE_SIZE,PAGE_SIZE);System.out.println("\n第"+I+"页*******************************");if (!sr.isErr()){List<Student> list = sr.getRtn();if (null!=list&&list.size()>0){for (Student student : list) {System.out.println(student);}}else{System.out.println("查无记录");}}else{System.out.println("出错");}}});}pool.destroy();}
}

结果:

1池连接对象创建成功
2池连接对象创建成功
3池连接对象创建成功
连接池初始化成功
池清理线程启动成功。。。。
成功取出池连接对象
释放池连接1对象成功
释放池连接2对象成功
释放池连接3对象成功
成功取出池连接对象第7页*******************************
Student{id=61, classId=5, stuName='葛优', stuGender='男', province='安徽省', city='蚌埠市', district='五河县'}
Student{id=62, classId=5, stuName='赵伟伟', stuGender='男', province='江苏省', city='南通市', district='海安市'}
Student{id=63, classId=5, stuName='汤伟杰', stuGender='男', province='山东省', city='潍坊市', district='诸城市'}
Student{id=64, classId=5, stuName='江玉', stuGender='男', province='江苏省', city='淮安市', district='盱眙县'}
Student{id=65, classId=5, stuName='戴媛媛', stuGender='女', province='安徽省', city='安庆市', district='桐城市'}
Student{id=66, classId=5, stuName='赵杰', stuGender='男', province='安徽省', city='六安市', district='霍山县'}
Student{id=67, classId=5, stuName='吴悠', stuGender='男', province='天津市', city='市辖区', district='滨海新'}
Student{id=68, classId=5, stuName='孙亚洲', stuGender='男', province='安徽省', city='安庆市', district='望江县'}
Student{id=69, classId=5, stuName='李佛光', stuGender='男', province='江苏省', city='南通市', district='如东县'}
Student{id=70, classId=5, stuName='王家能', stuGender='男', province='云南省', city='楚雄彝', district='族自治'}
成功取出池连接对象第5页*******************************
Student{id=41, classId=3, stuName='何健', stuGender='男', province='云南省', city='楚雄彝', district='族自治'}
Student{id=42, classId=3, stuName='唐明汉', stuGender='男', province='江西省', city='抚州市', district='乐安县'}
Student{id=43, classId=3, stuName='郑朋', stuGender='男', province='江苏省', city='泰州市', district='泰兴市'}
Student{id=44, classId=3, stuName='陈立志', stuGender='男', province='安徽省', city='亳州市', district='涡阳县'}
Student{id=45, classId=3, stuName='庄俊伟', stuGender='男', province='江苏省', city='南京市', district='浦口区'}
Student{id=46, classId=3, stuName='戴彬', stuGender='男', province='上海市', city='市辖区', district='浦东新'}
Student{id=47, classId=4, stuName='陈元生', stuGender='男', province='江苏省', city='泰州市', district='泰兴市'}
Student{id=48, classId=4, stuName='张刘留', stuGender='男', province='江苏省', city='南京市', district='高淳区'}
Student{id=49, classId=4, stuName='刘青青', stuGender='女', province='江苏省', city='扬州市', district='宝应县'}
Student{id=50, classId=4, stuName='周璟', stuGender='男', province='江苏省', city='扬州市', district='宝应县'}
成功取出池连接对象第1页*******************************
Student{id=1, classId=1, stuName='陈涛', stuGender='男', province='江苏省', city='南京市', district='浦口区'}
Student{id=2, classId=1, stuName='陈玉莹', stuGender='女', province='江苏省', city='盐城市', district='阜宁县'}
Student{id=3, classId=1, stuName='陈真', stuGender='男', province='安徽省', city='蚌埠市', district='五河县'}
Student{id=4, classId=1, stuName='丁聪', stuGender='男', province='江苏省', city='南通市', district='海安市'}
Student{id=5, classId=1, stuName='管若涵', stuGender='男', province='山东省', city='潍坊市', district='诸城市'}
Student{id=6, classId=1, stuName='胡正双', stuGender='男', province='江苏省', city='淮安市', district='盱眙县'}
Student{id=7, classId=1, stuName='江文涛', stuGender='男', province='安徽省', city='安庆市', district='桐城市'}
Student{id=8, classId=1, stuName='雷兵', stuGender='男', province='安徽省', city='六安市', district='霍山县'}
Student{id=9, classId=1, stuName='李楚鸿', stuGender='男', province='天津市', city='市辖区', district='滨海新'}
Student{id=10, classId=1, stuName='刘用兵', stuGender='男', province='安徽省', city='安庆市', district='望江县'}
成功取出池连接对象
成功取出池连接对象第9页*******************************
Student{id=81, classId=6, stuName='吴涛', stuGender='男', province='安徽省', city='宣城市', district='宁国市'}
Student{id=82, classId=6, stuName='徐磊', stuGender='男', province='安徽省', city='蚌埠市', district='怀远县'}
Student{id=83, classId=6, stuName='黄雄', stuGender='男', province='江苏省', city='盐城市', district='亭湖区'}
Student{id=84, classId=6, stuName='蔡志远', stuGender='男', province='江苏省', city='南通市', district='如皋市'}
Student{id=85, classId=6, stuName='陈稼轩', stuGender='男', province='安徽省', city='马鞍山', district='市和县'}
Student{id=86, classId=6, stuName='周继伟', stuGender='男', province='安徽省', city='安庆市', district='望江县'}第3页*******************************
Student{id=21, classId=2, stuName='徐凯强', stuGender='男', province='江苏省', city='扬州市', district='宝应县'}
Student{id=22, classId=2, stuName='姚成耀', stuGender='男', province='安徽省', city='安庆市', district='望江县'}
Student{id=87, classId=6, stuName='陈军华', stuGender='男', province='江苏省', city='南京市', district='江宁区'}
Student{id=88, classId=6, stuName='秦超', stuGender='男', province='江苏省', city='南京市', district='浦口区'}
Student{id=23, classId=2, stuName='应春雨', stuGender='女', province='安徽省', city='宣城市', district='宁国市'}
Student{id=24, classId=2, stuName='余杰', stuGender='男', province='安徽省', city='蚌埠市', district='怀远县'}
Student{id=25, classId=2, stuName='张超', stuGender='男', province='江苏省', city='盐城市', district='亭湖区'}
Student{id=89, classId=6, stuName='邵娅', stuGender='女', province='江苏省', city='盐城市', district='阜宁县'}
Student{id=26, classId=2, stuName='张志强', stuGender='男', province='江苏省', city='南通市', district='如皋市'}
Student{id=27, classId=2, stuName='张梓尧', stuGender='男', province='安徽省', city='马鞍山', district='市和县'}
Student{id=28, classId=2, stuName='黄超杰', stuGender='男', province='安徽省', city='安庆市', district='望江县'}
Student{id=29, classId=2, stuName='李刚', stuGender='男', province='江苏省', city='南京市', district='江宁区'}
Student{id=30, classId=2, stuName='吴凡', stuGender='男', province='江苏省', city='南京市', district='浦口区'}
成功取出池连接对象第6页*******************************
Student{id=51, classId=4, stuName='苏金峰', stuGender='男', province='安徽省', city='安庆市', district='望江县'}
Student{id=52, classId=4, stuName='吴凯', stuGender='男', province='安徽省', city='宣城市', district='宁国市'}
Student{id=53, classId=4, stuName='王纪文', stuGender='男', province='安徽省', city='蚌埠市', district='怀远县'}
Student{id=54, classId=4, stuName='孟涛涛', stuGender='男', province='江苏省', city='盐城市', district='亭湖区'}
Student{id=55, classId=4, stuName='李志杰', stuGender='男', province='江苏省', city='南通市', district='如皋市'}
Student{id=56, classId=4, stuName='丰笛', stuGender='男', province='安徽省', city='马鞍山', district='市和县'}
Student{id=57, classId=4, stuName='周炜', stuGender='男', province='安徽省', city='安庆市', district='望江县'}
Student{id=58, classId=4, stuName='张小港', stuGender='男', province='江苏省', city='南京市', district='江宁区'}
Student{id=59, classId=4, stuName='汪小天', stuGender='男', province='江苏省', city='南京市', district='浦口区'}
Student{id=60, classId=4, stuName='花磊', stuGender='男', province='江苏省', city='盐城市', district='阜宁县'}
成功取出池连接对象
成功取出池连接对象第8页*******************************
Student{id=71, classId=5, stuName='关敬元', stuGender='男', province='江西省', city='抚州市', district='乐安县'}
Student{id=72, classId=5, stuName='陈明秋', stuGender='男', province='江苏省', city='泰州市', district='泰兴市'}
Student{id=73, classId=5, stuName='李国旗', stuGender='男', province='安徽省', city='亳州市', district='涡阳县'}
Student{id=74, classId=5, stuName='韩睿刘洋', stuGender='男', province='江苏省', city='南京市', district='浦口区'}
Student{id=75, classId=5, stuName='张全根', stuGender='男', province='上海市', city='市辖区', district='浦东新'}
Student{id=76, classId=6, stuName='王跃', stuGender='男', province='江苏省', city='泰州市', district='泰兴市'}
Student{id=77, classId=6, stuName='王伟', stuGender='男', province='江苏省', city='南京市', district='高淳区'}
Student{id=78, classId=6, stuName='谢武阳', stuGender='男', province='江苏省', city='扬州市', district='宝应县'}
Student{id=79, classId=6, stuName='袁潇凯', stuGender='男', province='江苏省', city='扬州市', district='宝应县'}
Student{id=80, classId=6, stuName='邢慧斌', stuGender='男', province='安徽省', city='安庆市', district='望江县'}第4页*******************************
Student{id=31, classId=3, stuName='殷狄', stuGender='男', province='江苏省', city='盐城市', district='阜宁县'}
Student{id=32, classId=3, stuName='吴衍文', stuGender='男', province='安徽省', city='蚌埠市', district='五河县'}
Student{id=33, classId=3, stuName='范新宇', stuGender='男', province='江苏省', city='南通市', district='海安市'}
Student{id=34, classId=3, stuName='张琪', stuGender='男', province='山东省', city='潍坊市', district='诸城市'}
Student{id=35, classId=3, stuName='凌宇翔', stuGender='男', province='江苏省', city='淮安市', district='盱眙县'}
Student{id=36, classId=3, stuName='贾仲羽', stuGender='男', province='安徽省', city='安庆市', district='桐城市'}
Student{id=37, classId=3, stuName='黄鑫', stuGender='男', province='安徽省', city='六安市', district='霍山县'}
Student{id=38, classId=3, stuName='胡妙妙', stuGender='女', province='天津市', city='市辖区', district='滨海新'}
Student{id=39, classId=3, stuName='张啸尘', stuGender='男', province='安徽省', city='安庆市', district='望江县'}
Student{id=40, classId=3, stuName='许梦娟', stuGender='女', province='江苏省', city='南通市', district='如东县'}
成功取出池连接对象第2页*******************************
Student{id=11, classId=1, stuName='钱一鸣', stuGender='男', province='江苏省', city='南通市', district='如东县'}
Student{id=12, classId=1, stuName='苏郑梓梵', stuGender='男', province='云南省', city='楚雄彝', district='族自治'}
Student{id=13, classId=1, stuName='唐宇飞', stuGender='男', province='江西省', city='抚州市', district='乐安县'}
Student{id=14, classId=1, stuName='王波', stuGender='男', province='江苏省', city='泰州市', district='泰兴市'}
Student{id=15, classId=1, stuName='王春雷', stuGender='男', province='安徽省', city='亳州市', district='涡阳县'}
Student{id=16, classId=1, stuName='王亮', stuGender='男', province='江苏省', city='南京市', district='浦口区'}
Student{id=17, classId=2, stuName='王志远', stuGender='男', province='上海市', city='市辖区', district='浦东新'}
Student{id=18, classId=2, stuName='吴彦祥', stuGender='男', province='江苏省', city='泰州市', district='泰兴市'}
Student{id=19, classId=2, stuName='吴志远', stuGender='男', province='江苏省', city='南京市', district='高淳区'}
Student{id=20, classId=2, stuName='相羽', stuGender='男', province='江苏省', city='扬州市', district='宝应县'}Process finished with exit code -1

简述JDBC(BaseDao源码级)相关推荐

  1. visual MySQL 教程_MySql轻松入门系列——第二站 使用visual studio 对mysql进行源码级调试...

    一:背景 1. 讲故事 上一篇说了mysql的架构图,很多同学反馈说不过瘾,毕竟还是听我讲故事,那这篇就来说一说怎么利用visual studio 对 mysql进行源码级调试,毕竟源码面前,不谈隐私 ...

  2. Linux环境下用OpenJTAG实现Linux内核的源码级调试

    1.通过U-boot将uzImage格式的内核加载到内存中(可以从Flash中读取,也可以从U盘.SD卡读取,还可以通过网络): 2.登陆到OpenOCD上,在内核中__turn_mmu_on打上断点 ...

  3. linux 源码 调试,开发一个Linux调试器(六):源码级逐步执行

    我们计算编写这些函数异常简单的版本,但真正的调试器有 thread plan 的概念,它封装了所有的单步信息.例如,调试器可能有一些复杂的逻辑去决定断点的地位,然后有一些回调函数用于断定单步操作是否完 ...

  4. 在linux下做源码免杀,Cobaltstrike免杀从源码级到落地思维转变

    文章来源: https://www.freebuf.com/articles/web/258988.html 前言 距离上一篇文章<那些FastJson漏洞不为人知的事情(开发角度)>已经 ...

  5. 源码级剖析了 Naive UI 的 Button 完整过程

    注意:为了让篇幅尽可能简洁一丢丢,在有些地方贴源码时,我尽可能贴最能反映要讲解内容的源码,其他重复性的代码就略去了,所以如果你自己尝试去阅读源码时,可能会发现和文章里的代码有出入.文章跑通 Naive ...

  6. 儒猿秒杀季!ZooKeeper从0基础到源码级大神课

    疯狂秒杀季:238元秒杀 原价 2198元 的 <ZooKeeper从0基础到源码级大神课> 今天 上午10点,仅 36 套,先到先得! === 课程内容 === 由于疫情影响了线下的流量 ...

  7. 一头扎进JDBC视频教程 + 源码

    [@2015-3-7] 记录一下自己的脚印:看到一个一头扎进JDBC视频教程 + 源码果断下载下来,看了看比较到位, 下载地址:http://www.xiaomengku.com/topic?id=6 ...

  8. fork的黑科技,它到底做了个啥,源码级分析linux内核的内存管理

    最近一直在学习linux内核源码,总结一下 https://github.com/xiaozhang8tuo/linux-kernel-0.11 一份带注释的源码,学习用. fork的黑科技,它到底做 ...

  9. 源码级调试的XNU内核

    i春秋翻译小组-FWorldCodeZ 源码级调试的XNU内核 无论你是在开发内核扩展,进行漏洞研究,还是还有其他需要进入macOS / iOS内核,XNU,有时你需要附加调试器.当你这样做时,使用源 ...

最新文章

  1. 成长轨迹44 【ACM算法之路 百炼poj.grids.cn】【字符串处理】【2799、2976、2975、2742】...
  2. JavaScript基础笔记集合(转)
  3. 还在担心写的一手烂SQL,送你4款工具
  4. RDKit | 多肽HELM字符串格式与分子Mol格式间的转换
  5. 数据结构-顺序查找的二分查找法(折半查找)
  6. linux 引导程序修复工具,技术|Linux下修改引导器的工具:Boot-Repair
  7. HDU 3487 Play with Chain(Splay)
  8. 健康档案管理系统方案/案列/APP/软件/小程序
  9. 程序员大神常用软件:mybase,一款个人知识笔记管理神器
  10. vSphere ESXI 详细安装教程
  11. 纸张的规格A3.A4.A5.A6纸的尺寸大小
  12. 如何使用Internet Download Manager批量下载音乐素材?
  13. 解决没有配置本地nacos但是一直发生localhost8848连接异常的问题
  14. (翻译)关系型数据库工作原理(二)
  15. C语言100题练习计划 31——计算两数的和与差(函数实现)
  16. 龙迅LT6911GX
  17. 学习discovery studio对对接结果进行分析
  18. Linux中FTP设置登录欢迎词,怎么为FTP登陆用户设置欢迎语(servu)
  19. lpop 原子_Matter:碳载单原子催化剂用于能量转化和存储的最新进展 – 材料牛...
  20. Spring Boot 3.0.0-M1 Reference Documentation(Spring Boot中文参考文档)-附录A-C

热门文章

  1. Raspberry Pi 3 入门 安装Raspbian系统 使用NOOBS傻瓜工具 超超超超超简单
  2. ingress-nginx 实现内部局域网的url转发配置
  3. Java正则(3)— Matcher 详解
  4. PHP实现获取第一个中文首字母并进行排序的方法
  5. 把数组排成最小的数字,划分字母区间,最小覆盖子串,验证回文字符串II
  6. Android 10 HAL 层添加HIDL实现过程
  7. 电影记忆之19(搏击俱乐部)
  8. LaTeX命令之行间距设置及心形标记
  9. 3 live555源码分析(三)——live555 任务调度
  10. JS 判断是否为数字类型