JavaWeb的小结08
第2章-第8节
一、知识点
JDBC、SQL注入、事务。
二、目标
-
熟练使用JDBC。
-
理解SQL注入。
-
掌握事务的应用。
三、内容分析
-
重点
-
JDBC。
-
SQL注入。
-
事务。
-
-
难点
-
SQL注入。
-
事务。
-
四、内容
1、JDBC
Java Database Connectivity java语言操作数据库
本质:官方定义的一套操作所有关系型数据库的规则,即接口。各个数据库厂商去实现这套接口,提供数据库驱动jar包。我们可以使用这套接口编程,真正执行代码的是驱动jar包中的实现类。
1.1 使用步骤
-
导入驱动jar包。
-
注册驱动。
-
获取数据库连接对象 Connection。
-
定义sql。
-
获取执行sql语句对象 Statement。
-
执行sql,接收返回结果。
-
处理结果。
-
释放资源。
Connection connection = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 1、加载驱动包,初始化一些配置
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、获取连接对象, 连接数据库
// jdbc:mysql://IP:端口号/数据库名?userSSL=false&serverTimezone=Asia/Shanghai
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?userSSL=false&serverTimezone=Asia/Shanghai", "root", "123456");
// 3、获取用来执行sql语句的对象Statement
stmt = connection.createStatement();
// 4、执行sql语句, 返回一个ResultSet对象,里面放了sql语句执行的结果
rs = stmt.executeQuery("select * from student");
// 5、从ResultSet对象解析数据,有多少条数据就循环多少次
while(rs.next()) {
// String name = rs.getString(2); // 数字:填列的下标,从1开始算
String name = rs.getString("name"); // 字符串:填字段名称
int id = rs.getInt("id");
String sex = rs.getString("sex");
int age = rs.getInt("age");
System.out.println(id);
System.out.println(name);
students.add(new Student(id, name, sex, age));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(rs != null) {
rs.close();
}
if(stmt != null) {
stmt.close();
}
if(connection != null) {
connection.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
注意:查询使用executeQuery()方法,返回ResultSet对象,里面包含了查询的结果集。添加、删除、修改使用executeUpdate()方法,返回int值,代表影响了多少条数据,用于判断数据是否操作成功。
1.2 sql注入
SQL 注入就是指 web应用程序对用户输入的数据合法性没有过滤或者是判断,前端传入的参数是攻击者可以控制,并且参数带入数据库的查询,攻击者可以通过构造恶意的sql语句来实现 对数据库的任意操作。
通过案例解释sql注入带来的危害。
假设我们做一个登录的功能,首先要查询user表中是否包含用户填写的账号密码,如果验证通过代表登录成功,如果验证失败代表登录失败。
以下是使用Statement对象实现,最终用户实现了账号密码登录了系统。
Connection connection = null;
Statement stmt = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?userSSL=false&serverTimezone=Asia/Shanghai", "root", "123456");
stmt = connection.createStatement();
// 假设username、password是页面填写的账号密码
String username = "111"; // 用户名随便填写
String password = "' or '1' = '1"; // 密码是用户恶意填写的sql片段
// 拼接参数会造成sql注入安全问题
// select * from user where username = '111' and password = '' or '1' = '1'
String sql = "select * from user where username = '"
+ username
+ "' and password = '"
+ password + "'";
System.out.println(sql);
rs = stmt.executeQuery(sql);
// 登录效果。
// 如果账号密码正确就登录成功,不正确就登录失败
if(rs.next()) {
System.out.println("登录成功!");
} else {
System.out.println("登录失败!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if(rs != null) {
rs.close();
}
if(stmt != null) {
stmt.close();
}
if(connection != null) {
connection.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
防止sql注入,可以使用PreparedStatement对象,进行sql语句预编译,语句中使用了占位符,规定了sql语句的结构,不会因为参数让sql语句结构发生变化。
以下是使用PreparedStatement对象实现登录查询效果,最终防止了sql注入的问题。
Connection connection = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?userSSL=false&serverTimezone=Asia/Shanghai", "root", "123456");
// 获取PreparedStatement对象,用来执行sql语句
// 获取的时候就要把sql语句传进去
String username = "111";
String password = "' or '1' = '1";
// 参数使用?号占位
String sql = "select * from user where username = ? and password = ?";
stmt = connection.prepareStatement(sql);
// 替换参数,这里set方法会对特殊符号进行处理
stmt.setString(1, username); // 第一个参数指的是第几个?号。第二个参数指的是要替换的值
stmt.setString(2, password);
// 执行sql语句
// executeQuery() 不需要传sql,因为创建PreparedStatement对象已经传sql语句了
rs = stmt.executeQuery();
if(rs.next()) {
System.out.println("登录成功!");
} else {
System.out.println("登录失败!");
}
} catch (Exception e) {
e.printStackTrace();
}
1.3 事务
事务:事务是一个最小的不可在分的工作单元;通常一个事务对应一个完整的业务。多个操作同时进行,要么同时成功,要么同时失败。这就是事务。
比如:张三给李四转账,这是一个完整的操作,包含了张三扣除金额,李四增加金额两个操作,这两个操作是不可分割的。或者是网上买东西的时候,生成订单,对应的商品库存要减掉,这也是两个不可分割的操作。
使用方法:
-
开启事务
setAutoCommit(boolean autoCommit) // 把默认的自动提交设置为false,调用该方法设置参数为false,即开启事务。在执行sql之前开启
-
提交事务/回滚事务
事务要么提交,要么回滚。
commit() // 当所有sql都执行完提交事务 rollback() // 出现错误回滚事务,在catch中回滚事务
以下代码演示了张三向李四转500块钱的效果。在两个executeUpdate()方法之间,我们假设出现异常了,导致第二个executeUpdate()方法没有执行,那么我们必须把数据回滚,不然最终的数据就会出现问题,即张三500块钱扣掉了,但是李四没有多加500。我们这里使用了事务就可以保证不会出现问题。
Connection connection = null;
PreparedStatement stmt1 = null;
PreparedStatement stmt2 = null;
try {
// 1、加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test23?userSSL=false&serverTimezone=Asia/Shanghai", "root", "123456");
// 开启事务,设置自动提交关闭
connection.setAutoCommit(false);
String sql1 = "update student set money = money - 500 where id = 1";
String sql2 = "update student set money = money + 500 where id = 2";
stmt1 = connection.prepareStatement(sql1);
stmt2 = connection.prepareStatement(sql2);
int rs1 = stmt1.executeUpdate();
// int i = 10 / 0;
int rs2 = stmt2.executeUpdate();
if(rs1 > 0 && rs2 > 0 ) {
System.out.println("成功!");
} else {
System.out.println("失败!");
}
connection.commit(); // 要执行commit(),前面开启事务之后的sql语句才会生效
} catch (Exception e) {
e.printStackTrace();
connection.rollback();
} finally {
stmt1.close();
stmt2.close();
connection.close();
}
1.4 连接池
数据库连接池是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。返回给连接池的这些连接并不会关闭(并没有关闭数据库的物理连接,而是把数据库连接释放,归还给了数据库连接池。),而是准备给下一个调用者进行分配。我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接。更为重要的是我们可以通过连接池的管理机制监视数据库的连接的数量、使用情况,为系统开发、测试及性能调整提供依据。
我们常用的连接池技术有C3P0、Druid两种。
1.4.1 C3P0
C3P0是一个开源的JDBC连接池。
使用步骤:
-
导入jar包,有两个分别是:c3p0和mchange-commons-java。
-
定义配置文件c3p0-config.xml或者c3p0.properties,直接将文件放在src目录下。
-
创建核心对象,数据库连接对象ComboPooledDataSource。
-
获取连接:getConnection。
<!-- c3p0-config.xml -->
<c3p0-config>
<default-config>
<!-- 数据库驱动名 -->
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<!-- 数据库的url -->
<property name="jdbcUrl">jdbc:mysql://localhost:3306/learn_mysql?userSSL=false&serverTimezone=Asia/Shanghai</property>
<!--用户名。Default: null -->
<property name="user">root</property>
<!--密码。Default: null -->
<property name="password">123456</property>
<!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
<property name="initialPoolSize">3</property>
<!--连接池中保留的最大连接数。Default: 15 -->
<property name="maxPoolSize">5</property>
<!--当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出 SQLException,如设为0则无限期等待。单位毫秒。Default: 0 -->
<property name="checkoutTimeout">0</property>
</default-config>
</c3p0-config>
// 获取一个默认数据库连接池
ComboPooledDataSource source = new ComboPooledDataSource();
for (int i = 0; i < 4; i++) {
Connection connection = source.getConnection();
System.out.println(connection.toString());
if (i == 3) {
connection.close();
}
}
1.4.2 Druid
德鲁伊,阿里巴巴提供的数据库连接池技术。
使用步骤:
-
导入jar包,druid.jar。
-
定义配置文件xxx.properties,可以放在任意目录下。
-
加载配置文件。
-
获取数据库连接池对象:获取DruidDataSourceFactory。
-
获取连接getConnection。
# xxx.properties
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/test?userSSL=false&serverTimezone=Asia/Shanghai
username=root
password=123456
initialSize=3
maxActive=5
maxWait=1000
封装成工具类,方便调用。
public class JdbcUtil {
private static DataSource dataSource;
static {
try {
ClassLoader classLoader = JdbcUtil.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("druid.properties");
Properties properties = new Properties();
properties.load(inputStream);
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() {
Connection connection = null;
try {
connection = dataSource.getConnection();
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
public static void close(ResultSet rs, PreparedStatement preStatement, Connection conn) {
try {
if (rs != null) {
rs.close();
}
if (preStatement != null) {
preStatement.close();
}
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
五、小结
本小节主要介绍JDBC、SQL注入、事务、连接池。介绍了Java如何连接数据库实现交互,需要熟练应用JDBC相关方法实现对数据库数据的操作,理解连接池的工作原理以及使用方法。