家校通源码解析:Android与Servlet数据库请求链路全拆解

发布时间:2026/9/16 14:56:03
家校通源码解析:Android与Servlet数据库请求链路全拆解 简介毕业设计选题为基于Android的校园家校通系统面向高校计算机相关专业学生、Android开发者以及需要快速搭建家校互通项目的毕设人员。系统按学生、教师两类角色设计学生端涵盖注册登录、课程表查询、在线作业提交、资料信息查看教师端包含用户管理、作业发布与批改、资料公告管理、家长咨询回复等模块整体功能完整适合作为毕业设计参考或二次开发基础。压缩包共395个文件包含67个Java源码、26个JSP页面、71个class文件、110个PNG图片、59个GIF动图以及jar依赖、SQL脚本等其中Java与JSP构成核心业务逻辑PNG和GIF展示界面效果与操作流程SQL脚本可直接初始化数据库便于整体部署。资源包大小约6.5MB目录结构清晰。目前已有116人学习浏览项目中附带说明文档可帮助快速理解模块划分与实现思路对完成论文、演示和答辩均有实际参考价值。1. 这份家校通源码真正值得拆的是 HttpUtil 到 JdbcUtils 这条调用链拿到这个 Android 家校通源码包我先翻的不是界面布局而是HttpUtil、JdbcUtils、RegisterAction这类类名。它跟那些只给客户端的“假毕设”不一样服务端逻辑也在包里学生注册登录、作业提交、家长咨询都能单独跑起来。对准备答辩的人来说最大的价值不是界面有多漂亮而是你能看着一条请求从 Android 端HttpUtil.doPost()发起经过 Servlet 的 Action 层落到RegisterDao/MessageDao最后通过JdbcUtils连上 MySQL。适合两类人一类是拿它当毕设底稿想快速改功能的在校生另一类是刚接手老项目、需要在短时间里看懂 Android 服务端连通逻辑的初级开发。下面我按请求链路顺序拆。2. 登录注册的请求链路RegisterAction 与 RegisterDao 如何配合 JdbcUtils2.1 JdbcUtils 的作用静态代码块还是连接池JdbcUtils在这个包里承担的是“数据库连接入口”。它从jdbc.properties读取四个参数jdbc.driver负责通知 JDBC 加载哪个数据库驱动jdbc.url决定连哪台机器上的 MySQL 实例jdbc.username和jdbc.password控制登录权限。一个合格的JdbcUtils会把参数读取放在静态代码块中这样类加载时只解析一次配置不会每次getConnection()都重复读文件。下面这种写法是毕业设计源码里最常见的public class JdbcUtils { private static String url; private static String username; private static String password; static { try { Properties props new Properties(); props.load(JdbcUtils.class.getClassLoader() .getResourceAsStream(jdbc.properties)); Class.forName(props.getProperty(jdbc.driver)); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }这里有两个细节第一Class.forName(...)在 MySQL 8 之后依然可以省略但保留它能让代码在 JDK 8 MySQL 5.7 的旧环境里稳定运行第二url里建议带上useSSLfalse和serverTimezoneAsia/Shanghai否则连接串会因为时区问题直接抛SQLException。常见配置参数如下表直接抄进jdbc.properties就能用参数示例值作用jdbc.drivercom.mysql.cj.jdbc.DriverMySQL 8 驱动的全类名5.x 用 com.mysql.jdbc.Driverjdbc.urljdbc:mysql://localhost:3306/jxt?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai数据库连接串中文乱码问题多半是少了 URL 参数jdbc.usernameroot本地开发账号jdbc.password123456仅演示用答辩前改成随机口令更强initialPoolSize5若换成 C3P0/Druid 时保留的初始连接数注意这段代码没有连接池每次请求都新建物理连接。在毕设并发量下没问题但如果答辩现场有人问“为什么不用连接池”可以回答注册和登录动作不频繁数据量小DriverManager足够演示线上项目会换成 Druid核心改动只在getConnection()内部不影响 DAO 层。这里真正要小心的不是连接池而是连接有没有关闭。用try (Connection conn JdbcUtils.getConnection())这种方式Connection和PreparedStatement会在 try 结束时自动释放否则 Tomcat 跑一个晚上就会出现Too many connections。2.2 RegisterAction 与 RegisterDao 的分工界限打开RegisterAction和RegisterDao可以看出一个明确的职责划分Action 负责接收 HTTP 参数Dao 只负责数据库读写。源码包里没有单独建LoginAction类而是用同一个RegisterAction配合action参数区分登录和注册分支这种组织方式在毕业设计里很常见。它的doPost()大致长这样WebServlet(/register) public class RegisterAction extends HttpServlet { private RegisterDao registerDao new RegisterDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); resp.setContentType(application/json;charsetutf-8); if (login.equals(action)) { String username req.getParameter(username); String password req.getParameter(password); User user registerDao.findUser(username, password); if (user ! null) { resp.getWriter().write({\code\:0,\role\: user.getRole() }); } else { resp.getWriter().write({\code\:1,\msg\:\账号或密码错误\}); } } else if (register.equals(action)) { String username req.getParameter(username); String password req.getParameter(password); String realname req.getParameter(realname); int role Integer.parseInt(req.getParameter(role)); boolean ok registerDao.insertUser(username, password, realname, role); resp.getWriter().write(ok ? {\code\:0,\msg\:\注册成功\} : {\code\:1,\msg\:\用户名重复\}); } } }角色用整数表示1学生2教师3家长。因为这个家校通里家长不直接注册而是由教师端“用户管理”创建所以RegisterAction先只开放学生自注册家长账号走另一条插入通道。RegisterDao.insertUser()内部的 SQL 不建议用字符串拼接常见误用是写成insert into t_user values( username )只要用户名里带上单引号SQL 就会碎掉。正确的做法是用PreparedStatement占位符public boolean insertUser(String username, String password, String realname, int role) { String sql insert into t_user(username, password, realname, role, create_time) values(?,?,?,?,now()); try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ps.setString(3, realname); ps.setInt(4, role); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }findUser查询也是同样的套路用select id, username, realname, role, class_id from t_user where username? and password?如果拿不到 ResultSet 就返回null。这里要特别提醒源码为了演示方便密码通常存的是明文。答辩时不要回避这个问题直接说明生产环境必须改成 BCrypt 哈希存储登录时校验哈希而不是比对明文这种答案反而比硬夸源码安全更让老师认可。2.3 Android 端 HttpUtil 如何把参数送过去HttpUtil是客户端调用的核心它把MapString, String形式的参数编码到 HTTP Body然后读取服务端返回的字符串。源码包里的HttpUtil如果是用HttpURLConnection写的多半长这样public class HttpUtil { public static String doPost(String urlStr, MapString, String params) throws IOException { HttpURLConnection conn null; try { URL url new URL(urlStr); conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(POST); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.setDoOutput(true); StringBuilder sb new StringBuilder(); for (Map.EntryString, String entry : params.entrySet()) { sb.append(URLEncoder.encode(entry.getKey(), UTF-8)); sb.append(); sb.append(URLEncoder.encode(entry.getValue(), UTF-8)); sb.append(); } sb.deleteCharAt(sb.length() - 1); conn.getOutputStream().write(sb.toString().getBytes(UTF-8)); return readFromStream(conn.getInputStream()); } finally { if (conn ! null) { conn.disconnect(); } } } }逻辑不复杂但有几个参数需要理解setConnectTimeout(5000)是连接到服务器的最长等待如果校园网 DHCP 慢或 WiFi 需要认证这里会先抛SocketTimeoutExceptionsetReadTimeout(5000)是服务器执行完 SQL 后写回的等待时间作业提交这类接口要单独放宽到 15 秒。URLEncoder.encode把中文参数转成%E4%BD%A0%E5%A5%BD这种编码否则服务端getParameter()取到的是乱码。调用注册页面的代码可以这样写把HttpUtil的返回值直接用JSONObject解析MapString, String params new HashMap(); params.put(action, register); params.put(username, etUsername.getText().toString()); params.put(password, etPassword.getText().toString()); params.put(realname, etRealname.getText().toString()); params.put(role, 1); new Thread(() - { try { String result HttpUtil.doPost(baseUrl /register, params); JSONObject json new JSONObject(result); runOnUiThread(() - toast(json.optString(msg))); } catch (Exception e) { runOnUiThread(() - toast(网络异常 e.getMessage())); } }).start();这里必须把HttpUtil调用放在子线程里HttpURLConnection会在 Android 4.0 以后直接以NetworkOnMainThreadException拒绝在主线程联网。还要注意Android 9 开始默认禁用明文 HTTP如果服务端是http://而不是https://需要在AndroidManifest.xml的application节点加上android:usesCleartextTraffictrue。我经常看到源码包在 Android Studio 里一跑就报CLEARTEXT communication not permitted十有八九是少了这个属性。3. 家校通信模块的数据流Chatroom、Group、Message 与 MessageDao3.1 Group 与 Chatroom 拆开建模的理由这个项目的类清单里同时出现了Group和Chatroom第一次看很容易觉得字段重复。Group可以理解为“班级群”它记录群名、群主、创建时间Chatroom是“具体的聊天窗口”它挂在某个群下面比如“班级通知”和“作业讨论”是同一个Group下的两个不同Chatroom。把两者拆开是为了复用群成员关系同时让每个聊天室有自己的类型和最近消息记录。表名关键字段作用t_groupgroup_id, group_name, owner_id, create_time维护群组基本信息owner_id 对应教师的 user_idt_chatroomroom_id, group_id, room_name, room_type, create_time维护聊天室room_type 区分通知群、讨论群、家长群t_messagemessage_id, room_id, sender_id, sender_role, content, msg_type, send_time存储一条条消息msg_type 为 0 文本、1 图片、2 作业提醒对应到 Java 类Group.class映射t_groupChatroom.class映射t_chatroomMessage.class映射t_message。客户端在进聊天室前先请求Chatroom列表拿到roomId后再按房间拉消息。服务端MessageAction接收roomId、lastId、pageSize三个参数返回一个 JSON 数组。MessageDao负责写入和查询它的insertMessage()通常这样写public int insertMessage(Message msg) { String sql insert into t_message(room_id, sender_id, sender_role, content, msg_type, send_time) values(?,?,?,?,?,now()); try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, msg.getRoomId()); ps.setInt(2, msg.getSenderId()); ps.setInt(3, msg.getSenderRole()); ps.setString(4, msg.getContent()); ps.setInt(5, msg.getMsgType()); return ps.executeUpdate() 0 ? 1 : 0; } catch (SQLException e) { e.printStackTrace(); return 0; } }注意send_time直接交给 MySQL 的now()而不是在 Java 代码里拼SimpleDateFormat。这样数据库服务器和应用服务器的时钟不一致时以数据库时间为准避免消息顺序错乱。sender_role字段在渲染端很关键教师发的通知和家长的提问要用不同气泡颜色区分这个字段省去了客户端再去查用户表的麻烦。3.2 分页拉取时为什么用 lastId 而不是 page聊天消息的查询和普通列表不太一样。普通列表用LIMIT 0,20翻页但消息列表在每次轮询时只关心“我上次看到哪一条之后的新消息”。用message_id游标比page更稳定因为新消息插入不会影响旧分页位置。MessageDao里可以提供一个这样的查询public ListMessage findMessages(int roomId, int lastId, int pageSize) { String sql select message_id, room_id, sender_id, sender_role, content, msg_type, send_time from t_message where room_id? and message_id? order by message_id asc limit ?; ListMessage list new ArrayList(); try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, roomId); ps.setInt(2, lastId); ps.setInt(3, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Message msg new Message(); msg.setMessageId(rs.getInt(message_id)); msg.setSenderRole(rs.getInt(sender_role)); msg.setContent(rs.getString(content)); list.add(msg); } } } catch (SQLException e) { e.printStackTrace(); } return list; }这里使用message_idlastId而不是避免一条消息被重复拉取order by message_id asc保证客户端按时间正序插入RecyclerView省去翻转列表的代码。LIMIT pageSize要放在 SQL 最后并且pageSize在 Android 端参数里固定为20。如果消息数量增长到上万条message_id游标仍然比offset高效这也是企业里聊天系统做增量拉取的通用做法。3.3 客户端用轮询还是长连接聊天室消息要实时但毕业设计源码一般不会引入WebSocket因为它需要另写握手协议、心跳包和服务端点。更常见的是用Handler.postDelayed()做固定间隔轮询每 3 秒请求一次新消息。Android 端把lastId存在内存变量里收到响应后更新Adapterprivate final Handler handler new Handler(Looper.getMainLooper()); private final Runnable pollTask new Runnable() { Override public void run() { new Thread(() - { MapString, String params new HashMap(); params.put(roomId, String.valueOf(roomId)); params.put(lastId, String.valueOf(lastId)); params.put(pageSize, 20); try { String json HttpUtil.doPost(baseUrl /message/poll, params); ListMessage result parseMessageList(json); if (!result.isEmpty()) { lastId result.get(result.size() - 1).getMessageId(); } handler.post(() - adapter.addNewMessages(result)); } catch (IOException e) { Log.e(Poll, poll error, e); } finally { handler.postDelayed(pollTask, 3000); } }).start(); } };这种写法把run末尾的postDelayed放在finally里即使网络异常下一轮依然会继续消息不会停在出错的那一次。轮询间隔取 3 秒是权衡结果太短会增加 Tomcat 线程压力太长让“家长咨询通知”显得迟钝。实验室里同一个服务器挂着 50 个模拟器每 3 秒一次基本不会把MessageDao打死如果真出现Connection refused先查 MySQL 的max_connections而不是改轮询代码。另外onDestroy()里记得handler.removeCallbacks(pollTask)否则 Activity 销毁后轮询仍在执行这是经典的内存泄漏来源。4. 作业提交与资料管理从 multipart 上传到批改状态回写4.1 作业表为什么需要两张表而不是一张作业功能在代码里通常表现为两个 JavaBean作业表t_homework和提交表t_homework_submit。前者由教师发布后者由学生填写二者通过hw_id关联。如果只做一张t_homework表同一个学生交第二次作业就要覆盖第一次的记录批改历史会丢。下面是一个可以在 MySQL 里直接执行的建表结构CREATE TABLE t_homework ( hw_id INT PRIMARY KEY AUTO_INCREMENT, class_id INT NOT NULL, teacher_id INT NOT NULL, title VARCHAR(100) NOT NULL, content TEXT, attach_url VARCHAR(255), deadline DATETIME, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_homework_submit ( id INT PRIMARY KEY AUTO_INCREMENT, hw_id INT NOT NULL, student_id INT NOT NULL, submit_content TEXT, attach_url VARCHAR(255), status TINYINT DEFAULT 0, score INT, comment TEXT, submit_time DATETIME, review_time DATETIME );status字段是一个小型状态机教师端“批改作业”模块就是根据这个字段过滤记录的status含义教师端显示0未提交灰色标签1已交待批橙色按钮点击进入批改2已批改显示分数和评语资料管理的表结构可以复用t_homework的思路单独建t_resource字段多一个sort_order用来做置顶排序。学生端“资料信息”页面请求这个表时按sort_order desc, create_time desc排序教师上传新资料后不需要改客户端逻辑数据自然排到最前面。4.2 Android 端作业文件上传手写 multipart 还是用现成库作业提交中最容易翻车的是文件上传。老一点的项目用MultipartEntity新项目直接引OkHttp但这份源码既然用HttpUtil做请求大概率是HttpURLConnection手写请求体。手写multipart/form-data的关键是每一步都要符合 HTTP 协议格式private static String upload(String urlStr, String filePath, MapString, String fields) throws IOException { String boundary ----JiaXiaoTong System.currentTimeMillis(); HttpURLConnection conn (HttpURLConnection) new URL(urlStr).openConnection(); conn.setRequestMethod(POST); conn.setConnectTimeout(8000); conn.setReadTimeout(15000); conn.setDoOutput(true); conn.setRequestProperty(Content-Type, multipart/form-data; boundary boundary); DataOutputStream out new DataOutputStream(conn.getOutputStream()); for (Map.EntryString, String entry : fields.entrySet()) { out.writeBytes(-- boundary \r\n); out.writeBytes(Content-Disposition: form-data; name\ entry.getKey() \\r\n\r\n); out.writeBytes(entry.getValue() \r\n); } String fileName filePath.substring(filePath.lastIndexOf(/) 1); out.writeBytes(-- boundary \r\n); out.writeBytes(Content-Disposition: form-data; name\file\; filename\ fileName \\r\n); out.writeBytes(Content-Type: application/octet-stream\r\n\r\n); FileInputStream fis new FileInputStream(filePath); byte[] buffer new byte[4096]; int len; while ((len fis.read(buffer)) ! -1) { out.write(buffer, 0, len); } out.writeBytes(\r\n-- boundary --\r\n); out.writeBytes(\r\n); fis.close(); out.flush(); out.close(); int code conn.getResponseCode(); if (code 200) { return new String(readAllBytes(conn.getInputStream()), UTF-8); } return {\code\:1,\msg\:\upload fail\}; }boundary是分隔线前后必须保持完全一致否则服务器端Part无法解析。普通字段在分隔线之后直接写\r\n\r\n文件字段要先写文件名再写二进制数据最后以--boundary--收尾。这里我把readTimeout放宽到 15 秒因为作业文件一般有几百 KB加上是校园网络默认 5 秒很容易在文件传输一半时断开。服务端用MultipartConfig接收时记得同步把maxFileSize调到 10 MB 以上否则会抛出FileSizeLimitExceededException。实际中如果不熟悉手写协议用 OkHttp 的MultipartBody更稳但答辩时能解释清楚Content-Disposition的细节比引包更有说服力这就是我推荐手写HttpURLConnection版本的原因。4.3 批改状态与家长咨询的更新事务和回滚教师批改作业不只是update t_homework_submit一个动作还要给学生和家长发一条通知。代码写不好就会出现“分数改了家长端没提醒”的问题。要解决它最简单的做法是把两条 SQL 包在同一个事务里public boolean reviewHomework(int submitId, int score, String comment) throws SQLException { String updateSubmit update t_homework_submit set status2, score?, comment?, review_timenow() where id?; String insertNotice insert into t_message(room_id, sender_id, sender_role, content, msg_type, send_time) values(1, ?, 2, ?, 3, now()); Connection conn JdbcUtils.getConnection(); try { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(updateSubmit)) { ps1.setInt(1, score); ps1.setString(2, comment); ps1.setInt(3, submitId); ps1.executeUpdate(); } try (PreparedStatement ps2 conn.prepareStatement(insertNotice)) { ps2.setInt(1, submitId); ps2.setString(2, 作业已批改得分 score); ps2.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }注意这里把sender_role写成2表示消息由教师角色发出msg_type3是自定义的作业提醒类型客户端收到后可以在聊天列表里显示一个小红点。conn.setAutoCommit(false)之后任何一条 SQL 失败都会回滚保证作业状态和通知消息不会出现“半成品”。家长咨询管理也是一样的套路t_consult表记录家长提问和教师回答两个字段教师端回复时执行一次update t_consult set answer?, reply_timenow(), status1 where consult_id? and status0这个and status0能防止前端连点两次造成的重复回复覆盖。5. 部署与排错从源码包到演示前要处理的三个细节5.1 Android Studio 打开源码包后先做环境对齐拿到源码包后不要急着点 Run。先打开AndroidManifest.xml确认最小的minSdkVersion再看build.gradle里的compileSdkVersion和targetSdkVersion。老项目经常用compileSdkVersion 28在最新的 Android Studio 里同步时会报NDK或Kotlin版本不匹配正确做法是打开 Android Studio 的 SDK Manager确认 Android SDK 平台版本已经装好然后把compileSdkVersion改成已安装的版本并同步更新依赖里androidx的版本号。5.2 用 curl 先验证服务端再打开 App在 Android Studio 里点 Run 之前先用 curl 在电脑上确认服务端接口是通的。这条命令模拟了 Android 端HttpUtil.doPost()发送的请求curl -s -X POST \ -d actionloginusernamestu01password123456 \ http://127.0.0.1:8080/register返回{code:0,role:1}说明 Servlet 和 MySQL 都正常返回 404 就要检查 Web 应用部署路径和WebServlet(/register)是否一致返回 500 则去 Tomcat 的logs/localhost.log里找异常栈。这一步能帮你把“App 问题”和“后端问题”快速分开不用每次都在模拟器里瞎点。如果服务端跑在电脑上Android 模拟器访问http://10.0.2.2:8080/指向宿主机真机访问http://192.168.x.x:8080/需要替换为电脑的局域网 IP而且 Windows 防火墙要放行 8080 端口。5.3 把异常边界写清楚比顺畅跑完更有答辩价值很多毕业设计源码为了演示顺利把catch块都写成e.printStackTrace()或者toast(系统异常)。真实答辩时考官更愿意看到你主动处理“连接失败”“作业文件太大”“手机断网”这些边界场景。在HttpUtil里把SocketTimeoutException单独 catch 一次提示“连接超时请检查 Tomcat 是否启动”比只弹“网络异常”更有区分度。调试阶段可以临时改用adb shell抓大段日志但演示机上保留Log.e输出会让讲解更可信。我一般还会在jdbc.properties里把密码换成随机串演示前再改回来然后用 curl 连着跑三遍登录接口确定连接池没有把异常抛到前端再开始录屏演示。本文还有配套的精品资源点击获取