Android 网络编程入门到实战:HttpURLConnection、JSON 处理、OkHttp 与 Retrofit2
前言
这一篇文章把 Android 里最常见的一条网络请求学习路径串起来:先用 HttpURLConnection 手动完成 GET、POST,再用 JSONObject 和 Gson 处理 JSON,接着切到 OkHttp 和 Retrofit2,把请求构造、回调处理和结果解析一步步收拢。
整条链路都围绕同一组接口展开:查询用户信息时使用 GET,登录时使用 POST。这样做的好处是,前后几种方案处理的是同一类业务目标,读起来能更清楚地看到“底层做了什么”和“高层库帮我们省掉了什么”。

目录
- Android 网络请求入门实战:从 HttpURLConnection、JSONObject、Gson 到 OkHttp 与 Retrofit2
- 1. 前言
- 2. 在真正写请求代码前,先把协议和 HTTP 链路看清楚
- 3. 在 Android 中发起网络请求前的准备
- 4. 使用 HttpURLConnection 发起第一个 GET 和 POST 请求
- 5. 先把 JSON 结构摸清,再用 JSONObject 手动读写数据
- 6. 使用 Gson 让 JSON 和实体类直接互转
- 7. 使用 OkHttp 简化请求发送与结果处理
- 8. 使用 Retrofit2 把接口定义、请求参数和结果解析收拢到一起
- 9. 下载文件的几种常见写法
- 10. 上传文件的几种常见写法
- 11. 相关代码附录
1. 前言
这一篇文章把 Android 里最常见的一条网络请求学习路径串起来:先用 HttpURLConnection 手动完成 GET、POST,再用 JSONObject 和 Gson 处理 JSON,接着切到 OkHttp 和 Retrofit2,把请求构造、回调处理和结果解析一步步收拢。
整条链路都围绕同一组接口展开:查询用户信息时使用 GET,登录时使用 POST。这样做的好处是,前后几种方案处理的是同一类业务目标,读起来能更清楚地看到“底层做了什么”和“高层库帮我们省掉了什么”。
2. 在真正写请求代码前,先把协议和 HTTP 链路看清楚
2.1 网络通信协议为什么重要
网络请求表面上只是“发一个地址,拿一段结果”,但客户端和服务端真正能把数据收发起来,依赖的是一整套协议约定。协议负责约束数据格式、传输规则、错误检测、连接建立与关闭方式,所以它不是背景知识,而是解释请求为何能成功、为何会失败、参数为何这样组织的底层依据。
从 Android 开发的角度看,只要开始写 HttpURLConnection、OkHttp 或 Retrofit2,本质上就是在用应用层协议去驱动更底层的传输链路。只有先知道链路里每一层负责什么,后面看 URL、请求头、请求体、状态码、JSON 响应这些对象时,才不会停留在“会调用 API”这一层。
2.2 协议的分层模型
网络协议不会把所有职责都堆在一层里,而是采用分层方式组织。学习 Android 网络请求时,最常见的两个模型是 OSI 模型和 TCP/IP 模型。

这张图的作用不是要求把所有层的定义全部背下来,而是帮助建立一个判断:Android 代码里直接接触最多的是应用层对象,例如 HTTP、JSON、接口地址和请求参数;但这些内容能送到服务器,还要依赖更下层的传输、寻址和物理承载能力。
2.2.1 OSI 模型
OSI 模型把网络通信拆成七层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。它更像一张“理论地图”,用于解释网络世界的职责边界。
对移动开发来说,这个模型的价值主要体现在两个地方:
- 能帮助理解为什么
HTTP不负责连接可靠性,而TCP要负责。 - 能帮助理解为什么
JSON、编码、加密、页面请求这些对象,看起来都在一次请求里出现,但实际属于不同层面的职责。
2.2.2 TCP/IP 模型
实际开发中更常用的是 TCP/IP 四层模型:网络接口层、网络层、传输层、应用层。Android 网络请求真正跑起来,更多是沿着这套模型去理解。
其中和本篇最相关的是两层:
- 传输层:决定数据怎么可靠或不可靠地送到对端,核心协议是
TCP和UDP。 - 应用层:决定客户端和服务端如何组织请求与响应,核心协议是
HTTP、HTTPS、FTP、SMTP等。
也就是说,后面写 GET、POST、请求头、请求体时,处理的是应用层语义;而一次 HTTP 请求最终能否可靠送达,背后通常由 TCP 去承担传输保证。
2.3 TCP、UDP、HTTP、HTTPS 在请求链路里的分工
HTTP 和 HTTPS 是应用层协议,描述的是“怎么请求资源、怎么返回结果”;TCP 和 UDP 是传输层协议,描述的是“数据怎么从一端送到另一端”。这几个协议既不是同一层,也不是互相替代关系。
移动端最常见的业务请求,通常是“应用层用 HTTP 或 HTTPS 组织数据,传输层由 TCP 负责可靠传输”。因此,学习网络请求时把 TCP 连接过程放在前面,是为了理解为什么一次 HTTP 请求不是凭空发出去的。
2.3.1 TCP 的三次握手
TCP 建立连接时,客户端和服务端会经历三次握手,核心目标是确认双方都具备收发能力,并同步初始序列号。

这个过程可以按下面的顺序理解:
- 客户端发送带
SYN标志的数据包,表示“我想建立连接”。 - 服务端返回
SYN + ACK,表示“我收到了,也同意建立连接”。 - 客户端再发送
ACK,表示“我确认你的响应,连接正式建立”。
理解这个过程后,再去看移动端请求超时、连接失败、服务端未响应这些问题,就不会只停留在“接口地址写错了”这一层,而是会知道连接本身也可能出问题。
2.3.2 TCP 的四次挥手
连接关闭时,TCP 会经历四次挥手,用来确保双方都把剩余数据处理完,再释放资源。

它的顺序是:
- 一方发送
FIN,表示自己没有更多数据要发。 - 对端返回
ACK,先确认收到这个关闭意图。 - 对端确认自己也发完数据后,再发送
FIN。 - 发起方最后回一个
ACK,连接关闭完成。
这也是为什么网络请求结束后,不应该一直占着连接和流对象不释放。后面在 HttpURLConnection 里看到 disconnect(),本质上就是要求代码把这条链路收干净。
2.3.3 UDP 的特点
UDP 同样属于传输层,但它和 TCP 完全是两种设计取向。UDP 无连接、不保证可靠、不保证顺序,优点是开销小、速度快、延迟低。
因此它更适合实时语音、视频、游戏同步这类场景。本文演示的接口查询和登录请求都要求结果可靠返回,所以选择的是建立在 TCP 语义之上的 HTTP 请求链路,而不是 UDP。
2.4 HTTP 的基本通信流程与请求方法
一次典型的 HTTP 通信流程非常固定:
- 客户端构建请求。
- 服务器接收并处理请求。
- 服务器返回响应。
- 客户端解析响应并更新界面或状态。
本文里会用到最常见的两种请求方法:
GET:用于获取资源。参数通常拼到URL查询串里,例如user_id=2&type=archives。POST:用于提交数据。常见做法是把JSON放进请求体,例如登录接口把账号和密码作为请求体提交给服务端。
之所以把 GET 和 POST 拆开讲,是因为它们不仅方法名不同,参数放置位置、请求体是否存在、代码构造方式也都不同。
2.5 HTTP 请求和响应到底由什么组成
写 Android 网络请求时,最容易混淆的是“参数到底该写在哪”。这一点只要把 HTTP 请求和响应结构拆开看,就会清楚很多。
请求结构通常包含三部分:
- 请求行:包含方法、路径、协议版本,例如
GET /addons/cms/api.user/userInfo HTTP/1.1。 - 请求头:描述请求附加信息,例如
Content-Type、Accept、Authorization。 - 请求体:真正要提交的数据。
GET往往没有请求体,POST则经常把JSON或表单数据放在这里。
例如登录接口提交的请求体,可以组织成这样一段 JSON:
{
"account": "lsxbug",
"password": "123456"
}
响应结构也分成三部分:
- 响应行:包含协议版本、状态码和状态描述。
- 响应头:描述返回内容属性,例如
Content-Type、Content-Length。 - 响应体:真正的业务数据,常见格式是
JSON。
本文后面会频繁处理下面这类登录响应:
{
"code": 1,
"msg": "登录成功",
"time": "1724677422",
"data": {
"token": "8583c6c7-a6c3-4d56-90e5-180712ffbb81",
"user_id": 4
}
}
这里的 code、msg、time 和 data 都位于响应体里。后面无论用 JSONObject、Gson、OkHttp 还是 Retrofit2,本质上都绕不开“把这段响应体读出来,再转成代码里可操作的数据结构”这件事。
3. 在 Android 中发起网络请求前的准备
在 Android 中常见的请求方案有 HttpURLConnection、OkHttp、Retrofit。这一篇会从最底层一路往上走,不过在正式发请求之前,先要把权限、明文访问和测试接口准备好,否则后面的代码即使语法正确,也跑不起来。
3.1 添加网络权限
如果应用连网络访问权限都没有,任何请求代码都不会真正发出去。因此第一步先在 NetworkByJavaProject/app/src/main/AndroidManifest.xml 里声明 INTERNET 权限:
<uses-permission android:name="android.permission.INTERNET" />
这条权限的作用很单纯,就是告诉系统当前应用需要访问网络。没有它,后面不管是 HttpURLConnection、OkHttp 还是 Retrofit2,都没有发起外部请求的前提。
3.2 usesCleartextTraffic 允许明文传输
项目里访问的测试接口使用的是 http://titok.fzqq.fun/,也就是明文 HTTP。为了让应用能直接访问这类地址,可以先在 application 节点上打开 android:usesCleartextTraffic:
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NetworkByJavaProject"
android:usesCleartextTraffic="true"
tools:targetApi="31">
这里最关键的是 android:usesCleartextTraffic="true"。它表示当前应用允许进行明文 HTTP 通信。因为本文演示接口不是 HTTPS,所以如果不打开这项配置,请求在 Android 9.0 及以上设备上很容易直接被系统拦掉。
3.3 配置网络安全策略(Android 9.0 及以上)
如果不想全局放开明文流量,也可以针对指定域名做网络安全策略配置。在 AndroidManifest.xml 中增加 android:networkSecurityConfig:
<application
android:networkSecurityConfig="@xml/network_security_config">
</application>
然后新建 res/xml/network_security_config.xml,只允许目标域名走明文流量:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">titok.fzqq.fun</domain>
</domain-config>
</network-security-config>
相比直接打开全局 usesCleartextTraffic,这种方式控制更细。它把“允许明文访问”限制在指定域名上,更适合真实项目里逐步收紧网络安全边界。
3.4 准备好网络接口
整篇示例围绕两类接口展开:
- 用户信息查询接口,用
GET,参数里带user_id和type。 - 登录接口,用
POST,请求体里带account和password。
对应的测试文档入口为:
http://titok.fzqq.fun
有了这两类接口,后面的所有实现都能围绕同一组业务对象展开,从而更容易看清不同请求库之间的差异。
4. 使用 HttpURLConnection 发起第一个 GET 和 POST 请求
HttpURLConnection 是 Android 原生内置的网络请求方式。它的特点不是“写起来最省事”,而是“网络链路中的每一个环节都要自己处理”。正因为如此,用它打底特别适合建立请求全过程的直觉。
4.1 前置代码:先把页面和点击事件准备好
先准备页面,让 GET 和 POST 两类请求都能从同一个页面触发。布局里放了一个 user_id 输入框、一组登录账号密码输入框,以及两个按钮:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".activity.HttpUrlConnectionActivity">
<TextView
android:id="@+id/tv_get_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="GET请求:"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_user_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入要查询的用户id"
android:inputType="number"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_get_label" />
<Button
android:id="@+id/btn_get"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发起GET请求"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_user_id" />
<TextView
android:id="@+id/tv_post_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:text="POST 请求:"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_get" />
<EditText
android:id="@+id/et_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入用户名"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_post_label" />
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入密码"
android:inputType="textPassword"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_user_name" />
<Button
android:id="@+id/btn_post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发起POST请求"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_password" />
</androidx.constraintlayout.widget.ConstraintLayout>
这套布局的作用很明确:上半部分负责 GET 查询,下半部分负责 POST 登录。这样两种请求方式虽然代码不同,但输入来源和交互触发方式是一致的。
页面逻辑也先接好,只负责分发点击事件:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_http_url_connection);
findViewById(R.id.btn_get).setOnClickListener(this);
findViewById(R.id.btn_post).setOnClickListener(this);
etUserId = findViewById(R.id.et_user_id);
etUserName = findViewById(R.id.et_user_name);
etPassword = findViewById(R.id.et_password);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_get) {
sendGetRequest();
} else if (v.getId() == R.id.btn_post) {
sendPostRequest();
}
}
之所以先把这段代码放在前面,是因为网络请求并不是“独立存在的一段工具代码”,它必须有明确的输入来源和触发时机。这里 EditText 负责承接用户输入,onClick() 负责把交互动作切到不同的请求方法里。
4.2 使用 HttpURLConnection 发送 GET 请求
GET 请求这部分最适合拿来理解 HttpURLConnection 的完整流程,因为它没有请求体,能让注意力集中在“建立连接、读取响应、切回主线程”这几个关键动作上。
先看完整代码:
private void sendGetRequest() {
new Thread(new Runnable() {
@Override
public void run() {
String id = etUserId.getText().toString();
String urlAdress = "http://titok.fzqq.fun/addons/cms/api.user/userInfo?user_id=" + id + "&type=archives";
Log.i(TAG, "urlAdress:" + urlAdress);
try {
URL url = new URL(urlAdress);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
InputStream inputStream = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
runOnUiThread(() -> {
String string = builder.toString();
Log.i(TAG, "网络访问结果:" + string);
});
connection.disconnect();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
这段代码可以按执行顺序拆开理解:
- 先从
etUserId里取出用户输入的id,再把它拼进查询地址。GET请求的参数就这样直接挂到URL后面。 - 用
new URL(urlAdress)创建目标地址对象。它只是描述连接目标,还没有真正发起请求。 - 调用
openConnection()打开连接,再强转为HttpURLConnection,因为设置请求方法、超时时间这些能力都定义在这个子类里。 setRequestMethod("GET")明确声明当前请求是GET。setConnectTimeout(8000)和setReadTimeout(8000)分别控制建立连接超时和读取数据超时,避免线程无限卡住。- 通过
getInputStream()拿到服务端返回的数据流,再包成BufferedReader,方便逐行读取。 - 用
StringBuilder把服务端每一行内容拼成完整响应字符串。 - 读取完成后用
runOnUiThread()回到主线程,因为界面更新和交互反馈不能放在子线程里做。 - 最后调用
disconnect()断开连接,释放底层资源。
这里最需要建立的习惯有两个。
- 第一个是:网络请求属于耗时操作,必须放到子线程里。所以最外层先开了一个
new Thread(...)。如果把这段逻辑直接放进主线程执行,应用很容易触发卡顿甚至抛出主线程网络访问异常。 - 第二个是:读取结果只是第一步,真正的业务处理从“拿到那段字符串”之后才开始。这里暂时只是打印日志,后面切到
JSONObject和Gson时,就会把这段结果继续转成结构化数据。
4.3 使用 HttpURLConnection 发送 POST 请求
POST 请求和 GET 最大的区别,是参数不再直接拼在 URL 后面,而是放进请求体里发送给服务端。因此它比 GET 多了“设置请求头”和“通过输出流写入请求体”这两个动作。
完整代码如下:
private void sendPostRequest() {
new Thread(new Runnable() {
@Override
public void run() {
String loginUrl = "http://titok.fzqq.fun/addons/cms/api.login/login";
try {
URL url = new URL(loginUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json;charset=utf-8");
connection.setRequestProperty("Accept", "appliction/json");
String account = etUserName.getText().toString();
String password = etPassword.getText().toString();
String jsonBody = "{\"account\":\"" + account + "\",\"password\": \"" + password + "\"}";
Log.i(TAG, "jsonBody: " + jsonBody);
OutputStream os = connection.getOutputStream();
byte[] bytes = jsonBody.getBytes("utf-8");
os.write(bytes, 0, jsonBody.length());
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
InputStream is = connection.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, "utf-8"));
StringBuilder builder = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
builder.append(line);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
String loginResult = builder.toString();
Log.i(TAG, "loginResult: " + loginResult);
}
});
} else {
Log.e(TAG, "run: 网络请求失败!");
}
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
这部分的关键点主要集中在请求体构造和请求头说明上。
setDoOutput(true) 的意思是告诉连接:这次请求需要向服务端写数据。如果不打开这个开关,后面即使调用 getOutputStream(),请求体也没有正确的输出前提。
Content-Type 用来声明请求体的数据格式,这里写成 application/json;charset=utf-8,表示提交的是 JSON,并且采用 utf-8 编码。这样服务端才能按正确格式解析账号和密码。
Accept 则是在告诉服务端:客户端希望收到哪种格式的响应数据。实际开发里这个值经常写成 application/json。
接着从 etUserName 和 etPassword 里取出输入值,手动拼成一段 JSON:
String jsonBody = "{\"account\":\"" + account + "\",\"password\": \"" + password + "\"}";
这段字符串就是整次 POST 请求的业务负载。把它转成字节数组,再通过 OutputStream 写给服务端,请求才算真正带着登录参数发送出去。
发送完请求体后,接下来的处理流程又回到了和 GET 类似的链路:先拿状态码,再读取输入流,再把结果拼成字符串,最后回到主线程处理界面逻辑。也正因为 HttpURLConnection 这一层都是手动步骤,所以它最能帮助建立“请求和响应到底经历了哪些环节”的全流程意识。
5. 先把 JSON 结构摸清,再用 JSONObject 手动读写数据
当服务端开始返回 JSON,或者 POST 需要我们自己组织 JSON 请求体时,下一步就不是“怎么发请求”,而是“怎么处理数据结构”。
5.1 Json 的基本结构
JSON 的核心只有两种容器:
- 对象,用
{}包裹,内部是键值对。 - 数组,用
[]包裹,内部是一组有序元素。
例如下面这段结构里,name、age、isStudent 是普通字段,address 是嵌套对象,courses 和 grades 是数组:
{
"name": "Alice",
"age": 30,
"isStudent": false,
"address": {
"city": "New York",
"zip": "10001"
},
"courses": ["Math", "Science", "History"],
"grades": [85, 90, 92],
"graduated": null
}
后面无论是用 JSONObject 还是 Gson,都是围绕这套结构在工作。区别只在于:JSONObject 更偏手动读取和手动拼装,Gson 更偏自动映射到 Java 对象。
5.2 使用 JSONObject 创建 JSON
页面先准备四个按钮,分别演示 JSONObject 和 Gson 的“创建”和“提取”两类操作:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_jsonobject);
findViewById(R.id.btn_object).setOnClickListener(this);
findViewById(R.id.btn_string).setOnClickListener(this);
findViewById(R.id.btn_gson_object).setOnClickListener(this);
findViewById(R.id.btn_gson_string).setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_object) {
getJSON();
} else if (v.getId() == R.id.btn_string) {
createJSON();
} else if (v.getId() == R.id.btn_gson_object) {
gsonCreateJSON();
} else if (v.getId() == R.id.btn_gson_string) {
gsonGetJSON();
}
}
接着看用 JSONObject 手动创建一段完整 JSON 的过程:
private void createJSON() {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("name", "Alice");
jsonObject.put("age", 30);
jsonObject.put("isStudent", false);
JSONObject addressJson = new JSONObject();
addressJson.put("city", "New York");
addressJson.put("zip", "10001");
jsonObject.put("address", addressJson);
JSONArray courses = new JSONArray();
courses.put("Math");
courses.put("Science");
courses.put("History");
jsonObject.put("courses", courses);
JSONArray grades = new JSONArray();
grades.put("85");
grades.put("90");
grades.put("92");
jsonObject.put("grades", grades);
jsonObject.put("graduated", null);
String objectString = jsonObject.toString();
Log.i(TAG, "createJSON: objectString:" + objectString);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
这段代码的重点不是 put() 这个方法本身,而是它展示了“代码里的数据结构如何一层层对应到 JSON”:
- 普通字段直接用
key + value放进去。 - 嵌套对象先单独创建一个
JSONObject,再整体挂到父对象上。 - 数组则用
JSONArray逐个put()元素。 - 调用
toString()之后,才会得到真正可发送、可存储、可打印的JSON字符串。
也就是说,前面在 HttpURLConnection 的 POST 里手动拼接字符串,只是最原始的一种做法;一旦结构稍微复杂一些,用 JSONObject 去显式构造层级关系会更清楚。
5.3 使用 JSONObject 提取参数
如果现在有一段现成的 JSON 字符串,JSONObject 也可以反过来把里面的字段逐个提取出来:
private void getJSON() {
String json = "{\"name\":\"Alice\",\"age\":30,\"isStudent\":false,\"address\":{\"city\":\"New York\",\"zip\":\"10001\"},\"courses\":[\"Math\",\"Science\",\"History\"],\"grades\":[\"85\",\"90\",\"92\"]}\n";
try {
JSONObject object = new JSONObject(json);
int age = object.getInt("age");
String name = object.getString("name");
Log.i(TAG, "getJSON: age = " + age);
Log.i(TAG, "getJSON: name = " + name);
JSONObject address = object.getJSONObject("address");
String city = address.getString("city");
Log.i(TAG, "getJSON: city = " + city);
JSONArray courses = object.getJSONArray("courses");
String h = courses.getString(2);
Log.i(TAG, "getJSON: h = = " + h);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
读取顺序同样严格对应 JSON 结构:
- 先用
new JSONObject(json)把整段字符串转成对象。 - 普通字段直接用
getInt()、getString()这类方法按key读取。 - 嵌套对象先用
getJSONObject("address")取出来,再继续读它内部的字段。 - 数组先用
getJSONArray("courses")拿到数组对象,再按索引读取元素。
JSONObject 的优点是直观,缺点也很明显:字段一多、层级一深,代码就会越来越碎。正因为这样,下一步才需要把 Gson 引进来,让“JSON 和实体类直接互转”这件事变得更省力。
6. 使用 Gson 让 JSON 和实体类直接互转
Gson 的价值在于把“读键值对”和“手动拼装层级”的工作,从逐字段操作转成“对象和字符串之间的转换”。这会直接改善请求体构造和响应体解析的可维护性。
6.1 引入依赖
先在 NetworkByJavaProject/app/build.gradle 的 dependencies 中加入 Gson:
implementation 'com.google.code.gson:gson:2.11.0'
加上这条依赖后,后面就可以直接创建 Gson 对象,用 toJson() 和 fromJson() 完成转换。
6.2 创建对象类
为了让 Gson 知道 JSON 要映射到什么结构,先准备一个 Student 实体类:
public class Student {
private String name;
private int age;
private boolean isStudent;
private Address address;
private String[] courses;
private Integer[] grades;
public static class Address {
private String city;
private String zip;
}
}
这里的字段设计和前面那段 JSON 是一一对应的:
name、age、isStudent对应普通字段。Address对应嵌套对象。courses和grades对应数组。
只要字段名和 JSON 中的 key 对得上,Gson 就能自动完成映射。
6.3 使用 Gson 将实体类转为 JSON
先创建对象,再交给 Gson 转成字符串:
private void gsonGetJSON() {
Student stu = new Student();
stu.setName("老孙");
stu.setAge(31);
stu.setStudent(true);
Student.Address address = new Student.Address();
address.setCity("China");
address.setZip("353333");
stu.setCourses(new String[]{"语文", "数学", "英语"});
stu.setGrades(new Integer[]{80, 70, 90});
stu.setAddress(address);
Gson gson = new Gson();
String stuJson = gson.toJson(stu);
Log.i(TAG, "gsonGetJSON: stuJson = " + stuJson);
}
这里 Gson 帮忙做了两件事:
- 沿着对象字段递归展开结构。
- 自动生成符合
JSON规范的字符串。
因此,前面手动拼 "{\"account\":\"...\"}" 的工作,在结构固定之后就可以交给 Gson 处理。这样字段一改,代码只需要改对象,而不是重新拼接整段字符串。
6.4 使用 Gson 将 JSON 转为实体类
反向转换时,调用的是 fromJson():
private void gsonCreateJSON() {
String json = "{\"address\":{\"city\":\"China\",\"zip\":\"353333\"},\"age\":31,\"courses\":[\"语文\",\"数学\",\"英语\"],\"grades\":[80,70,90],\"isStudent\":true,\"name\":\"老孙\"}\n";
Gson gson = new Gson();
Student student = gson.fromJson(json, Student.class);
String name = student.getName();
Integer[] grades = student.getGrades();
Log.i(TAG, "gsonCreateJSON: name = " + name);
Log.i(TAG, "gsonCreateJSON: grades[1] = " + grades[1]);
}
相比 JSONObject 一层层 getString()、getJSONArray() 去读,这种方式的好处很明显:
- 转换入口只有一次。
- 后续业务代码直接面向实体类字段,而不是字符串
key。 - 代码更容易和
OkHttp、Retrofit2这类请求库对接。
也正因为这一点,后面处理服务端返回结果时,都会越来越倾向于先定义实体类,再把响应体直接转成对象。
7. 使用 OkHttp 简化请求发送与结果处理
OkHttp 相比 HttpURLConnection 的优势,不是把网络请求变成“黑盒”,而是把那些高频的底层细节封装成更稳定的 API,例如请求构造、异步回调、超时配置和请求体封装。
7.1 前置代码
OkHttp 页面和 HttpURLConnection 基本一致,仍然是一个 GET 区域加一个 POST 区域:
private static final String TAG = "OkHttpActivity";
private EditText etUserId;
private EditText etUserName;
private EditText etPassword;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ok_http);
findViewById(R.id.btn_get).setOnClickListener(this);
findViewById(R.id.btn_post).setOnClickListener(this);
etUserId = findViewById(R.id.et_user_id);
etUserName = findViewById(R.id.et_user_name);
etPassword = findViewById(R.id.et_password);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_get) {
sendGetRequest();
} else if (v.getId() == R.id.btn_post) {
sendPostRequest();
}
}
这一步的作用,是让后面对比更纯粹:页面输入与交互逻辑没有变,变化的是底层请求发送方式。
7.2 引入依赖
先在 build.gradle 中加入 OkHttp:
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
有了这个依赖,后面请求发送的核心对象就从 HttpURLConnection 切成了 OkHttpClient、Request、Call 和 Callback。
7.3 使用 OkHttp 发起 GET 请求
先看完整实现:
private void sendGetRequest() {
String id = etUserId.getText().toString();
String urlAdress = "http://titok.fzqq.fun/addons/cms/api.user/userInfo?user_id=" + id + "&type=archives";
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url(urlAdress)
.get()
.build();
Call call = okHttpClient.newCall(request);
Callback callback = new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(OkHttpActivity.this, "网络请求失败", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (response.isSuccessful()) {
try {
String result = response.body().string();
Log.i(TAG, "run result: " + result);
UserInfoQuery userInfoQuery = new Gson().fromJson(result, UserInfoQuery.class);
String msg = "当前用户名为:" + userInfoQuery.getData().getUser().getNickname();
Toast.makeText(OkHttpActivity.this, msg, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
Toast.makeText(OkHttpActivity.this, "网络请求失败:" + response.code(),
Toast.LENGTH_SHORT).show();
}
}
});
}
};
call.enqueue(callback);
}
这一段和 HttpURLConnection 对比起来,简化主要体现在四个地方。
- 第一,超时配置被收进了
OkHttpClient.Builder()。这意味着连接策略属于客户端配置,而不是每次都从底层连接对象逐条设置。 - 第二,请求对象由
Request.Builder()构造。url()、get()、请求头等信息都集中挂在Request上,请求语义更清晰。 - 第三,
newCall(request)返回的是Call,它代表一次可执行请求。后面无论异步enqueue()还是同步execute(),都是围绕这个对象展开。 - 第四,结果回调被收敛进
Callback。请求失败走onFailure(),请求成功并收到响应走onResponse()。这样业务分支比手动读状态码更集中。
不过有一点必须特别注意:enqueue() 虽然已经帮我们开好了异步线程,但 onFailure() 和 onResponse() 仍然不是主线程环境。所以这里如果要弹 Toast 或操作界面,还是得用 runOnUiThread() 切回来。
另外,示例里保留了同步请求的提示:
// call.enqueue(callback);
Response execute = call.execute();
execute.isSuccessful();
ResponseBody body = execute.body();
这段同步发送请求的价值,是明确告诉读者:execute() 会阻塞当前线程。它不是不能用,而是不应该放在主线程里直接跑。
7.4 使用 Gson 处理服务端返回的结果
前面 JSONObject 和 Gson 已经把 JSON 处理思路铺开了。到了 OkHttp 这里,重点变成:服务端返回的字符串,怎样快速落到可用的实体类上。
7.4.1 根据服务器返回的示例数据创建实体类
用户信息查询接口返回的 JSON 结构比较深,所以先根据服务端示例数据创建 UserInfoQuery 实体类:
public class UserInfoQuery {
private int code;
private String msg;
private String time;
private DataBean data;
public static class DataBean {
private UserBean user;
private ListBean list;
private int archives;
private int comments;
public static class UserBean {
private int id;
private String nickname;
private String bio;
private String avatar;
private String status;
private String url;
}
public static class ListBean {
private int total;
private int per_page;
private int current_page;
private int last_page;
}
}
}
这一步的重点不是“把类写得多完整”,而是让字段层级和服务端结构保持一致。后续代码真正会用到的是 data.user.nickname,所以实体类必须先能承接这条字段路径。
7.4.2 使用 Gson 处理服务端返回数据为实体类对象
拿到响应字符串后,直接让 Gson 把它转成实体类:
String result = response.body().string();
Log.i(TAG, "run result: " + result);
UserInfoQuery userInfoQuery = new Gson().fromJson(result, UserInfoQuery.class);
String msg = "当前用户名为:" + userInfoQuery.getData().getUser().getNickname();
Toast.makeText(OkHttpActivity.this, msg, Toast.LENGTH_SHORT).show();
这一小段代码把“读取响应”和“消费业务字段”彻底接上了:
response.body().string()先把网络层的响应体拿成字符串。fromJson()再把字符串转成业务对象。- 最后通过
getData().getUser().getNickname()精确拿到界面真正关心的字段。
也就是说,OkHttp 负责把响应拿回来,Gson 负责把响应变成对象,这两层职责在这里第一次形成了非常稳定的配合。
7.5 使用 OkHttp 发起 POST 请求
POST 场景里,OkHttp 的封装优势会更明显,因为请求体不再需要自己手动拿输出流去写。
7.5.1 创建请求体对象
先把登录参数定义成一个实体类:
public class ReqLogin {
private String account;
private String password;
public ReqLogin(String account, String password) {
this.account = account;
this.password = password;
}
}
之所以先定义这个类,是为了让账号密码不再停留在“手动拼接字符串”的阶段。后面只要把 ReqLogin 对象交给 Gson,请求体就能自动生成。
7.5.2 发送 POST 请求并处理响应
完整代码如下:
private void sendPostRequest() {
String loginUrl = "http://titok.fzqq.fun/addons/cms/api.login/login";
OkHttpClient client = new OkHttpClient();
String account = etUserName.getText().toString();
String password = etPassword.getText().toString();
String jsonBody = new Gson().toJson(new ReqLogin(account, password));
MediaType mediaType = MediaType.get("application/json;charset=utf-8");
RequestBody requestBody = RequestBody.create(jsonBody, mediaType);
Request request = new Request.Builder()
.url(loginUrl)
.post(requestBody)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(OkHttpActivity.this, "网络请求失败", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (response.isSuccessful()) {
try {
String json = response.body().string();
Log.i(TAG, "run: " + json);
ResLogin resLogin = new Gson().fromJson(json, ResLogin.class);
Toast.makeText(OkHttpActivity.this, resLogin.getMsg() +
",欢迎用户" + resLogin.getData().getUser_id(), Toast.LENGTH_SHORT).show();
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
Toast.makeText(OkHttpActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
}
}
});
}
});
}
这里可以把整个链路拆成三段。
第一段是请求体生成:
String jsonBody = new Gson().toJson(new ReqLogin(account, password));
MediaType mediaType = MediaType.get("application/json;charset=utf-8");
RequestBody requestBody = RequestBody.create(jsonBody, mediaType);
ReqLogin 负责承接输入字段,Gson 负责把对象转成 JSON,MediaType 负责声明请求体的数据类型和编码方式,RequestBody 则把这两部分真正封装成 OkHttp 可发送的请求体对象。
第二段是请求构造:
Request request = new Request.Builder()
.url(loginUrl)
.post(requestBody)
.build();
和 GET 相比,唯一多出来的核心动作就是 post(requestBody)。这一步一旦挂上,请求方法和请求体就都明确了。
第三段是响应解析。登录接口返回的结果同样先转成字符串,再交给 ResLogin:
ResLogin resLogin = new Gson().fromJson(json, ResLogin.class);
Toast.makeText(OkHttpActivity.this, resLogin.getMsg() +
",欢迎用户" + resLogin.getData().getUser_id(), Toast.LENGTH_SHORT).show();
其中 msg 负责承接登录提示文案,data.user_id 则是业务上真正关心的用户身份结果。这样一来,请求和响应两端都已经摆脱手动字符串拼接,变成了“对象输入、对象输出”的模型。
7.6 OkHttp 拦截器可以统一处理什么
当请求数量开始增多,只在每个接口里单独写 header、通用参数和错误处理,很快就会重复。OkHttp 的拦截器正是用来解决这类横切逻辑的。
最常见的第一类用法,是统一加请求头:
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request request = chain.request().newBuilder()
.header("token", token)
.build();
return chain.proceed(request);
})
.build();
这里的思路是:只要某个 OkHttpClient 挂了这个拦截器,所有通过它发出的请求都会自动带上 token。这样用户鉴权信息就不必在每个 Request.Builder() 里重复写一遍。
第二类用法,是统一加通用参数:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
HttpUrl originalUrl = chain.request().url();
HttpUrl newUrl = originalUrl.newBuilder()
.addQueryParameter("apikey", "123456789")
.build();
Request newRequest = chain.request().newBuilder()
.url(newUrl)
.build();
return chain.proceed(newRequest);
})
.build();
这适合语言环境、渠道号、公共业务标记之类每次请求都要带上的参数。
第三类用法,是统一处理响应错误:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
Response response = chain.proceed(chain.request());
if (response.code() == 404) {
// 找不到资源
} else if (response.code() == 501) {
// 服务器错误
}
return response;
})
.build();
这类逻辑一旦集中到拦截器里,请求调用方就不需要每次都从零判断同一类错误,维护成本会明显下降。
8. 使用 Retrofit2 把接口定义、请求参数和结果解析收拢到一起
如果说 OkHttp 已经把“底层连接操作”收敛了,那么 Retrofit2 更进一步,它把“接口地址、方法类型、参数位置、返回类型”也抽成了声明式接口。
8.1 前置代码
Retrofit2 页面结构仍然和前面保持一致:
public class Retrofit2Activity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "Retrofit2Activity";
private EditText etUserId;
private EditText etUserName;
private EditText etPassword;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_retrofit2);
findViewById(R.id.btn_get).setOnClickListener(this);
findViewById(R.id.btn_post).setOnClickListener(this);
etUserId = findViewById(R.id.et_user_id);
etUserName = findViewById(R.id.et_user_name);
etPassword = findViewById(R.id.et_password);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_get) {
sendGetRequest();
} else if (v.getId() == R.id.btn_post) {
sendPostRequest();
}
}
}
这种保持输入区不变的做法,能让注意力完全落到接口定义和调用方式的变化上。
8.2 引入依赖
先加入 Retrofit2 本体依赖:
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
为了让响应体可以直接转成实体类,还要加 Gson 转换器:
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
这两条依赖配齐之后,Retrofit2 才能同时具备接口声明能力和自动结果转换能力。
8.3 Retrofit2 发起 GET 请求
8.3.1 定义网络请求接口
先把用户信息查询接口定义成一个 Java 接口:
public interface ApiService {
@GET("addons/cms/api.user/userInfo")
Call<UserInfoQuery> getUserInfo(
@Query("user_id") String userId,
@Query("type") String type);
}
这里的每一个注解都对应着明确的请求语义:
@GET(...)说明这是一个GET请求,同时给出相对路径。@Query("user_id")和@Query("type")表示这两个参数会作为查询参数自动拼到URL后面。Call<UserInfoQuery>表示这个接口最终会返回一个能拿到UserInfoQuery实体类的请求对象。
也就是说,前面在 HttpURLConnection 和 OkHttp 里手动拼地址字符串的工作,到了 Retrofit2 这里被拆成了“路径定义 + 参数声明”。
8.3.2 使用
调用时先创建 Retrofit 实例,再生成接口实现:
private void sendGetRequest() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://titok.fzqq.fun/")
.addConverterFactory(GsonConverterFactory.create())
.build();
ApiService apiService = retrofit.create(ApiService.class);
String userId = etUserId.getText().toString();
Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");
queryCall.enqueue(new Callback<UserInfoQuery>() {
@Override
public void onResponse(Call<UserInfoQuery> call, Response<UserInfoQuery> response) {
UserInfoQuery body = response.body();
String nickname = body.getData().getUser().getNickname();
Toast.makeText(Retrofit2Activity.this, "查询到:" + nickname, Toast.LENGTH_SHORT).show();
Log.i(TAG, "onResponse: body " + nickname);
}
@Override
public void onFailure(Call<UserInfoQuery> call, Throwable throwable) {
}
});
}
这段代码背后发生的事情,比表面看起来更值得注意。
baseUrl("http://titok.fzqq.fun/") 负责提供公共域名,所以接口注解里只需要写相对路径,不必每个请求都重复写完整地址。
addConverterFactory(GsonConverterFactory.create()) 则是把“响应体字符串转对象”的能力直接接进了 Retrofit2。因此回调里的 response.body() 已经不是字符串,而是 UserInfoQuery 实体类对象。
这就是 Retrofit2 和前面几种方案最明显的差别:请求构造声明化,响应解析自动化,调用层直接面向业务对象。
8.4 Retrofit2 发起 POST 请求
8.4.1 定义网络请求接口
登录接口在 ApiService 中这样定义:
public interface ApiService {
@GET("addons/cms/api.user/userInfo")
Call<UserInfoQuery> getUserInfo(
@Query("user_id") String userId,
@Query("type") String type);
@POST("addons/cms/api.login/login")
Call<ResLogin> login(@Body ReqLogin login);
}
这里 @Body ReqLogin login 的意义非常重要:它声明了当前请求体来自一个 ReqLogin 对象,Retrofit2 会结合前面配置好的 GsonConverterFactory,自动把这个对象转成 JSON 请求体发出去。
8.4.2 使用
完整调用代码如下:
private void sendPostRequest() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://titok.fzqq.fun/")
.addConverterFactory(GsonConverterFactory.create())
.build();
ApiService apiService = retrofit.create(ApiService.class);
String account = etUserName.getText().toString();
String password = etPassword.getText().toString();
Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));
loginCall.enqueue(new Callback<ResLogin>() {
@Override
public void onResponse(Call<ResLogin> call, Response<ResLogin> response) {
ResLogin resLogin = response.body();
if (response.isSuccessful() && resLogin.getCode() == 1) {
Toast.makeText(Retrofit2Activity.this, "欢迎你," +
response.body().getData().getUser_id(), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(Retrofit2Activity.this, resLogin.getMsg(), Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<ResLogin> call, Throwable throwable) {
Toast.makeText(Retrofit2Activity.this, "网络请求失败", Toast.LENGTH_SHORT).show();
}
});
}
这一段的阅读重点不再是“怎么拼请求体”,而是“怎么消费已经转好的业务结果”。
apiService.login(new ReqLogin(account, password)) 这一行就已经同时完成了三件事:
- 选中了
POST登录接口。 - 把账号密码封装成请求体对象。
- 返回一个类型明确的
Call<ResLogin>。
到了 onResponse() 里,response.body() 已经是 ResLogin,所以可以直接判断:
response.isSuccessful():网络层是否成功收到响应。resLogin.getCode() == 1:业务层是否真正登录成功。
这也是为什么 Retrofit2 特别适合接口数量较多的项目。它把“网络层成功”和“业务层成功”区分得更自然,代码也更接近接口文档本身。
9. 下载文件的几种常见写法
网络请求不只有取 JSON 这一种形态。文件下载同样是高频场景,只不过它最终要处理的不是字符串,而是字节流和本地文件写入。
9.1 使用 HttpURLConnection 下载文件
public void downloadFile(String fileUrl, String fileName) {
try {
URL url = new URL(fileUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new RuntimeException("Failed to download file: " + connection.getResponseCode());
}
InputStream input = new BufferedInputStream(connection.getInputStream());
FileOutputStream output = new FileOutputStream(fileName);
byte[] data = new byte[4096];
int count;
while ((count = input.read(data)) != -1) {
output.write(data, 0, count);
}
output.flush();
output.close();
input.close();
} catch (Exception e) {
e.printStackTrace();
}
}
下载文件时,响应体不再适合用 BufferedReader 按文本读取,而要按字节流处理。这里 InputStream 负责从网络侧读取字节,FileOutputStream 负责把字节持续落到本地文件里,while 循环则承担整段数据搬运。
9.2 使用 OkHttp 下载文件
public void downloadFileWithOkHttp(String fileUrl, String destinationPath) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(fileUrl).build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Failed to download file: " + response);
File file = new File(destinationPath);
try (InputStream inputStream = response.body().byteStream();
FileOutputStream fos = new FileOutputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fos.flush();
}
} catch (Exception e) {
e.printStackTrace();
}
}
这一版的核心变化是:响应体直接通过 response.body().byteStream() 提供字节流。也就是说,OkHttp 负责把请求和响应拿稳,文件写入逻辑仍然由代码自己控制。
9.3 使用 DownloadManager 下载文件
public void downloadFileUsingDownloadManager(Context context, String url, String fileName) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setTitle("Downloading...");
request.setDescription("正在下载xxx文件...");
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
downloadManager.enqueue(request);
}
如果下载任务需要后台持续执行、系统通知管理和统一下载目录管理,DownloadManager 会比手写线程和流更合适。它本质上是系统服务,适合长时间运行的文件下载任务。
9.4 使用 Retrofit 下载文件
public interface DownloadService {
@GET
Call<ResponseBody> downloadFile(@Url String fileUrl);
}
public void downloadFileWithRetrofit(String fileUrl, String destinationPath) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com/")
.build();
DownloadService service = retrofit.create(DownloadService.class);
Call<ResponseBody> call = service.downloadFile(fileUrl);
try {
ResponseBody body = call.execute().body();
if (body != null) {
InputStream inputStream = body.byteStream();
FileOutputStream fos = new FileOutputStream(destinationPath);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fos.flush();
fos.close();
inputStream.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
这一版说明了 Retrofit2 并不只适合处理 JSON。当返回类型改成 ResponseBody 后,它也可以接管下载接口的声明和调用,真正的文件写入仍然按字节流落盘。
10. 上传文件的几种常见写法
上传和下载一样,都属于网络通信的一部分,但它的重点不再是“如何读取响应”,而是“如何把本地文件包装成请求体并正确声明类型”。
10.1 Content-Type 为什么重要
上传文件时,Content-Type 不能再简单看成一个“可有可无的请求头”,因为它直接决定服务端怎么解析请求体。
常见类型包括:
application/json:用于JSON数据。application/x-www-form-urlencoded:用于传统表单字段。multipart/form-data:用于文件上传或混合表单提交。application/octet-stream:用于通用二进制流。image/png、image/jpeg:用于图片文件。
在普通 JSON 请求里,OkHttp、Retrofit2 往往能帮我们自动处理好 Content-Type。但一旦进入文件上传场景,就必须清楚自己要提交的是哪种媒体类型,以及整个请求体是否需要按 multipart/form-data 组织。
10.2 使用 HttpURLConnection 上传文件
public void uploadFileWithHttpURLConnection(String filePath, String serverUrl) {
HttpURLConnection connection = null;
DataOutputStream outputStream = null;
FileInputStream fileInputStream = null;
try {
File file = new File(filePath);
URL url = new URL(serverUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=--boundary");
outputStream = new DataOutputStream(connection.getOutputStream());
outputStream.writeBytes("--boundary\r\n");
outputStream.writeBytes("Content-Disposition: form-data; name=\"file\";filename=\"" + file.getName() + "\"\r\n");
outputStream.writeBytes("\r\n");
fileInputStream = new FileInputStream(file);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.writeBytes("\r\n");
outputStream.writeBytes("--boundary--\r\n");
outputStream.flush();
int responseCode = connection.getResponseCode();
Log.i("TAG","Response Code: " + responseCode);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fileInputStream != null) fileInputStream.close();
if (outputStream != null) outputStream.close();
if (connection != null) connection.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这段代码最关键的是两点:
- 请求头声明成
multipart/form-data,并带上边界串boundary。 - 请求体不再只是纯文本,而是要自己写入表单头、文件内容和结束边界。
也就是说,HttpURLConnection 上传文件时,开发者必须对 multipart 的组织格式有足够清晰的认识。
10.3 使用 OkHttp 上传文件
public void uploadFileWithOkHttp(String filePath, String serverUrl) {
OkHttpClient client = new OkHttpClient();
File file = new File(filePath);
RequestBody fileBody = RequestBody.create(file, MediaType.parse("image/png"));
MultipartBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", file.getName(), fileBody)
.build();
Request request = new Request.Builder()
.url(serverUrl)
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
Log.i("TAG","Upload successful: " + response.message());
} else {
Log.i("TAG","Upload failed: " + response.message());
}
} catch (IOException e) {
e.printStackTrace();
}
}
相比手动写边界,OkHttp 已经把文件上传最麻烦的部分封装掉了:
fileBody负责描述单个文件片段及其媒体类型。MultipartBody.Builder()负责组织整个表单请求体。addFormDataPart()负责声明字段名、文件名和文件内容。
这样做的好处是,代码能更专注于业务输入,而不是手搓 multipart 协议细节。
10.4 使用 Retrofit 上传文件
public interface FileUploadService {
@Multipart
@POST("upload")
Call<Void> uploadFile(@Part MultipartBody.Part file);
}
public void uploadFileWithRetrofit(String filePath, String serverUrl) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(serverUrl)
.addConverterFactory(GsonConverterFactory.create())
.build();
FileUploadService service = retrofit.create(FileUploadService.class);
File file = new File(filePath);
MultipartBody.Part filePart = MultipartBody.Part
.createFormData("file", file.getName(), RequestBody.create(file, MediaType.parse("image/png")));
Call<Void> call = service.uploadFile(filePart);
call.enqueue(new retrofit2.Callback<Void>() {
@Override
public void onResponse(Call<Void> call, retrofit2.Response<Void> response) {
if (response.isSuccessful()) {
Log.i("TAG", "Upload successful");
} else {
Log.i("TAG", "Upload failed");
}
}
@Override
public void onFailure(Call<Void> call, Throwable t) {
t.printStackTrace();
}
});
}
Retrofit2 在上传场景里的思路,和前面的 POST JSON 请求一脉相承:接口用注解描述,请求参数用类型约束,真正执行时只关心“传什么对象”。
其中:
@Multipart说明当前请求是多部分表单。@Part MultipartBody.Part file说明这个请求有一个文件片段参数。createFormData()负责把字段名、文件名和文件内容真正封装成一个可上传片段。
这样一来,声明接口、构造参数、发送请求三部分的职责就彻底拆开了。
11. 相关代码附录
11.1 AndroidManifest 与依赖配置
NetworkByJavaProject/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NetworkByJavaProject"
android:usesCleartextTraffic="true"
tools:targetApi="31">
</application>
</manifest>
NetworkByJavaProject/app/build.gradle
dependencies {
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
}
11.2 HttpURLConnection 页面与请求代码
NetworkByJavaProject/app/src/main/res/layout/activity_http_url_connection.xml
<EditText
android:id="@+id/et_user_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入要查询的用户id"
android:inputType="number" />
<EditText
android:id="@+id/et_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入用户名" />
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入密码"
android:inputType="textPassword" />
NetworkByJavaProject/app/src/main/java/com/ls/networkbyjavaproject/activity/HttpUrlConnectionActivity.java
private void sendGetRequest() {
new Thread(new Runnable() {
@Override
public void run() {
String id = etUserId.getText().toString();
String urlAdress = "http://titok.fzqq.fun/addons/cms/api.user/userInfo?user_id=" + id + "&type=archives";
try {
URL url = new URL(urlAdress);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
InputStream inputStream = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
}
runOnUiThread(() -> Log.i(TAG, "网络访问结果:" + builder));
connection.disconnect();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
private void sendPostRequest() {
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL("http://titok.fzqq.fun/addons/cms/api.login/login");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json;charset=utf-8");
connection.setRequestProperty("Accept", "appliction/json");
String jsonBody = "{\"account\":\"" + etUserName.getText() + "\",\"password\": \"" + etPassword.getText() + "\"}";
OutputStream os = connection.getOutputStream();
byte[] bytes = jsonBody.getBytes("utf-8");
os.write(bytes, 0, jsonBody.length());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
11.3 JSONObject 与 Gson 示例代码
NetworkByJavaProject/app/src/main/java/com/ls/networkbyjavaproject/activity/JSONObjectActivity.java
private void createJSON() {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("name", "Alice");
jsonObject.put("age", 30);
jsonObject.put("isStudent", false);
JSONObject addressJson = new JSONObject();
addressJson.put("city", "New York");
addressJson.put("zip", "10001");
jsonObject.put("address", addressJson);
JSONArray courses = new JSONArray();
courses.put("Math");
courses.put("Science");
courses.put("History");
jsonObject.put("courses", courses);
String objectString = jsonObject.toString();
Log.i(TAG, objectString);
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
private void gsonCreateJSON() {
String json = "{\"address\":{\"city\":\"China\",\"zip\":\"353333\"},\"age\":31,\"courses\":[\"语文\",\"数学\",\"英语\"],\"grades\":[80,70,90],\"isStudent\":true,\"name\":\"老孙\"}\n";
Gson gson = new Gson();
Student student = gson.fromJson(json, Student.class);
Log.i(TAG, student.getName());
}
NetworkByJavaProject/app/src/main/java/com/ls/networkbyjavaproject/bean/Student.java
public class Student {
private String name;
private int age;
private boolean isStudent;
private Address address;
private String[] courses;
private Integer[] grades;
public static class Address {
private String city;
private String zip;
}
}
11.4 OkHttp 请求与实体类
NetworkByJavaProject/app/src/main/java/com/ls/networkbyjavaproject/activity/OkHttpActivity.java
Request request = new Request.Builder()
.url(urlAdress)
.get()
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(callback);
String jsonBody = new Gson().toJson(new ReqLogin(account, password));
MediaType mediaType = MediaType.get("application/json;charset=utf-8");
RequestBody requestBody = RequestBody.create(jsonBody, mediaType);
Request request = new Request.Builder()
.url(loginUrl)
.post(requestBody)
.build();
NetworkByJavaProject/app/src/main/java/com/ls/networkbyjavaproject/bean/ReqLogin.java
public class ReqLogin {
private String account;
private String password;
public ReqLogin(String account, String password) {
this.account = account;
this.password = password;
}
}
NetworkByJavaProject/app/src/main/java/com/ls/networkbyjavaproject/bean/ResLogin.java
public class ResLogin {
private int code;
private String msg;
private String time;
private DataBean data;
public static class DataBean {
private String token;
private int user_id;
}
}
NetworkByJavaProject/app/src/main/java/com/ls/networkbyjavaproject/bean/UserInfoQuery.java
public class UserInfoQuery {
private int code;
private String msg;
private String time;
private DataBean data;
}
11.5 Retrofit2 接口与调用代码
NetworkByJavaProject/app/src/main/java/com/ls/networkbyjavaproject/reterofit/ApiService.java
public interface ApiService {
@GET("addons/cms/api.user/userInfo")
Call<UserInfoQuery> getUserInfo(
@Query("user_id") String userId,
@Query("type") String type);
@POST("addons/cms/api.login/login")
Call<ResLogin> login(@Body ReqLogin login);
}
NetworkByJavaProject/app/src/main/java/com/ls/networkbyjavaproject/activity/Retrofit2Activity.java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://titok.fzqq.fun/")
.addConverterFactory(GsonConverterFactory.create())
.build();
ApiService apiService = retrofit.create(ApiService.class);
Call<UserInfoQuery> queryCall = apiService.getUserInfo(userId, "archives");
Call<ResLogin> loginCall = apiService.login(new ReqLogin(account, password));
更多推荐




所有评论(0)