七、JDBC 与 DAO 模式

七、JDBC 与 DAO 模式

7.1 JDBC 介绍

7.1.1 什么是 JDBC

JDBC(Java Database Conectivity)

Java数据库连接技术的简称,提供连接各种常用数据库的能力

7.1.2 JDBC 的工作原理

JDBC API

内容:供程序员调用的接口与类,集成在java.sql和javax.sql包中,如

  • DriverManager类
  • Connection接口
  • Statement接口
  • ResultSet接口

DriverManager

  • 作用:管理各种不同的JDBC驱动

JDBC 驱动

  • 提供者:数据库厂商
  • 作用:负责连接各种不同的数据库

7.1.3 JDBC API

JDBC API 主要功能

  • 与数据库建立连接、执行SQL 语句、处理结果

  • DriverManager :依据数据库的不同,管理JDBC驱动

  • Connection :负责连接数据库并担任传送数据的任务

  • Statement :由 Connection 产生、负责执行SQL语句

  • esultSet:负责保存Statement执行后的查询结果


7.2 使用 JDBC 连接数据库

7.2.1 导入 JDBC 驱动 JAR 包

数据库版本:MySQL5.7

MySQL官网下载对应的JDBC驱动JAR包

  • mysql-connector-java-8.0.19.jar

驱动类

  • com.mysql.cj.jdbc.Driver

7.2.2 纯 Java 驱动方式

使用纯Java方式连接数据库

  • 由JDBC驱动直接访问数据库

  • 优点:完全Java代码,快速、跨平台

  • 缺点:访问不同的数据库需要下载专用的JDBC驱动

  • JDBC驱动由数据库厂商提供

7.2.3 JDBC编程模板

try {
      Class.forName(JDBC驱动类);
      # 1.加载JDBC驱动 
} catch (ClassNotFoundException e) {
    //异常输出代码
} //… …
try {
      Connection con=DriverManager.getConnection(数据连接字符串,数据库用户名,密码);
      // 2.与数据库建立连接 
      
      Statement stmt = con.createStatement();
      ResultSet rs = stmt.executeQuery("SELECT a, b, c FROM table1;");
	// 3.发送SQL语句,并得到返回结果 
      while (rs.next()) {
             int x = rs.getInt("a");
             String s = rs.getString("b");
             float f = rs.getFloat("c");
      }
      // 4.处理返回结果 
      rs.close();
      stmt.close();   
      con.close();
      // 5.释放资源
} //… …

7.2.4 数据库连接字符串

jdbc:数据库://ip:端口/数据库名称[连接参数=参数值]
  • 数据库:表示JDBC连接的目标数据库
  • ip: 表示JDBC所连接的目标数据库地址,如果是本地数据库,可为localhost,即本地主机名
  • 端口:连接数据库的端口号
  • 数据库名称:是目标数据库的名称
  • 连接参数:连接数据库时的参数配置

连接本地MySQL中hospital数据库

jdbc:mysql://localhost:3306/hospital?serverTimezone=GMT-8
// 我国处于东八区,时区设置为GMT-8

7.2.5 Connection 接口

Connection是数据库连接对象的类型

方法作用
Statement createStatement()创建一个Statement对象将SQL语句发送到数据库
PreparedStatement prepareStatement(String sql)创建一个PreparedStatement对象,将参数化的SQL语句发送到数据库
boolean isClosed()查询此Connection对象是否已经被关闭。如果已关闭,则返回true;否则返回false
void close()立即释放此Connection对象的数据库和JDBC资源

7.2.6 连接本地 hospital 数据库

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;

public class HospitalConn {
    private static Logger logger = LogManager.getLogger(HospitalConn.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC","root", "root");
            System.out.println("数据库连接成功");
        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 3、关闭数据库连接
            try {
                if (null != conn) {
                    conn.close();
                    System.out.println("数据库连接断开");
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.2.7 常见异常

使用JDBC连接数据库时,经常出现的错误

  • JDBC驱动类的名称书写错误,导致ClassNotFoundException异常
  • 数据连接字符串、数据库用户名、密码错误,导致SQLException异常
  • 数据库操作结束后,没有关闭数据库连接,导致仍旧占有系统资源
  • 关闭数据库连接语句没有放到finally语句块中,导致语句可能没有被执行

7.3 Statement 操作数据库

Java执行数据库操作的一个重要接口,在已经建立数据库连接的基础上,向数据库发送要执行的SQL语句

  • Statement对象:执行不带参数的简单SQL语句
  • PreparedStatement对象:执行带或不带In参数的预编译SQL语句
方法作用
ResultSet executeQuery(String sql)可以执行SQL查询并获取ResultSet对象
int executeUpdate(String sql)可以执行插入、删除、更新的操作,返回值是执行该操作所影响的行数
boolean execute(String sql)可以执行任意SQL语句。如果结果为 ResultSet 对象,则返回 true;如果其为更新计数或者不存在任何结果,则返回false

使用 executeQuery() 和 executeUpdate() 方法都需要啊传入 SQL 语句,因此,需要在 Java 中通过字符串拼接获得 SQL 字符串

7.3.1 Java 的字符串操作

String类

  • 字符串常量一旦声明则不可改变
  • String类对象可以改变,但改变的是其内存地址的指向
  • 使用“+”作为数据的连接操作
  • 不适用频繁修改的字符串操作

StringBuffer类

  • StringBuffer类对象能够被多次修改,且不产生新的未使用对象
  • 使用append()方法进行数据连接
  • 适用于字符串修改操作
  • 是线程安全的,支持并发操作,适合多线程

如果使用StringBuffer 生成了 String 类型字符串,可以通过 toString( ) 方法将其转换为一个 String 对象

  • 需要拼接的字符串
String patientName="李明";
String gender="男";
String birthDate="2010-09-03";
  • 使用+拼接字符串
//使用+拼接字符串
String sql = "insert into patient (patientName,gender,birthDate) values('"+
	patientName+"','"+
	gender+"','"+
	birthDate+"');";
System.out.println(sql);
  • 使用StringBuffer拼接字符串
//使用StringBuffer拼接字符串
StringBuffer sbSql = new StringBuffer("insert into patient (patientName,gender,birthDate)" +
      "  values('");sbSql.append(patientName+"','");
sbSql.append(gender+"','");
sbSql.append(birthDate+"');");
sql = sbSql.toString();
System.out.println(sql);
  • SQL语句中,字符"'是等效的
  • 但在Java代码中拼接字符串时使用字符'会使代码更加清晰
  • 也不容易出错引号、逗号或括号等符号必须成对出现
  • 可在控制台输出拼接后的字符串,检查SQL语句是否正确

7.3.1 Statement 插入数据

使用Statement接口执行插入数据的操作的方法

  • executeUpdate()方法
  • execute()方法

如果希望得到插入成功的数据行数,可以使用executeUpdate()方法;否则,使用execute()方法

实现步骤

  • 声明Statement变量
  • 创建Statement对象
  • 构造SQL语句
  • 执行数据插入操作
  • 关闭Statement对象
  • 关闭顺序是后创建的对象要先关闭释放资源

演示案例

使用JDBC,向hospital数据库病人表中添加一个新的病人记录关键代码

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

public class HospitalInsert {
    private static Logger logger = LogManager.getLogger(HospitalInsert.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        String name = "张菲";
        // 姓名
        String gender = "女";
        // 性别
        String birthDate = "1995-02-12";
        // 出生日期
        String phoneNum = "13887676500";
        // 联系电话
        String email = "fei.zhang@qq.com";
        //邮箱
        String password = "909000";
        //密码
        String identityNum = "610000199502126100";
        //身份证号
        String address = "北京市";
        //地址
        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "root");
            // 创建Statement对象
            stmt = conn.createStatement();
            //构造SQL
            StringBuffer sbSql = new StringBuffer("insert into patient (patientName,gender,birthDate,phoneNum,email,password,identityNum,address) values ( '");
            sbSql.append(name + "','");
            sbSql.append(gender + "','");
            sbSql.append(birthDate + "','");
            sbSql.append(phoneNum + "','");
            sbSql.append(email + "','");
            sbSql.append(password + "','");
            sbSql.append(identityNum + "','");
            sbSql.append(address + "');");
            System.out.println(sbSql.toString());
            //3、执行插入操作
            stmt.execute(sbSql.toString());
        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 4、关闭数据库连接
            try {
                if (null != stmt) {
                    stmt.close();
                }
                if (null != conn) {
                    conn.close();
                    System.out.println("数据库连接断开");
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

为了避免可能出现的乱码问题,可将指定数据库连接的编码集为UTF8,多个参数间使用字符&进行分隔

jdbc:mysql://localhost:3306/hospital?serverTimezone=GMT-8&useUnicode=true&characterEncoding=utf-8

7.3.2 Statement 更新数据

  • 使用executeUpdate()方法或execute()方法实现更新数据的操作
  • 使用Statement接口更新数据库中的数据的步骤与插入数据类似

实现步骤

  • 声明Statement变量
  • 创建Statement对象
  • 构造SQL语句
  • 执行数据更新操作
  • 关闭Statement对象

需关注拼接的SQL字符串,以避免出错

演示案例

使用JDBC,将hospital数据库中patientID为13的病人电话更新为13627395833

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

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

public class HospitalUpdate {
    private static Logger logger = LogManager.getLogger(HospitalUpdate.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        int patientID = 13;
        // 病人编号
        String phoneNum = "13627395833";
        // 联系电话
        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "root");
            // 创建Statement对象
            stmt = conn.createStatement();
            //构造SQL
            StringBuffer sbSql = new StringBuffer("update patient ");
            sbSql.append("set phoneNum='" + phoneNum + "' ");
            sbSql.append("where patientID=" + patientID + ";");
            System.out.println(sbSql.toString());
            //3、执行插入更新操作
            int effectRowNum = stmt.executeUpdate(sbSql.toString());
            System.out.println("更新数据的行数:" + effectRowNum);
        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 4、关闭数据库连接
            try {
                if (null != stmt) {
                    stmt.close();
                }
                if (null != conn) {
                    conn.close();
                    System.out.println("数据库连接断开");
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.3.3 ResultSet 接口

保存和处理Statement执行后所产生的查询结果

  • 由查询结果组成的一个二维表
  • 每行代表一条记录
  • 每列代表一个字段
方法说明
boolean next()将游标从当前位置向下移动一行
void close()关闭ResultSet 对象
int getInt(int colIndex)以int形式获取结果集当前行指定列号值
int getInt(String colLabel)以int形式获取结果集当前行指定列名值
float getFloat(int colIndex)以float形式获取结果集当前行指定列号值
float getFloat(String colLabel)以float形式获取结果集当前行指定列名值
String getString(int colIndex)以String形式获取结果集当前行指定列号值
String getString(String colLabel)以String形式获取结果集当前行指定列名值
  • 要从中获取数据的列号或列名可作为方法的参数
  • 根据值的类型选择对应的方法

ResultSet 接口 getXxx() 方法

  • 获取当前行中某列的值
  • 要从中获取数据的列号或列名可作为方法的参数
  • 根据值的类型选择对应的方法
int类型       ->   getInt()
float类型    ->   getFloat()
String类型  ->   getString()

假设结果集的第一列为patientID,存储类型为int类型,能够获得该列值的两种方法

//使用列号提取数据
int id = rs.getInt(1);
//使用列名提取数据
int id = rs.getInt("patientID");
  • 列号从1开始计数,与数组下标从0开始计数不同
  • 采用列名来标识列可读性强,且不容易出错

7.3.4 Statement 和 ResultSet 查询数据

使用 JDBC 从 hospital 数据库中查询前3个病人的编号、姓名、性别、住址信息并输出到控制台上

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;

public class HospitalQuery {
    private static Logger logger = LogManager.getLogger(HospitalQuery.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        int patientID = 13;
        // 病人编号
        String phoneNum = "13627395833";
        // 联系电话
        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "root");
            System.out.println("建立连接成功 !");
            // 创建Statement对象
            stmt = conn.createStatement();
            //构造SQL
            String sql = "select patientID,patientName,gender,address from patient limit 3;";
            //3、执行查询更新操作
            rs = stmt.executeQuery(sql);
            //4、移动指针遍历结果集并输出查询结果
            while (rs.next()) {
                System.out.println(rs.getInt("patientID") + "\t" +
                        rs.getString("patientName") + "\t" +
                        rs.getString("gender") + "\t" +
                        rs.getString("address"));
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != stmt) {
                    stmt.close();
                }
                if (null != conn) {
                    conn.close();
                    System.out.println("数据库连接断开");
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.4 PreparedStatement 操作数据库

7.4.1 SQL 注入攻击

通过提交一段SQL代码,执行超出用户访问权限的数据操作称为SQL注入(SQL Injection),SQL注入攻击是应用安全领域的一种常见攻击方式,会造成的数据库安全风险包括:刷库、拖库和撞库等,主要是没有对用户输入数据的合法性进行判断,导致应用程序存在安全隐患

使用JDBC实现医院管理系统用户登录验证功能

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;
import java.util.Scanner;

public class HospitalLogin {
    private static Logger logger = LogManager.getLogger(HospitalLogin.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        //根据控制台提示输入用户身份证号和密码
        Scanner input = new Scanner(System.in);
        System.out.println("用户登录");
        System.out.print("请输入身份证号:");
        String identityNum = input.next();
        System.out.print("请输入密码:");
        String password = input.next();

        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "123456");
            // 创建Statement对象
            stmt = conn.createStatement();
            //构造SQL
            StringBuffer sbSql = new StringBuffer("SELECT patientName FROM patient WHERE ");
            sbSql.append("password='" + password + "'");
            sbSql.append(" and identityNum='" + identityNum + "';");
            //3、执行查询更新操作
            rs = stmt.executeQuery(sbSql.toString());
            System.out.println(sbSql.toString());
            //4、验证用户名和密码
            if (rs.next()) {
                System.out.println("欢迎" + rs.getString("patientName") + "登录系统!");
            } else {
                System.out.println("密码错误!");
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != stmt) {
                    stmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LWZU5FBo-1680617691979)(./assets/%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20230404172438.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oFRlcayJ-1680617691982)(./assets/%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20230404172502.png)]

修改查询结构

7.4.2 PreparedStatement 接口

使用PreparedStatement 接口

  • 继承自 Statement接口
  • 与Statement对象相比,使用更加灵活,更有效率

PreparedStatement接口 (预编译的 SQL 语句)

  • 提高代码可读性和可维护性

  • 提高安全性

  • 提高SQL语句执行的性能

方 法作 用
boolean execute()执行SQL语句,可以是任何SQL语句。如果结果是Result对象,则返回true。如果结果是更新计数或没有结果,则返回false
ResultSet executeQuery()执行SQL查询,返回该查询生成的ResultSet对象
int executeUpdate()执行SQL语句,该语句必须是一个DML语句,比如:INSERT、UPDATE或DELETE语句;或者是无返回内容的SQL语句,比如DDL语句。返回值是执行该操作所影响的行数
void setXxx(int index,xxx x)方法名Xxx和第二个参数的xxx均表示(如int,float,double等)基本数据类型,且两个类型需一致,参数列表中的x表示方法的形式参数。把指定数据类型(xxx)的值x设置给index位置的参数。根据参数类型的不同,常见方法有:setInt(int index,int x) 、setFloat(int index,float x)、setDouble(int index,double x)等
void setObject(int index,Object x)除基本数据类型外,参数类型也可以是Object,可以将Object对象x设置给index位置的参数

7.4.3 PreparedStatement 操作数据

创建PreparedStatement对象

  • 使用Connection接口prepareStatement(String sql)方法创建PreparedStatement对象
  • 需要提前设置该对象将要执行的SQL语句
  • SQL语句可具有一个或多个输入参数

设置输入参数的值

  • 调用setXxx()方法完成参数赋值

执行SQL语句

  • 调用PreparedStatement接口
  • executeQuery()
  • executeUpdate()
  • execute()
  • 方法执行SQL语句

验证用户输入的身份证号和密码

  • 如果通过验证,则输出“欢迎[姓名]登录系统!”的信息;
  • 否则输出“密码错误!”
package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;
import java.util.Scanner;

public class HospitalLogin {
    private static Logger logger = LogManager.getLogger(HospitalLogin.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        //根据控制台提示输入用户身份证号和密码
        Scanner input = new Scanner(System.in);
        System.out.println("用户登录");
        System.out.print("请输入身份证号:");
        String identityNum = input.next();
        System.out.print("请输入密码:");
        String password = input.next();

        // 1、加载驱动
        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }
        try {
            // 2、建立连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/hospital?serverTimezone=UTC", "root", "123456");

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT patientName FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, identityNum);
            pstmt.setString(2, password);
            rs = pstmt.executeQuery();
            //4、验证用户名和密码
            if (rs.next()) {
                System.out.println("欢迎" + rs.getString("patientName") + "登录系统!");
            } else {
                System.out.println("密码错误!");
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.4.4 PreparedStatement 的优势

实际开发中,推荐使用PreparedStatement接口执行数据库操作

  • PreparedStatement与Statement接口相比,具有的优势
  • 可读性和可维护性高
  • SQL语句执行性能高
  • 安全性更高

7.5 Properties 配置文件

7.5.1 为什么使用 Properties 类

使用JDBC技术访问数据库数据的关键代码

private String driver = "com.mysql.jdbc.Driver";
private String url = "jdbc:mysql://localhost:3306/hospital?serverTimezone=GMT-8";
private  String user = “root"; 	
private  String password=123456"; 
// 修改后需重新编译
Connection conn = null;
public Connection getConnection() {
    if(conn==null) {
        try {
            Class.forName(driver);
            conn = DriverManager.getConnection(url, user, password);
        } catch (Exception e) {//省略代码……}
    }	
    return conn;// 返回连接对象
}

让用户脱离程序本身修改相关的变量设置——使用配置文件

7.5.2 properties配置文件

Java的配置文件常为properties文件

  • 后缀为.properties
  • 以“键=值”格式储存数据
  • 使用“#”添加注释

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vLwQh0OR-1680617691982)(./assets/%E6%90%9C%E7%8B%97%E6%88%AA%E5%9B%BE20230404204942.png)]

通常,为数据库访问添加的配置文件是database.properties

7.5.3 读取配置文件信息

使用java.util包下的Properties类读取配置文件

方法描述
String getProperty(String key)用指定的键在此属性列表中搜索属性,通过参数key得到其所对应的值
Object setProperty(String key, String value)通过调用基类Hashtable的put()方法设置键-值对
void load(InputStream inStream)从输入流中读取属性列表 (键和元素对),通过对指定文件进行装载获取该文件中所有键-值对
void clear()清除所装载的键-值对,该方法由基类Hashtable提供

使用Properties配置文件的方式改造医院管理系统

package XaunZiShare;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;

public class HospitalSystem {
    private static Logger logger = LogManager.getLogger(HospitalSystem.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        String patientID = null;
        boolean isExist = false;
        //根据控制台提示输入用户身份证号和密码
        Scanner input = new Scanner(System.in);
        System.out.println("用户登录");
        System.out.print("请输入身份证号:");
        String identityNum = input.next();
        System.out.print("请输入密码:");
        String password = input.next();


        Properties params = new Properties();
        String configFile = "database.properties";
        //配置文件路径
        String url = null;
        String username = null;
        String pwd = null;
        //加载配置文件到输入流中
        try {
            InputStream is = HospitalSystem.class.getClassLoader().getResourceAsStream(configFile);
            params.load(is);
            //根据指定的获取对应的值
            String driver = params.getProperty("driver");
            url = params.getProperty("url");
            username = params.getProperty("username");
            pwd = params.getProperty("password");

            // 1、加载驱动
            Class.forName(driver);
        } catch (IOException e) {
            logger.error(e);
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }

        try {
            // 2、建立连接
            conn = DriverManager.getConnection(url, username, pwd);

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT patientID, patientName FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, identityNum);
            pstmt.setString(2, password);
            rs = pstmt.executeQuery();
            //4、验证用户名和密码
            if (rs.next()) {
                patientID = rs.getString("patientID");
                System.out.println("欢迎" + rs.getString("patientName") + "登录系统!");
                while (!isExist) {
                    System.out.println("1.查询检查记录\t 0.退出");
                    System.out.print("请输入要执行的操作:");
                    String action = input.next();
                    if (action.equals("1")) {
                        pstmt = conn.prepareStatement("SELECT depName, checkItemName, checkResult, checkItemCost, examDate FROM prescription p  INNER JOIN department d ON p.depID = d.depID INNER JOIN checkitem c ON p.checkItemID = c.checkItemID WHERE p.patientID=?;");
                        pstmt.setString(1, patientID);
                        rs = pstmt.executeQuery();
                        System.out.println("检查科室\t检查项目\t检查结果\t检查费用\t检查时间");
                        while (rs.next()) {
                            System.out.println(rs.getString("depName") + "\t" + rs.getString("checkItemName") + "\t" + rs.getString("checkResult") + "\t" + rs.getString("checkItemCost") + "\t" + rs.getString("examDate") + "\t");
                        }
                    } else if (action.equals("0")) {
                        isExist = true;
                        System.out.println("再见");
                    } else {
                        System.out.println("输入错误,请重新输入");
                    }
                }
            } else {
                System.out.println("密码错误!");
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.6 DAO 模式

7.6.1 什么是 DAO

非常流行的数据访问模式——DAO模式

  • Data Access Object(数据存取对象)
  • 位于业务逻辑和持久化数据之间
  • 实现对持久化数据的访问

DAO起着转换器的作用,把实体类转换为数据库中的记录

7.6.2 DAO 模式的组成

组成部分

  • DAO接口
  • DAO实现类
  • 实体类
  • 数据库连接和关闭工具类

优势

  • 隔离了数据访问代码和业务逻辑代码
  • 隔离了不同数据库实现

7.6.3 使用实体类传递数据

数据访问代码和业务逻辑代码之间通过实体类来传输数据

业务逻辑代码
数据访问代码

实体类特征

  • 属性一般使用private修饰
  • 提供public修饰的getter/setter方法
  • 实体类提供无参构造方法,根据业务提供有参构造
  • 实现java.io.Serializable接口,支持序列化机制

7.6.4 实体类

实体类(Entity)是Java应用程序中与数据库表对应的类

  • 用于存储数据,并提供对这些数据的访问
  • 通常,实现类是持久的,需要存储于文件或数据库中
  • 访问操作数据库时,以实体类的方式组织数据库中的实体及关系
  • 通常,在Java工程中创建一个名为entity的Package,用于集中保存实体类
  • 一个数据库表对应一个实体类

7.6.5 定义实体类

package XaunZiShare;

import java.io.Serializable;

public class Patient implements Serializable {
    private static final long serialVersionUID = -8762235641468472877L;
    private String patientID;  //病人编号
    private String password; //登录密码
    private String birthDate; //出生日期
    private String gender; //性别
    private String patientName; //姓名
    private String phoneNum; //联系电话
    private String email; //邮箱
    private String identityNum; //身份证号
    private String address; //地址

    /**
     * 无参构造方法
     */
    public Patient() {

    }

    /**
     * 有参构造方法,根据需要提供
     *
     * @param identityNum 身份证号
     * @param name        姓名
     */
    public Patient(String identityNum, String name) {
        this.identityNum = identityNum;
        this.patientName = name;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public String getPatientID() {
        return patientID;
    }

    public void setPatientID(String patientID) {
        this.patientID = patientID;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getBirthDate() {
        return birthDate;
    }

    public void setBirthDate(String birthDate) {
        this.birthDate = birthDate;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getPatientName() {
        return patientName;
    }

    public void setPatientName(String patientName) {
        this.patientName = patientName;
    }

    public String getPhoneNum() {
        return phoneNum;
    }

    public void setPhoneNum(String phoneNum) {
        this.phoneNum = phoneNum;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getIdentityNum() {
        return identityNum;
    }

    public void setIdentityNum(String identityNum) {
        this.identityNum = identityNum;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

7.6.6 使用实体类传递数据

package XaunZiShare;

import com.javamysql.entity.Patient;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;

public class HospitalSystem {
    private static Logger logger = LogManager.getLogger(HospitalSystem.class.getName());

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        Patient patient = new Patient();
        boolean isExist = false;
        //根据控制台提示输入用户身份证号和密码
        Scanner input = new Scanner(System.in);
        System.out.println("用户登录");
        System.out.print("请输入身份证号:");
        patient.setIdentityNum(input.next());
        System.out.print("请输入密码:");
        patient.setPassword(input.next());

        Properties params = new Properties();
        String configFile = "database.properties";
        //配置文件路径
        String url = null;
        String username = null;
        String pwd = null;
        //加载配置文件到输入流中
        try {
            InputStream is = HospitalSystem.class.getClassLoader().getResourceAsStream(configFile);
            params.load(is);
            //根据指定的获取对应的值
            String driver = params.getProperty("driver");
            url = params.getProperty("url");
            username = params.getProperty("username");
            pwd = params.getProperty("password");

            // 1、加载驱动
            Class.forName(driver);
        } catch (IOException e) {
            logger.error(e);
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }

        try {
            // 2、建立连接
            conn = DriverManager.getConnection(url, username, pwd);

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT * FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, patient.getIdentityNum());
            pstmt.setString(2, patient.getPassword());
            rs = pstmt.executeQuery();
            //4、验证用户名和密码
            if (rs.next()) {
                //从MySQL读取用户信息,并加载到patient对象中
                patient.setPatientID(rs.getString("patientID"));
                patient.setAddress(rs.getString("address"));
                patient.setBirthDate(rs.getString("birthDate"));
                patient.setEmail(rs.getString("email"));
                patient.setGender(rs.getString("gender"));
                patient.setPatientID(rs.getString("patientName"));
                patient.setPhoneNum(rs.getString("phoneNum"));

                System.out.println("欢迎" + patient.getPatientName() + "登录系统!");
                while (!isExist) {
                    System.out.println("1.查询检查记录\t2.查询病人信息\t 0.退出");
                    System.out.print("请输入要执行的操作:");
                    String action = input.next();
                    if (action.equals("1")) {
                        pstmt = conn.prepareStatement("SELECT depName, checkItemName, checkResult, checkItemCost, examDate FROM prescription p  INNER JOIN department d ON p.depID = d.depID INNER JOIN checkitem c ON p.checkItemID = c.checkItemID WHERE p.patientID=?;");
                        pstmt.setString(1, patient.getPatientID());
                        rs = pstmt.executeQuery();
                        System.out.println("检查科室\t检查项目\t检查结果\t检查费用\t检查时间");
                        while (rs.next()) {
                            System.out.println(rs.getString("depName") + "\t" + rs.getString("checkItemName") + "\t" + rs.getString("checkResult") + "\t" + rs.getString("checkItemCost") + "\t" + rs.getString("examDate") + "\t");
                        }
                    } else if (action.equals("2")) {
                        System.out.println(patient.getPatientID() + "\t" + patient.getPatientName() + "\t" + patient.getGender() + "\t" + patient.getBirthDate() + "\t" + patient.getIdentityNum() + "\t" + patient.getPhoneNum() + "\t" + patient.getEmail() + "\t" + patient.getAddress());
                    } else if (action.equals("0")) {
                        isExist = true;
                        System.out.println("再见");
                    } else {
                        System.out.println("输入错误,请重新输入");
                    }
                }
            } else {
                System.out.println("密码错误!");
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
    }
}

7.6.7 实体类的特征

实体类特征

  • 属性一般使用private修饰
  • 提供public修饰的getter/setter方法
  • 实体类提供无参构造方法,根据业务提供有参构造
  • 实现java.io.Serializable接口,支持序列化机制,可以将该对象转换成字节序列而保存在磁盘上或在网络上传输

如果实体类实现了java.io.Serializable接口,应该定义属性serialVersionUID,解决不同版本之间的序列化问题

为serialVersionUID赋值的方法

  • 手动

  • 使用IDEA生成

  • private static final long serialVersionUID = -8762235641468472877L;
    

一旦为一个实体类的serialVersionUID赋值,就不要再修改;否则,在反序列化之前版本的数据时,会报java.io.InvalidClassException异常


7.7 实现 JDBC 封装

7.7.1 JDBC

将程序中数据在瞬时状态和持久状态间转换的机制为数据持久化

JDBC

  • 读取
  • 删除
  • 查找
  • 修改
  • 保存

7.7.2 持久化的实现方式

数据库

普通文件

XML文件

7.7.3 为什么进行 JDBC 封装

Scanner input = new Scanner(System.in);
System.out.print("请输入登录名:");
String name=input.next();
System.out.print("请输入登录密码:");
String password=input.next();
// 业务相关代码
// ……省略加载驱动
try {
    conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/hospital?serverTimezone=GMT-8",
                "root", "123456");
    // … …省略代码 … …
    if(rs.next())
        System.out.println("登录成功,欢迎您!");
    else
	System.out.println("登录失败,请重新输入!");	
    // ……省略代码
} catch (SQLException e) {			
    // ……省略代码
} finally {}
// 数据访问代码

业务代码和数据访问代码耦合

  • 可读性差
  • 不利于后期修改和维护
  • 不利于代码复用

采用面向接口编程,可以降低代码间的耦合性

采用面向接口编程,可以降低代码间的耦合性

业务逻辑代码
数据访问代码
MySQL
SQLServer
Oracle

业务逻辑代码调用数据访问接口

7.7.4 使用 DAO 模式改造 Hospital

将HospitalSystem中对病人的所有数据库操作抽象成接口

对病人的数据库操作包括修改病人信息、通过身份证号和密码验证登录

设计接口时,尽量以对象为单位,给调用者提供面向对象的接口

  • 使用实体类作为接口的参数和返回值,可以让接口更加清晰简洁
  • 如果以Patient类的各个属性为形参进行传递,不仅会导致参数个数很多,还会增加接口和实现类中方法的数量等

接口的命名,应以简明为主

  • “实体类名+Dao”格式如
  • PatientDao
  • 作为工程中相对独立的模块
  • 所有DAO接口文件都放在dao包中

接口由不同数据库的实现类分别实现

PatientDao 接口

package XaunZiShare;

import com.javamysql.entity.Patient;

public interface PatientDao {
    /**
     * 更新病人信息
     *
     * @param patient 病人
     */
    int update(Patient patient);

    /**
     * 根据身份证号和登录密码返回病人信息
     *
     * @param identityNum 身份证号
     * @param pwd         登录密码
     * @return 病人
     */
    Patient getPatientByIdNumAndPwd(String identityNum, String pwd);
}

PatientDao实现类的方法:update()方法

package XaunZiShare;

import com.javamysql.HospitalSystem;
import com.javamysql.dao.PatientDao;
import com.javamysql.entity.Patient;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

public class PatientDaoMySQLImpl implements PatientDao {
    private static Logger logger = LogManager.getLogger(HospitalSystem.class.getName());

    @Override
    public int update(Patient patient) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        int result = 0;

        Properties params = new Properties();
        String configFile = "database.properties";//配置文件路径
        String url = null;
        String username = null;
        String password = null;
        //加载配置文件到输入流中
        try {
            InputStream is = HospitalSystem.class.getClassLoader().getResourceAsStream(configFile);
            params.load(is);
            //根据指定的获取对应的值
            String driver = params.getProperty("driver");
            url = params.getProperty("url");
            username = params.getProperty("username");
            password = params.getProperty("password");

            // 1、加载驱动
            Class.forName(driver);
        } catch (IOException e) {
            logger.error(e);
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }

        try {
            // 2、建立连接
            conn = DriverManager.getConnection(url, username, password);

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("UPDATE patient SET address=?, birthDate=?, email=?, gender=?, patientName=?, phoneNum=?, identityNum=?,password=? WHERE patientID=?");
            pstmt.setString(1, patient.getAddress());
            pstmt.setString(2, patient.getBirthDate());
            pstmt.setString(3, patient.getEmail());
            pstmt.setString(4, patient.getGender());
            pstmt.setString(5, patient.getPatientName());
            pstmt.setString(6, patient.getPhoneNum());
            pstmt.setString(7, patient.getIdentityNum());
            pstmt.setString(8, patient.getPassword());
            pstmt.setString(9, patient.getPatientID());
            result = pstmt.executeUpdate();
        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
        return result;
    }

    @Override
    public Patient getPatientByIdNumAndPwd(String identityNum, String pwd) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        Patient patient = null;
        Properties params = new Properties();
        String configFile = "database.properties";
        //配置文件路径
        String url = null;
        String username = null;
        String password = null;
        //加载配置文件到输入流中
        try {
            InputStream is = HospitalSystem.class.getClassLoader().getResourceAsStream(configFile);
            params.load(is);
            //根据指定的获取对应的值
            String driver = params.getProperty("driver");
            url = params.getProperty("url");
            username = params.getProperty("username");
            password = params.getProperty("password");

            // 1、加载驱动
            Class.forName(driver);
        } catch (IOException e) {
            logger.error(e);
        } catch (ClassNotFoundException e) {
            logger.error(e);
        }

        try {
            // 2、建立连接
            conn = DriverManager.getConnection(url, username, password);

            //3、构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT * FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, identityNum);
            pstmt.setString(2, pwd);
            rs = pstmt.executeQuery();
            //4、验证用户名和密码
            if (rs.next()) {
                //从MySQL读取用户信息,并加载到patient对象中
                patient = new Patient();
                patient.setPatientID(rs.getString("patientID"));
                patient.setAddress(rs.getString("address"));
                patient.setBirthDate(rs.getString("birthDate"));
                patient.setEmail(rs.getString("email"));
                patient.setGender(rs.getString("gender"));
                patient.setPatientName(rs.getString("patientName"));
                patient.setPhoneNum(rs.getString("phoneNum"));
                patient.setIdentityNum(rs.getString("identityNum"));
                patient.setPassword(rs.getString("password"));
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            // 5、关闭数据库连接
            try {
                if (null != rs) {
                    rs.close();
                }
                if (null != pstmt) {
                    pstmt.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (SQLException e) {
                logger.error(e);
            }
        }
        return patient;
    }
}

通用的操作是否能够进一步简化?


7.8 BaseDao基类

7.8.1 将通用的操作(打开、关闭连接等)封装到基类

package XaunZiShare;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

public class BaseDao {
    private static String driver;
    // 数据库驱动字符串
    private static String url;
    // 连接URL字符串
    private static String user;
    // 数据库用户名
    private static String password;
    // 用户密码

    // 数据连接对象
    static {//静态代码块,在类加载的时候执行
        init();
    }

    Connection conn = null;

    /**
     * 初始化连接参数,从配置文件里获得
     */
    public static void init() {
        Properties params = new Properties();
        String configFile = "database.properties";
        //配置文件路径
        //加载配置文件到输入流中
        InputStream is = BaseDao.class.getClassLoader().getResourceAsStream(configFile);
        try {
            //从输入流中读取属性列表
            params.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //根据指定的获取对应的值
        driver = params.getProperty("driver");
        url = params.getProperty("url");
        user = params.getProperty("username");
        password = params.getProperty("password");
    }

    /**
     * 获取数据库连接对象
     */
    public Connection getConnection() {
        try {
            if (conn == null || conn.isClosed()) {
                // 获取连接并捕获异常
                try {
                    Class.forName(driver);
                    conn = DriverManager.getConnection(url, user, password);
                } catch (Exception e) {
                    e.printStackTrace();
                    // 异常处理
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
        // 返回连接对象
    }

    /**
     * 关闭数据库连接
     *
     * @param conn 数据库连接
     * @param stmt Statement对象
     * @param rs   结果集
     */
    public void closeAll(Connection conn, Statement stmt, ResultSet rs) {
        // 若结果集对象不为空,则关闭
        if (rs != null) {
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 若Statement对象不为空,则关闭
        if (stmt != null) {
            try {
                stmt.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 若数据库连接对象不为空,则关闭
        if (conn != null) {
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

7.8.2 基类 BaseDao:增、删、改的通用方法

    /**
     * 增、删、改的操作
     *
     * @param preparedSql 预编译的 SQL 语句
     * @param param       参数的字符串数组
     * @return 影响的行数
     */
    public int exceuteUpdate(String preparedSql, Object[] param) {
        PreparedStatement pstmt = null;
        int num = 0;
        conn = getConnection();
        try {
            pstmt = conn.prepareStatement(preparedSql);
            if (param != null) {
                for (int i = 0; i < param.length; i++) {
                    //为预编译sql设置参数
                    pstmt.setObject(i + 1, param[i]);
                }
            }
            num = pstmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            closeAll(conn, pstmt, null);
        }
        return num;
    }

7.8.3 实现类实现接口并继承 BaseDao 基类

package XaunZiShare;

import com.javamysql.HospitalSystem;
import com.javamysql.dao.BaseDao;
import com.javamysql.dao.PatientDao;
import com.javamysql.entity.Patient;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class PatientDaoMySQLImpl extends BaseDao implements PatientDao {
    private static Logger logger = LogManager.getLogger(HospitalSystem.class.getName());

    @Override
    public int update(Patient patient) {
        //构造SQL语句
        String preparedSQL = "UPDATE patient SET address=?, birthDate=?, email=?, gender=?, patientName=?, phoneNum=?, identityNum=?,password=? WHERE patientID=?";
        //构造SQL执行参数数组
        List<String> params = new ArrayList<String>();
        params.add(patient.getAddress());
        params.add(patient.getBirthDate());
        params.add(patient.getEmail());
        params.add(patient.getGender());
        params.add(patient.getPatientName());
        params.add(patient.getPhoneNum());
        params.add(patient.getIdentityNum());
        params.add(patient.getPassword());
        params.add(patient.getPatientID());
        //调用BaseDao中的更新
        return exceuteUpdate(preparedSQL, params.toArray());
    }

    @Override
    public Patient getPatientByIdNumAndPwd(String identityNum, String pwd) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        Patient patient = null;

        conn = getConnection();

        try {
            //构造PreparedStatement对象
            pstmt = conn.prepareStatement("SELECT * FROM patient WHERE identityNum=? and password=?");
            pstmt.setString(1, identityNum);
            pstmt.setString(2, pwd);
            rs = pstmt.executeQuery();
            //验证用户名和密码
            if (rs.next()) {
                //从MySQL读取用户信息,并加载到patient对象中
                patient = new Patient();
                patient.setPatientID(rs.getString("patientID"));
                patient.setAddress(rs.getString("address"));
                patient.setBirthDate(rs.getString("birthDate"));
                patient.setEmail(rs.getString("email"));
                patient.setGender(rs.getString("gender"));
                patient.setPatientName(rs.getString("patientName"));
                patient.setPhoneNum(rs.getString("phoneNum"));
                patient.setIdentityNum(rs.getString("identityNum"));
                patient.setPassword(rs.getString("password"));
            }

        } catch (SQLException e) {
            logger.error(e);
        } finally {
            //关闭数据库连接
            closeAll(conn, pstmt, rs);
        }
        return patient;
    }
}

此种封装JDBC的模式称为DAO模式


7.9 项目结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X6RO8XGh-1680617691983)(./assets/SRC.png)]

7.9.1 项目名

项目名其实就是反过来的网站网址

7.9.2 enrity(实体类)

这个包里面放的是都是,数据表的实体类,(POJO)。即具有,基础属性,属性封装(getter/setter),构造函数,和Tostring()的普通类

7.9.3 dao(接口类)

dao 包里面还有一层包叫``impl即实现类,dao 包下的接口类只定义方法,具体实现由impl`包下的实现类实现

7.9.4 service (业务类)

service 为业务层,将基础实现类,整合为复杂业务,与dao 包一样包内部还有一层包impl service 包下的接口类只定义方法,具体实现由impl包下的实现类实现

7.9.5 Main (运行类)

代码运行类

7.9.6 lib(依赖包)

项目所需的 JDBC 驱动 放置在此包内

7.9.7 database (连接数据库配置文件)

项目连接数据库所需要的配置文件


7.10 命名规范

直接参考阿里巴巴发布的《阿里巴巴Java开发手册(终极版)v1.3版本》

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YTAOLKOH-1680617691983)(./assets/image-20230404213509656.png)]

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值