JDBC二部曲之_入门

JDBC

1 什么是JDBC?

JDBC(Java DataBase Connectivity),即Java数据库连接!也就是说,Java程序员可以使用JDBC API来操作数据库。

最早JDBC是Java EE中的规范,但是现在已经添加到Java SE中了。也就是说,JDBC API在JDK中就已经存在了。与JDBC相关的包有:java.sql和javax.sql两个包!

2 JDBC原理

  早期SUN公司的天才们想编写一套可以连接天下所有数据库的API,但是当他们刚刚开始时就发现这是不可完成的任务,因为各个厂商的数据库服务器差异太大了。后来SUN开始与数据库厂商们讨论,最终得出的结论是,由SUN提供一套访问数据库的规范,并提供连接数据库的协议标准,然后各个数据库厂商会遵循SUN的规范提供一套访问自己公司的数据库服务器的API出现。SUN提供的范围命名为JDBC,而各个厂商提供的,遵循了JDBC规范的,可以访问自己数据库的API被称之为驱动!

现在大家已经知道了,想使用JDBC必须要有驱动才可以,驱动就是JDBC中接口的实现,并且是针对自己厂商的数据库服务器的实现。我们使用的是MySQL数据库服务器,那么就需要有MySQL驱动。

3.JDBC编码步骤

连接数据库需要MySQL的JDBC驱动,即:mysql-connector-java-5.1.13-bin.jar。

对数据库的操作可以大致分为两种:更新操作、查询操作。更新操作就是增、删、改,它没有结果,而查询操作是有结果的。

  1. A.       注册驱动
  2. B.        获取与数据库的连接
  3. C.       得到代表SQL语句的对象
  4. D.       发送SQL语句:DML、DQL
  5. E.        获取结果集
  6. F.        从结果集中获取数据
  7. G.       关闭

1、注册驱动

       Driver d = new Driver();

       DriverManager.registerDriver(d);

其中Driver是com.mysql.jdbc.Driver类型,这种方法使用了硬编码,也就是说将来如果想更换数据库是不可以的,因为代码中使用了MySQL提供的类。

其实JDBC为了让我们不出现硬编码,要求各个厂商提供的驱动都要可以把自己来注册到DriverManager中,答案在Driver()的源码中。

    static {

       try {

           java.sql.DriverManager.registerDriver(new Driver());

       } catch (SQLException E) {

           throw new RuntimeException("Can't register driver!");

       }

    }

上面代码是com.mysql.jdbc.Driver类中的静态代码块,它会new一个自己类型的对象,然后再把自己注册到DriverManager中。我们都知道static块会在类被加载时就会执行,也就是说,在JVM加载Driver类时,已经把一个Driver类的对象注册到DriverManager中了,所以我们就不需要再去注册了。我们只需要保证Driver类被加载就OK了!修改上面加载驱动的代码如下:

Class.forName("com.mysql.jdbc.Driver");

上面代码是用来加载com.mysql.jdbc.Driver类的代码,这样就可以把Driver注册到DriverManager中了!

  在我们的代码中,不要出现驱动Jar包中的类!这方便我们将来切换数据库!!!

2、获取与数据库的连接

//方式一:

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/day10", "root", "sorry");

//方式二:

Properties props = new Properties();

props.put("user", "root");//key看数据库的规定

props.put("password", "sorry");

props.put("useUnicode", "true");

props.put("characterEncoding", "utf8");

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/day10", props);

//方式三:

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/day10?user=root&password=sorry");

还可以使用DriverManager的registerDriver()方法,但这个方法通常我们是不会使用的,因为使用它会导致对特定驱动的依赖。

注意,数据库的URL需要查看相关文档,不同数据库不同版本,URL都不一定一样.

URL:   协议(jdbc):子协议(mysql):主机:端口/数据库

  我们知道在使用DriverManager的getConnection()方法时需要给出三个参数,其中一个是url,下面我们来聊一聊这个url是什么东西。JDBC使用的URL用于描述数据的来源,它的语法如下:

可以把它分为三个部分,每个部分中间都是冒号:

l  其中第一部分是jdbc,这是固定的;

l  第二部分为子协议名称,一般都是特定厂商的数据库名称,例如MySQL就是jdbc:mysql:…,Oracle就是jdbc:oracle:…;

l  第三部分由数据库厂商来确定,这一部分通常需要说明数据库服务器主机的IP、端口,以及数据库名称。

MySQL的url:jdbc:mysql://localhost:3306/mydb1

其实还可以给url添加参数,最为常见的参数就是:

l  useUnicode=true

l  characterEncoding=UTF8

这两个参数是指定获取的Connection使用的编码!当然如果没有指定,那么会使用当前MySQL数据库的字符编码集。因为我们在安装MySQL时已经指定的UTF8,所以就算不指定这两个参数,获取到的连接也同样是UTF8的。

其实在不指定主机和端口时,默认也是连接localhost主机的3306端口,所以可以把url写成如下的样子:

jdbc:mysql:///mydb1

当然,最后的mydb1是要连接的数据库,这部分是不能少的。

建议把url写完整了:

jdbc:mysql://localhost:3306/mydb1?useUnicode=true&characterEncoding=UTF8

3、得到代表SQL语句的对象

Statement stmt = conn.createStatement();

4、发送SQL语句:DML、DQL

ResultSet rs = stmt.executeQuery("selcet chinese,english,math from student");

//Statement常用方法

//ResultSet executeQuery(String sql):sql一般是DQL语句, 用来发送查询语句.

//int executeUpdate(String sql):sql一般是没有返回结果集的语句,比如DML、DDL。返回值是影响到的行数, 用来发送增、删、改语句

//boolean execute(String sql):sql可以任何的语句。返回值:如果执行的sql语句有结果集,返回true,没有结果集,返回false

5、如果是DQL语句,有结果,得到返回的结果

6、遍历结果集

while(rs.next()){

    System.out.println("---------------------------");

    System.out.print(rs.getObject("chinese")+" ");

    System.out.print(rs.getObject("english")+" ");

    System.out.println(rs.getObject("math"));

}

7、释放占用的资源(官方文档,最好写成工具类.)

前面我们在获取Connection时使用了硬编码,把driverClassName、url、username、password都直接写到了Java代码中,如果将来需要更换数据库,或者更换用户来登录数据库,这都不方便,所以我们应该把连接数据库的数据写到配置文件中,例如:

//JDBC工具类

public class JdbcUtil {

    private static String driverClass;

    private static String url;

    private static String user;

    private static String password;

    

    static{

        try {

            //从配置文件中读取信息

            InputStream in = JdbcUtil.class.getClassLoader().getResourceAsStream("dbcfg.properties");

            Properties pro = new Properties();

            pro.load(in);

            driverClass = pro.getProperty("driverClass");

            url = pro.getProperty("url");

            user = pro.getProperty("user");

            password = pro.getProperty("password");

            

            Class.forName(driverClass);

        } catch (Exception e) {

            throw new ExceptionInInitializerError(e);

        }

    }

    

    public static Connection getConnection() throws Exception {

        return DriverManager.getConnection(url,user,password);

    }

    

    public static void release(ResultSet rs,Statement stat,Connection conn){

        if(rs!=null){

            try {

                rs.close();

            } catch (SQLException e) {

                e.printStackTrace();

            }

            rs = null;

        }

        if(stat!=null){

            try {

                stat.close();

            } catch (SQLException e) {

                e.printStackTrace();

            }

            stat = null;

        }

        if(conn!=null){

            try {

                conn.close();

            } catch (SQLException e) {

                e.printStackTrace();

            }

            conn = null;

        }

    }

}

 

3.SQL攻击

在需要用户输入的地方,用户输入的是SQL语句的片段,最终用户输入的SQL片段与我们DAO中写的SQL语句合成一个完整的SQL语句!例如用户在登录时输入的用户名和密码都是为SQL语句的片段!

演示SQL攻击

首先我们需要创建一张用户表,用来存储用户的信息。

CREATE TABLE tab_user(

       uid  CHAR(32) PRIMARY KEY,

       username       VARCHAR(30) UNIQUE KEY NOT NULL,

       PASSWORD        VARCHAR(30)

);

 

INSERT INTO tab_user VALUES('U_1001', 'zs', 'zs');

SELECT * FROM tab_user;

现在用户表中只有一行记录,就是zs。

下面我们写一个login()方法!

    public void login(String username,   String password) {

       Connection con = null;

       Statement stmt = null;

       ResultSet rs = null;

       try {

           con = JdbcUtils.getConnection();

           stmt = con.createStatement();

           String sql = "SELECT * FROM tab_user WHERE " +

                  "username='" + username +

                  "' and password='" +   password + "'";

           rs = stmt.executeQuery(sql);

           if(rs.next()) {

              System.out.println("欢迎" + rs.getString("username"));

           } else {

              System.out.println("用户名或密码错误!");

           }

       } catch (Exception e) {

           throw new RuntimeException(e);

       } finally {

           JdbcUtils.close(con, stmt,   rs);

       }     

    }

下面是调用这个方法的代码:

login("a' or 'a'='a", "a' or 'a'='a");

这行当前会使我们登录成功!因为是输入的用户名和密码是SQL语句片段,最终与我们的login()方法中的SQL语句组合在一起!我们来看看组合在一起的SQL语句:

SELECT * FROM tab_user WHERE username='a' or 'a'='a' and password='a' or 'a'='a'

2 防止SQL攻击

过滤用户输入的数据中是否包含非法字符;

分步交验!先使用用户名来查询用户,如果查找到了,再比较密码;

使用PreparedStatement

PreparedStatement是Statement的子接口,你可以使用PreparedStatement来替换Statement。

PreparedStatement的好处:

防止SQL攻击;

提高代码的可读性,以可维护性;

提高效率。(支持预编译SQL –占位符’? ’)

2 PreparedStatement的使用

String sql = “select * from   tab_student where s_number=?”;

PreparedStatement pstmt = con.prepareStatement(sql);

pstmt.setString(1, “S_1001”);

ResultSet rs = pstmt.executeQuery();

rs.close();

pstmt.clearParameters();//再次使用时需要把原来的设置清空。

pstmt.setString(1, “S_1002”);

rs = pstmt.executeQuery();

在使用Connection创建PreparedStatement对象时需要给出一个SQL模板,所谓SQL模板就是有“?”的SQL语句,其中“?”就是参数。

在得到PreparedStatement对象后,调用它的setXXX()方法为“?”赋值,这样就可以得到把模板变成一条完整的SQL语句,然后再调用PreparedStatement对象的executeQuery()方法获取ResultSet对象。

注意PreparedStatement对象独有的executeQuery()方法是没有参数的,而Statement的executeQuery()是需要参数(SQL语句)的。因为在创建PreparedStatement对象时已经让它与一条SQL模板绑定在一起了,所以在调用它的executeQuery()和executeUpdate()方法时就不再需要参数了。

PreparedStatement最大的好处就是在于重复使用同一模板,给予其不同的参数来重复的使用它。这才是真正提高效率的原因。

所以,建议大家在今后的开发中,无论什么情况,都去需要PreparedStatement,而不是使用Statement

4.DAO解耦

把控制权转移到外面,想要创建对象,必须先传参数依赖注入

public void setDao(User dao){

this.dao = dao;

{

另一种方式通过构造函数转过来

单开就是  单例设计模式

    //到底使用哪一个,是由自己指定的。是new出来的,没法解耦    

    //如果由外部传入使用的实现类,这个过程称之为控制反转,就可以解耦了.IoC DI:依赖注入:Spring的核心

    //工厂创建模式,就是把创建的细节隐藏起来.

    private UserDao dao = DaoFactory.getInstance().getUserDaoImpl();// = new UserDaoMySQLEnhanceImpl();

//    public UserServiceImpl(UserDao dao){//依赖注入方法一,通过构造方式

//        this.dao = dao;

//    }

//    public void setDao(UserDao dao){//依赖注入方式二,通过set方法

//        this.dao = dao;

//    }

//创建DAO实例的工厂:饿汉子式单例

public class DaoFactory {

    //先创建工厂类,需要私有化,为了能一家在就实例化,需要静态修饰

    private static DaoFactory instance = new DaoFactory();

    //无参构造函数私有化

    private DaoFactory(){}

    //定义静态方法,调用类

    public static DaoFactory getInstance(){

        return instance;

    }

    //私有Properties类,

    private static Properties props = new Properties();

    static{

        //静态代码块,初始化就加载,这样就参数就可以使用字符串,就可以写到配置文件中.

        InputStream in = DaoFactory.class.getClassLoader().getResourceAsStream("dao.properties");

        try {

            props.load(in);

        } catch (IOException e) {

            throw new RuntimeException(e);

        }

    }

    //创建一个UserDaoImpl实例类,需要的参数读取配置文件

    //这样就进行了解耦,所有需要的参数可以通过修改配置文件来完成

    public UserDao getUserDaoImpl(){

        try {

            String daoImplName = props.getProperty("userDao");

            return (UserDao)Class.forName(daoImplName).newInstance();

        } catch (Exception e) {

            throw new RuntimeException(e);

        }

    }

}

5 小知识点

读取二进制文件

我们一直在向数据库保存varhcar、int、date等类型的数据,现在我们要尝试把一张图片或一个mp3保存到数据库中去。

首先我们需要创建一张表,表中要有一个mediumblob(16M)类型的字段。

CREATE TABLE tab_bin(

       id   INT       PRIMARY   KEY AUTO_INCREMENT,

       filename VARCHAR(100),

       data       MEDIUMBLOB

);

  向数据库插入二进制数据需要使用PreparedStatement为原setBinaryStream(int, InputSteam)方法来完成。

           con = JdbcUtils.getConnection();

           String sql = "insert into tab_bin(filename,data) values(?,   ?)";

           pstmt = con.prepareStatement(sql);

           pstmt.setString(1, "a.jpg");

           InputStream in = new FileInputStream("f:\a.jpg");//得到一个输入流对象

           pstmt.setBinaryStream(2, in);// 为第二个参数赋值为流对象

           pstmt.executeUpdate();

读取二进制数据,需要在查询后使用ResultSet类的getBinaryStream()方法来获取输入流对象。也就是说,PreparedStatement有setXXX(),那么ResultSet就有getXXX()。

           con = JdbcUtils.getConnection();

           String sql = "select filename,data from tab_bin where   id=?";

           pstmt = con.prepareStatement(sql);

           pstmt.setInt(1, 1);

           rs = pstmt.executeQuery();

           rs.next();

          

           String filename = rs.getString("filename");

           OutputStream out = new   FileOutputStream("F:\" + filename);

           //使用文件名来创建输出流对象。

           InputStream in =   rs.getBinaryStream("data");//读取输入流对象

           IOUtils.copy(in, out);// 把in中的数据写入到out中。

           out.close();

  还有一种方法,就是把要存储的数据包装成Blob类型,然后调用PreparedStatement的setBlob()方法来设置数据

       con = JdbcUtils.getConnection();

       String sql = "insert into tab_bin(filename,data) values(?, ?)";

       pstmt = con.prepareStatement(sql);

       pstmt.setString(1, "a.jpg");

       File file = new File("f:\a.jpg");

       byte[] datas = FileUtils.getBytes(file);//获取文件中的数据

       Blob blob = new SerialBlob(datas);//创建Blob对象

       pstmt.setBlob(2, blob);//设置Blob类型的参数

       pstmt.executeUpdate();

       con = JdbcUtils.getConnection();

       String sql = "select filename,data from tab_bin where id=?";

       pstmt = con.prepareStatement(sql);

       pstmt.setInt(1, 1);

       rs = pstmt.executeQuery();

       rs.next();

      

       String filename = rs.getString("filename");

       File file = new File("F:\" + filename) ;

       Blob blob = rs.getBlob("data");

       byte[] datas = blob.getBytes(0, (int)file.length());

       FileUtils.writeByteArrayToFile(file,   datas);

批处理SQL

语句不同,有很多行,可以使用addBatch方法批处理,别忘了executeBatch一下.

其实Batch里面就是封装了一个缓存

    //向t1表中插入两条记录,删除第一条记录

    //利用statement可以执行SQL语句不同的批处理

    public void testBatch1(){

        Connection conn = null;

        Statement stmt = null;

        try{

            conn = JdbcUtil.getConnection();

            stmt = conn.createStatement();

            //把要输入的SQL语句放入字符串变量中去;

            String sql1 = "insert into t1 (id,name) values(1,'aaa1')";

            String sql2 = "insert into t1 (id,name) values(2,'bbb1')";

            String sql3 = "delete from t1 where id=1";

            //注入SQL语句,是注入,并没有执行;

            stmt.addBatch(sql1);

            stmt.addBatch(sql2);

            stmt.addBatch(sql3);

            

            //对注入的SQL语句执行

            //数组的元素表示每条语句影响到的行数。

            int[] i = stmt.executeBatch();

        }catch(Exception e){

            throw new RuntimeException(e);

        }finally{

            JdbcUtil.release(null, stmt, conn);

        }

        

    }

 

利用PreparedStatement执行批处理,只能用在SQL语句相同的情况下。参数有可能不同

    //向t1表中批量插入10条记录,语句相同,参数不同

    @Test

    public void testBatch2(){

        Connection conn = null;

        PreparedStatement stmt = null;

        try{

            conn = JdbcUtil.getConnection();

            //一开始就需要把SQL语句加载到PreparedStatement中,用占位符

            String sql="insert into t1 (id,name) values(?,?)";

            stmt = conn.prepareStatement(sql);

            //输入SQL语句中的参数

            for(int x=0;x<10;x++){

                stmt.setInt(1, x+1);

                stmt.setString(2, "aaa"+(x+1));

                //对参数进行注入

                stmt.addBatch();

            }

            //对注入的参数执行

            stmt.executeBatch();

        }catch(Exception e){

            throw new RuntimeException(e);

        }finally{

            JdbcUtil.release(null, stmt, conn);

        }

    }

 

OCI  效率高  但是要装客户文件

Thin 效率低 但是不需要装

一般开发用这个Thin

插入1000001条记录,大数据插入

    //向t1表中批量插入1000001条记录

    @Test

    public void testBatch3(){

        Connection conn = null;

        PreparedStatement stmt = null;

        try{

            conn = JdbcUtil.getConnection();

            //一开始就需要把SQL语句加载到PreparedStatement中,用占位符

            String sql="insert into t1 (id,name) values(?,?)";

            stmt = conn.prepareStatement(sql);

            //输入SQL语句中的参数

            for(int x=0;x<1000001;x++){

                stmt.setInt(1, x+1);

                stmt.setString(2, "aaa"+(x+1));

                stmt.addBatch();

                //定义一个判断,每插入1000条就执行一次,还需要清空注入的语句.

                if(x%1000==0){

                    stmt.executeBatch();//执行

                    stmt.clearBatch();//清空

                }

            }

            //扫尾执行,因为可能会有尾数

            stmt.executeBatch();

        }catch(Exception e){

            throw new RuntimeException(e);

        }finally{

            JdbcUtil.release(null, stmt, conn);

        }

    }

返回自增长主键

当表的主键是自增长的,那么就是由数据库来维护主键的值。当我们使用Java向数据库插入一行记录后,主键的值我们是不知道的。当然你可以去再查询一次!但是这不是最好的办法。

想获取主键自增长的值,需要在创建PreparedStatement时开始:

String sql = "insert into tt2(name) values(?)";

pstmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

//表示在执行SQL语句后会去获取主键自增长的值。

pstmt.setString(1,   "hello");

pstmt.executeUpdate();

在执行完INSERT语句之后,可以通过PrepareStatement的getGeneratedKeys()方法获取一个结果集对象,然后再获取结果集中唯一一行,唯一一列的数据,这就是主键自增长的值。

ResultSet rs =   pstmt.getGeneratedKeys();//获取自增长主键结果集

rs.next();

int id = rs.getInt(1);

System.out.println(id);

调用存储过程

1 创建存储过程

首先创建3个存在过程:

l  无参的;

l  入参的;

l  出参的。

DELIMITER //

CREATE PROCEDURE pro1()

BEGIN

       SELECT   * FROM stu;

END//

DELIMITER ;

DELIMITER //

CREATE PROCEDURE pro2(IN _sid   VARCHAR(10))

BEGIN

    SELECT * FROM stu WHERE sid=_sid;

END//

DELIMITER ;

DELIMITER //

CREATE PROCEDURE pro3(IN _sid   VARCHAR(10), OUT _sname VARCHAR(20))

BEGIN

    SELECT sname INTO _sname FROM stu WHERE sid=_sid;

END//

DELIMITER ;

2 调用pro1(无参存储过程)

要调用存储过程需要使用CallableStatement,获取CallableStatement需要调用Connection的prepareCall(String sql)方法。

           con = JdbcUtils.getConnection();

           String sql = "{call pro1()}";//调用存储过程的SQL语句需要一对大括号括起来

           cstmt = con.prepareCall(sql);

           rs = cstmt.executeQuery();//如果存储过程会返回结果集,那么就调用executeQuery()方法,否则调用executeUpdate()方法。

           while(rs.next()) {

              int colCnt =   rs.getMetaData().getColumnCount();

              for(int i = 1; i <= colCnt; i++) {

                  Object o = rs.getObject(i);

                  System.out.print(o + " ");

              }

              System.out.println();

           }

3 调用有入参存储过程

           con = JdbcUtils.getConnection();

           String sql = "{call pro2(?)}";//给出一个参数

           cstmt = con.prepareCall(sql);

           cstmt.setString(1, "S_100");//为参数赋值

           rs = cstmt.executeQuery();

           rs.next();

           System.out.println(rs.getString(1) + ", " + rs.getString(2)  

                  +   ", " + rs.getInt(3) + ", " + rs.getString(4));

4 调用有出参的存储过程

           con = JdbcUtils.getConnection();

           String sql = "{call   pro3(?,?)}";//第一个参数是入参,第二个是出参。

 

           cstmt = con.prepareCall(sql);

           cstmt.setString(1, "S_100");

           cstmt.registerOutParameter(2, Types.VARCHAR);// 注册一个出参!说明它的SQL类型

 

           cstmt.execute();//这里调用executeUpdate()、executeQuery()或是execute()都行!

           String name = cstmt.getString(2);// 获取出参的值

           System.out.println(name);

元数据

1 DatabaseMetaData

获取DatabaseMetaData对象:

Connection con =   ...

DatabaseMetaData   dbmd = con.getMetaData();

基本功能

String name =   dbmd.getDatabaseProductName();//获取数据库名称:MySQL

int v1 = dbmd.getDatabaseMajorVersion();//获取数据库主版本号:5

int v2 = dbmd.getDatabaseMinorVersion();//获取数据库次版本号:1

System.out.println(name + v1 + "." + v2);

String driverName   = dbmd.getDriverName();//获取驱动名

String   driverVersion = dbmd.getDriverVersion();//获取驱动版本

System.out.println(driverName   + ", " + driverVersion);

String url =   dbmd.getURL();//获取URL

String username =   dbmd.getUserName();//获取用户名

System.out.println(url);

System.out.println(username);

获取所有数据库名称

ResultSet rs =   dbmd.getCatalogs();

while(rs.next()) {

    System.out.println(rs.getString(1));

}

获取指定数据库中所有表名称

ResultSet rs =   dbmd.getTables("mydb1", null, null, new String[]{"TABLE"});

while(rs.next())      {             

    System.out.println(rs.getString("TABLE_NAME"));

}

2 ParameterMetaData

得到ParameterMataData对象

Connection con = ...

String sql = "insert into tab_student value(?,?,?,?)";

PreparedStatement   pstmt = con.prepareStatement(sql);

ParameterMetaData pmd =   pstmt.getParameterMetaData();

ParameterMetaData是针对“?”的元数据!但是很多驱动对它的支持不是很好。

int cnt = pmd.getParameterCount();//参数的个数

for(int i = 1; i <= cnt; i++) {

    System.out.println(pmd.getParameterTypeName(i));//当前参数类型名

    System.out.println(pmd.getParameterType(i));//当前参数类型

    System.out.println(pmd.getParameterClassName(i));//当前参数Java类型名

    System.out.println(pmd.isNullable(i));//当前参数是否可以为NULL

}

3 结果集列数据:ResultSetMetaDate

获取ResultSetMetaData

ResultSet rs = ...;

ResultSetMetaData rsmd = rs.getMetaData();

方法介绍:

       ResultSetMetaData rsmd =   rs.getMetaData();

       int cnt = rsmd.getColumnCount();//获取结果集列数

      

       for(int i = 1; i <= cnt; i++) {

           System.out.print(rsmd.getColumnName(i));//获取当前列名称

           if(i < cnt) {

              System.out.print(", ");

           }

       }

       System.out.println();

       for(int i = 1; i <= cnt; i++) {

           System.out.print(rsmd.getColumnClassName(i));//获取当前列Java类型名

           if(i < cnt) {

              System.out.print(", ");

           }

       }

       System.out.println();

       while(rs.next()) {

           for(int i = 1; i <= cnt; i++) {

              System.out.print(rs.getObject(i));//看清楚,这个是rs不是rsmd

              if(i < cnt) {

                  System.out.print(", ");

              }

           }

           System.out.println();

       }

原文地址:https://www.cnblogs.com/lulu638/p/4438853.html