安卓 App 读 MySQL 多行崩溃? 详解 PHP 与 JSON 常见错误
2025-04-19 08:18:32
安卓 App 获取 MySQL 多行数据就崩溃?揪出 PHP 和 JSON 的那些坑
写安卓 App,从服务器数据库拉数据是家常便饭。但有时候,明明感觉代码没啥大问题,可一请求多条数据,App 就“duang”一下崩了,留下一堆看不懂的错误日志。别急,这事儿不少人都遇到过。咱们今天就来扒一扒,特别是当你的后端用 PHP + MySQL 时,这种诡异的崩溃是咋回事,又该怎么解决。
啥情况?App 一读 MySQL 多行数据就崩了
你可能正在做一个功能,比如根据用户名,从 MySQL 数据库里捞出这个用户相关的所有优惠券信息。你在安卓这边写了个 AsyncTask
,用一个网络请求库(这里看起来像个自定义的 jsonParser
)去访问你的 PHP 接口。
你的 doInBackground
代码大概是这样:
protected String doInBackground(String... args) {
try {
// 构建请求参数
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("name", name)); // 根据 name 查询
// 通过 URL 发起 GET 请求,获取 JSON 字符串
JSONObject json = jsonParser.
makeHttpRequest(url_all_coupons, "GET", params);
// 在 Logcat 里看看返回了啥
Log.d("All Products: ", json.toString()); // 这里可能就是崩溃点
// 检查返回状态码,看看请求成功没
int success = json.getInt(TAG_SUCCESS); // 如果 json 是 null,这里就炸了 (NullPointerException)
if (success == 1) {
// 成功找到数据
// 获取名叫 TAG_COUPONS 的 JSON 数组
coupons = json.getJSONArray(TAG_COUPONS);
// 遍历数组里的每个优惠券对象
for (int i = 0; i < coupons.length(); i++) {
JSONObject c = coupons.getJSONObject(i); // 解析每个 JSON 对象
// 从 JSON 对象里提取具体字段
String couponcreated = c.getString(TAG_COUPONCREATED);
String couponexpires = c.getString(TAG_COUPONEXPIRES);
String coupondetails = c.getString(TAG_COUPONDETAILS);
// ... 后续处理数据 ...
}
} else {
// 处理失败情况,比如没找到数据
}
} catch (JSONException e) {
Log.e("JSON Parser", "Error parsing data " + e.toString()); // JSON 解析出错会被这里捕获
e.printStackTrace();
} catch (Exception e) {
// 其他异常,比如网络问题
Log.e("Network Error", "Error in network operation " + e.toString());
e.printStackTrace();
}
return null; // 返回结果给 onPostExecute
}
然后,你满怀期待地运行 App,结果……崩了!Logcat 刷出一堆红字:
E/JSON Parser﹕ Error parsing data org.json.JSONException: Value <br of type java.lang.String cannot be converted to JSONObject
3961-4003/info.androidhive.loginandregistration E/AndroidRuntime﹕ FATAL EXCEPTION: AsyncTask #1
java.lang.RuntimeException: An error occured while executing doInBackground()
at android.os.AsyncTask$3.done(AsyncTask.java:299)
// ... 省略一堆堆栈跟踪 ...
Caused by: java.lang.NullPointerException
at info.androidhive.loginandregistration.CouponPageActivity$LoadAllCoupons.doInBackground(CouponPageActivity.java:98) // 注意这行,空指针发生在这里
// ... 省略更多堆栈 ...
关键信息有两个:
org.json.JSONException: Value <br of type java.lang.String cannot be converted to JSONObject
: 安卓想把服务器返回的数据当成一个 JSON 对象来解析,结果发现开头是个<br
标签,这根本不是 JSON 格式,解析器直接懵了,抛出JSONException
。java.lang.NullPointerException
(空指针异常): 紧接着 JSON 解析失败后,代码试图在CouponPageActivity.java
的第 98 行(对应上面代码里的json.getInt(TAG_SUCCESS);
)操作一个null
对象。这说明,你的jsonParser.makeHttpRequest
在遇到那个无效的<br
后,可能没能成功构建JSONObject
,而是返回了null
。
所以,根源在于服务器返回的不是纯净的 JSON 数据。
刨根问底:为啥会收到 <br>
而不是 JSON?
既然安卓端收到的是 <br ...
这样的东西,那问题多半出在提供数据的 PHP 脚本那边。咱们来看看你的 PHP 代码片段:
<?php
// ... 省略了数据库连接代码 ...
// 获取从安卓 App 传来的 'name' 参数
$name = $_GET['name']; // 假设你做了必要的清理防注入,这里没显示
// 执行 SQL 查询
// 注意:mysql_* 系列函数已经过时并且不安全,后面会说怎么改
$result = mysql_query("SELECT * FROM coupons WHERE name = '$name'") or die(mysql_error()); // 问题可能在这里!
// 检查查询结果是不是空的
if (mysql_num_rows($result) > 0) {
// 找到数据了,准备构建 JSON
$response["products"] = array(); // PHP 5.4+ 可以用 []
// 循环读取每一行数据
while ($row = mysql_fetch_array($result)) {
// 创建一个临时数组存这一行的数据
$product = array();
$product["couponcreated"] = $row["couponcreated"];
$product["couponexpires"] = $row["couponexpires"];
$product["coupondetails"] = $row["coupondetails"];
// 把这行数据加到最终的 response 数组里
array_push($response["products"], $product);
}
// 设置成功标志
$response["success"] = 1;
// 把 PHP 数组转换成 JSON 字符串并输出
echo json_encode($response);
} else {
// 没找到数据
$response["success"] = 0;
$response["message"] = "No products found"; // 最好加个消息
// 输出 JSON 格式的错误信息
echo json_encode($response);
}
?> // 确保这后面没有多余的空格或字符
瞄准这行:$result = mysql_query("SELECT * FROM coupons WHERE name = '$name'") or die(mysql_error());
这里的 or die(mysql_error())
是个常见的 “懒人” 错误处理方式。它的意思是:如果 mysql_query
执行失败(比如 SQL 语法错了、数据库连接断了、表或字段不存在等),就立刻停止脚本执行 (die
),并且把 mysql_error()
返回的数据库错误信息直接 输出 到页面上。
MySQL 的错误信息有时候会包含 HTML 标签(比如为了在网页上显示得好看些),或者 PHP 配置本身可能会在输出错误信息前加上 <br />
标签。即使不包含 HTML,只要 die()
被触发,并且输出了任何非 JSON 的文本,安卓那边接收到的就不再是纯净的 JSON 数据,解析自然失败,进而导致 JSONException
。
另外,还有几种可能性导致 PHP 输出不干净:
- PHP 警告或通知 (Warnings/Notices): 如果你的 PHP 配置 (
php.ini
里的display_errors
) 设置为On
,并且代码里有一些不影响运行但 PHP 觉得“不规范”的地方(比如使用了未定义的变量,或者像这里用了已废弃 的mysql_*
函数),PHP 可能会直接把这些警告信息输出。这些警告信息通常包含 HTML 标签,比如<br />
或者<b>Warning</b>:
。 echo
或print
调试信息: 开发过程中可能随手加了echo "Debug point 1";
忘了删掉。<?php ... ?>
标签之外的字符: 比如在<?php
之前或?>
之后不小心加了空格、换行或者任何 HTML 代码。
知道了病根,咱们就能对症下药了。
治标更要治本:修复方案来了
解决这个问题,不能头痛医头脚痛医脚,得从 PHP 和安卓两方面入手,并且顺便把潜在的安全风险也处理掉。
方案一:净化 PHP 输出,只给纯净的 JSON
这是最直接的办法,确保你的 PHP 脚本只输出 JSON,其他啥都没有。
原理与作用:
保证 HTTP 响应体就是一串干干净净、格式正确的 JSON 字符串。安卓的 JSON 解析器就能正常工作了。
操作步骤:
-
优雅处理数据库错误,别用
die()
:
把or die(mysql_error())
改掉。改成检查$result
是否为false
,如果失败了,应该记录错误日志(给开发者看),然后给客户端返回一个表示“出错”的 JSON。<?php // ... 连接数据库 ... // 先设置响应头,告诉客户端这是 JSON header('Content-Type: application/json; charset=utf-8'); // 很重要! $response = array(); // 初始化响应数组 $name = $_GET['name']; // 再次提醒:实际项目要严格过滤和清理用户输入! $query = "SELECT * FROM coupons WHERE name = '$name'"; // 拼接 SQL,有风险 $result = mysql_query($query); // 检查查询是否成功 if ($result === false) { // 查询失败了! $response["success"] = 0; $response["message"] = "数据库查询失败,请稍后重试。"; // 给用户的提示 // 【重要】把实际的 MySQL 错误记录到服务器日志里,而不是直接给用户 error_log("MySQL Query Error: " . mysql_error() . " in script " . __FILE__ . " for query: " . $query); } else { // 查询成功,继续处理结果 if (mysql_num_rows($result) > 0) { $response["products"] = array(); while ($row = mysql_fetch_assoc($result)) { // 建议用 mysql_fetch_assoc 获取关联数组 $product = array(); $product["couponcreated"] = $row["couponcreated"]; $product["couponexpires"] = $row["couponexpires"]; $product["coupondetails"] = $row["coupondetails"]; array_push($response["products"], $product); } $response["success"] = 1; } else { // 查询成功,但没找到匹配的数据 $response["success"] = 0; // 可以用 0 代表没找到,或用其他状态码区分 $response["message"] = "未找到名为 '$name' 的优惠券。"; } } // 最终统一输出 JSON echo json_encode($response); // 脚本结束,不要有任何其他输出了 exit; // 确保后面不会意外输出任何东西 ?>
-
关闭或重定向 PHP 错误显示:
- 开发环境: 可以暂时保留错误显示方便调试,但要明白这可能干扰 API 输出。
- 生产环境: 绝对 要禁止向浏览器输出错误信息。修改
php.ini
:
如果你无法修改display_errors = Off log_errors = On error_log = /path/to/your/php_error.log ; 指定日志文件路径
php.ini
,可以在 PHP 脚本开头临时设置:<?php ini_set('display_errors', 0); // 禁止向浏览器输出错误 ini_set('log_errors', 1); // 允许记录错误日志 // ini_set('error_log', '/path/to/log/file'); // 如果需要指定日志文件位置 header('Content-Type: application/json; charset=utf-8'); // 记得设置 Content-Type // ... 你的其他 PHP 代码 ... ?>
- 原理: 让 PHP 错误信息写到服务器本地日志文件里,这样既能排查问题,又不污染给客户端的 JSON 输出。
-
仔细检查代码: 确保 PHP 文件从头到尾,
<?php
标签之前和?>
标签之后,没有任何空格、换行或者 HTML 代码。并且,代码逻辑里没有意外的echo
或print
。
安全建议:
- 这个方案虽然解决了 JSON 格式问题,但 PHP 代码里直接拼接
$name
到 SQL 语句中,存在严重的 SQL 注入 风险!攻击者可以通过构造特殊的name
值(比如' OR '1'='1
)来绕过验证或窃取、篡改数据。这必须在方案二中解决。
方案二:升级到更现代、更安全的数据库交互方式 (PDO 或 MySQLi)
既然都动手改 PHP 代码了,强烈建议把过时的 mysql_*
函数换掉,用 PDO(PHP Data Objects)或者 MySQLi(MySQL Improved Extension)。
原理与作用:
- 安全性: PDO 和 MySQLi 支持预处理语句 (Prepared Statements) ,这是防御 SQL 注入最有效的手段。它把 SQL 命令和用户输入的数据分开处理,数据库本身知道哪些是指令、哪些是数据,从而避免了恶意数据被当成指令执行。
- 功能性: 提供更丰富的数据库操作功能和更好的错误处理机制。
- 维护性:
mysql_*
在 PHP 5.5.0 中被废弃,并在 PHP 7.0.0 中被移除。继续使用意味着你的代码无法运行在较新版本的 PHP 上,也得不到官方维护。
操作步骤与代码示例 (以 PDO 为例):
假设你已经配置好了 PDO 连接数据库 ($pdo
对象)。
<?php
// ... 假设你已经通过 PDO 连接了数据库,得到了 $pdo 对象 ...
// 例如: $dsn = 'mysql:host=localhost;dbname=your_database;charset=utf8mb4';
// $username = 'db_user';
// $password = 'db_password';
// $options = [
// PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 让 PDO 抛出异常,方便捕获
// PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 默认取关联数组
// PDO::ATTR_EMULATE_PREPARES => false, // 禁用模拟预处理,使用真正的预处理
// ];
// try {
// $pdo = new PDO($dsn, $username, $password, $options);
// } catch (\PDOException $e) {
// // 实际项目中应该记录日志,并返回通用错误信息
// throw new \PDOException($e->getMessage(), (int)$e->getCode());
// }
header('Content-Type: application/json; charset=utf-8');
$response = array();
$name = $_GET['name']; // 从请求获取 name
// 【安全核心】使用预处理语句
$sql = "SELECT couponcreated, couponexpires, coupondetails FROM coupons WHERE name = :name"; // 使用命名占位符 :name
try {
// 1. 准备 SQL 语句
$stmt = $pdo->prepare($sql);
// 2. 绑定参数值到占位符 (提供了类型安全)
$stmt->bindParam(':name', $name, PDO::PARAM_STR); // 明确告诉 PDO,这是个字符串
// 3. 执行查询
$stmt->execute();
// 4. 获取所有匹配的行 (PDO::FETCH_ASSOC 使每行是关联数组)
$coupons = $stmt->fetchAll(); // 或者使用 fetch() 逐行处理
if ($coupons) {
// 找到了数据
$response['success'] = 1;
$response['products'] = $coupons; // 直接就是我们想要的数组结构
} else {
// 没找到数据
$response['success'] = 0;
$response['message'] = "未找到名为 '$name' 的优惠券。";
}
} catch (PDOException $e) {
// 捕获数据库操作过程中可能发生的异常
$response['success'] = 0;
$response['message'] = "数据库操作出错,请稍后再试。"; // 对外显示通用错误
// 对内记录详细错误日志
error_log("PDOException in " . __FILE__ . ": " . $e->getMessage() . " for name: " . $name);
}
// 输出 JSON 结果
echo json_encode($response);
exit;
?>
代码示例 (以 MySQLi 面向对象方式为例):
<?php
// ... 假设你已经通过 MySQLi 连接了数据库,得到了 $mysqli 对象 ...
// 例如: $mysqli = new mysqli('localhost', 'db_user', 'db_password', 'your_database');
// if ($mysqli->connect_error) {
// // 记录日志,返回错误
// die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error); // 不推荐直接 die
// }
// $mysqli->set_charset('utf8mb4'); // 设置字符集
header('Content-Type: application/json; charset=utf-8');
$response = array();
$name = $_GET['name'];
// 【安全核心】使用预处理语句
$sql = "SELECT couponcreated, couponexpires, coupondetails FROM coupons WHERE name = ?"; // 使用问号 ? 作为占位符
try {
// 1. 准备 SQL 语句
$stmt = $mysqli->prepare($sql);
if ($stmt === false) {
// 准备失败,通常是 SQL 语法问题
throw new Exception("MySQLi prepare failed: " . $mysqli->error);
}
// 2. 绑定参数值到占位符 (类型 "s" 代表 string)
$stmt->bind_param("s", $name);
// 3. 执行查询
if (!$stmt->execute()) {
// 执行失败
throw new Exception("MySQLi execute failed: " . $stmt->error);
}
// 4. 获取结果
$result = $stmt->get_result(); // 获取结果集对象
if ($result->num_rows > 0) {
$response['success'] = 1;
$response['products'] = $result->fetch_all(MYSQLI_ASSOC); // 一次性获取所有关联数组行
} else {
$response['success'] = 0;
$response['message'] = "未找到名为 '$name' 的优惠券。";
}
// 5. 关闭 statement
$stmt->close();
} catch (Exception $e) {
// 捕获过程中可能发生的异常
$response['success'] = 0;
$response['message'] = "数据库操作出错,请稍后再试。";
// 记录详细错误日志
error_log("MySQLi Exception in " . __FILE__ . ": " . $e->getMessage() . " for name: " . $name);
// 如果 $stmt 已创建但发生错误,也尝试关闭它
if (isset($stmt) && $stmt instanceof mysqli_stmt) {
$stmt->close();
}
}
// 关闭数据库连接(如果是持久连接则不需要)
// $mysqli->close();
// 输出 JSON 结果
echo json_encode($response);
exit;
?>
安全建议:
- 永远使用预处理语句 处理来自用户(或任何不可信来源)的数据。这是防止 SQL 注入的黄金法则。
- 最小权限原则: 数据库用户只授予执行所需操作的最小权限。比如,只读操作就给只读权限。
- 验证和清理输入: 除了预处理,PHP 端还应该对
$name
进行基本的验证(比如检查长度、格式等),防止无效或恶意输入。
进阶使用技巧:
- PDO 错误模式: 设置
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
,让 PDO 在出错时直接抛出PDOException
,可以用try...catch
块来捕获和处理,代码更清晰。 - MySQLi 错误报告: 使用
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
让 MySQLi 在出错时抛出异常,而不是仅返回 false 或警告。 - Fetch Modes: PDO 和 MySQLi 都支持多种数据获取模式(如
PDO::FETCH_ASSOC
,MYSQLI_ASSOC
只获取关联数组;PDO::FETCH_OBJ
,MYSQLI_FETCH_OBJ
获取对象等),选择合适的模式能简化后续代码。 - 事务处理: 对于需要多个数据库操作一起成功或失败的场景(如转账),使用事务(
beginTransaction
,commit
,rollBack
)。
方案三:优化 Android 端的 JSON 解析和错误处理
即使后端 API 设计得很好,安卓端代码也应该有防御性编程,处理可能出现的异常情况。
原理与作用:
让安卓 App 在遇到网络问题、API 返回非预期数据(比如 null、空字符串、格式错误)时,不会直接崩溃,而是能优雅地处理错误,给用户一个友好的提示,或者至少记录下详细日志方便排查。
操作步骤与代码示例 (Java):
修改 doInBackground
方法:
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
// ... 其他 import ...
protected String doInBackground(String... args) {
JSONObject json = null; // 初始化为 null
try {
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("name", name));
// 【改进点】发起请求后,得到 json 对象
json = jsonParser.makeHttpRequest(url_all_coupons, "GET", params);
// 【改进点】非常重要:检查 json 是否为 null
if (json == null) {
Log.e("API Error", "Received null JSON response from server.");
// 这里可以返回一个特定的错误代码或消息给 onPostExecute
return "error_null_response";
}
Log.d("API Response", json.toString()); // 确认收到了什么
// 【改进点】将 JSON 解析操作也包在 try-catch 中,更精细地捕获 JSONException
try {
int success = json.getInt(TAG_SUCCESS);
if (success == 1) {
JSONArray couponsArray = json.getJSONArray(TAG_COUPONS);
for (int i = 0; i < couponsArray.length(); i++) {
JSONObject c = couponsArray.getJSONObject(i);
// 【改进点】对每个 getString 也可以做更细致的检查,比如用 optString
String couponcreated = c.optString(TAG_COUPONCREATED, "N/A"); // 如果 key 不存在或值为 null,返回默认值 "N/A"
String couponexpires = c.optString(TAG_COUPONEXPIRES, ""); // 返回空字符串
String coupondetails = c.getString(TAG_COUPONDETAILS); // 如果确定这个字段必须有,可以用 getString,但需处理可能的 JSONException
// ... 处理数据 ...
}
return "success"; // 操作成功
} else {
// API 返回 success != 1 的情况
String message = json.optString("message", "Unknown server error (success=0)");
Log.w("API Logic Error", "Server response indicates failure: " + message);
return "error_server_logic: " + message; // 返回带消息的错误标识
}
} catch (JSONException jsonEx) {
// 这个 catch 专门处理 JSON 结构解析错误,比如 key 不存在、类型不匹配
Log.e("JSON Parsing Error", "Error parsing JSON fields: " + jsonEx.toString());
// json 对象本身不是 null,但内部结构有问题
return "error_json_structure";
}
} catch (Exception e) {
// 这个 catch 捕获网络请求过程中的异常或其他未预料的错误
// 注意:如果 jsonParser.makeHttpRequest 内部抛出了 JSONException,可能在这里被捕获
Log.e("Background Task Error", "Error during doInBackground: " + e.toString());
e.printStackTrace(); // 打印完整堆栈信息对于调试很重要
return "error_generic_exception";
}
}
进阶使用技巧:
-
使用 Gson 或 Moshi: 手动解析 JSON 容易出错且代码冗长。推荐使用像 Gson 或 Moshi 这样的库。你可以定义一个 Java 或 Kotlin 的数据类 (POJO/Data Class) 来匹配 JSON 结构,然后一行代码就能完成转换,它们还能更好地处理 null 值和类型转换。
// 假设你有 Kotlin Data Class data class Coupon(val couponcreated: String?, val couponexpires: String?, val coupondetails: String?) data class ApiResponse(val success: Int, val products: List<Coupon>?, val message: String?) // 使用 Gson // ... 在 doInBackground ... val responseBody = // 获取服务器返回的 String val gson = Gson() try { val apiResponse = gson.fromJson(responseBody, ApiResponse::class.java) if (apiResponse != null && apiResponse.success == 1 && apiResponse.products != null) { // 使用 apiResponse.products return "success" } else { // 处理错误情况 Log.w("API Error", "Gson parsed response indicates failure or null data: ${apiResponse?.message}") return "error_api_logic" } } catch (e: JsonSyntaxException) { Log.e("Gson Error", "Failed to parse JSON: $responseBody", e) return "error_json_syntax" } catch (e: Exception) { // ... 其他异常处理 ... }
安全建议:
- 虽然主要是健壮性问题,但注意不要在客户端日志或用户界面上暴露过多敏感的错误信息(比如数据库内部错误详情)。服务器应返回通用的错误提示。
举一反三:一些额外的建议
- 日志记录的重要性: 别只依赖 Logcat。服务器端(PHP)记录详细的错误和请求日志至关重要。安卓端也可以考虑集成更专业的日志框架(如 Timber),并可能在用户同意的情况下将关键错误日志上传到分析平台(如 Firebase Crashlytics)。
- API 响应规范: 建立一套统一的 API 响应格式。比如,所有响应都是 JSON 对象,总包含
success
(或status
) 字段和可选的data
字段 (成功时) 或error
字段(包含错误码code
和消息message
,失败时)。这让客户端处理起来更方便、更一致。 - HTTP 状态码: 合理使用 HTTP 状态码。比如,服务器内部错误用 500,请求参数错误用 400,资源未找到用 404,身份验证失败用 401/403。虽然在你的
jsonParser
里可能已经处理了,但这是良好 API 设计的一部分。
解决这类问题往往需要前后端配合。仔细分析错误日志,理解数据流动的每一步,特别是服务器端到底输出了什么,通常就能找到症结所在。别忘了,安全第一,升级老旧代码总是个好主意。