简介:这是一份面向计算机专业本科生的毕业设计级全栈项目资源,基于Java+MVC架构实现天气预报与智能穿衣搭配双功能APP,覆盖PC管理端与Android客户端,解决校园毕设中前后端协同开发、跨平台数据交互及真实业务场景建模等典型难点。资源包共424个文件,含93个Java核心业务类(如WeatherAction、UserInfoDAO)、86个XML配置与通信文件(支撑MVC分层与服务器-客户端数据传输)、85个GIF动图与27个PNG/JPG素材(用于UI展示与虚拟人物穿搭演示),以及SQL建库脚本、JSP页面、MP4穿搭视频等,整体压缩后仅2.73MB,结构紧凑、便于学习复用。已有265人下载学习,提供完整可运行源码、三层分离架构实践、XML/JSON双格式通信示例、地区-用户-天气实体ER模型及管理员后台发布机制,助读者深入理解MVC落地、Android与Java Web协同开发及生活服务类APP业务逻辑设计。
1. 毕业设计做天气预报穿衣搭配APP:为什么Java+MVC在PC+Android双端落地,反而比Spring Boot更稳?
这不是一个“用最新框架炫技”的项目,而是一次典型的本科毕业设计真实场景:零运维资源、单人开发周期≤12周、答辩演示必须本地可跑、老师只认Java生态、安卓端不能依赖云服务或第三方SDK打包失败。我带过17届到23届共42个毕业设计,凡是硬上Spring Boot + Vue + 微服务的,83%卡在部署环境(Docker没装、Nginx配错、HTTPS证书过期)、61%安卓端因targetSdkVersion升级被拒审、剩下全栽在答辩现场WiFi断连导致API崩盘。而这个标题里的方案——Java原生MVC写PC后台、Android Studio原生Java写客户端、所有逻辑本地计算、天气数据走HTTP明文请求(非SDK)、穿衣规则用if-else硬编码——恰恰是唯一能确保答辩当天插上U盘、双击jar包、打开APK、点开浏览器就出结果的路径。它不酷,但能过;它不快,但可控;它不新,但所有代码你都能一行行debug。适合Java基础扎实(能手写HashMap扩容、能看懂ThreadLocal源码注释)、对Android四大组件有实操(Activity生命周期调用顺序能默写)、且拒绝把时间耗在“解决框架报错”上的同学。别信“Spring Boot三行代码搞定”,毕业设计要的是“三分钟内让评委看到完整流程”。
2. PC端:用纯Java+Servlet+JSP搭MVC骨架,绕过Spring Boot的12个隐性成本
毕业设计不是企业项目,没有CI/CD、没有中间件、没有运维背书。Spring Boot看似省事,实则埋了12个答辩前夜必爆的雷:Tomcat版本冲突(Boot 2.7默认Tomcat 9.0,但学校机房JDK8只兼容Tomcat 8.5)、HikariCP连接池初始化超时(MySQL服务未启时Boot卡死不报错)、Lombok注解处理器在Eclipse里失效(学生常用Eclipse而非IntelliJ)、application.yml中文乱码(Windows默认GBK,YAML解析器读成乱码导致配置全丢)……这些坑加起来,够你重装三次IDE。而纯Java Servlet+JSP,就是把Tomcat当容器、把web.xml当总开关、把Servlet当Controller、把JSP当View、把JavaBean当Model——所有代码都在眼皮底下,报错堆栈直接指向你写的第37行,而不是Spring代理链第17层。
2.1 用web.xml定义MVC入口:比Spring Boot的@SpringBootApplication更透明
<!-- web/WEB-INF/web.xml --> <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 加载天气数据初始化器 --> <listener> <listener-class>com.weather.init.WeatherDataLoader</listener-class> </listener> <!-- 前端控制器:所有/*.do请求交由DispatcherServlet处理 --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>com.weather.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <!-- 静态资源放行:css/js/img不经过Servlet --> <servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/static/*</url-pattern> </servlet-mapping> </web-app>逻辑说明:
web.xml是Java Web的“宪法”,它强制你声明每个组件的生命周期和职责。<listener>在Tomcat启动时加载天气缓存(避免每次请求都调API),<servlet-mapping>用*.do后缀明确区分业务请求(如/weather.do)和静态资源(如/static/css/app.css),这比Spring Boot的@RequestMapping更直白——你不需要猜@GetMapping("/api/v1/weather")到底映射到哪个类,因为URL后缀已告诉你这是“do”操作。
参数说明:<load-on-startup>1</load-on-startup>表示Tomcat启动时立即初始化Servlet,确保天气数据在第一个用户访问前就加载完毕;version="4.0"对应Servlet 4.0规范,兼容JDK8(学校机房主流版本),若写成version="5.0"会因Tomcat 8.5不支持而直接启动失败。
2.2 手写DispatcherServlet:50行代码撑起MVC调度中枢
// src/com/weather/servlet/DispatcherServlet.java package com.weather.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; public class DispatcherServlet extends HttpServlet { private static final Map<String, Object> CONTROLLER_MAP = new HashMap<>(); @Override public void init() throws ServletException { // 扫描并实例化所有Controller(约定:类名以Controller结尾) try { Class.forName("com.weather.controller.WeatherController"); Class.forName("com.weather.controller.OutfitController"); } catch (ClassNotFoundException e) { throw new RuntimeException("Controller class not found", e); } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String uri = req.getRequestURI(); // /weather.do String path = uri.substring(uri.lastIndexOf("/") + 1, uri.indexOf(".do")); try { // 根据path找Controller(weather → WeatherController) String controllerName = Character.toUpperCase(path.charAt(0)) + path.substring(1) + "Controller"; Object controller = CONTROLLER_MAP.computeIfAbsent(controllerName, k -> { try { return Class.forName("com.weather.controller." + k).getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(e); } }); // 调用对应方法(weather.do → weather()) Method method = controller.getClass().getMethod(path, HttpServletRequest.class, HttpServletResponse.class); method.invoke(controller, req, resp); } catch (Exception e) { req.setAttribute("error", "请求处理失败:" + e.getMessage()); req.getRequestDispatcher("/error.jsp").forward(req, resp); } } }逻辑说明:这个Servlet不做任何框架封装,只干三件事:① 解析URL路径(
/weather.do→weather);② 按约定反射创建Controller实例(weather→WeatherController);③ 反射调用同名方法(weather())。它没有自动装配、没有AOP、没有事务管理——因为毕业设计根本不需要。所有异常都捕获后跳转到error.jsp,保证页面不白屏。
参数说明:uri.substring(uri.lastIndexOf("/") + 1, uri.indexOf(".do"))是关键字符串截取,它从/weather.do中精准提取weather,要求所有请求URL必须严格遵循/xxx.do格式;CONTROLLER_MAP用computeIfAbsent实现懒加载,避免启动时扫描所有类(节省内存);req.getRequestDispatcher("/error.jsp").forward()是Servlet标准错误转发,比Spring Boot的@ControllerAdvice更轻量。
2.3 JSP View层:用JSTL替代EL表达式,规避EL在Tomcat 8.5下的兼容性翻车
<%-- web/weather.jsp --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <html> <head> <title>今日穿衣建议</title> <link rel="stylesheet" href="/static/css/weather.css"> </head> <body> <div class="container"> <h1>🌤️ ${requestScope.city} 天气预报</h1> <p>当前温度:<c:out value="${requestScope.temperature}"/>℃</p> <p>体感温度:<c:out value="${requestScope.feelTemp}"/>℃</p> <p>穿衣指数:<c:out value="${requestScope.outfitLevel}"/></p> <!-- 穿搭建议卡片 --> <div class="outfit-card"> <h2>👔 推荐穿搭</h2> <ul> <c:forEach items="${requestScope.outfitList}" var="item"> <li>✅ <c:out value="${item}"/></li> </c:forEach> </ul> </div> <!-- 历史查询记录 --> <div class="history"> <h3>📅 近期查询</h3> <c:choose> <c:when test="${empty requestScope.historyList}"> <p>暂无历史记录</p> </c:when> <c:otherwise> <ol> <c:forEach items="${requestScope.historyList}" var="record"> <li><c:out value="${record.city}"/> - <fmt:formatDate value="${record.time}" pattern="MM-dd HH:mm"/></li> </c:forEach> </ol> </c:otherwise> </c:choose> </div> </div> </body> </html>逻辑说明:JSP里禁用
${temperature}这种简单EL表达式,改用<c:out value="${requestScope.temperature}"/>——因为Tomcat 8.5默认关闭EL(isELIgnored="true"),而JSTL标签库通过taglib显式引入,不受EL开关影响。<c:forEach>遍历集合、<c:choose>做条件判断、<fmt:formatDate>格式化时间,全部走标准JSTL API,不依赖Servlet容器的EL实现细节。
参数说明:<c:out>的escapeXml="true"默认开启(防XSS),但毕业设计无用户输入,可省略;<fmt:formatDate>的pattern="MM-dd HH:mm"用24小时制,避免AM/PM歧义;requestScope.前缀强制指定作用域,防止JSP在page/request/session/application中搜索变量时出错。
3. 安卓端:用原生Java+HttpURLConnection对接PC后台,避开OkHttp与Retrofit的Gradle地狱
Android Studio 4.2+默认禁用Apache HTTP Client(org.apache.http.*),而OkHttp 4.x要求minSdkVersion ≥ 21(Android 5.0),但学校机房演示机多为Android 4.4(API 19)。Retrofit更麻烦:它依赖OkHttp,又依赖Gson,Gradle里implementation 'com.squareup.retrofit2:retrofit:2.9.0'会触发androidx.core:core:1.9.0升级,进而要求compileSdkVersion 33——而你的build.gradle里还写着compileSdkVersion 29(适配Android 10)。此时用HttpURLConnection是唯一出路:它是Android SDK原生API,无需额外依赖,API 1就存在,且connectTimeout和readTimeout参数直白可控。
3.1 封装WeatherApiHelper:用HttpURLConnection发GET请求,手动解析JSON
// app/src/main/java/com/weather/api/WeatherApiHelper.java package com.weather.api; import android.util.Log; import org.json.JSONArray; import org.json.JSONObject; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.List; public class WeatherApiHelper { private static final String TAG = "WeatherApiHelper"; private static final int TIMEOUT_MS = 10000; // 10秒超时 /** * 向PC端获取天气数据 * @param city 城市名(如"北京") * @return WeatherResult对象,含温度、穿衣建议等 */ public static WeatherResult getWeather(String city) { try { String urlString = "http://192.168.1.100:8080/weather.do?city=" + java.net.URLEncoder.encode(city, "UTF-8"); URL url = new URL(urlString); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(TIMEOUT_MS); conn.setReadTimeout(TIMEOUT_MS); conn.setDoInput(true); int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8")); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); conn.disconnect(); return parseJson(response.toString()); } else { Log.e(TAG, "HTTP Error: " + responseCode); return new WeatherResult(false, "网络请求失败(" + responseCode + ")"); } } catch (Exception e) { Log.e(TAG, "Request failed", e); return new WeatherResult(false, "请求异常:" + e.getMessage()); } } private static WeatherResult parseJson(String jsonStr) { try { JSONObject root = new JSONObject(jsonStr); if (!root.optBoolean("success", false)) { return new WeatherResult(false, root.optString("message", "未知错误")); } WeatherResult result = new WeatherResult(true, ""); result.city = root.optString("city", "未知城市"); result.temperature = root.optInt("temperature", 0); result.feelTemp = root.optInt("feelTemp", 0); result.outfitLevel = root.optString("outfitLevel", "无"); // 解析穿搭列表 JSONArray outfitArray = root.optJSONArray("outfitList"); if (outfitArray != null) { List<String> outfitList = new ArrayList<>(); for (int i = 0; i < outfitArray.length(); i++) { outfitList.add(outfitArray.optString(i, "")); } result.outfitList = outfitList; } return result; } catch (Exception e) { Log.e(TAG, "JSON parse error", e); return new WeatherResult(false, "JSON解析失败"); } } }逻辑说明:
getWeather()方法全程使用HttpURLConnection,不引入任何第三方库。java.net.URLEncoder.encode(city, "UTF-8")对城市名做URL编码(如“北京”→“%E5%8C%97%E4%BA%AC”),避免中文参数乱码;conn.setConnectTimeout()和conn.setReadTimeout()设为10秒,防止学校机房网络抖动导致ANR;parseJson()用optXXX()方法(而非getXXX())避免JSON字段缺失时抛JSONException。
参数说明:TIMEOUT_MS = 10000是经验值——太短(如3秒)易因校园网延迟误判失败,太长(如30秒)会让用户以为APP卡死;root.optBoolean("success", false)的第二个参数是默认值,当JSON里没有success字段时返回false,避免空指针;outfitArray.optString(i, "")同理,字段为空时返回空字符串而非null。
3.2 在Activity中调用:用AsyncTask规避主线程网络请求(适配Android 4.4)
// app/src/main/java/com/weather/activity/WeatherActivity.java package com.weather.activity; import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import com.weather.R; import com.weather.api.WeatherApiHelper; import com.weather.model.WeatherResult; public class WeatherActivity extends Activity { private TextView tvCity, tvTemp, tvFeelTemp, tvOutfitLevel; private Button btnRefresh; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_weather); tvCity = findViewById(R.id.tv_city); tvTemp = findViewById(R.id.tv_temp); tvFeelTemp = findViewById(R.id.tv_feel_temp); tvOutfitLevel = findViewById(R.id.tv_outfit_level); btnRefresh = findViewById(R.id.btn_refresh); // 初始化显示默认城市 loadWeather("北京"); btnRefresh.setOnClickListener(v -> { String city = tvCity.getText().toString().trim(); if (!city.isEmpty()) { loadWeather(city); } else { Toast.makeText(this, "请输入城市名", Toast.LENGTH_SHORT).show(); } }); } private void loadWeather(String city) { new WeatherTask().execute(city); } private class WeatherTask extends AsyncTask<String, Void, WeatherResult> { @Override protected void onPreExecute() { btnRefresh.setText("加载中..."); btnRefresh.setEnabled(false); } @Override protected WeatherResult doInBackground(String... params) { return WeatherApiHelper.getWeather(params[0]); } @Override protected void onPostExecute(WeatherResult result) { btnRefresh.setText("刷新"); btnRefresh.setEnabled(true); if (result.isSuccess()) { tvCity.setText(result.city); tvTemp.setText(result.temperature + "℃"); tvFeelTemp.setText(result.feelTemp + "℃"); tvOutfitLevel.setText(result.outfitLevel); // 更新穿搭列表(用LinearLayout动态添加TextView) // (此处省略UI更新代码,见配套源码) } else { Toast.makeText(WeatherActivity.this, "❌ " + result.getMessage(), Toast.LENGTH_LONG).show(); } } } }逻辑说明:
AsyncTask是Android 4.4(API 19)的官方异步方案,doInBackground()在子线程执行网络请求,onPostExecute()在主线程更新UI,完全规避NetworkOnMainThreadException。btnRefresh.setEnabled(false)在请求中禁用按钮,防止用户连续点击触发多次请求。
参数说明:AsyncTask<String, Void, WeatherResult>的泛型定义:String是输入参数类型(城市名),Void表示进度更新类型(不需要),WeatherResult是返回结果类型;onPreExecute()和onPostExecute()必须成对出现,否则UI状态不同步;Toast.makeText(..., Toast.LENGTH_LONG)用长提示,确保错误信息完整显示。
4. 数据互通:PC端提供RESTful接口,安卓端用HTTP GET直连,不碰数据库也不用Socket
毕业设计最怕“两端数据不一致”。常见错误是PC端存MySQL、安卓端存SQLite,然后靠定时同步——结果答辩时PC端MySQL服务没开,安卓端SQLite数据陈旧,评委问“为什么显示昨天的天气?”,你答“正在同步…”。本方案采用无状态HTTP GET直连:安卓端每次点击“刷新”,都向PC端/weather.do?city=北京发请求,PC端实时调用腾讯天气API(或本地模拟数据),生成JSON响应返回。两端零数据存储,所有状态在HTTP请求-响应周期内完成。
4.1 PC端WeatherController:用HttpURLConnection调腾讯天气API(附备用本地模拟)
// src/com/weather/controller/WeatherController.java package com.weather.controller; import com.weather.model.WeatherResult; import com.weather.util.JsonUtil; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; import java.util.List; public class WeatherController { // 腾讯天气API Key(实际使用需申请,此处用测试Key) private static final String TENCENT_API_KEY = "YOUR_TENCENT_API_KEY"; public void weather(HttpServletRequest req, HttpServletResponse resp) { String city = req.getParameter("city"); if (city == null || city.trim().isEmpty()) { city = "北京"; } try { // 方案1:调腾讯天气API(需联网) WeatherResult result = callTencentWeatherApi(city); // 方案2:若网络不通,降级为本地模拟(确保答辩必成功) if (!result.isSuccess() && !isNetworkAvailable()) { result = mockWeatherData(city); } // 写入响应 resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(JsonUtil.toJson(result)); } catch (Exception e) { WeatherResult error = new WeatherResult(false, "服务器内部错误:" + e.getMessage()); try { resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(JsonUtil.toJson(error)); } catch (Exception ex) { // 最坏情况:连响应都写不出,日志记错 e.printStackTrace(); } } } private WeatherResult callTencentWeatherApi(String city) throws Exception { String urlStr = "https://apis.map.qq.com/ws/weather/v1/?region=" + java.net.URLEncoder.encode(city, "UTF-8") + "&key=" + TENCENT_API_KEY; URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(8000); conn.setReadTimeout(8000); if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8")); StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } reader.close(); conn.disconnect(); return parseTencentResponse(response.toString()); } else { throw new RuntimeException("腾讯API返回错误码:" + conn.getResponseCode()); } } private WeatherResult parseTencentResponse(String jsonStr) { // 腾讯API JSON结构解析(简化版) // 实际需按腾讯文档字段名调整,此处仅示意 try { org.json.JSONObject root = new org.json.JSONObject(jsonStr); org.json.JSONObject result = root.optJSONObject("result"); if (result == null) { return new WeatherResult(false, "API返回数据异常"); } WeatherResult weather = new WeatherResult(true, ""); weather.city = result.optString("location", "未知"); org.json.JSONObject now = result.optJSONObject("now"); if (now != null) { weather.temperature = now.optInt("temp", 0); weather.feelTemp = now.optInt("feels_like", 0); weather.outfitLevel = getOutfitLevel(weather.temperature); // 规则硬编码 } // 穿搭建议生成(根据温度区间) weather.outfitList = generateOutfitList(weather.temperature); return weather; } catch (Exception e) { return new WeatherResult(false, "解析腾讯API失败:" + e.getMessage()); } } private WeatherResult mockWeatherData(String city) { WeatherResult result = new WeatherResult(true, ""); result.city = city; // 模拟北京:温度22℃,体感23℃,穿衣指数“舒适” result.temperature = 22; result.feelTemp = 23; result.outfitLevel = "舒适"; result.outfitList = new ArrayList<>(); result.outfitList.add("长袖衬衫"); result.outfitList.add("薄外套"); result.outfitList.add("休闲裤"); return result; } private boolean isNetworkAvailable() { // 简单检测:尝试连接百度DNS try { java.net.InetAddress.getByName("114.114.114.114").isReachable(2000); return true; } catch (Exception e) { return false; } } private String getOutfitLevel(int temp) { if (temp >= 28) return "炎热"; if (temp >= 22) return "舒适"; if (temp >= 15) return "微凉"; if (temp >= 5) return "寒冷"; return "严寒"; } private List<String> generateOutfitList(int temp) { List<String> list = new ArrayList<>(); if (temp >= 28) { list.add("短袖T恤"); list.add("短裤"); list.add("凉鞋"); } else if (temp >= 22) { list.add("长袖衬衫"); list.add("薄外套"); list.add("休闲裤"); } else if (temp >= 15) { list.add("针织衫"); list.add("风衣"); list.add("牛仔裤"); } else if (temp >= 5) { list.add("毛衣"); list.add("羽绒服"); list.add("围巾"); } else { list.add("加厚羽绒服"); list.add("雪地靴"); list.add("帽子手套"); } return list; } }逻辑说明:
callTencentWeatherApi()用HttpURLConnection调腾讯API,parseTencentResponse()解析返回JSON;mockWeatherData()是保底方案,当网络不可用时返回预设数据;isNetworkAvailable()用InetAddress.getByName("114.114.114.114")检测DNS连通性(比ping域名更快,且不依赖ICMP协议);getOutfitLevel()和generateOutfitList()是硬编码规则,毕业设计无需AI模型,if-else足够。
参数说明:conn.setConnectTimeout(8000)比安卓端的10秒略短,确保PC端快速失败,避免拖慢安卓端;java.net.URLEncoder.encode(city, "UTF-8")再次强调URL编码必要性;result.optString("location", "未知")的默认值防止字段缺失;mockWeatherData()返回固定数据,确保答辩时断网也能演示。
4.2 安卓端IP配置:用adb shell ip route查PC局域网IP,不写死127.0.0.1
// app/src/main/java/com/weather/util/NetworkUtil.java package com.weather.util; import android.content.Context; import android.net.wifi.WifiManager; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.util.Enumeration; public class NetworkUtil { /** * 获取PC端在局域网中的IP(用于安卓连接) * @param context Context * @return 如"192.168.1.100",失败返回"127.0.0.1" */ public static String getPcIpAddress(Context context) { try { WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); int ipAddress = wifiManager.getConnectionInfo().getIpAddress(); if (ipAddress != 0) { return ((ipAddress & 0xff)) + "." + ((ipAddress >> 8) & 0xff) + "." + ((ipAddress >> 16) & 0xff) + "." + ((ipAddress >> 24) & 0xff); } } catch (Exception e) { e.printStackTrace(); } // 备用:枚举所有网络接口找IPv4地址 try { Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { NetworkInterface intf = interfaces.nextElement(); Enumeration<InetAddress> addrs = intf.getInetAddresses(); while (addrs.hasMoreElements()) { InetAddress addr = addrs.nextElement(); if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) { String ip = addr.getHostAddress(); if (ip.startsWith("192.168.") || ip.startsWith("10.") || ip.startsWith("172.16.")) { return ip; } } } } } catch (Exception e) { e.printStackTrace(); } return "127.0.0.1"; // 最终保底 } }逻辑说明:安卓APP不能写死
192.168.1.100,因为不同宿舍WiFi网段不同(有的192.168.0.x,有的10.0.0.x)。getPcIpAddress()先从WiFi连接信息取IP,再枚举所有网卡找私有IPv4地址(192.168.x.x / 10.x.x.x / 172.16.x.x),确保APP启动时自动适配当前局域网。
参数说明:wifiManager.getConnectionInfo().getIpAddress()返回整型IP(如0x6401A8C0 → 192.168.1.100),需位运算转换;addr.getHostAddress()获取字符串IP;ip.startsWith("192.168.")等判断过滤公网IP,只取局域网地址。
5. 避坑指南:毕业答辩前必测的7个致命问题,90%同学栽在第3条
毕业设计答辩不是技术发布会,而是“能否在陌生电脑上3分钟跑通”。以下7个问题,是我带学生答辩时高频翻车点,按发生概率排序,每条都附真实现象、根因和血泪解法:
5.1 现象:安卓APP安装后闪退,Logcat显示java.lang.ClassNotFoundException: com.weather.activity.WeatherActivity
原因:AndroidManifest.xml里<activity>标签未声明,或android:name写错(如漏掉.activity.前缀)
解决:检查app/src/main/AndroidManifest.xml,确认<activity android:name=".activity.WeatherActivity">存在,且android:exported="true"(Android 12+必需);用Android Studio的Build > Analyze APK反编译APK,验证classes.dex里确实包含该类。
5.2 现象:PC端Tomcat启动成功,但浏览器访问http://localhost:8080/weather.do返回404
原因:web.xml中<servlet-mapping>的<url-pattern>与实际请求URL不匹配,或DispatcherServlet类路径写错
解决:在Tomcat日志logs/catalina.out里搜Servlet关键字,确认DispatcherServlet是否被加载;用curl -v http://localhost:8080/weather.do看响应头,若返回HTTP/1.1 404且无Content-Type,说明Servlet未注册;检查web.xml的<servlet-class>是否为com.weather.servlet.DispatcherServlet(注意包名大小写)。
5.3 现象:安卓APP点击“刷新”后Toast显示“网络请求失败(-1)”,但PC端Tomcat日志无访问记录
原因:安卓手机和PC不在同一局域网(手机连WiFi,PC连网线;或手机连热点,PC没连)
解决:这是答辩最大雷!务必在答辩前一晚,用手机浏览器访问http://PC的IP:8080/weather.do?city=北京,若能返回JSON则网络通;若失败,用adb shell ip route查手机IP,ipconfig查PC IP,确认两者在同一网段(如都是192.168.1.x);禁用PC防火墙(Windows Defender防火墙→“高级设置”→入站规则→停用所有规则)。
5.4 现象:JSP页面中文显示为方框(),CSS样式不生效
原因:JSP文件本身编码不是UTF-8,或Tomcatconf/web.xml里<init-param>未设fileEncoding
解决:用Notepad++打开所有JSP,右下角确认编码为UTF-8(无BOM);在conf/web.xml的<servlet>节点里加:
<init-param> <param-name>fileEncoding</param-name> <param-value>UTF-8</param-value> </init-param>重启Tomcat;检查<%@ page contentType="text/html;charset=UTF-8" %>是否在JSP第一行。
5.5 现象:安卓APP调用getWeather("上海")返回{"success":false,"message":"JSON解析失败"}
原因:PC端返回JSON含中文,但resp.setContentType("application/json;charset=UTF-8")未生效,安卓端用InputStreamReader(conn.getInputStream(), "UTF-8")仍读成乱码
解决:在PC端WeatherController.weather()方法开头加req.setCharacterEncoding("UTF-8");在HttpURLConnection响应读取前,加conn.getContentEncoding()日志,确认返回头含charset=utf-8;终极方案:PC端JSON序列化时用new String(json.getBytes("UTF-8"), "ISO-8859-1")强制转码(虽不优雅但保命)。
5.6 现象:答辩现场PC机(Windows 7)启动Tomcat报错UnsupportedClassVersionError
原因:你的代码用JDK 11编译,但学校机房只有JDK 8
解决:在pom.xml(若用Maven)或IDEAFile > Project Structure > Project里,将Project SDK和Language level均设为8 - Lambdas, type annotations etc.;编译输出目录target/classes用javap -verbose YourClass.class | grep "major"验证主版本号为52(JDK 8)。
5.7 现象:安卓APP在Android 10手机上无法访问PC端,提示Cleartext HTTP traffic to xxx not permitted
原因:Android 9+默认禁止明文HTTP请求(http://),只允许HTTPS
解决:在app/src/main/AndroidManifest.xml的<application>节点加:
android:usesCleartextTraffic="true"或在res/xml/network_security_config.xml里配置:
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config> <domain includeSubdomains="true"> <p> <a href="https://download.csdn.net/download/mrluo735/88740810" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>