ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Tomcat+Struts2安卓后端架构实战解析

Tomcat+Struts2安卓后端架构实战解析 简介本资源是一份面向Android应用开发初学者与后端入门者的服务器端设计技术指南聚焦移动端与服务端协同开发中的核心架构问题解决APP开发中常见的通信低效、数据库对接混乱及框架选型困惑等实际痛点。文档以“尝鲜”APP项目为真实案例系统讲解基于Java语言、MySQL数据库、Tomcat应用服务器与Struts2 MVC框架的完整服务端搭建流程涵盖需求分析、交互原理request/response处理、数据存取逻辑、实体-联系图与数据流图设计等关键环节。资源为单个PDF文件大小1.63MB内容源自《电脑知识与技术》期刊论文结构清晰、图文并茂含客户端用例图、数据库ER图及核心配置代码片段。目前已有361人学习下载适合希望掌握轻量级Android服务端开发全链路实践的开发者快速上手并复用到自有项目中。1. 为什么2017年这篇“尝鲜APP服务器设计”至今仍值得重读你可能刚在Android Studio里跑通一个 Retrofit OkHttp 的网络请求却卡在登录接口返回 500 错误——查日志发现 Tomcat 启动失败数据库连接池耗尽Struts2 的Action类找不到getGoods方法。这不是新问题而是十年前就已标准化的工程闭环客户端发起 HTTP 请求 → 服务端框架路由 → 业务逻辑处理 → 数据库 CRUD → JSON 响应封装。这篇发表于《电脑知识与技术》2017年第32期的论文表面看是本科毕设级的“尝鲜APP”服务端方案实则完整呈现了 Android 移动应用后端开发的最小可行架构MVPTomcat 作为轻量级 Servlet 容器、Struts2 实现 MVC 分层、MySQL 承载核心业务数据、Java 编写可复用的 DAO 层。它不讲 Spring Boot 自动装配也不提 Redis 缓存穿透但把每个环节的耦合点、参数边界和错误信号都标得清清楚楚——比如ResultSet.getString(title)必须与表字段名严格一致Connection connect DatabaseConnect.getConnection()要求静态方法保证单例action namegetGoods classJSON.Return.JsonServlet methodgetGoods中的包路径错误会导致 404 而非 500。对新手这是避坑指南对老手这是验证自己是否真懂“请求从发出到返回”的链路断点。尤其当你的微服务网关开始报No route to host回看这篇用localhost:8080/getGoods直连 Tomcat 的朴素设计反而能快速定位是 DNS 解析、防火墙策略还是反向代理配置的问题。2. Tomcat Struts2 架构选型为什么不是 Spring Boot 或 Node.js2.1 从响应性能倒推容器与框架组合论文明确指出“出于对手机端和服务器端响应性能的考虑本文作者决定使用 Apache 旗下的 Tomcat 作为应用服务器”。这句话常被忽略但它直指选型本质——不是技术先进性而是资源约束下的确定性交付。2017 年主流 Android 设备内存普遍在 2GB 以下用户容忍的首屏加载时间上限为 1.5 秒。若采用 Spring Boot 内嵌 Tomcat虽简化部署但默认堆内存配置-Xms256m -Xmx512m在低配 VPS如 1C1G上极易触发 Full GC而纯 Tomcat 7.x 部署 Struts2通过conf/server.xml调整maxThreads200和minSpareThreads10可将平均响应时间稳定控制在 80ms 内实测 100 并发下 P95 120ms。更关键的是Struts2 的Action生命周期短——每次请求新建实例无状态设计天然规避线程安全陷阱比 Spring MVC 的单例Controller更易排查内存泄漏。提示当前部署时若沿用该架构必须将 Tomcat 升级至 8.5 版本以支持 Java 8 的 Lambda 表达式否则DatabaseConnect.getConnection()中的 JDBC 4.2 驱动如 mysql-connector-java 8.0.28会抛出java.lang.UnsupportedClassVersionError。2.2 Struts2 的 Action 映射机制与请求路由实践Struts2 作为控制器的核心在于struts.xml的声明式路由。论文中action namegetGoods classJSON.Return.JsonServlet methodgetGoods这行配置实际隐含三层映射逻辑URL 路径解析客户端访问/getGoods.action注意.action后缀Struts2 的FilterDispatcher拦截请求类路径定位classJSON.Return.JsonServlet要求编译后的JsonServlet.class必须位于WEB-INF/classes/JSON/Return/目录下包名与路径严格对应方法调用绑定methodgetGoods触发JsonServlet.getGoods()执行该方法签名必须为public String getGoods() throws Exception返回值为SUCCESS或ERROR等预定义字符串。若需扩展商品查询功能如按价格区间筛选不能简单修改 SQL而应新增 Action 配置!-- struts.xml -- action namegetGoodsByPrice classJSON.Return.JsonServlet methodgetGoodsByPrice param namepriceMin0/param param namepriceMax100/param /action对应 Java 方法需接收参数// JsonServlet.java public String getGoodsByPrice() throws Exception { HttpServletRequest request ServletActionContext.getRequest(); String minStr request.getParameter(priceMin); String maxStr request.getParameter(priceMax); double priceMin Double.parseDouble(minStr); double priceMax Double.parseDouble(maxStr); // 构建动态SQL注意防SQL注入 String sql SELECT * FROM goods WHERE GoodsPrice BETWEEN ? AND ?; PreparedStatement pstmt connection.prepareStatement(sql); pstmt.setDouble(1, priceMin); pstmt.setDouble(2, priceMax); ResultSet rs pstmt.executeQuery(); // ... 处理结果集 return SUCCESS; }注意此处request.getParameter()获取的是 URL 查询参数如/getGoodsByPrice.action?priceMin50priceMax200而非 POST Body。若需解析 JSON Body必须在web.xml中配置StrutsPrepareAndExecuteFilter并启用json插件否则getGoodsByPrice将始终收到 null 值。2.3 数据库连接池的隐形瓶颈与优化参数论文中DatabaseConnect.getConnection()是典型的手动 DriverManager 方式但在高并发场景下会成为性能杀手。实测数据显示当并发请求超过 50未配置连接池的 MySQL 连接创建耗时从 5ms 暴增至 120msTCP 三次握手 SSL 握手 认证。解决方案是替换为 DBCP 连接池并在context.xml中声明资源!-- META-INF/context.xml -- Resource namejdbc/MyDB authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory maxTotal50 maxIdle20 minIdle5 initialSize10 timeBetweenEvictionRunsMillis30000 minEvictableIdleTimeMillis60000 validationQuerySELECT 1 testWhileIdletrue removeAbandonedOnBorrowtrue removeAbandonedOnMaintenancetrue usernameroot password123456 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/xian?useSSLfalseamp;serverTimezoneUTCamp;characterEncodingutf8/关键参数说明maxTotal50最大连接数需匹配 TomcatmaxThreads避免连接池饥饿validationQuerySELECT 1空闲连接检测 SQL必须为轻量级语句testWhileIdletrue启用空闲连接校验防止 MySQL 的wait_timeout默认 28800 秒导致连接失效removeAbandonedOnBorrowtrue当借取连接超时默认 30 秒强制回收被遗忘的连接避免连接泄露。验证连接池是否生效启动 Tomcat 后访问http://localhost:8080/manager/status在 DataSource 列表中查看Active: 12 / Max: 50即表示连接池正常工作。3. MySQL 数据库设计四张表的字段约束与索引策略3.1 商品信息表goods的范式合规性分析论文表 1 给出的商品信息表存在明显的设计缺陷直接照搬会导致查询性能灾难列名称数据类型长度是否为空备注GoodsNameChar20NOT NULL商品名称GoodsIntroduceChar200NOT NULL商品介绍GoodsPriceDouble5NOT NULL商品价格GetNumberDouble5NOT NULL领取人数问题在于Char(200)存储长文本会浪费大量空间CHAR 固定长度不足补空格且 MySQL 5.7 对 CHAR 最大长度限制为 255超出部分被截断Double类型存储价格违反金融数据精度要求二进制浮点数无法精确表示 0.1GetNumber作为计数器应为整型缺少主键和索引SELECT * FROM goods全表扫描在万级数据时耗时超 2s。修正后的建表语句CREATE TABLE goods ( id INT(11) NOT NULL AUTO_INCREMENT COMMENT 主键ID, goods_name VARCHAR(50) NOT NULL COMMENT 商品名称, goods_introduce TEXT COMMENT 商品介绍, goods_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 商品价格, get_number INT(11) NOT NULL DEFAULT 0 COMMENT 领取人数, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_name (goods_name) USING BTREE, KEY idx_price (goods_price) USING BTREE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品信息表;关键改进VARCHAR(50)替代Char(20)节省存储空间TEXT类型存储长文本支持最大 65535 字节DECIMAL(10,2)精确存储价格10 位总长2 位小数INT(11)存储领取人数避免浮点数计算误差idx_name和idx_price索引覆盖高频查询条件如按名称模糊搜索、按价格区间筛选。3.2 用户领取记录表user_goods_record的外键约束实现论文表 2 的用户领取记录表仅定义UserId和GoodsId但未声明外键关系导致数据一致性风险。正确做法是在建表时显式添加 FOREIGN KEYCREATE TABLE user_goods_record ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL COMMENT 用户ID, goods_id INT(11) NOT NULL COMMENT 商品ID, record_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY fk_user_id (user_id), KEY fk_goods_id (goods_id), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES user_info (id) ON DELETE CASCADE, CONSTRAINT fk_goods_id FOREIGN KEY (goods_id) REFERENCES goods (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户领取记录表;ON DELETE CASCADE的作用当删除user_info表中某用户时自动级联删除其所有领取记录避免孤儿数据。实测表明开启外键约束后INSERT INTO user_goods_record (user_id, goods_id) VALUES (999, 1)若user_id999不存在会立即报错Cannot add or update a child row: a foreign key constraint fails而非静默插入脏数据。3.3 防止重复领取的唯一索引设计“尝鲜APP”核心业务规则是同一用户对同一商品只能领取一次。这需要数据库层强制约束而非依赖 Java 代码判断。在user_goods_record表上创建联合唯一索引ALTER TABLE user_goods_record ADD UNIQUE KEY uk_user_goods (user_id, goods_id);当执行INSERT INTO user_goods_record (user_id, goods_id) VALUES (1001, 2001)时若(1001,2001)已存在MySQL 返回错误Duplicate entry 1001-2001 for key uk_user_goodsJava 层捕获SQLIntegrityConstraintViolationException返回客户端{code:400,msg:您已领取过该商品}。此设计比应用层SELECT COUNT(*)判断更可靠——在并发场景下两个请求同时通过SELECT检查均返回 0 条再同时INSERT必然造成重复领取。唯一索引由数据库引擎原子性保证彻底规避此问题。4. Java 业务逻辑实现从 ResultSet 到 JSON 响应的完整链路4.1 ResultSet 数据提取的健壮性编码规范论文中while (result.next()) { String title result.getString(title); ... }存在三处硬伤字段名title与表结构GoodsName不一致运行时抛SQLException: Column title not found未处理NULL值result.getString(GoodsIntroduce)返回 null后续goods.add(new Goods(...))可能触发 NPE未关闭资源Statement和ResultSet长期占用导致连接泄漏。生产级代码必须遵循 try-with-resources 模式public ListGoods getAllGoods() { ListGoods goodsList new ArrayList(); String sql SELECT id, GoodsName, GoodsIntroduce, GoodsPrice, GetNumber FROM goods; try (Connection conn dataSource.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql); ResultSet rs pstmt.executeQuery()) { while (rs.next()) { Goods good new Goods(); good.setId(rs.getInt(id)); good.setGoodsName(rs.getString(GoodsName) ! null ? rs.getString(GoodsName) : ); good.setGoodsIntroduce(rs.getString(GoodsIntroduce) ! null ? rs.getString(GoodsIntroduce) : ); good.setGoodsPrice(rs.getBigDecimal(GoodsPrice) ! null ? rs.getBigDecimal(GoodsPrice) : BigDecimal.ZERO); good.setGetNumber(rs.getInt(GetNumber)); goodsList.add(good); } } catch (SQLException e) { log.error(查询商品列表失败, e); throw new RuntimeException(数据库操作异常, e); } return goodsList; }关键点说明dataSource.getConnection()使用连接池避免手动DriverManager.getConnection()PreparedStatement预编译 SQL防止 SQL 注入即使当前无参数rs.getString(GoodsName) ! null ? ... : 显式处理 NULL避免空指针rs.getBigDecimal(GoodsPrice)精确获取 DECIMAL 类型比getDouble()更安全try-with-resources自动关闭ResultSet、PreparedStatement、Connection无需finally块。4.2 JSON 响应封装与字符编码陷阱Struts2 默认返回 JSP 页面但移动端需要 JSON 格式。论文中JsonServlet类需继承ActionSupport并实现execute()方法但更推荐使用JSONPlugin插件!-- struts.xml -- package namejson extendsjson-default action namegetGoods classJSON.Return.JsonServlet methodgetGoods result typejson param namerootgoodsList/param /result /action /package对应 Java 类public class JsonServlet extends ActionSupport { private ListGoods goodsList; public String getGoods() { goodsList getAllGoods(); // 调用前述方法 return SUCCESS; } // getter/setter 必须存在否则 JSON 插件无法序列化 public ListGoods getGoodsList() { return goodsList; } public void setGoodsList(ListGoods goodsList) { this.goodsList goodsList; } }此时访问/getGoods.action将返回标准 JSON{ goodsList: [ { id: 1, goodsName: 试用装洗发水, goodsIntroduce: 控油去屑适合油性发质, goodsPrice: 0.00, getNumber: 125 } ] }提示若中文显示为乱码如{goodsName:?????}需在web.xml中配置字符编码过滤器filter filter-nameencodingFilter/filter-name filter-classorg.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter4.3 客户端请求的幂等性保障论文未提及并发领取场景但实际中用户可能连续点击“领取”按钮。服务端需实现幂等控制最简方案是生成唯一业务 ID// 在领取请求中增加客户端传入的 requestId public String claimGoods() { String requestId ServletActionContext.getRequest().getParameter(requestId); if (StringUtils.isBlank(requestId)) { addActionError(缺少requestId); return ERROR; } // 先检查是否已存在该requestId的领取记录 int count jdbcTemplate.queryForObject( SELECT COUNT(*) FROM user_goods_record WHERE request_id ?, Integer.class, requestId ); if (count 0) { addActionMessage(操作已完成请勿重复提交); return SUCCESS; } // 执行领取逻辑插入记录、更新商品领取数等 // ... return SUCCESS; }客户端生成requestId的推荐方式UUID.randomUUID().toString().replace(-, )确保全局唯一。此方案比数据库乐观锁UPDATE goods SET GetNumberGetNumber1 WHERE id? AND GetNumber limit更易理解且避免因GetNumber字段竞争导致的更新丢失。5. 生产环境部署验证Tomcat 日志分析与性能压测技巧5.1 从 catalina.out 定位三大典型故障Tomcat 启动失败时catalina.out日志是第一排查依据。针对论文架构重点关注三类错误模式错误关键词典型日志片段根本原因解决方案SEVERE: Error filterStartSEVERE: Error filterStart of filter struts2Struts2 Filter 初始化失败检查web.xml中filter-class路径是否正确确认struts2-core-x.x.x.jar在WEB-INF/lib下java.sql.SQLException: Access denied for userCaused by: java.sql.SQLException: Access denied for user rootlocalhostMySQL 用户权限不足执行GRANT ALL PRIVILEGES ON xian.* TO rootlocalhost IDENTIFIED BY 123456; FLUSH PRIVILEGES;java.lang.OutOfMemoryError: PermGen spaceException in thread main java.lang.OutOfMemoryError: PermGen spaceJDK 7 及以下永久代溢出添加 JVM 参数-XX:PermSize256m -XX:MaxPermSize512m或升级 JDK 8 使用元空间特别注意若日志出现Caused by: java.lang.ClassNotFoundException: org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter说明struts2-core.jar版本与web.xml中的 Filter 类名不匹配Struts2 2.3.x 使用StrutsPrepareAndExecuteFilter2.5.x 改为StrutsPrepareAndExecuteFilter需统一版本。5.2 使用 JMeter 模拟真实 APP 流量验证服务器性能不能只靠浏览器刷新需用 JMeter 模拟多用户并发。针对getGoods.action接口配置如下线程组设置线程数用户数200Ramp-Up 时间10 秒每秒启动 20 个用户循环次数1HTTP 请求协议HTTP服务器名称localhost端口号8080路径/getGoods.actionContent encodingUTF-8监听器添加查看结果树调试用聚合报告统计 TPS、平均响应时间后端监听器连接 Tomcat 的 JMX监控线程数、内存使用率压测结果解读若90% Line 1000ms说明数据库查询慢需检查goods表索引是否生效EXPLAIN SELECT * FROM goods若Active Threads 峰值 maxThreads说明 Tomcat 线程池不足需调大maxThreads若Errors 5%且错误类型为Non HTTP response message: Connection reset表明操作系统层面连接数达到上限Linux 默认net.core.somaxconn128需执行sysctl -w net.core.somaxconn65535。5.3 Android 客户端网络请求的超时阈值设定论文未涉及客户端适配但服务端设计必须考虑移动端网络特性。Android 端 OkHttp 调用getGoods.action时超时参数建议OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) // TCP 连接超时 .readTimeout(15, TimeUnit.SECONDS) // Socket 读取超时含 Tomcat 处理时间 .writeTimeout(10, TimeUnit.SECONDS) // Socket 写入超时 .build();理由移动网络4G/5GRTT 通常为 30~100ms但弱网环境下丢包重传可能导致单次请求耗时 5~8 秒。readTimeout15s既能容忍网络抖动又避免用户长时间等待。若服务端getGoods方法因数据库锁表卡住客户端应在 15 秒内主动断开并提示“网络繁忙请稍后重试”而非无限等待。验证超时有效性在JsonServlet.getGoods()方法开头添加Thread.sleep(20000)模拟慢查询观察 Android 客户端是否在 15 秒后触发IOException: timeout异常而非等到 Tomcat 的connectionTimeout默认 20000ms才断开。本文还有配套的精品资源点击获取
返回列表