第12章 JDBC

JDBC技术:JDBC(Java Data Base Connectivity)是Java程序与数据库系统通信的标准API,它定义在JDK中。通过JDBC技术,Java程序可以非常方便的与各种数据库(MySql,Oracle,SqlServer等等)交互。这里需要特别提醒的是,在使用JDBC链接数据库之前,需要将数据库厂商的数据库驱动类注册到JDBC的驱动管理器中。而这里的数据库驱动类则是由数据库厂商来提供的,它的本质就是一个封装好的JAR文件。例如,我们的Java程序想要访问MySql数据库的话,就需要MySql提供的驱动,我们可以去MySql官方下载。

https://downloads.mysql.com/archives/c-j/

鉴于我们基本都是Windows平台下的开发,我们下载第二项“mysql-connector-java-5.1.49.zip”即可。下载完毕后,将其解压开,找到里面的“mysql-connector-java-5.1.49-bin.jar”文件。然后将其复制到项目工程目录“WebContent\WEB-INF\lib”就可以了。

同样是添加JAR库文件,这个与添加Tomcat的Servlet包(“Add External JARs...”)是不一样的。因为mysql驱动包要跟随项目一起部署到Tomcat中才能正确运行。而Tomcat的Servlet包本身就在Tomcat中,因此不需要添加到咱们的项目中去。

在进行JDBC操作之前,我们提前在MySql中创建一个“test”数据库,并创建一个“users”表,该表中只有四个字段,id,name,age用来表示用户的唯一标识,姓名和年龄。

CREATE DATABASE `test` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

USE `test`;

DROP TABLE IF EXISTS `users`;

CREATE TABLE `users` (
  `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(30) NOT NULL DEFAULT '',
  `age` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;

INSERT  INTO `users`(`id`,`name`,`age`) VALUES (1,'张三',20),(2,'李四',22);

JDBC的使用

JDBC API主要位于java.sql包中,关键的接口和类包括如下:

Driver接口和DriverManager类:驱动管理
Connection接口:数据库连接
Statement接口:负责执行SQL语句
PreparedStatement接口:负责执行预定义SQL语句
CallableStatement接口:负责执行SQL存储过程
ResultSet接口:表示SQL查询语句返回的结果集

SQL(结构化查询语言)是由IBM公司提出的,不仅具有丰富的查询功能,而且具有数据定义和数据控制功能,是集数据定义(DDL),数据查询(DQL),数据操纵(DML)和数据控制(DCL)于一体的关系数据语言。它是关系数据库的标准语言。JDBC技术就是使用Java代码向数据库提交SQL语句,然后返回执行结果。因此,它的本质就是一个通信通道而已。如果大家不清楚SQL语句的,请查看以前的课程:https://blog.csdn.net/richieandndsc/article/details/142763743

从上面的几个API中,我们大致能够明白操作数据库的一个简单流程。因为涉及到我们的Java程序与Mysql程序通信,因此肯定需要建立通信。而能够建立通信的前提是,MySQL数据库会在本机的3306端口开放通信接口了。于是,我们就可以使用Java程序与MySQL程序建立 “连接通道”,然后在这个 “连接通道” 上可以传递SQL语句,MySQL接收到SQL语句并执行,最后再通过这个 “连接通道” 将结果返回给我们Java程序。一般情况下,当我们执行完SQL语句之后,就需要关闭这个 “连接通道”,毕竟这个 “连接通道” 是需要消耗双方的性能的。

接下来,我们先创建一个 “TestServlet”,对应的访问URL就是 “/test”

package com.demo;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/test")
public class TestServlet extends HttpServlet {
	
	private static final long serialVersionUID = 1L;

	public TestServlet() {
        	super();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		response.getWriter().append("Served at: ").append(request.getContextPath());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		doGet(request, response);
	}

}

然后再创建一个工程入口文件“index.jsp”

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Index</title>
</head>
<body>

<a href="${pageContext.request.contextPath}/test">test</a>

</body>
</html>

准备工作好了之后,我们就开始使用JDBC,首先是加载驱动类,连接数据库test

import java.sql.Connection;
import java.sql.DriverManager;

@WebServlet("/test")
public class TestServlet extends HttpServlet {
	
	private static final long serialVersionUID = 1L;

	// 数据库连接参数
	private final String USERNAME = "root";
	private final String PASSWORD = "123456";
	private final String URL = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true";
	
	// 数据库连接对象(连接通道)
	private Connection conn = null;

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		// 加载驱动并连接MySQL数据库
		try {
			// 加载驱动程序
			Class.forName("com.mysql.jdbc.Driver");
			// 获得数据库连接
			conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
		} catch(Exception e) { e.printStackTrace(); }
		
		//response.getWriter().append("Served at: ").append(request.getContextPath());
	}
}

我们稍微解释一下上面的代码。首先是 Class.forName静态方法来加载我们的驱动程序,然后通过DriverManager.getConnection静态方法来创建“连接通道”,这里需要的参数包括连接URL,数据账户和密码。请注意,连接URL中有固定的部分,也有不固定的部分。因为我们连接的是本机MySQL,因此写的是 "localhost",或者 “127.0.0.1” 也行。如果我们要连接的是他人电脑上面的 MySQL,我们就需要填写他人的IP地址。紧接着就是MySQL提供的端口 3306,这个端口是可以修改的,但是修改权在于 MySQL,而不是我们的Java程序。再紧接着就是我们的数据库名称 “test” (我们刚刚使用SQL脚本创建的数据库)。剩下的就是一些连接参数,我们保持固定即可。数据账户和密码也是在MySQL中创建的,它就是Mysql客户端访问Mysql服务的账号密码。上面的所有都正确之后,我们就可以连接上MySQL了,当然我们的本机MySQL服务也必须是启动状态。否者就会抛出异常,这也是为什么我们上面的代码被包裹在 try - catch 中间的一个原因。

接下来,我们就使用 Statement 执行SQL 语句

		try {
			
			// 查询SQL语句
			String sql = "select * from users";
			// 创建 Statement 对象
			Statement stmt = conn.createStatement();
			// 执行查询SQL语句,返回结果集 ResultSet 对象
			ResultSet rs = stmt.executeQuery(sql);
			// 循环 ResultSet 对象
			while (rs.next()) {
			    // 控制台输出 学生编号 和 学生姓名
			    System.out.print(rs.getInt("id") + " ");
			    System.out.println(rs.getString("name"));
			}

			// 关闭 ResultSet 对象 和 Statement 对象
			rs.close();
			stmt.close();
			
		} catch (SQLException e) { e.printStackTrace(); }

数据库连接对象Connection不能直接执行SQL语句,它将这个任务交给了 Statement 对象。因此,我们需要通过Connection的createStatement方法来创建 Statement 对象。这个 Statement 对象的 executeQuery 方法就可以执行 Select 语句,它将返回一个 结果集 ResultSet 对象,ResultSet对象是一个集合,我们需要通过循环来读取里面的每一条数据记录。ResultSet 对象就像一个数组,每执行一次 next 方法,数组下标就会加1,我们就可以通过它的 getXXX 方法来获取数据记录,getXXX 方法的参数就是字段名称。因为我们的用户主键是整型,所以使用getInt方法来获取,而用户姓名是字符串,所以使用getString方法来获取。请注意,我们的代码需要包裹在“java.sql.SQLException”异常下执行哦。

接下来,我就运行工程,查看输出结果。因为我们是控制台输出,因此只给出控制台截图

Statement 对象将我们的Select, Update, Insert, Delete语句分成两种,一种是查询语句,使用 executeQuery 来执行,它返回的是数据记录,毫无疑问,Select就是这样的语句;而剩余的 Update, Insert, Delete语句 则是第二种,称之为执行语句,需要使用 executeUpdate 方法来执行,它返回的结果是一个整数,代表SQL语句影响的行数。例如,我们删除了 3 条记录,那么 executeUpdate 方法就返回整数3。因此,剩余的Update, Insert, Delete语句全部都由 executeUpdate 方法来执行。演示代码如下

		try {
			// 写入新数据
			String sql2 = "INSERT INTO `users` VALUES (3, '王五', 23)";
			// 修改数据
			//String sql2 = "UPDATE `users` SET `name` = '赵六', `age` = 24 WHERE `id` = 3";
			// 删除数据
			//String sql2 = "DELETE FROM `users` WHERE `id` = 3";
			
			// 执行修改语句
			Statement stmt2 = conn.createStatement();
			int rows = stmt2.executeUpdate(sql2);
			// 返回影响的数据行数
			System.out.println("rows = " + rows);
			stmt2.close();

		} catch (SQLException e) { e.printStackTrace(); }

这个代码比较简单,我们先执行插入语句,然后查看控制台输出结果。

我们再去数据库中核实一下,新数据是否插入成功了。

剩余的update和delete语句,大家可以使用同样的方式来执行一下试试。

以下是“TestServlet”类的完整代码,如下所示

package com.demo;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

@WebServlet("/test")
public class TestServlet extends HttpServlet {
	
	private static final long serialVersionUID = 1L;

	// 数据库连接参数
	private final String USERNAME = "root";
	private final String PASSWORD = "123456";
	private final String URL = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true";
	
	// 数据库连接对象(连接通道)
	private Connection conn = null;
		
	public TestServlet() {
		super();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		// 加载驱动并连接MySQL数据库
		try {
			// 加载驱动程序
			Class.forName("com.mysql.jdbc.Driver");
			// 获得数据库连接
			conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
		} catch(Exception e) { e.printStackTrace(); }
		
		
		try {
			
			// 查询SQL语句
			String sql = "select * from users";
			// 创建 Statement 对象
			Statement stmt = conn.createStatement();
			// 执行查询SQL语句,返回结果集 ResultSet 对象
			ResultSet rs = stmt.executeQuery(sql);
			// 循环 ResultSet 对象
			while (rs.next()) {
			    // 控制台输出 学生编号 和 学生姓名
			    System.out.print(rs.getInt("id") + " ");
			    System.out.println(rs.getString("name"));
			}

			// 关闭 ResultSet 对象 和 Statement 对象
			rs.close();
			stmt.close();
			
		} catch (SQLException e) { e.printStackTrace(); }
		
		try {
			// 写入新数据
			//String sql2 = "INSERT INTO `users` VALUES (3, '王五', 23)";
			// 修改数据
			//String sql2 = "UPDATE `users` SET `name` = '赵六', `age` = 24 WHERE `id` = 3";
			// 删除数据
			String sql2 = "DELETE FROM `users` WHERE `id` = 3";
			
			// 执行修改语句
			Statement stmt2 = conn.createStatement();
			int rows = stmt2.executeUpdate(sql2);
			// 返回影响的数据行数
			System.out.println("rows = " + rows);
			stmt2.close();

		} catch (SQLException e) { e.printStackTrace(); }
		
		// 最后不要忘了关闭数据库连接
		try {
			conn.close();
		} catch (SQLException e) { e.printStackTrace(); }
		
		response.getWriter().append("Served at: ").append(request.getContextPath());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		doGet(request, response);
	}

}

接下来,我们介绍一下事务操作。事务是数据库提供的一种保证数据一致性的机制。 它的原理很简单,当我们向数据库中写入一堆数据的时候,为了保证这些数据全部正确的写入,我们会将这些SQL语句打包成一个事务。作为一个事务,只有所有SQL语句全部正确执行,这些数据才会被真正的写入到数据库中,但凡出现任何错误,这些数据都不会写入到数据库中。也就是说,要么这些数据全部正确写入,要么一条也不写入。除了insert写入操作之外,update语句和delete语句也可以打包到事务中,也就是说,只要涉及到数据变动的操作都可以被列入事务,显然Select语句是不需要列入事务的。经典的例子就是银行汇款。例如,甲向乙汇款100元,需要做的操作分为两步。第一步,减少甲的账户100元;第二步,增加乙的账户100元。将两个操作打包一个事务可以保证账目是正确的。否则,当第一步成功,而第二部失败的时候,这个账目就不对了。对于银行来讲,账目不对是天大的事情。最后我们还要提醒大家的是,事务是MySQL数据库提供给我们的,不是Java程序提供的,并且它要求我们的表引擎为innodb才行。因为,只有innodb引擎才支持事务。

接下来,我们就来演示如何使用Java代码来实现事务。接下来,我们创建“TranServlet”,对应的请求url是“/tran”。完整代码如下:

package com.demo;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

@WebServlet("/tran")
public class TranServlet extends HttpServlet {
	
	private static final long serialVersionUID = 1L;
    
	// 数据库连接参数
	private final String USERNAME = "root";
	private final String PASSWORD = "123456";
	private final String URL = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true";	
	
	// 数据库连接对象(连接通道)
	private Connection conn = null;
	
	public TranServlet() {
		super();
	}

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		// 加载驱动并连接MySQL数据库
		try {
			// 加载驱动程序
			Class.forName("com.mysql.jdbc.Driver");
			// 获得数据库连接
			conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);		
		} catch(Exception e) { e.printStackTrace(); }
		
		// 声明 Statement 对象
		Statement stmt = null;
		
		try {
			
			// 开启事务: 禁止自动提交
			conn.setAutoCommit(false);
			
			// 创建 Statement 对象
			stmt = conn.createStatement();
			
			String sql1 = "insert into users(name, age) values('王五', 25)";
			int r1 = stmt.executeUpdate(sql1);
			System.out.println("r1 = " + r1);
			
			String sql2 = "insert into users(name, age) values('赵六', 26)";
			int r2 = stmt.executeUpdate(sql2);
			System.out.println("r2 = " + r2);
			
			// 提交事务
			conn.commit();
			System.out.println("提交事务");
			
		} catch (SQLException e) {
			
			// 回滚事务
			try { conn.rollback(); } catch (SQLException e1) {}
			System.out.println("回滚事务");
			
		} finally {
			
			// 关闭 Statement 对象和数据库连接
			try { stmt.close(); } catch (SQLException e) {}
			try { conn.close(); } catch (SQLException e) {}
			
			// 关闭事务: 恢复自动提交
			try { conn.setAutoCommit(true); } catch (SQLException e) {}
		}

		response.getWriter().append("Served at: ").append(request.getContextPath());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		doGet(request, response);
	}
}

默认情况下,MySQL是不开启事务的。当我们需要开启事务的时候,需要执行 conn.setAutoCommit(false); 语句即可,等我们的代码执行完毕后,记得再关闭事务:conn.setAutoCommit(true);

事务的两种重要方式就是 commit 方法和rollback方法,对应的就是提交和回滚。当我们的SQL语句都能够正确执行的时候,我们就可以使用 commit 方法提交,而SQL语句发生错误的时候,就使用 rollback 回滚。数据归滚后,相当于没有任何变化。这里大家注意的是,判断SQL语句是否发生错误的依据是:是否抛出异常。因此,我们再捕获到异常的时候,才会调用 rollback 方法回滚事务。

上面的SQL语句是没有任何问题的,可以正确执行,最终会执行 commit 提交事务。

我们再去数据库中核实一下

数据库中成功写入了两条记录。

如何让事务回滚呢?如何发生异常呢?在实际代码开发中,大部分情况都是因为SQL语句的问题导致发生异常而产生事务回滚。当然,也可能是我们根据业务判断手动抛出异常而主动让事务回滚。这里,我们只需修改一下SQL语句,即可发生异常让事务回滚。

String sql1 = "insert into users(name, age) values('王五', 25)";

String sql2 = "insert into users(name, age) values('赵六', 26)";

这是我们的两条插入SQL语句,其中“age”字段是整数类型,我们估计改成字符串类型。

String sql2 = "insert into users(name, age) values('赵六', '26岁')";

如果我们不将它们列入一个事务中的话,那么第一条是可以执行成功的,第二条则会失败。但是,我们将两条语句列为同一个事务,那么只要其中一个失败,所有都会失败的。

			String sql1 = "insert into users(name, age) values('王五', 25)";
			int r1 = stmt.executeUpdate(sql1);
			System.out.println("r1 = " + r1);
			
			//String sql2 = "insert into users(name, age) values('赵六', 26)";
			String sql2 = "insert into users(name, age) values('赵六', '26岁')";
			int r2 = stmt.executeUpdate(sql2);
			System.out.println("r2 = " + r2);

根据日志显示,第一条SQL语句是执行成功的,但是第二条就失败了,导致事务回滚。

我们可以去数据库中再核实一下,数据是没有任何变动的。

最后我们介绍一下 PreparedStatement 执行预编译SQL语句。什么是预编译语句呢?

MySQL在执行SQL语句时,分为几个阶段。

第一,词法解析、语义解析。

第二,优化SQL语句,生成执行计划。

第三,执行SQL并返回结果。

很多情况下SQL会反复执行,只是SQL中的某些具体的值不同。例如:select * from users where id=xxx 这样的语句,多次反复执行的时候,只是xxx不同而已。这些SQL只要解析一次,以后再次执行时,就不再需要进行词法解析、语义解析、SQL语句优化、生成执行计划,这些过程都不再需要,只要告诉MySQL具体的参数值变了就行。通常我们称这类SQL为预编译语句。使用预编译语句,是SQL性能优化的一个方便。如果我们要执行预编译SQL语句的话,我们需要 PreparedStatement 对象。

接下来,我们创建“PreServlet”,对应的请求url是“/pre”。

package com.demo;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

@WebServlet("/pre")
public class PreServlet extends HttpServlet {
	
	private static final long serialVersionUID = 1L;
    
	// 数据库连接参数
	private final String USERNAME = "root";
	private final String PASSWORD = "123456";
	private final String URL = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true";	
	
	// 数据库连接对象(连接通道)
	private Connection conn = null;
	
    public PreServlet() {
        super();
    }

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		// 加载驱动并连接MySQL数据库
		try {
			// 加载驱动程序
			Class.forName("com.mysql.jdbc.Driver");
			// 获得数据库连接
			conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
		} catch(Exception e) { e.printStackTrace(); }

		try {
			
			// 预编译 SQL 语句,里面可以包含多个 占位符 ?
			String sql = "select * from users where id=?";
			
			// 创建预编译 PreparedStatement 对象,参数就是预编译SQL语句 sql
			PreparedStatement pstmt = conn.prepareStatement(sql);
			
			// 设置第一个参数(占位符)为数值 1
			int id = 1;
			pstmt.setInt(1, id);
			
			// 执行查询语句,返回 ResultSet 结果集对象
			ResultSet rs = pstmt.executeQuery();
			
			// 循环遍历结果集
			while(rs.next()) {
				// 输出学生编号和姓名
				System.out.print(rs.getInt("id") + " ");
				System.out.println(rs.getString("name"));
			}
			
			// 关闭ResultSet对象和PreparedStatement对象
			rs.close();
			pstmt.close();
			
			// 定义修改预编译语句
			id = 1;
			String name = "小明";
			String sql2 = "update users set name = ? where id = ?";
			// 创建 PreparedStatement 对象,参数就是预编译SQL语句 sql2
			PreparedStatement pstmt2 = conn.prepareStatement(sql2);
			// 设置第一个占位符 name = ?
			pstmt2.setString(1, name);
			// 设置第二个占位符 id = ?
			pstmt2.setInt(2,id);
			// 执行预编译语句
			int row = pstmt2.executeUpdate();
			System.out.println("row = " + row);
			// 关闭PreparedStatement对象
			pstmt2.close();
			
			// 关闭数据库连接
			conn.close();
			
		} catch(SQLException e) { e.printStackTrace(); }

		response.getWriter().append("Served at: ").append(request.getContextPath());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		doGet(request, response);
	}
}

上面的代码分为两个部分,一个是执行查询语句,一个是执行修改语句。预编译SQL语句与普通的SQL语句相比,它里面的包含 占位符 ,也就是 问号。这个问号 就是我们要替换成的数据,回顾上面讲解的 "xxx" 的例子。接下来就是创建 PreparedStatement 对象,创建这个对象需要我们的预编译SQL语句作为参数,这个与上面的 Statement 对象的创建是不一样的。PreparedStatement 对象创建完毕之后,我们就可以使用 setXXX 方法来替换预编译SQL语句中的占位符。它的第一个参数就是占位符的序号,序号就是问号在SQL语句里面出现的顺序号。第一个问号的序号就是1,第二个问号的序号就是2,以此类推。如果你替换的数据是整数,就使用 setInt方法;如果你替换的数据是字符串,就使用setString方法。这个类似于从 ResultSet 中获取字段数据一个道理。数据替换完毕之后,就可以调用 executeQuery 或 executeUpdate方法来执行SQL语句,他们两个的区别我们不详细介绍了。剩余的代码,我们也不需要再解释了。我们直接运行查看结果吧。

我们再去数据库里面核实一下

另外,预编译SQL还可以防止SQL注入的风险。

package com.demo;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

@WebServlet("/inject")
public class InjectServlet extends HttpServlet {
	
	private static final long serialVersionUID = 1L;

	// 数据库连接参数
	private final String USERNAME = "root";
	private final String PASSWORD = "123456";
	private final String URL = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true";	
	
	// 数据库连接对象(连接通道)
	private Connection conn = null;
	
    public InjectServlet() {
        super();
    }

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		// 加载驱动并连接MySQL数据库
		try {
			// 加载驱动程序
			Class.forName("com.mysql.jdbc.Driver");
			// 获得数据库连接
			conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
		} catch(Exception e) { e.printStackTrace(); }
		
		try {
		
			// 执行普通SQL语句
			String name = "李四";
			String sql = "select * from users where name = '" + name + "'";
			Statement stmt = conn.createStatement();
			ResultSet rs = stmt.executeQuery(sql);
			if(rs.next()) {
			    System.out.print(rs.getInt("id") + " ");
			    System.out.println(rs.getString("name"));
			}
			rs.close();
			stmt.close();
			
		} catch(SQLException e) { e.printStackTrace(); }
		
		// 最后不要忘了关闭数据库连接
		try {
			conn.close();
		} catch (SQLException e) { e.printStackTrace(); }
		
		response.getWriter().append("Served at: ").append(request.getContextPath());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

		doGet(request, response);
	}
}

在上述代码中,我们将变量name拼接到了SQL语句中。如果用户正确输入了自己的姓名“李四”,那么我们就可以查询出这条记录。相反,如果用户输入错误的,就无法查询。这个就类似于登录验证。只有用户正确输入了账号和密码,才能够登录。请注意,账号和密码是用户自己手动输入的,也就是说,用户是可以输入非法的一些字符的。假如,上文中的name变量就来源于用户的输入,如果用户输入的不是“李四”,而是“a' OR 1”。那么,我们的SQL语句就会被改变:

SELECT * FROM users WHERE name = '李四'

变成

SELECT * FROM users WHERE name = 'a' OR '1'

也就是说,用户输入的内容改变了我们的SQL原来的意思。本来我们之前的SQL语句是查询“小红”同学的,但是由于SQL注入,我们查询了其他的用户。设想一下,这是登录账号和密码的检查SQL语句,那么后果就是,即使该用户并没有真实的账户和密码,它依然可以通过注入的方式改变SQL语句的方式,让这个SQL语句正确执行。最终他绕过了我们的SQL检查,随便查询出一条记录返回给我们的Java程序。它甚至注入 Delete 语句来删除我们的数据。由此可见,SQL注入是一件非常可怕的事情。因此我们必须防止SQL注入的产生。有两种方式,第一种就是过滤掉敏感词汇,例如上面的单引号,以及 OR 逻辑操作符。第二种就是我们的预编译语句。

		// 加载驱动并连接MySQL数据库
		try {
			// 加载驱动程序
			Class.forName("com.mysql.jdbc.Driver");
			// 获得数据库连接
			conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
		} catch(Exception e) { e.printStackTrace(); }
		
		try {
		
			// 执行普通SQL语句
			//String name = "李四";
			String name = "a' OR '1";
			String sql = "select * from users where name = '" + name + "'";
			Statement stmt = conn.createStatement();
			ResultSet rs = stmt.executeQuery(sql);
			if(rs.next()) {
			    System.out.print(rs.getInt("id") + " ");
			    System.out.println(rs.getString("name"));
			}
			rs.close();
			stmt.close();
			
		} catch(SQLException e) { e.printStackTrace(); }
		
		try {
			// 使用预编译语句
			String name = "a' OR '1";
			String sql = "select * from users where name = ?";
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, name);
			ResultSet rs = pstmt.executeQuery();
			if(rs.next()) {
			    System.out.print(rs.getInt("id") + " ");
			    System.out.println(rs.getString("name"));
			}else {
				System.out.println("没有数据");
			}
			rs.close();
			pstmt.close();
			
		} catch(SQLException e) { e.printStackTrace(); }
		
		// 最后不要忘了关闭数据库连接
		try {
			conn.close();
		} catch (SQLException e) { e.printStackTrace(); }

再次执行的时候,我们就发现,普通查询返回了结果,而预编译无法查询任何结果了。因为它输入的不是正确参数,因此无法查询到任何结果,它的SQL注入也就失败了。

如果需要手动过滤特殊字符号的话,应该过滤那些特殊符号呢?

第一,单引号 (')
第二,双引号 ('')
第三,反斜杠 (\)
第四,分号 (;)
第五,逻辑或 (or)
第六,逻辑且 (and)
第七,中划线 (-)
第八,井号(#)
第九,百分号(%)
第十,星号(*)
……

在Mysql的库函数中有一个 mysql_real_escape_string(),它可以用来转义特殊字符:NUL、\n、\r、\、'、" 和 Control-Z(结束符)。简单地讲,MySQL仅需要转义反斜杠和引号字符,基本上就能阻止SQL注入了。如果我们使用Java代码来过滤特殊字符的话,可以使用正则表达式,代码如下

String sql = "abc \" \' \\\\ ; or and - -- * # + % 123";
sql = sql.replaceAll("\"|\'|\\\\|;|or|and|-|#|%|\\*|\\+", "");
System.out.println(sql);

最后讨论一个关于数据库连接释放问题?

如果我们把关闭连接的代码放置到每个方法的最后,那么,如果同时调用两个方法的话,那么第二个方法将不能正确执行,因为数据库连接已经被关闭了。如果我们在每个方法的开始创建连接,在方法末尾释放连接,这样的操作又太消耗性能。因为创建连接的过程,是非常消耗性能的。理想状态下,应该是整个程序运行开始,创建数据库连接,程序运行结束,再释放数据库连接。我们将数据库连接和释放放置到一个业务类(学生类,班级类等等)中,也不是一件很好的办法。因为一个复杂的业务,可能会涉及多个业务类的同时调用。因此,数据库连接和释放的操作,应该从业务类中独立出来,做成一个独一无二的类,供所有的业务类使用。当然这种使用,并不是同时的,也是先后顺序的使用,毕竟代码是按照顺序结构执行的。一个更不错的设计方案就是数据库连接池。也就是一个数据库连接列表。我们每次进行数据库操作的时候,可以从列表中获取一个连接,操作完毕后,在返还给池。在这个池中的所有连接,都不会被释放掉。这样做的目的就是为了提高数据库操作的性能,避免频繁创建和释放数据库带来的性能消耗。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值