JDBC从入门到精通 - 笔记(二):登录功能实战

视频资源:用户登录业务介绍_哔哩哔哩_bilibili

实现功能

1、需求:模拟用户登录功能

2、业务描述:

  • 程序运行时,提供输入入口,可以让用户输入用户名和密码
  • 用户输入并提交后,java程序收集到用户信息,并验证其是否合法
  • 合法则显示登录成功,否则登录失败

3、数据准备:表的设计可以使用专业工具进行数据库表建模 - PowerDesigner,用user-login.sql脚本初始化数据库数据。

PowerDesigner

简介

PowerDesigner是SAP公司收购Sybase公司的一款产品,它支持多种数据库系统。PowerDesigner 用于创建复杂的数据模型、架构和企业架构模型,并支持数据仓库设计。它支持多种模型类型,包括概念模型、逻辑模型、物理模型和数据仓库模型。

PowerDesigner面向数据架构师和企业架构师,需要收费。

注意PowerDesigner是设计工具,如果需要开发,可以使用Navicat等工具。

设计数据表

参考资料:使用PowerDesigner工具进行物理建模_哔哩哔哩_bilibili

新建项目后,选择所需数据库,并在其中一个格子里添加数据表,在数据表中添加相应的字段名称和类型等信息。

设计完成后点击应用,可以看到对应的字段和类型,打开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工具类,方便使用。

  1. 工具类的构造方法要私有化,因为工具类不需要new对象,里面的方法都是静态方法,直接用类名调用。

  2. 加载驱动时,可以直接用静态代码块加载。

  3. 获取连接和释放资源的方法按原样即可,注意输入输出。

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会立马给出更新的结果。

相关推荐
小屁孩大帅-杨一凡4 分钟前
java后端请求想接收多个对象入参的数据
java·开发语言
java1234_小锋10 分钟前
使用 RabbitMQ 有什么好处?
java·开发语言
TangKenny23 分钟前
计算网络信号
java·算法·华为
肘击鸣的百k路24 分钟前
Java 代理模式详解
java·开发语言·代理模式
城南vision31 分钟前
Docker学习—Docker核心概念总结
java·学习·docker
wyh要好好学习38 分钟前
SpringMVC快速上手
java·spring
尢词40 分钟前
SpringMVC
java·spring·java-ee·tomcat·maven
Mr. zhihao1 小时前
享元模式在 JDK 中的应用解析
java·享元模式
茶馆大橘1 小时前
微服务系列五:避免雪崩问题的限流、隔离、熔断措施
java·jmeter·spring cloud·微服务·云原生·架构·sentinel
wrx繁星点点1 小时前
享元模式:高效管理共享对象的设计模式
java·开发语言·spring·设计模式·maven·intellij-idea·享元模式