简介:一份面向JavaWeb课程设计的完整翻译功能实现源码包,适合正在学习Servlet、JSP与MVC架构的开发者用来打通前端交互、后端API调用与缓存优化的完整链路。资源围绕在线翻译场景,覆盖Cookie缓存、Redis缓存、限流与错误处理等关键实践,并配有课程设计报告,便于直接参考或二次开发。压缩包共94个文件,包含java源码、class编译文件、jar依赖库、js/css前端资源、xml配置及docx报告等,整体仅2.23MB,结构清晰,适合快速部署与调试。目前已有197人学习,可作为理解JavaWeb请求处理、第三方API对接及缓存机制的落地样例。
1. 一个翻译功能为什么值得用 JavaWeb 完整走一遍
你在浏览器里输入一句中文,点击“翻译”,不到一秒看到英文结果。这个交互背后不是单个接口调用那么简单,而是一条从 JSP 页面到 Servlet、再到远程翻译 API、最后经由 Redis 缓存返回的完整链路。我拆过一个 javaweb 课程设计项目,代码里有 javax.servlet 依赖、Servlet 工具类、JSP 页面,还有 Redis 缓存的身影,说明它不是在玩具级地“调一个接口”,而是把 Web 应用的核心知识点串进来了。
这个项目的核心是:前端接收用户输入,后端封装 HTTP 请求调用翻译 API,返回结果并在 Redis 和 Cookie 中做两层缓存。对于正在做课程设计的人,它能让你一次性接触 Servlet 生命周期、HTTP 协议、JSON 解析和缓存策略;对于有几年经验的开发者,它的价值在于那些容易被忽略的细节,比如签名编码顺序、API 限流后的重试边界、缓存 key 的粒度怎么设计。下面我把拆解过程、可复现代码和踩坑点完整写出来。
2. 翻译 API 的选型、签名算法与接口协议
2.1 选型:为什么优先考虑百度翻译开放平台
市面上的翻译 API 不少,百度翻译开放平台、有道翻译云、腾讯云机器翻译是课程设计中最常出现的三家。我的选择标准是先看免费额度和接入成本。百度翻译标准版每月有免费调用量,QPS 限制在每秒几次,这个量级足够学习使用;有道的签名方式类似,但控制台配置项更多;腾讯云适合已经在用腾讯云产品的情况。
| 平台 | 免费额度 | 签名方式 | 主要限制 |
|---|---|---|---|
| 百度翻译 | 标准版每月免费量 | MD5(appid+q+salt+密钥) | QPS 被限制在每秒数次 |
| 有道翻译 | 按控制台应用额度 | 签名由应用层生成 | 需要先在云平台建应用 |
| 腾讯云 | 有免费体验包 | 腾讯云 API v3 | 计费维度较多 |
这个表格不是让你照抄选型,而是理解不同厂商的 API 设计思路是一样的:用 AppID 标识调用者,用密钥参与签名,用 QPS 限制保证服务稳定性。实际触发限流时,返回体里通常带“访问频率受限”之类的错误码,比如百度翻译的 54003。所以选型阶段就要想好后端怎么应对这个错误,而不是等上线了再补。
2.2 请求协议:POST 表单、UTF-8 与 MD5 签名
百度翻译 API 的请求是 POST 到/api/trans/vip/translate,参数以表单格式传递,核心参数有q、from、to、appid、salt、sign。sign的算法是取appid + q + salt + 密钥这个字符串做 MD5,结果转成小写十六进制。最容易出错的是顺序:q必须用原始文本,不能在前面做 URL 编码或 HTML 转义后参与签名。
public class SignUtil { public static String computeSign(String appid, String query, String salt, String secretKey) { String raw = appid + query + salt + secretKey; return md5(raw); } private static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("MD5 not supported", e); } } }这段代码里的关键点是 UTF-8 编码。中文文本在签名前必须先编码成字节,如果你的应用默认字符集是 GBK,算出来的 MD5 会和服务器端不一致,最终返回签名错误。另外,salt一般用当前时间戳,同一个时间戳如果重复使用并且请求内容相同,签名也会相同,这在低并发场景没问题,但高并发时最好加上随机数。
2.3 响应 JSON 结构与错误码映射
成功响应的 JSON 格式通常是:
{ "from": "en", "to": "zh", "trans_result": [ { "src": "Hello", "dst": "你好" } ] }失败时不是 HTTP 500,而是返回 HTTP 200 + 错误码,例如{"error_code":"54001","error_msg":"Invalid Sign"}。这就意味着后端不能只看 HTTP 状态码,必须解析响应体里的error_code字段。常见错误码如下:
| 错误码 | 含义 | 处理建议 |
|---|---|---|
| 52001 | 请求超时 | 可以在业务层重试一次 |
| 52002 | 系统错误 | 稍后重试 |
| 54001 | 签名错误 | 检查拼接顺序和密钥 |
| 54003 | 访问频率受限 | 增加后端限流,或查 Redis 缓存 |
| 58001 | 语言方向不支持 | 校验前端传入的 from/to |
这里有一个工程上容易忽略的点:把错误码直接抛给前端是没有意义的,用户只会看到“翻译失败”。比较好的做法是在后端把错误码翻译成用户可读的提示,比如“请求太频繁,请稍后再试”,同时记录完整错误日志,方便排查到底是谁触发了限流。
2.4 JavaWeb 项目骨架与依赖准备
课程设计版本的源码里,典型结构是src/servlet放控制层,src/utils放 HTTP 和签名工具,web/js放前端资源,web/WEB-INF/web.xml做 Servlet 映射。如果使用 Maven,核心依赖如下:
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.10.1</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.4.3</version> </dependency>提示:javax.servlet-api的 scope 设为provided,因为 Tomcat 容器里已经有实现类,如果不加这个 scope,打 WAR 包时会把 servlet-api 也打进去,轻则冲突,重则启动报错。如果你拿到的项目是 Eclipse 直接导出的非 Maven 工程,lib 目录里通常已经放了javax.servlet.jar、javax.annotation.jar等文件,那就不需要再引入 Maven 依赖。
3. Servlet 接收请求、调用翻译 API 与 Redis 缓存的完整实现
3.1 分层设计:为什么不在 Servlet 里直接写 HTTP 调用
很多新手会把远程 API 调用代码直接写在doGet或doPost里,一个方法几百行,没法测试。实际项目中可以拆成四层:Servlet 负责参数解析和 JSON 响应;TranslateService 负责缓存判断和 API 调度;HttpClientUtil 负责发起 POST 请求;RedisCacheUtil 负责缓存读写。拆分的直接收益是,换一家翻译厂商时只改 Service 和 Client,Controller 永远不动。
| 层 | 类名 | 职责 |
|---|---|---|
| 控制层 | TranslateServlet | 读取请求参数,调用 Service,写 JSON |
| 业务层 | TranslateService | 先查缓存,再决定是否调 API |
| 工具层 | HttpClientUtil | 封装 POST、超时、流读取 |
| 缓存层 | RedisCacheUtil | 基于 Jedis 的字符串读写和过期设置 |
3.2 HttpClientUtil:用 HttpURLConnection 发送 POST 表单请求
不引入 Apache HttpClient 或 OKHttp 时,Java 标准库的 HttpURLConnection 足够完成这个功能。下面的工具类接收 URL 和参数 Map,返回响应字符串:
public class HttpClientUtil { public static String postForm(String url, Map<String, String> params, int timeoutMillis) throws IOException { HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); conn.setRequestMethod("POST"); conn.setConnectTimeout(timeoutMillis); conn.setReadTimeout(timeoutMillis); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); String body = params.entrySet().stream() .map(e -> encode(e.getKey()) + "=" + encode(e.getValue())) .collect(Collectors.joining("&")); try (OutputStream os = conn.getOutputStream()) { os.write(body.getBytes(StandardCharsets.UTF_8)); } int code = conn.getResponseCode(); InputStream is = code >= 400 ? conn.getErrorStream() : conn.getInputStream(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { return reader.lines().collect(Collectors.joining("\n")); } } private static String encode(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } }这段代码里有两个关键参数:timeoutMillis一般设 3000~5000 毫秒。太短容易因网络抖动失败,太长会占住 Servlet 线程池。另一个是Content-Type必须写成表单格式,因为翻译 API 接收的是application/x-www-form-urlencoded,不是 JSON。如果你改成application/json,API 会直接拒绝请求。
3.3 TranslateService:Redis 缓存命中、过期与穿透
Redis 在这里的价值是减少 API 调用次数,因为翻译接口的计费和限流都按调用次数算。缓存 key 我习惯设计成trans:{from}:{to}:{md5(q)},用原文的 MD5 生成 key,避免长字符串直接暴露在 key 里。value 直接存翻译结果的 JSON 字符串。
public class TranslateService { private static final int CACHE_TTL_SECONDS = 86400; public String translate(String q, String from, String to) { String cacheKey = buildCacheKey(q, from, to); String cached = RedisCacheUtil.get(cacheKey); if (cached != null) { return cached; } String result = callRemoteApi(q, from, to); RedisCacheUtil.setex(cacheKey, CACHE_TTL_SECONDS, result); return result; } private String buildCacheKey(String q, String from, String to) { return "trans:" + from + ":" + to + ":" + SignUtil.md5(q); } }setex设置了 24 小时过期时间,这是为了避免 Redis 里存太多历史翻译结果导致内存膨胀。课程设计阶段的数据量不大,24 小时足够;如果做成公共服务,建议改成“最近 7 天无访问自动淘汰”,或者直接用 Redis 的EXPIRE设置不固定 TTL。另外要注意,调用失败时不要把错误信息缓存进去,否则后面请求会一直拿到错误结果。
3.4 超时重试与错误码分流
远程 API 调用总会遇到网络抖动或对方服务端异常。我的处理原则是:只有超时类异常才重试,而且最多重试一次;业务错误码如 54003 限流则不要重试,因为重试会放大压力。示例逻辑如下:
try { return service.translate(q, from, to); } catch (SocketTimeoutException e) { return service.translate(q, from, to); } catch (ApiLimitException e) { response.setStatus(429); response.getWriter().write("{\"error\":\"翻译请求太频繁,请稍后再试\"}"); }注意,这里重试翻译请求是幂等的,因为同样的输入会得到同样的结果,不会产生重复数据。但如果你的 API 调用包含“写入”或“扣费”语义,重试前必须确认上一次请求是否真的失败了。对翻译场景来说,重试间隔建议大于 100 毫秒,否则可能连续触发限流。
4. JSP 与 JavaScript 协作:Cookie 缓存、异步刷新和 MVC 边界
4.1 前端为什么需要 Cookie 缓存
翻译 API 有 QPS 限制,用户反复翻译同一句话时,即使 Redis 能扛住,前端也仍然多了一次网络往返。更快的方式是把最近几次翻译结果存在浏览器 Cookie 里,用户点击翻译按钮前先查本地。Cookie 总大小大约 4KB,所以不要存整段长文章,而是存最近 10 条或只存当前输入对应的结果。
4.2 Cookie 读写与编码:别把中文直接塞进 Cookie
JSP 页面本身是服务端渲染,但翻译请求适合用 AJAX 完成,避免整页刷新。下面这段 JavaScript 实现 Cookie 的读取和写入:
function getCookie(name) { const prefix = name + "="; const parts = document.cookie.split("; "); for (const part of parts) { if (part.indexOf(prefix) === 0) { return decodeURIComponent(part.substring(prefix.length)); } } return null; } function setCookie(name, value, hours) { const expires = new Date(Date.now() + hours * 3600 * 1000).toUTCString(); document.cookie = name + "=" + encodeURIComponent(value) + "; expires=" + expires + "; path=/"; }这里必须对 value 做encodeURIComponent,因为翻译结果包含中英文、标点和换行,这些字符在 Cookie 里会被切断或导致解析错误。path=/也很关键,如果不写,Cookie 只对当前 JSP 路径生效,换到其他 Servlet 路径就读取不到。
然后通过fetch调用后端:
async function doTranslate() { const q = document.getElementById("q").value.trim(); if (!q) return; const cachedKey = "t_" + q; const cached = getCookie(cachedKey); if (cached) { document.getElementById("result").innerText = cached; return; } const param = new URLSearchParams(); param.append("q", q); param.append("from", "auto"); param.append("to", "zh"); const resp = await fetch("translateServlet", { method: "POST", body: param }); const json = await resp.json(); const dst = json.trans_result ? json.trans_result[0].dst : "翻译失败"; document.getElementById("result").innerText = dst; setCookie(cachedKey, dst, 24); }这里的URLSearchParams会在发送请求时自动把 body 编码为表单格式,Servlet 端用request.getParameter("q")能直接读取。注意from=auto是让 API 自动识别源语言,翻译有效但会降低一点点准确性,如果是固定中译英,建议在前端写死。
4.3 Cookie 与 Redis 的双层缓存边界
Cookie 和 Redis 属于两层缓存,二者不一定强一致。Redis 更新了某个翻译结果,用户浏览器里的 Cookie 可能还是旧值。我的处理方式是:Cookie 有效期设短一点,比如 24 小时;用户手动清缓存后,请求会落到 Redis,此时两个缓存会有短暂不一致,但翻译结果本身变化不频繁,所以这个策略是可接受的。
| 比较项 | Cookie | Redis |
|---|---|---|
| 存放位置 | 浏览器 | 后端服务器 |
| 容量限制 | 约 4KB | 由内存和配置决定 |
| 生命周期 | expires 控制 | EXPIRE 控制 |
| 适用数据 | 当前用户最近少量记录 | 全站高频翻译结果 |
4.4 Servlet 返回 JSON:Gson 与 UTF-8 编码
Servlet 输出 JSON 时,必须设置Content-Type=application/json; charset=UTF-8,否则中文会出现乱码。推荐用 Gson 把 Map 转成 JSON,而不是手动拼接字符串:
Map<String, Object> result = new HashMap<>(); result.put("from", from); result.put("to", to); result.put("trans_result", List.of(Map.of("src", q, "dst", translated))); response.setContentType("application/json; charset=UTF-8"); response.getWriter().write(new Gson().toJson(result));这段代码里的Map.of要求 Java 9 以上,如果你在 JDK8 环境下开发,换成LinkedHashMap并按顺序 put 即可。注意List.of创建的列表不可变,如果你后面要对trans_result做修改,需要额外new ArrayList<>(...)。这不算大问题,但如果是老项目使用的 javax.servlet 4.0,通常 Tomcat 8.5 + JDK8 就能跑,不需要升级 JDK。
5. 进阶技巧:API 密钥安全、curl 全链路验证与批量翻译拆分
5.1 把密钥请出源码,用 web.xml 配置环境参数
课程设计里最常见的隐患是把 AppID 和密钥硬编码在 Servlet 常量中,一旦源码被提交到 Git 仓库,密钥就等于公开了。常见做法是放到web.xml的context-param,部署时替换成真实值:
<context-param> <param-name>translate.api.appid</param-name> <param-value>你的APPID</param-value> </context-param> <context-param> <param-name>translate.api.secret</param-name> <param-value>你的SECRET</param-value> </context-param>然后在 ServletContext 初始化时读取,存到配置对象中。相比硬编码,这样至少能保证代码和配置分离。如果项目要交到 GitHub,web.xml里也不要写真实密钥,可以用占位符。更工程化的方案是环境变量或 JNDI,但对课程设计来说,做到这一步已经足够展示安全意识。
5.2 用 curl 验证完整链路,确认 Redis 是否命中
部署到 Tomcat 后,先用 curl 绕过浏览器和 JavaScript,直接验证后端接口。假设项目上下文路径是/translate,Servlet 映射为translateServlet:
curl -s -X POST "http://localhost:8080/translate/translateServlet" \ -d "q=hello&from=auto&to=zh"第一次请求会打到远程 API,Redis 中写入缓存;第二次执行相同命令,后端直接从 Redis 读取。想确认 Redis 里真的有缓存,可以执行:
redis-cli --scan --pattern "trans:*"如果看到对应q=hello的 key,说明缓存生效。还可以用redis-cli ttl trans:en:zh:5d41402abc4b2a76b9719d911017c592查看剩余过期时间。验证时注意 curl 的-d默认使用表单编码,正好匹配 Servlet 的读取方式;如果用了-H "Content-Type: application/json",后端就要改成从 request body 里解析 JSON,两者不要搞混。
5.3 批量翻译时的 QPS 控制与文本拆分
翻译 API 对单次请求的文本长度有限制,长文章需要按段落切分成多个请求。直接 for 循环调用会把 QPS 拉满,触发 54003 限流。常见的做法是固定线程池并发,同时用同步阻塞控制请求间隔:
ExecutorService pool = Executors.newFixedThreadPool(4); for (String paragraph : paragraphs) { pool.submit(() -> service.translate(paragraph, "zh", "en")); Thread.sleep(120); } pool.shutdown();这个 120 毫秒的间隔意味着每秒最多约 8 个请求,具体数值要根据你申请的 QPS 配额调整。如果 API 返回 54003,说明间隔还不够大,把 120 改成 150 或 200 毫秒再试。注意,Thread.sleep会让当前线程暂停,如果放在主线程里,页面会等待所有翻译任务完成才返回,建议把整个批量过程放到独立的后台线程中执行,再用轮询接口返回进度。
本文还有配套的精品资源,点击获取