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中,等待后续调用

    /**
     * 初始化用户界面
     * @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语句中,与数据库已有数据进行比对。

    /**
     * 用户登录
     * @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函数

    public static void main(String[] args) {
        // 初始化界面
        Map<String, String> userLoginInfo = initUI();
        // 验证用户名密码
        boolean loginResult = login(userLoginInfo);
        System.out.println(loginResult ? "登录成功":"登录失败");
    }

运行得到

用户名: 
zhangsan
密码: 
123
登录成功
用户名: 
jack
密码: 
123
登录失败

 sql注入现象

如果在上述用户名中输入不正确的用户名和密码,也可以登录

用户名: 
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步)

    /**
     * 用户登录
     * @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’。
      // 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(),参数用?替代 

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数量。

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它们,初始化表格

mysql> source C:\Documents\code\t_account.sql

source C:\Users\danrwang\Documents\Documents\java learning\code\t_account.sql

假设是一个银行转账的简单程序,要修改两个账户中的金额。

在账户一给账户二转账的时候,先从账户一扣除金额,然后再在账户二加上金额,如果这两步中间出现了异常,由于jdbc的自动提交机制,此时账户一的金额已经扣除,而账户二的金额还没添加就已经出现异常了,肯定不行。

账户转账事务修改

所以实际做的时候,要取消自动提交机制,在事务完成以后再提交所有的操作,如果碰到异常,必须回滚。具体分为三步:

// 将自动提交机制修改为手动提交
connection.setAutoCommit(false);

// do something

// 最后一次性提交
connection.commit();

// 捕捉异常
catch (Exception e) {
            // 如果碰到异常,事务回滚
            if (connection != null) {
                try {
                    connection.rollback();
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
            e.printStackTrace();
        }

具体代码如下

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. 获取连接和释放资源的方法按原样即可,注意输入输出。

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和处理结果集外,其他方法都可以用工具类调用。

模糊查询时注意 ? 里面的内容,需要包含%。

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

/**
 * 开启事务,执行查询,使用悲观锁
 */
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

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

 

 

  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值