说实话,看到“Android Studio”和“JSP”放在一起,很多老程序员的第一反应是:这俩能兼容吗?一个是Java EE时代写后端页面的神器,一个是原生Android开发的环境。听起来像是让法拉利去拉犁。但如果你仔细观察现在的企业级应用架构,会发现这种“前后端分离”或者“移动端对接传统Web服务”的场景其实非常普遍。很多公司的核心业务系统还是跑在Tomcat上,用JSP或Servlet提供服务,而老板又觉得是时候做个Android APP来让业务触达更多用户了。这时候,你不需要推翻现有的后端,只需要写一个轻量级的Android客户端去调用那些接口。今天我们就把这个坑填平,从最基础的Java语法开始,一直讲到如何让你的APP在Android 14上也跑得顺顺溜溜。
先别急着打开Android Studio,咱们得先聊聊这里面的一个核心误区。很多人觉得学Android就是学Kotlin,是。但如果你要对接的遗留系统是用Java写的,或者你的需求是快速验证、快速原型开发,那么Java依然是Android开发的基石。更重要的是,理解Java如何通过网络请求与JSP后端通信,比单纯会写界面更有价值。咱们今天不讲那些花里胡哨的动画,咱们讲怎么让数据真正流动起来。
第一站:Java基础——Android开发的隐形骨架
你可能会问,我都用Android Studio了,为什么还要讲Java基础?因为当你遇到那些让人抓狂的NullPointerException或者网络超时错误时,Java的底层逻辑是你唯一的救命稻草。
在Android开发中,Java不仅仅是一种语言,它是一种思维模式。比如,理解引用类型和值类型的区别。在Java里,String不是基本数据类型,它是一个对象。这意味着String s1 = "hello"; String s2 = "hello";时,s1 == s2是真,因为它们在字符串常量池中指向同一个对象;但如果你用new String("hello"),那就是两个不同的对象。在Android开发中,尤其是在处理从JSP接口返回的JSON数据时,这种细节决定了你的APP是丝滑还是卡顿,甚至会不会内存溢出。
再比如集合框架。ArrayList是Android开发中最常用的容器。但你知道吗?在高频操作场景下,随意使用ArrayList可能会导致性能问题。比如,你知道ArrayList底层是数组,插入和删除中间的元素需要移动大量元素吗?所以在Android的列表视图(RecyclerView)适配器中,我们通常会缓存一些数据,避免每次刷新都重新查询或计算。
这里有一个简单的例子,展示如何在Android中使用Java处理一个从JSP返回的数据结构。假设JSP返回的是一个JSON字符串:
// 模拟从JSP接口获取的数据
String jsonData = "{\"userId\": 1001, \"userName\": \"张三\", \"balance\": 500.50}";
// 解析JSON
JSONObject jsonObject = new JSONObject(jsonData);
int userId = jsonObject.getInt("userId");
String userName = jsonObject.getString("userName");
double balance = jsonObject.getDouble("balance");
// 在Android中,我们通常将这些数据封装成一个Model类
User user = new User();
user.setUserId(userId);
user.setUserName(userName);
user.setBalance(balance);
// 注意:在主线程中不要进行耗时操作,比如网络请求或数据库读写
// 否则会导致ANR(Application Not Responding)
这个例子看似简单,但它蕴含了Android开发的核心原则:UI线程不能阻塞。这也是为什么后续我们要引入异步网络请求的原因。
第二站:构建JSP后端——让数据“说话”
既然我们要开发移动端,那么后端必须有数据可提供。虽然现在很多新项目都用RESTful API或者GraphQL,但在实际工作中,尤其是老项目维护,JSP+Servlet依然是主力。我们需要构建一个简单的JSP页面和对应的Servlet来提供数据。
首先,确保你的开发环境已经安装了Apache Tomcat。在IDEA或者Eclipse中创建一个Dynamic Web Project。
接下来,我们创建一个简单的Servlet,用于处理用户的登录请求并返回JSON数据。注意,现代移动端开发更倾向于接收JSON而不是HTML片段,这样APP端的解析效率更高,界面渲染更灵活。
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@WebServlet("/api/login")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 设置响应头,允许跨域请求(CORS),这在移动端开发中至关重要
response.setHeader("Access-Control-Allow-Origin", "*");
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
Map<String, Object> result = new HashMap<>();
// 简单的模拟验证逻辑
if ("admin".equals(username) && "123456".equals(password)) {
result.put("code", 200);
result.put("message", "登录成功");
result.put("data", new HashMap<String, String>() {{
put("token", "abc123xyz");
put("userId", "1001");
}});
} else {
result.put("code", 401);
result.put("message", "用户名或密码错误");
}
PrintWriter out = response.getWriter();
out.print(new com.google.gson.Gson().toJson(result)); // 假设引入了Gson库
out.flush();
out.close();
}
}
这段代码有几个关键点:
Access-Control-Allow-Origin: *:这是解决跨域问题的关键。当你从Android APP发起网络请求时,如果后端没有这个头,浏览器(如果是H5混合开发)或某些网络库会拦截请求。虽然Android原生APP的HTTP客户端对CORS的处理与浏览器略有不同,但加上这个头是最佳实践。- 返回JSON而非HTML:移动端不需要渲染JSP页面,只需要数据。所以Servlet应该返回
application/json。 - 字符编码:务必设置
UTF-8,否则中文数据在传输过程中会变成乱码,这在调试时会让人崩溃。
接下来,创建一个简单的JSP页面作为测试入口,虽然APP不直接访问它,但你可以用浏览器测试接口是否正常:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>JSP API Test</title>
</head>
<body>
<h2>登录接口测试</h2>
<form action="api/login" method="post">
用户名: <input type="text" name="username"><br>
密码: <input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
部署这个Web应用到Tomcat后,启动Tomcat,确保可以通过http://localhost:8080/your-app/api/login访问到接口(用Postman测试最佳)。
第三站:Android Studio实战——从零构建客户端
现在,后端准备好了,我们开始构建Android APP。打开Android Studio,创建一个新项目,选择Empty Activity,语言选择Java,Minimum SDK建议选择Android 8.0 (API 26),因为太低版本的系统在安全和网络库支持上会有诸多限制。
1. 配置网络权限
首先,打开AndroidManifest.xml,添加网络权限。没有这个权限,你的APP连不上网,一切免谈。
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
同时,Android 9(Pie)及以上版本默认禁止明文HTTP流量,如果你后端是用http://而不是https://,还需要在res/xml/network_security_config.xml中配置,并在AndroidManifest.xml中引用:
<!-- network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain> <!-- 模拟器本地回环地址 -->
<domain includeSubdomains="true">your-server-ip.com</domain>
</domain-config>
</network-security-config>
然后在AndroidManifest.xml的application标签中添加:
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="true"
2. 设计UI界面
打开res/layout/activity_main.xml,我们需要一个简单的登录界面:用户名输入框、密码输入框、登录按钮和结果显示文本。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center_horizontal">
<EditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="用户名"
android:inputType="text"
android:layout_marginBottom="12dp"/>
<EditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="密码"
android:inputType="textPassword"
android:layout_marginBottom="24dp"/>
<Button
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="登录"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/tvResult"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="等待登录..."
android:textSize="16sp"
android:gravity="center"/>
</LinearLayout>
3. 编写Java逻辑——核心难点
这是最关键的部分。在Android中,任何网络请求都不能在主线程(UI线程)执行,否则APP会直接卡死并弹出ANR错误。我们需要使用异步方式。以前我们用AsyncTask,但现在已经废弃了。现在推荐的做法是使用ExecutorService配合Handler,或者使用Retrofit等网络库。为了让你深入理解原理,我们先用最基础的HttpURLConnection配合线程池来实现。
创建MainActivity.java:
package com.example.jspclient;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MainActivity extends AppCompatActivity {
private EditText etUsername;
private EditText etPassword;
private Button btnLogin;
private TextView tvResult;
// 定义线程池,用于执行网络请求
private ExecutorService executor = Executors.newFixedThreadPool(4);
private Handler mainHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etUsername = findViewById(R.id.etUsername);
etPassword = findViewById(R.id.etPassword);
btnLogin = findViewById(R.id.btnLogin);
tvResult = findViewById(R.id.tvResult);
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String username = etUsername.getText().toString().trim();
String password = etPassword.getText().toString().trim();
if (username.isEmpty() || password.isEmpty()) {
Toast.makeText(MainActivity.this, "请输入用户名和密码", Toast.LENGTH_SHORT).show();
return;
}
// 提交任务到线程池
executor.execute(new NetworkTask(username, password));
}
});
}
// 内部类,封装网络请求逻辑
private class NetworkTask implements Runnable {
private String username;
private String password;
public NetworkTask(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public void run() {
String url = "http://10.0.2.2:8080/YourJspApp/api/login"; // 模拟器访问本机用10.0.2.2
HttpURLConnection conn = null;
InputStream is = null;
BufferedReader br = null;
try {
URL serverUrl = new URL(url);
conn = (HttpURLConnection) serverUrl.openConnection();
// 设置请求方法为POST
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setDoOutput(true);
// 发送请求参数
String params = "username=" + java.net.URLEncoder.encode(username, "UTF-8")
+ "&password=" + java.net.URLEncoder.encode(password, "UTF-8");
try (OutputStream os = conn.getOutputStream()) {
os.write(params.getBytes(StandardCharsets.UTF_8));
}
// 获取响应码
int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
// 读取响应内容
is = conn.getInputStream();
br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
// 解析JSON并更新UI
final String resultJson = sb.toString();
mainHandler.post(new Runnable() {
@Override
public void run() {
try {
JSONObject json = new JSONObject(resultJson);
int code = json.getInt("code");
String message = json.getString("message");
if (code == 200) {
tvResult.setText("登录成功: " + message);
tvResult.setTextColor(getResources().getColor(android.R.color.holo_green_dark));
} else {
tvResult.setText("登录失败: " + message);
tvResult.setTextColor(getResources().getColor(android.R.color.holo_red_dark));
}
} catch (Exception e) {
tvResult.setText("数据解析错误");
e.printStackTrace();
}
}
});
} else {
mainHandler.post(new Runnable() {
@Override
public void run() {
tvResult.setText("请求失败,状态码: " + responseCode);
}
});
}
} catch (Exception e) {
e.printStackTrace();
mainHandler.post(new Runnable() {
@Override
public void run() {
tvResult.setText("网络异常: " + e.getMessage());
Toast.makeText(MainActivity.this, "网络连接失败", Toast.LENGTH_SHORT).show();
}
});
} finally {
if (br != null) try { br.close(); } catch (Exception ignored) {}
if (is != null) try { is.close(); } catch (Exception ignored) {}
if (conn != null) conn.disconnect();
}
}
}
// 在Activity销毁时关闭线程池,防止内存泄漏
@Override
protected void onDestroy() {
super.onDestroy();
executor.shutdown();
}
}
这段代码是理解Android网络编程的典范。它展示了:
- 线程分离:网络请求在后台线程执行,UI更新在主线程执行。
- 资源管理:使用
try-finally确保流和连接被关闭,disconnect()释放连接。 - 异常处理:对网络异常、解析异常都有捕获,避免APP崩溃。
- 内存泄漏防护:在
onDestroy中关闭线程池。
第四站:兼容性大作战——从Android 8到Android 14
开发完功能后,真正的挑战才开始。Android系统的碎片化是全球闻名的。你的APP可能在Android 10上跑得好好的,一到Android 14就崩了。下面我来分享几个最头疼的兼容性问题及解决方案。
1. 后台启动限制(Background Execution Limits)
从Android 8.0(API 26)开始,Google严格限制了后台启动服务。如果你尝试在后台隐式启动Service,可能会失败。虽然我们的例子用的是按钮点击触发的网络请求,不涉及后台Service,但如果你后续要开发后台同步功能,必须使用Foreground Service(前台服务),并显示一个通知。
2. 明文HTTP流量限制
前面已经提到了android:usesCleartextTraffic="true"。这是Android 9+的硬性规定。如果你的JSP后端是内网http://,而不配置这个选项,所有网络请求都会静默失败,你在Logcat中会看到Cleartext HTTP traffic not permitted的错误。
3. 权限模型变化
Android 6.0(API 23)引入了运行时权限。虽然INTERNET权限不需要运行时申请,但如果你后续需要读取存储、访问相机等,必须在代码中动态申请。
// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 申请权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_CODE_STORAGE);
}
4. Java版本兼容性
Android Studio默认的Java编译版本可能较高,但如果你要支持较低的Android版本,需要注意某些Java特性是否
