0%

Android/Java连接Mysql数据库

引入

听说其实Android直接连接数据库的情况是比较少的,出于安全和内存都不建议,一般是连接服务器,通过服务器操作数据库。
但毕竟还是有必要掌握这项技能(其实是已经记不清前几天是为什么要写这么个东西了,纠缠几天下来已然混乱)
这几天这是焦头烂额,每一步都会卡一下,网上的博客也是看了不少,没能很直接解决问题(虽然最后的结果也是令我哭笑不得)

可能出现的问题:
JDBC URL中的IP地址或主机名错误
本地DNS服务器无法识别JDBC URL中的主机名
JDBC URL中的端口号丢失或错误
数据库服务器关闭
数据库服务器不接受TCP/IP连接
数据库服务器已用完连接
Java和DB之间的某些东西阻止了连接,例如防火墙或代理

可以尝试以下操作:
测试ip地址能否ping通
刷新DNS或使用JDBC URL中的IP地址
根据MySQL DB的my.cnf进行验证
启动数据库服务
验证是否在没有–skip-networking选项的情况下启动mysqld
重新启动数据库并相应地修改代码,以便最终关闭连接
禁用防火墙和/或配置防火墙/代理以允许/转发端口
注意别忘了给app添加internet权限 <uses-permission android:name="android.permission.INTERNET"/>

以上是在其他地方看到简单总结的一些要点。
然后要开始来讲故事了。。

Mysql的远程登录授权

Mysql需要在允许远程登录的情况下才能被其他主机访问。像在android中就不可能可以本地登录吧,无论是在实体机中还是虚拟机中。

这里顺便记录一下一些虚拟机中主机的IP地址:
Google模拟器
emulator-5554
开发机地址: [net.eth0.gw]: [10.0.2.2]
模拟器本机地址: [net.gprs.local-ip]: [10.0.2.15]
夜神模拟器
127.0.0.1:62001
开发机地址: [dhcp.eth1.gateway]: 172.17.100.2
模拟器本机地址: [dhcp.eth1.ipaddress]: [172.17.100.15]
逍遥模拟器
127.0.0.1:21503
开发机地址: [dhcp.eth1.gateway]: [10.0.3.2]
模拟器本机地址: [dhcp.eth1.ipaddress]: [10.0.3.15]

开放3306端口

查看3306端口情况

1
2
$ netstat -an | grep 3306
tcp6 0 0 :::3306 :::* LISTEN

说明3306只是监听本地,拒绝了其他IP访问,mysql默认状态下是不开放对外访问功能。那么开放端口。
打开etc/mysql/my.cnf(也有人说是/etc/mysql/mysql.conf.d/mysqld.cnf),找到bind-address = 127.0.0.1(大概47行)加 # 注释即可。

  • 其实可能是因为我是tar二进制直接安装的缘故,没有找到mysql.cnf,但后来也是可以连接上emmm
1
2
3
连接成功时3306的状态如下
$ netstat -an | grep 3306
tcp6 0 0 :::3306 :::* ESTABLISHED

MySQL授权

启动mysql.server,进入账户

1
2
3
4
$ sudo ./mysql.server start
Starting MySQL
[ ok ..
$ mysql -uroot -p

然后问题来了。其他IP的访问授权方式有两种,授权法改表法。注意Mysql5和Mysql8的授权方法有所不同,主要是语法上的改变。

授权法

1
2
3
4
5
6
7
grant all on 数据库名.表名 to 'root'@'%' identified by 'password';

# all 代表权限,可以是select,insert,update,delete等一个或多个,all和all privileges代表所有权限
# 数据库.表名 指定具体开放的数据库表,不用加''
# root 是远程登录用的用户名,加不加''都可以
# % 是指定开放的具体IP,%指对所有主机开放,需加'',@两边不要有空格
# password是远程登录时用的密码

以上在Mysql5下使用,也是网上查到的99%的方法。然而在Mysql8中会报语法错误
ERROR 1064 (42000): 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 ‘identified by ‘psw’’ at line 1
这个问题也折腾了我一下午,最后是求助了学长才意识到版本的问题。
Mysql8 下的授权方法如下,没错就是没有identified部分。
而且需要 先创建一个用户,在对其进行授权,否则直接grant会报错 You are not allowed to create a user with GRANT。

1
2
3
4
5
6
7
8
# 先创建用户
create user 'root'@'%' identified by 'pwd';

# 再进行授权
grant all on *.* to 'root'@'%' with grant option;

# 权限刷新
flush privileges;

注意最后 刷新权限

改表法

其实最后我是通过这个方法完成的。

1
2
3
4
5
6
mysql> use mysql;
mysql> update user set host='%' where user='root';
Query OK, 1 row affected (0.63 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> flush privileges;
Query OK, 0 rows affected (0.21 sec)

如上,进入mysql数据库,查看user,host可看到root用户的host是localhost。修改该项为%,刷新权限。
我是使用idea集成的数据库工具测试连接,连接成功。

Android/Java数据库连接

导入数据库驱动JDBC

到官网下载获取JDBC的jar包,常见的版本是mysql-connector-java-5.1.47.jar,较新的版本是mysql-connector-java-8.0.13。(有空了会试着把这两个上传)。
导包方式:复制jar到项目的libs目录下
右键jar选择Add as libs
或者直接在app的build.gradle中添加
implementation files(‘libs/mysql-connector-java-5.1.47.jar’)

主要代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// 动态加载类
Class.forName("com.mysql.jdbc.Driver");
// 设置连接地址以及数据库名称
String DB_URL = "jdbc:mysql://mysql.数据库所在主机IP:3306/表名";
// 连接数据库
Connection conn = DriverManager.getConnection(DB_URL, "用户名", "密码");
// 创建Statement对象(可执行SQL语句的对象)
Statement stmt = conn.createStatement();
// 执行sql语句 返回一个ResultSet对象(返回数据集合)
ResultSet rs = stmt.executeQuery(sql);
// rs调用数据
rs.getString(1); // 遍历索引
rs.getString("name"); // 字段名索引

注:

新建线程

Android不支持在转线程执行网络请求(耗时操作)会有UnsupportedOperationException。
注意包括stmt.executeQuery(str) 方法也是联网操作,也要放在多线程中执行

版本问题

5.0和8.0驱动包有几个地方不同

  • 加载8.0驱动的时候报错

    Loading class 'com.mysql.jdbc.Driver'. This is deprecated. The new driver class is 'com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.

    提示,8.0包的驱动名应该改为 com.mysql.cj.jdbc.Driver

  • 建立连接时报错

    Establishing SSL connection without server’s identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn’t set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to ‘false’. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.

    mysql8.0是不需要建立ssl连接,需要使用useSSL=false手动关闭。即URL改为 jdbc:mysql://mysql.数据库所在主机IP:3306/表名?useSSL=false

  • 8.0驱动包只能兼容minSdkVersion = 26,我的测试机SdkVersion是24的,所以我只能用5.0的驱动包

一些报错

  • serverTimezone=UTC,可以指定时区(非必要)
  • 数据库或表明出错会有报错:No address associated with hostname,Unknown database
  • 用户名或密码错误:Access denied for user (using password: YES)
  • 网络原因(无权限,无网络连接,在主线程执行):com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Could not create connection to database server.
  • 注意在适当位置关闭相关资源流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// java
import java.sql.*;

public class Test {
private final static String JDBC_DRIVER = "com.mysql.jdbc.Driver";
private final static String DB_URL = "jdbc:mysql://127.0.0.1:3306/table";
// 注意修改DB_URL,和UER,PASS
private final static String USER = "root";
private final static String PASS = "123456";

public static void main(String[] args){
ResultSet re;
Connection con;
Statement stmt;
String str = "select * from books";

try{
Class.forName(JDBC_DRIVER);
}catch (ClassNotFoundException e){
System.out.println("Fail to load JDBC dirver.");
e.printStackTrace();
}
try{
con = DriverManager.getConnection(DB_URL, USER, PASS);
stmt = con.createStatement();

re = stmt.executeQuery(str);
while(re.next()){
System.out.println(re.getString(2));
}
con.close();
}catch (SQLException e){
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// android
import java.sql.*;

public class MainActivity extends AppCompatActivity {

private final static String JDBC_DRIVER = "com.mysql.jdbc.Driver";
private final static String DB_URL = "jdbc:mysql://127.0.0.1:3306/table?useSSL=false";

private final static String USER = "root";
private final static String PASS = "123456";

private ResultSet re;
private Connection con;
private Statement stmt;

@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

final String str = "select book_title from books";

new Thread() {
public void run() {
try {
Class.forName(JDBC_DRIVER);
} catch (ClassNotFoundException e) {
Log.e("驱动加载失败", e.toString());
}
try {
con = DriverManager.getConnection(DB_URL, USER, PASS);
stmt = con.createStatement();

re = stmt.executeQuery(str);
if (re != null)
Log.i("标记", "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n");
while (re.next())
Log.i("Data", re.getString(1));

} catch (SQLException e) {
Log.e("连接失败", e.toString());
}
}
}.start();
}
}

我碰到的问题:

以下内容非重点

  • Communications link failure
  • The last packet sent successfully to the server was 0 milliseconds ago.The driver has not received any packets from the server.

这两个报错贯穿了我两天的debug过程,也大概是android连接Mysql最常见的报错
查到的都是超时相关问题,超时回收机制云云。该问题是程序运行过程中使用的连接池不知道连接被回收了所以报出的异常,解决方案大概是 修改连接池配置 或 修改mysql空闲超时时间配置,否则默认是8小时。

但这是数据库连接中断的问题,我连都连不上。这大年初一的,就是熬夜掉头发,想起这一年的漫漫debug之路,忍不住捏了把汗。
我不抱希望修改了如下URL,然后气急败坏的跑去和我妹看电视。

1
jdbc:mysql://localhost:3306/table?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false

半晌,就在我回来继续时候,看到logout上终于打上了数据库的内容,欣喜若狂,扯着一脸问号的妹妹条了半天广场舞。
接下来的事令我不知该哭还是该笑,倒不是程序又不能跑了,是就在我将上面的指令一个一个删去,尝试找到罪归祸首时,发现已经是把代码恢复到原来状态了,app还是十分乖巧的连上了数据库,即jdbc:mysql://ip:3306/table。
我:?????
刚刚到底发生什么了,难道还有打开某个开关以后他就默认开启之类的操作?

无论如何,总算是成功了。老泪纵横。
愿天下码农和八哥终成眷属。。TAT


参考博客


完结 撒花 ฅ>ω<*ฅ