实现功能
1、需求:模拟用户登录功能
2、业务描述:
- 程序运行时,提供输入入口,可以让用户输入用户名和密码
- 用户输入并提交后,java程序收集到用户信息,并验证其是否合法
- 合法则显示登录成功,否则登录失败
3、数据准备:表的设计可以使用专业工具进行数据库表建模 - PowerDesigner,用user-login.sql脚本初始化数据库数据。
PowerDesigner
简介
PowerDesigner是SAP公司收购Sybase公司的一款产品,它支持多种数据库系统。PowerDesigner 用于创建复杂的数据模型、架构和企业架构模型,并支持数据仓库设计。它支持多种模型类型,包括概念模型、逻辑模型、物理模型和数据仓库模型。
PowerDesigner面向数据架构师和企业架构师,需要收费。
注意PowerDesigner是设计工具,如果需要开发,可以使用Navicat等工具。
设计数据表
新建项目后,选择所需数据库,并在其中一个格子里添加数据表,在数据表中添加相应的字段名称和类型等信息。
设计完成后点击应用,可以看到对应的字段和类型,打开preview也可以看到建表的sql语句。
实际做的时候,需要先在本地安装mysql Server,创建MySQL服务,然后可以用Navicat或者Mysql Workbench之类的工具,连接服务器进行类似操作。PowerDesigner不是必填项。
如何安装mysql并启用,参考007-MySQL8的安装与配置_哔哩哔哩_bilibili
项目代码
登录功能界面初始化
用Scanner记录输入的用户名和密码,并保存在map中,等待后续调用
java
/**
* 初始化用户界面
* @return 用户输入的用户名和密码等登录信息
*/
private static Map<String, String> initUI() {
Scanner scanner = new Scanner(System.in);
System.out.println("用户名: ");
String userName = scanner.nextLine();
System.out.println("密码: ");
String password = scanner.nextLine();
Map<String, String> userLoginInfo = new HashMap<>();
userLoginInfo.put("loginName", userName);
userLoginInfo.put("loginPwd", password);
return userLoginInfo;
}
实现用户登录
将输入的用户名和密码,拼接到sql语句中,与数据库已有数据进行比对。
java
/**
* 用户登录
* @param userLoginInfo 用户登录信息
* @return 登录成功/失败
*/
private static boolean login(Map<String, String> userLoginInfo) {
boolean loginSuccess = false;
// 用户名密码
String loginName = userLoginInfo.get("loginName");
String loginPwd = userLoginInfo.get("loginPwd");
// JDBC代码
Connection connection = null;
Statement statement = null;
ResultSet rs = null;
try {
// 1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");
// 3、获取数据库操作对象
statement = connection.createStatement();
// 4、执行sql
String sql = "select * from t_user where loginName = '" + loginName + "' and loginPwd = '" + loginPwd + "'";
rs = statement.executeQuery(sql);
// 5、处理结果集
if (rs.next()) {
loginSuccess = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6、释放资源
try {
if (rs != null) {
rs.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (statement != null) {
statement.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (connection != null) {
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return loginSuccess;
}
main函数
java
public static void main(String[] args) {
// 初始化界面
Map<String, String> userLoginInfo = initUI();
// 验证用户名密码
boolean loginResult = login(userLoginInfo);
System.out.println(loginResult ? "登录成功":"登录失败");
}
运行得到
bash
用户名:
zhangsan
密码:
123
登录成功
bash
用户名:
jack
密码:
123
登录失败
sql注入现象
如果在上述用户名中输入不正确的用户名和密码,也可以登录
bash
用户名:
mary
密码:
mary' or '1'='1
登录成功
为什么?因为此时的sql语句拼接后变为
select * from t_user where loginName = 'mary' and loginPwd = 'mary' or '1' = '1'
or '1'='1'永远为true,这种方式存在漏洞,导致了sql注入。
根本原因:用户输入的信息含有sql语句关键字,executeQuery(sql)的含义是将sql语句发给DBMS,DBMS对其进行编译,如果此时sql中含有"非法信息",会导致原sql语句被扭曲。
如何解决sql注入问题
1、用PreparedStatement替代Statement,即用预编译的数据库操作对象,替代普通的操作对象。
PreparedStatement接口继承了Statement,原理:预先对SQL语句进行编译,然后再给sql框架传值。用户提供的信息不参与sql语句的编译过程,即使sql语句中含有sql关键字,因为没有被编译,所以不会起作用。
2、用占位符替代原先的拼接字符串,再用connections.prepareStatement替代createStatement(sql)
3、用PreparedStatement.setString等set方法,替代占位符的内容
4、最后直接executeQuery
修改如下:(修改第3、4步)
java
/**
* 用户登录
* @param userLoginInfo 用户登录信息
* @return 登录成功/失败
*/
private static boolean login(Map<String, String> userLoginInfo) {
boolean loginSuccess = false;
// 用户名密码
String loginName = userLoginInfo.get("loginName");
String loginPwd = userLoginInfo.get("loginPwd");
// JDBC代码
Connection connection = null;
PreparedStatement ps = null; // 使用预编译的数据库操作对象
ResultSet rs = null;
try {
// 1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");
// 3、获取预编译的数据库操作对象
// SQL语句框架,一个?表示一个占位符
String sql = "select * from t_user where loginName = ? and loginPwd = ?";
// 程序执行到此处,会发送sql语句给DBMS,然后DBMS进行sql语句的预先编译
ps = connection.prepareStatement(sql);
// 给占位符传值,第一个问号下标为1,第二个为2,以此类推
ps.setString(1, loginName); // setString, 传入的值自动会加上单引号
ps.setString(2, loginPwd);
// 4、执行sql
rs = ps.executeQuery();
// 5、处理结果集
if (rs.next()) {
loginSuccess = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6、释放资源
try {
if (rs != null) {
rs.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (ps != null) {
ps.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (connection != null) {
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return loginSuccess;
}
Statement和PreparedStatement对比
- Statement有Sql注入风险,PreparedStatement解决了这个问题
- PreparedStatement效率更高:因为数据库执行已经编译过的语句时,不会重新编译,而是直接执行。PreparedStatement已经预编译过了,所以传值以后不需要重新编译,相当于编译一次,执行多次。而Statement编译一次执行一次,效率相对较低。
- PreparedStatement会在预编译阶段做类型安全检查,更为常用。Statement在极少数情况下会使用:如果业务方面要求必须支持sql注入的时候,如业务方面要求必须使用sql拼接的情况。
-
如果要进行升序或降序排列,而升序降序是由user提供的,这时用PreparedStatement会无法识别关键字,报错'You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''asc'' at line 1'。
java// SQL语句框架,一个?表示一个占位符 String sql = "select loginName from t_user order by loginName ?"; // 程序执行到此处,会发送sql语句给DBMS,然后DBMS进行sql语句的预先编译 ps = connection.prepareStatement(sql); ps.setString(1, keyWords);
-
PreparedState实现增删改
executeUpdate(),参数用?替代
java
public class JavaTest09 {
public static void main(String[] args) {
// JDBC代码
Connection connection = null;
PreparedStatement ps = null; // 使用预编译的数据库操作对象
try {
// 1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");
// 3、获取预编译的数据库操作对象
// // 添加
// String sql = "insert into dept(dname, loc, deptno) value (?, ?, ?)";
// // 程序执行到此处,会发送sql语句给DBMS,然后DBMS进行sql语句的预先编译
// ps = connection.prepareStatement(sql);
// // 给占位符传值,第一个问号下标为1,第二个为2,以此类推
// ps.setString(1, "Sales");
// ps.setString(2, "Beijing");
// ps.setInt(3, 70);
// // 修改
// String sql = "update dept set deptno = ? where dname = ?";
// // 程序执行到此处,会发送sql语句给DBMS,然后DBMS进行sql语句的预先编译
// ps = connection.prepareStatement(sql);
// // 给占位符传值,第一个问号下标为1,第二个为2,以此类推
// ps.setInt(1, 10);
// ps.setString(2, "Sales");
// 删除
String sql = "delete from dept where dname = ?";
// 程序执行到此处,会发送sql语句给DBMS,然后DBMS进行sql语句的预先编译
ps = connection.prepareStatement(sql);
// 给占位符传值,第一个问号下标为1,第二个为2,以此类推
ps.setString(1, "Dev");
// 4、执行sql
int count = ps.executeUpdate();
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6、释放资源
try {
if (ps != null) {
ps.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (connection != null) {
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
JDBC事务
JDBC事务自动提交机制
JDBC事务自动提交,即只要执行任意一条DML语句,就自动提交一次。但实际业务中,都是多条DML语句共同联合完成,在同一个事务中同时成功或者同时失败。
例子1:验证JDBC事务自动提交机制
每修改参数运行一次connection.prepareStatement,JDBC会直接提交,返回修改的record数量。
java
package JDBCTest;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
/**
* JDBC事务机制:
* 1、JDBC事务自动提交,即只要执行任意一条DML语句,就自动提交一次
* 但实际业务中,都是多条DML语句共同联合完成,在同一个事务中同时成功或者同时失败
* 2、以下验证JDBC的事务自动提交机制
*/
public class JDBCTest10 {
public static void main(String[] args) {
// JDBC代码
Connection connection = null;
PreparedStatement ps = null; // 使用预编译的数据库操作对象
try {
// 1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");
// 3、获取预编译的数据库操作对象
String sql1 = "update dept set dname = ? where deptno = ?";
ps = connection.prepareStatement(sql1);
// 第一次给占位符传值
ps.setString(1, "X");
ps.setInt(2, 15);
int count = ps.executeUpdate(); // 执行第一条update语句
System.out.println(count); // 2
// 重新给占位符传值
ps.setString(1, "Y");
ps.setInt(2, 20);
count = ps.executeUpdate(); // 执行第二条update语句
System.out.println(count); // 2
System.out.println(count);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6、释放资源
try {
if (ps != null) {
ps.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (connection != null) {
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
例子2:银行转账
小tip:想要复制注释里面的内容又不想带注释符号,按alt再选注释里面的内容
按alt+shift+insert,再把光标放到需要编辑的位置,可以同时插入多行
将sql语句放入文档中,用mysql的source 来run它们,初始化表格
bash
mysql> source C:\Documents\code\t_account.sql
source C:\Users\danrwang\Documents\Documents\java learning\code\t_account.sql
假设是一个银行转账的简单程序,要修改两个账户中的金额。
在账户一给账户二转账的时候,先从账户一扣除金额,然后再在账户二加上金额,如果这两步中间出现了异常,由于jdbc的自动提交机制,此时账户一的金额已经扣除,而账户二的金额还没添加就已经出现异常了,肯定不行。
账户转账事务修改
所以实际做的时候,要取消自动提交机制,在事务完成以后再提交所有的操作,如果碰到异常,必须回滚。具体分为三步:
java
// 将自动提交机制修改为手动提交
connection.setAutoCommit(false);
// do something
// 最后一次性提交
connection.commit();
// 捕捉异常
catch (Exception e) {
// 如果碰到异常,事务回滚
if (connection != null) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
}
具体代码如下
java
public class JDBCTest11 {
public static void main(String[] args) {
// JDBC代码
Connection connection = null;
PreparedStatement ps = null; // 使用预编译的数据库操作对象
try {
// 1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");
// 将自动提交机制修改为手动提交
connection.setAutoCommit(false);
// 3、获取预编译的数据库操作对象
String sql1 = "update t_account set balance = ? where id = ?";
ps = connection.prepareStatement(sql1);
// 第一次给占位符传值
ps.setDouble(1, 10000);
ps.setInt(2, 111);
int count = ps.executeUpdate();
// 重新给占位符传值
ps.setDouble(1, 10000);
ps.setInt(2, 222);
count += ps.executeUpdate(); // 执行第一条update语句
System.out.println(count == 2 ? "Succeed":"Fail"); // 2
// 程序能走到这里,说明以上程序没有异常,事务结束,手动提交数据即可
connection.commit(); // 提交事务
} catch (Exception e) {
// 如果碰到异常,事务回滚
if (connection != null) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 6、释放资源
try {
if (ps != null) {
ps.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (connection != null) {
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
拓展:如果涉及到从一地的数据库给另一地的数据库转账的情况,需要使用分布事务的功能,比单机事务更加复杂。
JDBC工具类封装
DBUtil工具类
可以将获取数据库连接,释放资源等方法,封装成DBUtil工具类,方便使用。
-
工具类的构造方法要私有化,因为工具类不需要new对象,里面的方法都是静态方法,直接用类名调用。
-
加载驱动时,可以直接用静态代码块加载。
-
获取连接和释放资源的方法按原样即可,注意输入输出。
java
package JDBCTest.utils;
import java.sql.*;
/**
* JDBC工具类
* 简化JDBC编程
*/
public class DBUtils {
/**
* 工具类中的构造方法私有化
* 因为工具类的方法是静态的,不需要new对象,直接采用类名调用
*/
private DBUtils() {}
// 静态代码块在类加载时执行,且只执行一次
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取数据库连接对象
* @return 连接对象
* @throws SQLException
*/
public static Connection getConnection() throws SQLException{
return DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");
}
/**
* 关闭资源
* @param connection 连接对象
* @param ps 数据库操作对象
* @param rs 结果集
*/
public static void close(Connection connection, Statement ps, ResultSet rs) {
try {
if (ps != null) {
ps.close();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
if (connection != null) {
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用工具类实现模糊查询
除了调用sql和处理结果集外,其他方法都可以用工具类调用。
模糊查询时注意 ? 里面的内容,需要包含%。
java
import JDBCTest.utils.DBUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
/**
* 用DBUtils实现模糊查询
*/
public class JDBCTest12 {
public static void main(String[] args) {
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 获取连接
connection = DBUtils.getConnection();
// 获取预编译操作对象
String sql = "select loc from dept where loc like ?";
ps = connection.prepareStatement(sql);
ps.setString(1, "H%");
rs = ps.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("loc"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.close(connection, ps, rs);
}
}
}
悲观锁和乐观锁
机制
在执行select语句时,如果在其最后加上for update,如
select ename.job.salary from emp where job = 'MANAGER' for update;
那在当前事务没有结束之前,这几行数据就上锁了,不会改变,其他事务也无法改变这些数据。
这个称为悲观锁,也称为行级锁。
悲观锁要求事务必须排队执行,锁定事务时不允许开发
乐观锁支持并发,事务也不需要排队,但需要版本号。
乐观锁例子:假设事务1和事务2同时在进行,它们开始执行时,都读到版本号1.1。事务1修改后,提交时读到版本号是1.1,直接提交,版本号变为1.2。事务2修改后,提交时读到版本号是1.2,与一开始不一致,所以回滚事务。
演示
写两个脚本,一个用来使用悲观锁开启事务,另一个在事务未结束的时候执行含DML的事务。
JDBCTest13.java
java
/**
* 开启事务,执行查询,使用悲观锁
*/
public class JDBCTest13 {
public static void main(String[] args) {
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 获取连接
connection = DBUtils.getConnection();
// 开启事务
connection.setAutoCommit(false);
String sql = "select dname, deptno from dept where loc = ? for update"; // 加行级锁
ps = connection.prepareStatement(sql);
ps.setString(1, "Hangzhou");
rs = ps.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("dname") + " " + rs.getString("deptno"));
}
// 提交事务(事务结束)
connection.commit();
} catch (Exception e) {
if (connection != null) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
DBUtils.close(connection, ps, rs);
}
}
}
JDBCTest14.java
java
public class JDBCTest14 {
public static void main(String[] args) {
Connection connection = null;
PreparedStatement ps = null;
try {
connection = DBUtils.getConnection();
connection.setAutoCommit(false);
String sql = "update dept set dname = 'Z' where loc = ?";
ps = connection.prepareStatement(sql);
ps.setString(1, "Hangzhou");
int count = ps.executeUpdate();
System.out.println(count);
connection.commit();
} catch (Exception e) {
if (connection != null) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
}
e.printStackTrace();
} finally {
DBUtils.close(connection, ps, null);
}
}
}
在Test13提交事务之前打断点, 然后运行,接着运行Test14.
在Test13没有运行完成之前,Test14一直处于卡住的状态。
一旦Test13继续运行,完成了commit,即事务结束,Test14会立马给出更新的结果。