JDBC(du)

JDBC

1、 jdbc是什么

java Database connectivity(java语句连接数据库)

2、JDBC 的本质是什么

JDBC是sun公司制定的一套接口(interface),
接口都有调用者和实现者。
面向接口调用,面向接口实现类。这都是属于面向接口的编程、
接口也是抽象编程
java.sql
为什么要面向接口编程?
解耦合:降低程序的耦合度,提高程序的扩展力。

多态机制是非常典型的的:面向抽象的编程。(不要面向具体编程)
建议
	Animal a=new Cat();
	Animal a=new Dog();
	public void feed(Animal a){
    
    //面向父类形编程。
	}
不建议
	Dog d=new Dog();
	cat c=new cat();
	public void feed(Dog a){
    
    
	}
为什么sun制定一套jdbc接口?
因为每一个数据库的底层实现原理都不一样。
ORacle 数据库有自己的原理。
MYSQL数据库也有自己的原理
.............
数据库产品都有自己独特的实现原理。

数据库驱动:所有数据库都是以jar包形式的存在,jar 包中有很多.class文件 就是对jdbc接口的实现

3、jdbc开发

下载对应的驱动jar 包,然后将其配置到classpath环境变量里

4、重要类

重要的类
DriverManager :依据数据库的不同,管理JDBC驱动
Connection :负责连接数据库并担任传送数据的任务——通过DriverManager获取的
Statement :由 Connection 产生、负责执行SQL语句
ResultSet:负责保存Statement执行后所产生的查询结果
ResultSetMetaData:换取关于ResultDate对象中的类型和属性信息的对象
PreparedStatement:继承Statement接口

DriverManager
驱动程序管理器,跟踪可用的驱动程序,并在数据库和相应的驱动程序之间建立连接。
注册驱动程序:
Class.forName(“com.microsoft.sqlserver.jdbc. SQLServerDriver”);
Class.forName(“sun.jdbc.odbc.JdbcOdbcDriver”);
Class.forName( "oracle.jdbc.driver.OracleDriver ");
Class.forName(“com.mysql.jdbc.Driver”);
注册的驱动程序类名称必须在用户的classPath中。

  //第一种注册加载驱动
        //DriverManger.registerDriver(new Driver()); 放到静态代码块
        try {
    
    
            Class.forName("com.mysql.cj.jdbc.Driver");//第二种
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }

Connection

换取数据库连接 对象


Connection conn= DriverManager.getConnection(url,user,pwd);
url:地址: jdbc : <subprotocol> : <subname>
user:数据库名
pwd:密码

Statement
执行sql语句
由Connection接口的createStatement()方法创建。
编译一次执行一次

  //获取Statement
   //executeupdate 增删改 executequery 查
             Statement stm=conn.createStatement();
            //4 执行SQL:executeupdate 就是执行增删改查
            int i=stm.executeUpdate(sql);

ResultSet
处理查询结果集
在JDBC中数据库的所有查询记录将使用ResultSet接收并显示内容
ResultSet的常用方法:
next():判断有没有下一行,有指向下一行,并返回true,没有返回false
getObject(int index):索引,获取当前行第几列的值。1–Object
getObject(String lieming):列名,根据列名获取列名对应的值
getInt—int
getString-----

注意:随便的用来接注意类型

ResultSetMetaData
可以从这个对象获得有关数据库管理系统的各种信息,包括数据库中的各个表,表中的各个列,数据类型,触发器,存储过程等各方面的信息

ResultSetMetaData是在DatabaseMetaData类的对象上实现的,DataBaseMetaData对象又是在Connection对象上获得的。
 getURL():返回一个String类对象,代表数据库的URL。
 getUserName():返回连接当前数据库管理系统的用户名。
 isReadOnly():返回一个boolean值,指示数据库是否只允许读操作。
 getDatabaseProductName():返回数据库的产品名称。
 getDatabaseProductVersion():返回数据库的版本号。
 getDriverName():返回驱动驱动程序的名称。
 getDriverVersion():返回驱动程序的版本号。

PreparedStatement
 PreparedStatement 接口继承 Statement接口比普通的Statement对象更加灵活,有效解决SQL 注入问题(登录不要用statement)

区别

  1. Statement 可以先行创建, 然后将sql语句写入.执行时传入sql
  2. PreparedStatement 在创建时一定要传入 sql语句, 因为它要先运送到数据库执行预编译
  3. PreparedStatement 在执行之前 先要设置 语句中的参数. (预处理的sql语句有占位? 执行前需要给?指定参数值,执行时可以直接执行不需要传入sql)
  4. PreparedStatement 解决了sql注入问题
  5. PreparedStatement 效率高 (一个SQL语句一模一样的重复执行不会重新编译 用这个类的 话 执行不同的内容但是sql框架已经好了 编译一次可以执行n次)
  6. PreparedStatement 会在执行编译前阶段进行安全检查

总结::PreparedStatement 使用较多 ,只要少数情况的情况下需要使用Statement
只要在业务需要使用SQL注入的情况下必须使用Statement(需要代码拼接的·)

5、 jdbc编程六步(记)

  1. 注册驱动(作用:告诉java程序,即将连接的是哪一个品牌的数据库)
  2. 获取连接(表示jvm的进程和数据库进程之间的通道打开了,重量级的,使用完必须释放)
  3. 获取数据库操作对象(执行sql语句对象)
  4. 执行sql语句(DQL DML。。。)
  5. 处理查询结果集(只有当第四部执行的是select语句的时候,才有处理查询结果集)
  6. 释放资源(使用完一定要释放资源)

6 、 注入

当前程序存在的问题
用户名:随便
密码: ’ or ‘1’=’
登录成功
这种现象被称为sql注入。(黑客经常使用)
程序中有一个bug
导致的sql注入的根本原因是什么
用户输入的信息中含有SQL语句的关键字,那这些关键字参与啦sql语句的编译过程,
导致sql语句原意被扭曲,进而达到sql注入
使用PreparedStatement的预编译功能来解决sql的住入问题可以

7、事务

mysql的事务
事务:将多条sql当成整体去运行,要成功都成功,要失败都失败,事务的原子性(不可分割)
事务的特性:持久性(一经提交,数据会存储到数据库–存储磁盘中永久存在)
JDBC的事务
默认情况下连接COnnection属于自动模式 在自动提交模式下,每个SQL更新语句(insert,update,delete)成功执行完后就会自动提交到数据库中。

批处理

一次性需要新增上万条 上千数据,此时一条条处理,效率慢,数据库交互时间,可以进行批处理,可以将多条sql打成一个批次,推送给数据库执行一次。
conn.setAutoCommit(false);//关闭自动提交 默认为true
关闭来自动模式 每个sql都是事务的一部分
然后需要用commit()来进行显示的进行提交
在自动提交关闭后,不成功的提交会导致数据库进行隐式的回滚,所有的更新都会丢失。
conn.rollback();//回滚

      conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/kong?&serverTimezone=GMT%2B8&yseSSL=false","root","04161220");
            conn.setAutoCommit(false);//关闭自动提交
            String sql="insert  into students_copy1(sno,sname,ssex) values(?,?,?)";//SQL语句
            prstm=conn.prepareStatement(sql);
            for (int i=1;i<=100008;i++){
    
    
                prstm.setObject(1,"aa"+1);
                prstm.setObject(2,"bb");
                prstm.setObject(3,"bb");
                //pstm.executeUpdate() 需要数据库交互100万次
                prstm.addBatch();//添加
                if(i%100==0){
    
    //没100条进行来打包
                    prstm.executeBatch();//执行一批
                    prstm.clearBatch();
                }
            }
            prstm.executeBatch();//

            conn.commit();//
            
  try {
    
    
                conn.rollback();//回滚
            } catch (SQLException ex) {
    
    
                ex.printStackTrace();
            }

JDBC日期

日期赋值
1、给日期赋值 直接赋值日期格式的字符串
2、给日期赋值、java.util.Date dateUtil=new java.util.Date();//当前时间的年月日时分秒
java.sql.Date datesql =new java.sql.Date(dateUtil.getTime());//可以取出sql格式的年月日 当前的日期
3、给日期赋值 Jdk8中的LocakDate LocalDateTIme
LocalDateTime dateTime= LocalDateTime.now();//当前时间的年月日时分秒
LocalDate date=LocalDate.now();//当前时间的年月日时分秒
4、给日期赋值 java.sql.Timestamp dates=new java.sql.Timestamp(dateUtil.getTime());
5、 java.sql.Date datesql =new java.sql.Date(dateUtil.getTime());//可以取出sql格式的年月日 当前的日期

 java.util.Date dateUtil=new  java.util.Date();//当前时间的年月日时分秒
            
            java.sql.Date datesql =new java.sql.Date(dateUtil.getTime());//可以取出sql格式的年月日  当前的日期
            
            java.sql.Timestamp dates=new java.sql.Timestamp(dateUtil.getTime());
            
            LocalDateTime dateTime= LocalDateTime.now();//当前时间的年月日时分秒
            
            LocalDate date=LocalDate.now();//当前时间的年月日时分秒

JDBC查询日期


     ResultSetMetaData resultSetMetaData= rs.getMetaData();//吧ResultSet 查询1的结果 给ResultSetMetaData 这样就有时间类型了
            while (rs.next()){
    
    
             // 注意在 高版本的jdk中 日期只有 LocalDate
                for (int i=1;i<resultSetMetaData.getColumnCount();i++){
    
    //循环他列数量
                    if(resultSetMetaData.getColumnClassName(i).equals("java.sql.Timestamp")){
    
    //判断这个例的值的属性是不是时间
                        java.sql.Timestamp timestamp=rs.getTimestamp(i);// 获取这个时间
                        java.util.Date date =new java.util.Date(timestamp.getTime()); //获取他
                        SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd");
                        System.out.println(format.format(date));
                    }
                }
            }

Dao模式

dao全称是data access object,数据库访问对象,主要的功能就是用于进行数据操作的,在程序的标准开发架构中属于数据层的操作。
什么是Dao模式
分离了业务逻辑代码和数据访问代码,分工明确,降低耦合性,提高可重用性。
采用面向接口编程,提高了项目的可扩展性和可维护性。

1、DAO接口: 把对数据库的所有操作定义成抽象方法,可以提供多种实现
2、DAO 实现类: 针对不同数据库给出DAO接口定义方法的具体实现。
3、实体类:用于存放与传输对象数据。没有不影响
4、数据库连接和关闭工具类: 避免了数据库连接和关闭代码的重复使用,方便修改。

配置文件

A. 后缀为.properties;
B. 格式是“键=值”格式;
C. 使用“#”来注释D. 让用户脱离程序本身修改相关的变量设置——使用配置文件
E. Java中提供了Properties类来读取配置文件F. 注意:第一:前面有 “ / ”:“ / ”代表了工程的根目录,例如工程名叫做myproject,“ / ”代表了myproject 。 eg:me.class.getResourceAsStream("/com/x/file/myfile.xml"); 第二:前面没有 “ / ”::代表当前类的目录。

代码示例

jdbc基础

public class Maintest01 {
    
    
    //1.加载驱动
    //获取连接
    public static void main(String[] args) {
    
    
        //第一种注册加载驱动
        //DriverManger.registerDriver(new Driver()); 放到静态代码块
        try {
    
    
            Class.forName("com.mysql.cj.jdbc.Driver");//第二种
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }
        //2 获取连接
        String user="root";
        String pwd="04161220";
                //协议   指明数据类型  连接谁的电脑  连接哪一个库 localhost
        String url="jdbc:mysql://localhost:3306/scool?&serverTimezone=GMT%2B8&useSSL=false";
        Connection conn=null;
        Statement stm=null;
        try {
    
    
              conn= DriverManager.getConnection(url,user,pwd);
            //System.out.println(conn);
            //3 书写sql
            String sql=" insert into t_b values('a','b','c');";
            //获取Statement
            //executeupdate 增删改 executequery 查
              stm=conn.createStatement();
            //4 执行SQL:executeupdate 就是执行增删改查
            int i=stm.executeUpdate(sql);
		//查询的数据应该要excuteQuery
            System.out.println(i);
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }finally{
    
    
            //5 释放内存
            //保证资源释放
            //必须并列 一个不能关闭不能影响另一个的关闭
            try {
    
    
               if(stm!=null){
    
     stm.close();}
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
            try {
    
    
               if(conn!=null){
    
     conn.close();}
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

处理查询结果集

   public static void main(String[] args) {
    
    
        Connection conn=null;
        Statement stmt=null;
        PreparedStatement stm=null;
        ResultSet rs=null;

        //1
        try {
    
    
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }
        //是Statement对象的子接口,Statement可以进行它也可以——执行拼接字符串的sql   fd
        //解决sql注入:?占位,避开字符串拼接的,需要动态写的内容不走拼接走?占位,在执行之前使用固定的方法给?赋值。
        //2  &serverTimezone=GMT%2B8&useSSL=false
        try {
    
    
            //"jdbc:mysql://localhost:3306/scool?&serverTimezone=GMT%2B8&useSSL=false";
            conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/scool?&serverTimezone=GMT%2B8&useSSL=false","root","04161220");
            //3
            stmt=conn.createStatement();
            //4  insert into 表名(列名)values(列值),(),(),(),(),()。。。。
            rs= stmt.executeQuery(
                    "select * from T_b");
            System.out.println(rs);
            while(rs.next()){
    
    
            //数据列的位置 第一个位置的值第二个位置的值(用列的位置来换取这个是不安全的)
                String a=rs.getString(1);
                String b=rs.getString(2);
                String c=rs.getString(3);
			//不通过列的下标通过列的名字换取比较安全
			//名称
			 	String a=rs.getString("B1");
                String b=rs.getString("B2");
                String c=rs.getString("A1");
				//除了可以以String 类型取出之外还可以,以特定的2类型取出
				int a=rs.getInt(1);
				St
				
                System.out.println(a+","+b+","+c);
            }
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }finally{
    
    
            if(stmt!=null){
    
    
                try {
    
    
                    stmt.close();
                } catch (SQLException e) {
    
    
                    e.printStackTrace();
                }
            }
            if(conn!=null){
    
    
                try {
    
    
                    conn.close();
                } catch (SQLException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }

返回添加内容的主键的方法

/**
 * @author zhangyifan
 * @version 8.0
 * @description: 获取新增的主键
 * @date 2021/8/31 15:42
 */
public class JDBC04 {
    
    
    static  int age;
    public static void main(String[] args) {
    
    
        Connection con=null;
        PreparedStatement pstm=null;
        ResultSet rs=null;       
        try {
    
    //1
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();//&serverTimezone=GMT%2B8&useSSL=false
        }
        //2
        try {
    
    //"jdbc:mysql://localhost:3306/kong?&serverTimezone=GMT%2B8&yseSSL=false"
            con= DriverManager.getConnection("jdbc:mysql://localhost:3306/scool?&serverTimezone=GMT%2B8&yseSSL=false","root","04161220");

            //3
            String sql="insert into classinfo(classname,Begintime,endtime) values(?,?,?)";//sql语句
            pstm= con.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);//返回新增的一个主键 
            pstm.setString(1,"aa");
            pstm.setString(2,"2011-01-01");
            pstm.setString(3,"2022-02-02");
            //4
            int  i = pstm.executeUpdate();//新增的时候需要返回两个值 :1 影响条数  2 主键
            rs= pstm.getGeneratedKeys();// pstm中有一个属性来存储主键 getGeneratedKeys(); 可以将主键值取出来  
            System.out.println(i);
            //5
            if(rs.next()){
    
    
                System.out.println(rs.getInt(1));
            }
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            if (rs!=null){
    
    
                try {
    
    
                    rs.close();
                } catch (SQLException e) {
    
    
                    e.printStackTrace();
                }
            }
            if (pstm!=null){
    
    
                try {
    
    
                    pstm.close();
                } catch (SQLException e) {
    
    
                    e.printStackTrace();
                }
            }
            if (con!=null){
    
    
                try {
    
    
                    con.close();
                } catch (SQLException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }

}

封装的JDBC方法
工具类

/**
 * @author zhangyifan
 * @version 8.0
 * @description: 03测试
 * @date 2021/8/31 10:14
 */
public class JdbcTest03 {
    
    
    public static void main(String[] args) {
    
    
        //1加载驱动
        Jdbc03 baseDao=new Jdbc03();
        //2/获取连接
        Connection conn=null;
        PreparedStatement pstm=null;
        ResultSet rs=null;
        try {
    
    
            conn=baseDao.getConn();
            //3写SQL语句
            String sql="select * from students";
            pstm=conn.prepareStatement(sql);
            //4执行sql语句
            rs=pstm.executeQuery();
            while (rs.next()){
    
    //5处理结果集
                System.out.println(rs.getString(1));
            }
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            baseDao.close(rs,pstm,conn);
        }
        //6释放资源


    }
}
public class Jdbc03 {
    
    
/*
    工具类中的方法都是私有的
    因为工具类中的方法都是是类名直接调用的
    * */
    private  static String user;
    private  static String pwd;
    private static String url;
    private  static String classname;
    static{
    
    // 静态代码块在类加载时只执行一次
        //1加载驱动
        try {
    
    
            Properties m=new Properties();
            InputStream is=Jdbc03.class.getResourceAsStream("db.properties");
            m.load(is);//引进来
            url=(String)m.get("db.url");
            pwd=(String)m.get("db.pwd");
            user=(String)m.get("db.user");
            classname=(String) m.get("db.className");
            Class.forName(classname);
        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    public Connection getConn() throws SQLException {
    
    
        return DriverManager.getConnection(url,user,pwd);
    }
    public void close(ResultSet rs, Statement stm, Connection conn){
    
    
        if(rs!=null){
    
    
            try {
    
    
                rs.close();
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
        if(stm!=null){
    
    
            try {
    
    
                stm.close();
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
        if(conn!=null){
    
    
            try {
    
    
                conn.close();
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
    }
    }
properties文件
db.user=root
db.pwd=04161220
db.url=jdbc:mysql://localhost:3306/kong?&serverTimezone=GMT%2B8&useSSL=false
db.className=com.mysql.cj.jdbc.Driver

封装jdbc增删改查示例

package com.preson.JDBC04;

import com.preson.Jdbc_03lx.JDBC01;

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

/**
 * @author zhangyifan
 * @version 8.0
 * @description: 封装
 * @date 2021/9/2 8:50
 */
public class BaseDao {
    
    
    private  static  String url;
    private  static  String use;
    private  static  String pwd;
    private  static  String classname;
    static {
    
    
        Properties properties=new Properties();//获取对象
        InputStream in=BaseDao.class.getResourceAsStream("db.properties");//获取连接
        try {
    
    
            properties.load(in);//用properties对象获取里面的对象
            //然后一个一个付值
            url= (String) properties.get("db.4url");
            use= (String) properties.get("db.4use");
            pwd= (String) properties.get("db.4pwd");
            classname= (String) properties.get("db.4classname");
            Class.forName(classname);//获取连接
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
    /**
     *
     * @return 连接数据库
     * @throws SQLException
     */
    public static Connection getconn() throws SQLException {
    
    
        return DriverManager.getConnection(url,use,pwd);
    }
    /**
     *
     *增删改查封装
     * @param sql 语句
     * @param objects 添加的对象
     * @return 返回确认成功不
     * @throws SQLException
     */
    public  int execupatd(String sql,Object[] objects) throws SQLException {
    
    
        Connection con = null;
        PreparedStatement prsm = null;
        try {
    
    
            con = this.getconn();//把添加中的两个需要改变的元素取出  sql语句 和 修改的内容
            // String sql="insert into classinfo(classname,begintime,gradeid) values(?,?,?)";
            prsm = con.prepareStatement(sql); //获取预编译sql语句
            //修改的内容可以用数组来创建 吸收
            //   Object[] objects={"aaa","2021-01-02",3};
            if (objects!=null&&objects.length>0) {
    
    
                for (int i = 0; i < objects.length; i++) {
    
    //用遍历数组的方式把?的值循环付上去
                    prsm.setObject(i + 1, objects[i]);
                }
            }
          int i=prsm.executeUpdate();//运行添加  返回的影响条数返回
            return i;
        } catch (SQLException e) {
    
    
            e.printStackTrace();
            throw e;
        } finally {
    
    
            this.close(con, prsm, null);
        }

    }

    /**
     * 查询新增主键
     * @param sql
     * @param objects
     * @return 主键
     * @throws SQLException
     */
    public Object execupatdkey(String sql,Object[] objects) throws SQLException {
    
    
        Connection con=null;
        PreparedStatement prsm=null;
        ResultSet rs=null;
        try {
    
    
            con=this.getconn();
          //  String sql="insert into classinfo(classname,begintime,gradeid) values(?,?,?)";
            prsm=con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
          //  Object[] objects={"aaa","2021-01-02",3};
            for(int i=0;i<objects.length;i++){
    
    
                prsm.setObject(i+1,objects[i]);
            }
            prsm.executeUpdate();
            rs= prsm.getGeneratedKeys();
            if(rs.next()){
    
    
                return rs.getInt(1);
            }
       return -1;
        } catch (SQLException e) {
    
    
            e.printStackTrace();
            throw e;
        }finally {
    
    
            this.close(con,prsm,null);
        }
    }

    /**
     * 查询封装
     * @param sql
     * @param objects
     */
    public void exeQuery(String sql,Object[] objects){
    
    
        Connection con=null;
        PreparedStatement prtm=null;
        ResultSet rs=null;
        try {
    
    
            con=this.getconn();
        } catch (SQLException e) {
    
    
            e.printStackTrace();
        }
        try {
    
    
            prtm=con.prepareStatement(sql);
            if(objects!=null&&objects.length>0) {
    
    //进行判断 判断数组是否为空 如果为空证明没有用?
                for (int i = 0; i < objects.length; i++) {
    
    
                    prtm.setObject(i + 1, objects[i]);
                }
            }
                rs = prtm.executeQuery();
                ResultSetMetaData rsm = rs.getMetaData();
                while (rs.next()) {
    
    
                    for (int i = 1; i < rsm.getColumnCount(); i++) {
    
    
                        System.out.print(rsm.getColumnName(i) + "----" + rs.getObject(i) + "\t");
                    }
                    System.out.println();
                }


        } catch (Exception e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            this.close(con,prtm,rs);
        }
    }
    //kong?&serverTimezone=GMT%2B8&yseSSL=false

    /**
     * 释放资源
     * @param con
     * @param stm
     * @param rs
     */
    public static void close(Connection con, Statement stm, ResultSet rs){
    
    
        if(rs!=null){
    
    
            try {
    
    
                rs.close();
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
        if(stm!=null){
    
    
            try {
    
    
                stm.close();
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
        if(con!=null){
    
    
            try {
    
    
                con.close();
            } catch (SQLException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_45438019/article/details/119966412
du