Java数据库连接,(Java Database Connectivity,简称JDBC)是Java语言中用来规范 客户端 程序如何来 访问数据库 的应用程序接口,提供了诸如查询和更新数据库中数据的方法。JDBC也是Sun Microsystems的商标。JDBC是 面向关系型 数据库的。
简单解释:通过Java语言执行sql语句,从而操作数据库
想要通过Java操作不同的数据库,应该根据数据库的不同而执行特定的API,而出于简化的想法,Sun公司,定义了一套面向所有关系型数据库的 API 即 JDBC ,其只提供接口,而具体实现去交给数据库厂商实现,而我们作为开发者,我们针对数据数据库的操作,只需要基于JDBC即可
我们简单的使用JDBC去查询数据库中的数据,并且输出到控制台中
为了快速演示,我们新建一张非常简单的表
CREATE TABLE student( id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(20), score DOUBLE(4,1) ); INSERT student(id,NAME,score) VALUES (1,'张三',98); INSERT student(id,NAME,score) VALUES (2,'李四',96); INSERT student(id,NAME,score) VALUES (3,'王五',100);
我们根据数据库中的信息写一个对应的学生类
public class Student { private int id; private String name; private double score; //省略构造、Get、Set、toString方法 ...... }
下面是对 JDBC 查询功能的简单使用
package cn.ideal.jdbc; import cn.ideal.domain.Student; import java.sql.*; public class JdbcDemo { public static void main(String[] args) { //导入数据库驱动包 Connection connection = null; Statement statement = null; ResultSet resultSet = null; try { //加载驱动 Class.forName("com.mysql.jdbc.Driver"); //获取与数据库的连接对象 connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db1", "root", "root99"); //定义sql语句 String sql = "SELECT * FROM student"; //获取执行sql语句的对象statement statement = connection.createStatement(); //执行sql语句,获取结果集 resultSet = statement.executeQuery(sql); //遍历获取到的结果集 while (resultSet.next()) { int id = resultSet.getInt(1); String name = resultSet.getString(2); Double score = resultSet.getDouble(3); Student student = new Student(); student.setId(id); student.setName(name); student.setScore(score); System.out.println(student.toString()); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } finally { //释放资源,后调用的先释放 if (resultSet != null) { try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } } //运行结果 Student{id=1, name='张三', score=98.0} Student{id=2, name='李四', score=96.0} Student{id=3, name='王五', score=100.0}
下面我们开始详细的解释一下上面所用到的各个对象
首先我们要知道加载驱动和注册驱动这两个词是什么意思,刚刚接触的时候,会有人总有朋友将 Class.forName(com.mysql.jdbc.Driver)
当做注册数据库驱动的语句,但实际不然,它的作用是将参数表示的类加载到内存中,并且 初始化 ,同时其中的静态变量也会被初始化,静态代码块也会被执行
疑惑:能否使用ClassLoader 类中的loadClass()方法呢?
//Class类源码节选 -jdk8 * A call to {@code forName("X")} causes the class named * {@code X} to be initialized.
关于初始化问题这里简单提及一下,我们还是先回到我们主线来
这是因为真正实现 注册驱动 (告诉程序使用哪一个数据库驱动jar)的是:
static void registerDriver(Driver driver)
我们在jar包中找到Driver这个类,查看其源码
//com.mysql.jdbc.Driver类中的静态代码块 static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } }
类被加载后,执行了类中的静态方法DriverManager进行了注册驱动
我们也可能有见过下面2中的代码,但是实际上驱动会被加载两次,因为执行
new com.mysql.jdbc.Driver()
已经加载了一次驱动
//1.推荐 Class.forName("com.mysql.jdbc.Driver"); //2.不推荐 DriverManager.registerDriver(new com.mysql.jdbc.Driver())
那么何必这么麻烦呢? new com.mysql.jdbc.Driver()
直接这样写不就挺好了吗?
但我们还是选择 拒绝!为什么呢?
如果我们这样写,对于jar包的依赖就比较重了,我们如果面临多个项目,或者需要修改数据库,就需要修改代码,重新编译,但是如果使用Class类加载的方式,既保证了静态代码块中所包含的注册驱动方法会被执行 ,而又将参数变成了字符串形式,我们之后便可以通过修改配置文件 “ ” 内的内容 + 添加jar包 的方式更灵活的处理问题,并且不需要重新编译!
注意:mysql5之后的驱动jar包可以省略注册驱动这一步,原因查看jar包中META-INF/services/java.sql.Driver文件
com.mysql.jdbc.Driver com.mysql.fabric.jdbc.FabricMySQLDriver
static Connection getConnection(String url, String user, String password) /* jdbc:mysql://ip地址(域名):端口号/数据库名称 Eg:jdbc:mysql://localhost:3306/db1 本地mysql,且端口为默认3306,则可简写:jdbc:mysql:///数据库名称 */
//创建向数据库发送sql语句的statement对象 Statement createStatement() //创建向数据库发送预编译sql语句的PrepareStement对象 PreparedStatement prepareStatement(String sql)
//开启事务:设置参数为false,即开启事务 setAutoCommit(boolean autoCommit) //提交事务 commit() //回滚事务 rollback()
//执行DQL(查询数据库中表的记录(数据)) ResultSet executeQuery(String sql) //执行DML(对数据库中表的数据进行增删改) int executeUpdate(String sql) //执行任意sql语句,但是目标不够明确,较少使用 boolean execute(String sql) //把多条sql的语句放到同一个批处理中 addBatch(String sql) //向数据库总发送一批sql语句执行 executeBatch()
package cn.ideal.jdbc; import java.sql.*; public class StatementDemo { public static void main(String[] args) { Connection connection = null; Statement statement = null; try { //加载驱动 Class.forName("com.mysql.jdbc.Driver"); //获取数据库连接对象 connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db1", "root", "root99"); //定义sql语句 String sql = "INSERT student(id,NAME,score) VALUES (NULL,'马六',88);"; //获取执行sql语句的对象 statement = connection.createStatement(); //执行sql语句 int count = statement.executeUpdate(sql); System.out.println(count); if (count > 0) { System.out.println("添加成功"); } else { System.out.println("添加失败"); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); }finally { if(statement != null){ try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (connection != null){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
ResultSet所代表的的是sql语句的结果集——执行结果,当Statement对象执行excuteQuery()后,会返回一个ResultSet对象
//游标向下移动一行,判断当前行是否是最后一行末尾(是否有数据) //如果是,则返回false,如果不是则返回true boolean next() //获取数据,Xxx代表数据类型 getXxx(参数) Eg:int getInt() , String getString() 1. int:代表列的编号,从1开始 如: getString(1) 2. String:代表列名称。 如: getDouble("name")
案例可参考开头快速使用部分,自行尝试读取数据库中数据后用集合框架装载
通过封装一些方法,使得出现一个更加通用的工具类,我们可以通过properties配置文件 ,使得信息更加直观且容易维护
package cn.ideal.jdbc; import java.io.FileReader; import java.io.IOException; import java.net.URL; import java.sql.*; import java.util.Properties; public class JDBCUtils { private static String url; private static String user; private static String password; private static String driver; /** * 文件读取 */ static { try { //创建Properties集合类 Properties pro = new Properties(); //获取src路径下的文件 ClassLoader classLoader = JDBCUtils.class.getClassLoader(); URL res = classLoader.getResource("jdbc.properties"); String path = res.getPath(); //加载文件 pro.load(new FileReader(path)); //获取数据 url = pro.getProperty("url"); user = pro.getProperty("user"); password = pro.getProperty("password"); driver = pro.getProperty("driver"); //注册驱动 Class.forName(driver); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 获取连接 * * @return 连接对象 */ public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } /** * 释放资源 * * @param statement * @param connection */ public static void close(Statement statement, Connection connection) { if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } /** * 释放资源 * * @param resultSet * @param statement * @param connection */ public static void close(ResultSet resultSet, Statement statement, Connection connection) { if (resultSet != null) { try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
package cn.ideal.jdbc; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; public class JDBCUtilsTest { public static void main(String[] args) { Connection connection = null; Statement statement = null; try { connection = JDBCUtils.getConnection(); //定义sql语句 String sql = "INSERT student(id,NAME,score) VALUES (NULL,'马六',88)"; //获取执行sql语句的对象 statement = connection.createStatement(); //执行sql语句 int count = statement.executeUpdate(sql); System.out.println(count); if (count > 0) { System.out.println("添加成功"); } else { System.out.println("添加失败"); } } catch (SQLException e) { e.printStackTrace(); } finally { JDBCUtils.close(statement,connection); } } }
之前的文章中分别通过集合实现、IO实现、而学习数据库后,我们可以试着通过数据库存储数据,写一个简单的登录注册小案例!在第五大点中有提到吼
//创建向数据库发送预编译sql语句的prepareStatement PreparedStatement prepareStatement(String sql)
prepareStatement继承自Statement,总而言之,它相较于其父类,更强更简单!
Statement 直接编译 SQL 语句,直接送到数据库去执行,而且其多次重复执行sql语句,PreparedStatement 会 对SQL进行预编译 ,再填充参数,这样效率会比较高( 预编译的SQL存储在PreparedStatement中 )
定义 SQL 语句的时候,常常需要使用到 Java 中的变量,在一些复杂的情况下,需要频繁的使用到引号和单引号的问题,变量越多,越复杂,而PreparedStatement可以使 用占位符 ‘ ?’ 代替参数,接下来再进行参数的赋值,这样有利于代码的可读性
PreparedStatement 由于预编译,可以避免Statement中可能需要采取字符串与变量的拼接而导致SQL注入攻击【编写永等式,绕过密码登录】
我们先按照我们之前的做法,写一个简单的登录Demo,先创一张表!
CREATE TABLE USER( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32), PASSWORD VARCHAR(32) ); SELECT * FROM USER; INSERT INTO USER VALUES(NULL,'admin','admin888'); INSERT INTO USER VALUES(NULL,'zhangsan','123456');
接着编写代码
package cn.ideal.login; import cn.ideal.jdbc.JDBCUtils; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Scanner; public class LoginDemo { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请输入用户名"); String username = sc.nextLine(); System.out.println("请输入密码"); String password = sc.nextLine(); boolean flag = new LoginDemo().login(username, password); if (flag) { System.out.println("登录成功"); } else { System.out.println("用户名或密码错误"); } } /** * 登录方法 */ public boolean login(String username, String password) { if (username == null || password == null) { return false; } Connection connection = null; Statement statement = null; ResultSet resultSet = null; try { connection = JDBCUtils.getConnection(); //定义sql String sql = "SELECT * FROM USER WHERE username = '" + username + "' AND password = '" + password + "' "; //获取执行sql的对象 statement = connection.createStatement(); //执行查询 resultSet = statement.executeQuery(sql); return resultSet.next(); } catch (SQLException e) { e.printStackTrace(); }finally { JDBCUtils.close(resultSet,statement, connection); } return false; } }
简单的来说,这样一个简单的登录Demo就写好了,但是这个时候,SQL注入问题中的一种情况就出现了,或许你听过,在早些年的时候,漏洞还是蛮常见的,一些黑客或者脚本小子们常常使用一些SQL注入的手段进行目标网站后台的入侵,我们今天所讲的这一种,就是其中一种,叫做SQL万能注入(SQL万能密码)
我们先来观察一下上述代码中关于SQL语句的部分
String sql = "SELECT * FROM USER WHERE username = '" + username + "' AND password = '" + password + "' ";
也就是说它将我们所输入的 username
和 password
合成为SQL查询语句, 当数据库中不存在这样的字段就代表输入错误,但是对于存在SQL注入漏洞的程序,则可以通过构造一些特殊的字符串,达到登录的目的,先贴出来测试结果
//运行结果 请输入用户名 admin 请输入密码 1' or '1' = '1 登录成功
如果我们将上述代码中密码 (username) 部分用我们的这些内容代替是怎么样的呢
String sql = "SELECT * FROM USER WHERE username = 'admin' AND PASSWORD = '1' or '1' = '1' ";
补充:在SQL语句中逻辑运算符具有优先级,= 优先于 and ,and 优先于 or
所以上面的式子中 AND先被执行,当然返回错,接着执行or部分,对于一个永等式 ‘1’ = ‘1‘ 来说返回值永远是true,所以SQL查询结果为true,即可以登录成功
//使用PrepareStemen替代主要部分 //定义sql String sql = "SELECT * FROM USER WHERE username = ? AND password = ?"; //获取执行sql的对象 preparedStatement = connection.prepareStatement(sql); //给?赋值 preparedStatement.setString(1, username); preparedStatement.setString(2, password); //执行查询 resultSet = preparedStatement.executeQuery(); //运行结果 请输入用户名 admin 请输入密码 1' or '1' = '1 用户名或密码错误
如果内容中有什么不足,或者错误的地方,欢迎大家给我留言提出意见, 蟹蟹大家 !^_^
如果能帮到你的话,那就来关注我吧!(系列文章均会在公众号第一时间更新)
一个坚持推送原创Java技术的公众号:理想二旬不止