JDBC详解

 JDBC(Java Data Base Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。

   参考:https://www.cnblogs.com/erbing/p/5805727.html

https://blog.csdn.net/shuaicihai/article/details/53416045

2.数据库驱动

  我们安装好数据库之后,我们的应用程序也是不能直接使用数据库的,必须要通过相应的数据库驱动程序,通过驱动程序去和数据库打交道。其实也就是数据库厂商的JDBC接口实现,即对Connection等接口的实现类的jar文件。

 

1. 数据持久化

持久化(persistence):对象在内存中创建后,不能永久存在。把对象永久的保存起来就是持久化的过程。而持久化的实现过程大多通过各种关系数据库来完成。

持久化的主要应用是将内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、XML数据文件中。

2. 在java中数据库存储技术分类

JDBC直接访问数据库。

第三方O/R工具,如Hibernate,mybatis。这些工具都是对JDBC的封装。 

3. JDBC概念

JDBC(Java Datebase Connectivity)是一个独立于特定数据库管理系统、通用的sql数据库存取和操作的公共接口。它是JAVA语言访问数据库的一种标准。 

4. JDBC常用(重要)类/接口

Java.sql.Driver接口是所有JDBC驱动程序需要实现的接口。这个接口是提供给数据库厂商使用的,不同数据库厂商提供不用的实现。

在程序中不需要直接去访问实现了Driver接口的类,而是由驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现。

DriverManager类,用来创建连接,它本身就是一个创建Connection的工厂,设计的时候使用的就是Factory模式,给各数据库厂商提供接口,各数据库厂商需要实现它;

Connection接口,根据提供的不同驱动产生不同的连接;

Statement接口,用来发送SQL语句;

Resultset接口,用来接收查询语句返回的查询结果。 

5. JDBC应用步骤

1.注册加载一个驱动 
2.创建数据库连接(Connection) 
3.创建statement,发送sql语句 
4.执行sql语句 
5.处理sql结果 
6.关闭statement和connection 

二、加载与注册驱动

加载 JDBC 驱动需调用 Class 类的静态方法 forName(),向其传递要加载的 JDBC 驱动的类名: 

1
Class.forName(driver);

如: 
注册MYSQL数据库驱动器 

1
Class.forName("com.mysql.jdbc.Driver");

注册ORACLE数据库驱动器 

1
Class.forName("oracle.jdbc.driver.OracleDriver");

三、建立连接

可以调用 DriverManager 类的 getConnection(…….) 方法建立到数据库的连接: 

1
Connection conn = DriverManager.getConnection(url,uid,pwd);

JDBC URL 用于标识一个被注册的驱动程序,驱动程序管理器通过这个 URL 选择正确的驱动程序,从而建立到数据库的连接。

JDBC URL的标准由三部分组成,各部分间用冒号“:”分隔。 

JDBC URL格式: 

1
协议:<子协议>:<子名称>

说明: 
协议:JDBC URL中的协议总是jdbc  
子协议:子协议用于标识一个数据库驱动程序 
子名称:一种标识数据库的方法。子名称可以依不同的子协议而变化,用子名称的目的是为了定位数据库提供足够的信息  

jdbc:<子协议>:<子名称>:是一个JNI方式的命名  
注:JNI是Java Native Interface的缩写。从Java 1.1开始,Java Native Interface (JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。 

如: 
mysql的JDBC URL: jdbc:mysql://localhost:3306/mydbname  
oracle的JDBC URL: jdbc:oracle:thin: @localhost :1521:mydbname 

四、访问数据库

数据库连接被用于向数据库服务器发送命令和 SQL 语句,在连接建立后,需要对数据库进行访问,执行 sql 语句。 
在 java.sql 包中有 3 个接口分别定义了对数据库的调用的不同方式: 
Statement 
    PrepatedStatement 
        CallableStatement 

1. 用Statement来执行sql语句

Statement对象用于执行静态的 SQL 语句,并且返回执行结果。

通过调用 Connection 对象的 createStatement 方法创建该对象:

1
Statement sm = conn.createStatement();

Statement 接口中定义了下列方法用于执行 SQL 语句:

sm.executeQuery(sql); // 执行数据查询语句(select) 
sm.executeUpdate(sql); // 执行数据更新语句(delete、update、insert、drop等) 

2. 用PreparedStatement来执行sql语句

PreparedStatement 接口是 Statement 的子接口,它表示一条预编译过的 SQL 语句。

可以通过调用 Connection 对象的 preparedStatement() 方法获取 PreparedStatement 对象:

1
2
3
4
5
6
7
String  sql  = "INSERT INTO user (id,name) VALUES (?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, 1);
ps.setString(2, "admin");
 
ResultSet rs = ps.executeQuery(); // 查询
int c = ps.executeUpdate(); // 更新

PreparedStatement 对象所代表的 SQL 语句中的参数用问号(?)来表示,调用 PreparedStatement 对象的 setXXX() 方法来设置这些参数。 setXXX() 方法有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引(从 1 开始),第二个是设置的 SQL 语句中的参数的值。

3. PreparedStatement与Statement比较

(1)使用PreparedStatement,代码的可读性和可维护性比Statement高。

(2)PreparedStatement 能最大可能提高性能。 
DBServer会对预编译语句提供性能优化。因为预编译语句有可能被重复调用,所以语句在被DBServer的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中就会得到执行。 

在statement语句中,即使是相同操作但因为数据内容不一样,所以整个语句本身不能匹配,没有缓存语句的意义。事实是没有数据库会对普通语句编译后的执行代码缓存。这样每执行一次都要对传入的语句编译一次。 

(3)PreparedStatement能保证安全性,但 Statement有sql注入等安全问题。 

4. Statement不安全性

SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令,从而利用系统的 SQL 引擎完成恶意行为的做法。如下代码:

1
2
3
String username="a' or 1=1 or 1='";
String psw="b";
String sql = "select count(*) from t_user where username='"+username+"' and psw='"+psw+"'";

sql语句如下: 

1
select count(*) from t_user where username='a' or 1=1 or 1='' and psw='b'

用 PreparedStatement 取代 Statement 就可以解决。 

5. 使用Callable Statement

当不直接使用SQL语句,而是调用数据库中的存储过程时,要用到Callable Statement。

CallabelStatement从PreparedStatement继承。

例如: 

1
2
3
4
5
6
7
String sql = "{call insert_users(?,?)}";
// 调用存储过程
CallableStatement st = conn.prepareCall(sql);
st.setInt(1, 1);
st.setString(2, "admin");
// 在此 CallableStatement对象中执行 SQL 语句,该语句可以是任何种类的 SQL 语句。
st.execute();

五、处理执行结果

查询语句,返回记录集ResultSet。 
更新语句,返回数字,表示该更新影响的记录数。 

ResultSet: 
ResultSet 对象以逻辑表格的形式封装了执行数据库操作的结果集,ResultSet 接口由数据库厂商实现。 

ResultSet 接口的常用方法: 
next():将游标往后移动一行,如果成功返回true;否则返回false。ResultSet 对象维护了一个指向当前数据行的游标,初始的时候,游标在第一行之前,可以通过 ResultSet 对象的 next() 方法移动到下一行。 
getXxx(String name):返回当前游标下某个字段的值。如:getInt("id")或getSting("name")。 

六、释放数据库连接

rs.close();

ps.close(); 或者 stat.close();

conn.close();

一般是在finally里面进行释放资源。 

七、数据库事务

1. 概述

在数据库中,所谓事务是指一组逻辑操作单元,使数据从一种状态变换到另一种状态。

为确保数据库中数据的一致性,数据的操纵应当是离散的成组的逻辑单元:当它全部完成时,数据的一致性可以保持,而当这个单元中的一部分操作失败,整个事务应全部视为错误,所有从起始点以后的操作应全部回退到开始状态。 

事务的操作:先定义开始一个事务,然后对数据作修改操作,这时如果提交(COMMIT),这些修改就永久地保存下来,如果回退(ROLLBACK),数据库管理系统将放弃您所作的所有修改而回到开始事务时的状态。 

2. 事务的ACID属性

2.1 原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。 

2.2 一致性(Consistency)

事务必须使数据库从一个一致性状态变换到另外一个一致性状态。(数据不被破坏)

2.3 隔离性(Isolation)

事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

2.4 持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。 

3. JDBC 事务处理

在JDBC中,事务默认是自动提交的,每次执行一个 SQL 语句时,如果执行成功,就会向数据库自动提交,而不能回滚。

为了让多个 SQL 语句作为一个事务执行,需调用 Connection 对象的 setAutoCommit(false); 以取消自动提交事务:

1
conn.setAutoCommit(false);

在所有的 SQL 语句都成功执行后,调用 commit(); 方法提交事务 

1
conn.commit();

在出现异常时,调用 rollback(); 方法回滚事务,一般再catch模块中执行回滚操作。 

1
conn.rollback();

可以通过Connection的getAutoCommit()方法来获得当前事务的提交方式。

注意:在MySQL中的数据库存储引擎InnoDB支持事务,MyISAM不支持事务。 

八、批量处理JDBC语句

1. 概述

当需要批量插入或者更新记录时。可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更有效率。

JDBC的批量处理语句包括下面两个方法: 
addBatch(String):添加需要批量处理的SQL语句或是参数; 
executeBatch();执行批量处理语句; 

通常我们会遇到两种批量执行SQL语句的情况: 
多条SQL语句的批量处理; 
一个SQL语句的批量传参;  

2. Statement批量处理

1
2
3
4
5
6
7
8
Statement sm = conn.createStatement();
sm.addBatch(sql1);
sm.addBatch(sql2);
...
//批量处理
sm.executeBatch()
//清除sm中积攒的参数列表
sm.clearBatch();

3. PreparedStatement批量传参

1
2
3
4
5
6
7
8
9
10
11
12
13
PreparedStatement ps = conn.preparedStatement(sql);
for(int i=1;i<100000;i++){
    ps.setInt(1, i);
    ps.setString(2, "name"+i);
    ps.setString(3, "email"+i);
    ps.addBatch();
    if((i+1)%1000==0){
        //批量处理
        ps.executeBatch();
        //清空ps中积攒的sql
        ps.clearBatch();
    }
}

注意:MySQL不支持批量处理。

批量处理应该设置一个上限,当批量处理列表中的sql累积到一定数量后,就应该执行,并在执行完成后,清空批量列表。

一般在excel导入数据的时候会用到批处理。 

九、使用 JDBC 处理元数据 

1. 概述

Java 通过JDBC获得连接以后,得到一个Connection 对象,可以从这个对象获得有关数据库管理系统的各种信息,包括数据库中的各个表,表中的各个列,数据类型,触发器,存储过程等各方面的信息。根据这些信息,JDBC可以访问一个实现事先并不了解的数据库。

获取这些信息的方法都是在DatabaseMetaData类的对象上实现的,而DataBaseMetaData对象是在Connection对象上获得的。 

2. 获取数据库元数据 

DatabaseMetaData 类中提供了许多方法用于获得数据源的各种信息,通过这些方法可以非常详细的了解数据库的信息:

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

3. ResultSetMetaData

可用于获取关于 ResultSet 对象中列的类型和属性信息的对象:

getColumnName(int column):获取指定列的名称 
getColumnCount():返回当前 ResultSet 对象中的列数。  
getColumnTypeName(int column):检索指定列的数据库特定的类型名称。  
getColumnDisplaySize(int column):指示指定列的最大标准宽度,以字符为单位。  
isNullable(int column):指示指定列中的值是否可以为 null。  
isAutoIncrement(int column):指示是否自动为指定列进行编号,这样这些列仍然是只读的。 

十、创建可滚动、更新的记录集

1. Statement

1
Statement stmt = conn.createStatement(type,concurrency);

2. PreparedStatement

1
PreparedStatement stmt = conn.prepareStatement(sql,type,concurrency);

type说明:

ResultSet的Type 说明
TYPE_FORWARD_ONLY 结果集不能滚动,只可向前滚动
TYPE_SCROLL_INSENSITIVE 双向滚动,但不及时更新,就是如果数据库里的数据修改过,并不在ResultSet中反应出来
TYPE_SCROLL_SENSITIVE 双向滚动,并及时跟踪数据库的更新,以便更改ResultSet中的数据


Concurrency(并发类型)说明:

ResultSet的Concurrency(并发类型) 说明
CONCUR_READ_ONLY 结果集不可用于更新数据库
CONCUR_UPDATABLE 结果集可以用于更新数据库

3. ResultSet滚动的结果集使用

First:将指针移动到此 ResultSet 对象的第一行  
Last:将指针移动到此 ResultSet 对象的最后一行  
beforeFirst:将指针移动到此 ResultSet 对象的开头,正好位于第一行之前  
afterLast:将指针移动到此 ResultSet 对象的末尾,正好位于最后一行之后  
isFirst:检索指针是否位于此 ResultSet 对象的第一行  
isLast:检索指针是否位于此 ResultSet 对象的最后一行  
isBeforeFirst:检索指针是否位于此 ResultSet 对象的第一行之前  
isAfterLast:检索指针是否位于此 ResultSet 对象的最后一行之后  
Relative:按相对行数(或正或负)移动指针  
Next:将指针从当前位置下移一行  
Previous:将指针移动到此 ResultSet 对象的上一行  
Absolute:将指针移动到此 ResultSet 对象的给定行编号  

如: 

rs.absolute(80); //将指针移动到ResultSet 对象的第80行记录。

注意:该特性对Oralce数据有效。但是在Mysql数据库中无效,Mysql只支持TYPE_SCROLL_INSENSITIVE,CONCUR_READ_ONLY。 

十一、JDBC连接池

1. 为什么要使用JDBC连接池

普通的JDBC数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到内存中,再验证用户名和密码。需要数据库连接的时候,就向数据库要求一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间。数据库的连接资源并没有得到很好的重复利用.若同时有几百人甚至几千人在线,频繁的进行数据库连接操作将占用很多的系统资源,严重的甚至会造成服务器的崩溃。

对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将导致重启数据库。

这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。

为解决传统开发中的数据库连接问题,可以采用数据库连接池技术。

2. 数据库连接池(connection pool)

数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。

数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。

数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。 

3. 数据库连接池工作原理

 

4. 使用数据库连接池的优点

(1)资源重用: 

由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增加了系统运行环境的平稳性。

(2)更快的系统反应速度 

数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统的响应时间。

(3)新的资源分配手段 

对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源。

(4)统一的连接管理,避免数据库连接泄露 
在较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄露。 

5. 常用数据库连接池介绍

JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现,如: 
DBCP 数据库连接池 
C3P0 数据库连接池 
Proxpool 数据库连接池 

其中,DBCP和C3P0用得比较多。

Tomcat 在 7.0 以前的版本都是使用 commons-dbcp 做为连接池的实现。

数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。

当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但它并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。 

 

----------------------------------我是代码分割线---------------------------------------------

一个使用JDBC的小Demo

复制代码
 1 import java.sql.Connection;
 2 import java.sql.DriverManager;
 3 import java.sql.ResultSet;
 4 import java.sql.SQLException;
 5 import java.sql.Statement;
 6 
 7 
 8 /**
 9  * 使用JDBC连接数据库
10  * @author GXF
11  *
12  */
13 public class TestJdbc {
14 
15     public static void main(String[] args) {
16         TestJdbc testJdbc = new TestJdbc();
17         testJdbc.testJdbc();
18     }
19     
20     /**
21      * 连接MySQL数据库
22      * test数据库
23      * usertable表
24      */
25     public void testJdbc(){
26         //加载驱动->通过url得到数据库连接对象->获取statement或者prestatment对象->执行SQL语句->处理结果集->关闭连接,释放资源
27         try {
28             String userName = "root";
29             String passwd = "";
30             String dbName = "test";
31             //加载驱动
32             Class.forName("com.mysql.jdbc.Driver");
33             //获取连接对象
34             String url = "jdbc:mysql://localhost:3306/" + dbName;
35             Connection con = DriverManager.getConnection(url, userName, passwd);
36             //创建statement
37             Statement st = con.createStatement();
38             //执行SQL,查询表中所有数据
39             String sql = "select * from usertable";
40             ResultSet rs = st.executeQuery(sql);
41 
42             //处理结果集
43             while(rs.next()){                
44                 System.out.println(rs.getString("username") + " " + rs.getString("passwd"));
45             }//while
46             
47             //关闭连接,释放资源
48             rs.close();
49             st.close();
50             con.close();
51             
52         } catch (ClassNotFoundException e) {
53 
54             e.printStackTrace();
55         } catch (SQLException e) {
56 
57             e.printStackTrace();
58         }
59     }
60 
61 }

 

 

三、使用JDBC的步骤

  加载JDBC驱动程序 → 建立数据库连接Connection → 创建执行SQL的语句Statement → 处理执行结果ResultSet → 释放资源

1.注册驱动 (只做一次)

  方式一:Class.forName(“com.MySQL.jdbc.Driver”);
  推荐这种方式,不会对具体的驱动类产生依赖。
  方式二:DriverManager.registerDriver(com.mysql.jdbc.Driver);
  会造成DriverManager中产生两个一样的驱动,并会对具体的驱动类产生依赖。

2.建立连接

 Connection conn = DriverManager.getConnection(url, user, password); 

  URL用于标识数据库的位置,通过URL地址告诉JDBC程序连接哪个数据库,URL的写法为:

  

  其他参数如:useUnicode=true&characterEncoding=utf8

 

3.创建执行SQL语句的statement

复制代码
1 //Statement  
2 String id = "5";
3 String sql = "delete from table where id=" +  id;
4 Statement st = conn.createStatement();  
5 st.executeQuery(sql);  
6 //存在sql注入的危险
7 //如果用户传入的id为“5 or 1=1”,那么将删除表中的所有记录
复制代码

 

复制代码
1  //PreparedStatement 有效的防止sql注入(SQL语句在程序运行前已经进行了预编译,当运行时动态地把参数传给PreprareStatement时,即使参数里有敏感字符如 or '1=1'也数据库会作为一个参数一个字段的属性值来处理而不会作为一个SQL指令)
2 String sql = “insert into user (name,pwd) values(?,?)”;  
3 PreparedStatement ps = conn.preparedStatement(sql);  
4 ps.setString(1, “col_value”);  //占位符顺序从1开始
5 ps.setString(2, “123456”); //也可以使用setObject
6 ps.executeQuery(); 
复制代码

4.处理执行结果(ResultSet)

复制代码
1 ResultSet rs = ps.executeQuery();  
2 While(rs.next()){  
3     rs.getString(“col_name”);  
4     rs.getInt(1);  
5     //…
6 }  
复制代码

5.释放资源

复制代码
 //数据库连接(Connection)非常耗资源,尽量晚创建,尽量早的释放
//都要加try catch 以防前面关闭出错,后面的就不执行了
1 try { 2 if (rs != null) { 3 rs.close(); 4 } 5 } catch (SQLException e) { 6 e.printStackTrace(); 7 } finally { 8 try { 9 if (st != null) { 10 st.close(); 11 } 12 } catch (SQLException e) { 13 e.printStackTrace(); 14 } finally { 15 try { 16 if (conn != null) { 17 conn.close(); 18 } 19 } catch (SQLException e) { 20 e.printStackTrace(); 21 } 22 } 23 }

1.批处理Batch

 插入2万条数据的测试

2.测试事务的基本概念和用法

 测试事务的基本概念和用法
 控制台输出

五、时间处理(Date和Time以及Timestamp区别、随机日期生成

java.util.Date

  • 子类:java.sql.Date
  • 子类:java.sql.Time
  • 子类:java.sql.Timestamp

 

 测试时间处理(java.sql.Date,Time,Timestamp)
 测试时间处理(java.sql.Date,Time,Timestamp),取出指定时间段的数据

六、CLOB文本大对象操作

 

 测试CLOB 文本大对象的使用

七、BLOB二进制大对象的使用

 

 测试BLOB二进制大对象的使用

 八、总结(简单封装、资源文件properties处理连接信息

复制代码
 1 package com.test.jdbc;
 2 
 3 import java.sql.Connection;
 4 import java.sql.PreparedStatement;
 5 import java.sql.ResultSet;
 6 
 7 /**
 8  * 测试使用JDBCUtil工具类来简化JDBC开发
 9  */
10 public class Demo11 {
11     public static void main(String[] args) {
12         Connection conn = null;
13         PreparedStatement ps = null;
14         ResultSet rs = null;
15 
16         try {
17             conn = JDBCUtil.getMysqlConn();
18 
19             ps = conn.prepareStatement("insert into t_user (userName) values (?)");
20             ps.setString(1, "小高高");
21             ps.execute();
22 
23         } catch (Exception e) {
24             e.printStackTrace();
25         } finally{
26             JDBCUtil.close(rs, ps, conn);
27         }
28     }
29 }
复制代码

https://www.cnblogs.com/luckygxf/p/4687013.html

posted @ 2019-04-25 22:42  konglingbin  阅读(1390)  评论(0编辑  收藏  举报