简介这是一份面向 Java Web 学习者的饿了么平台后端项目实现采用 Vue.js 搭建前端界面、Servlet 处理请求、AJAX 完成异步数据交互整体为前后端分离架构适合正在练习 Servlet、Vue 与接口联调的开发者参考。压缩包共 131 个文件约 4.15MB包含 Java 源码与编译 class、Vue/HTML/CSS 页面样式、less/scss、JS 配置、项目工程文件及图片素材等可以对照学习登录校验、商品查询、订单处理等后端逻辑以及前端如何通过 JSON 与服务端通信。已有 1435 人学习说明该项目对同类课程设计或毕业设计有较高参考价值。通过阅读目录和源码可掌握 Servlet 生命周期、AJAX 请求封装、前后端分离部署及常见数据库操作思路适合快速搭建示例或改造为个人项目。1. 别把仿外卖平台后端想复杂了提到外卖平台后端很多人第一反应是微服务和分布式系统。我最近拆的这个仿某外卖平台后端是一个很典型的 JavaWeb 前后端分离应用VUE 负责页面Servlet 提供接口AJAX 负责异步取数。项目不大但把用户登录、菜品展示、下单的完整链路串起来了。只要搞明白 Servlet 生命周期、Session 和 JSON基本没有黑匣子。这个项目适合正在学 JavaWeb、想看看 Servlet 怎么配合 VUE 的同学也适合想补前端知识的后端开发者。它没有引入 Spring Boot请求处理过程更透明浏览器 Network 里能看到每个请求怎么发出去、Servlet 怎么处理、JSON 怎么返回。往下拆就是一个标准的前端工程加后端服务双目录结构跑起来并不玄学。2. 技术选型和整体架构从 Tomcat 到 VUE 的前后端分离链路2.1 为什么是 Servlet AJAX而不是 Spring Boot先明确这个项目不是商业级的系统而是一个教学向的完整闭环。用 Servlet 而不是 Spring Boot我理解是刻意保留 HTTP 请求处理的“原始味”。Servlet 的 doGet/doPost 让你直接面对 HttpServletRequest 和 HttpServletResponse能清楚看到参数解析、编码设置、响应写出这些底层动作Spring Boot 里的 RestController 把这些全部封装成了注解新手反而容易把框架当黑匣子。AJAX 在这里的角色是 VUE 与 Servlet 之间的数据通道。前端不跳转页面只是向后端某个 URL 发异步请求收到 JSON 后更新 DOM 或 Store。这种模式在真实企业项目里很常见只是企业通常会把 Servlet 换成一个更重的框架。这个项目用 Servlet 实现后端接口层正好验证了“前后端分离”不依赖特定框架只要约定好请求格式和响应格式就可以。所以技术栈的选择逻辑是语言用 JavaWeb 容器用 Tomcat后端用 Servlet JDBC前端用 VUE axios交互格式用 JSON。没有引入 SSM也没有用 MyBatis数据访问层就是手写 JDBC。这样每一行代码都在你面前调试时能一步一步跟进去。2.2 工程目录后端 war 包与前端静态资源的界线我看到的源码按前后端分成了两个顶层目录一个是后端 JavaWeb 工程最终打成 war 包放到 Tomcat 的 webapps 下另一个是 VUE 工程跑在 Node 的 dev server 上通过代理把 /api 前缀的请求转发到 Tomcat。这是前后端分离最直接的目录形态。foodie-web ├── backend │ ├── src │ │ ├── main │ │ │ ├── java/com/foodie │ │ │ │ ├── servlet │ │ │ │ │ ├── BaseServlet.java │ │ │ │ │ ├── UserServlet.java │ │ │ │ │ ├── DishServlet.java │ │ │ │ │ └── OrderServlet.java │ │ │ │ ├── service │ │ │ │ ├── dao │ │ │ │ ├── entity │ │ │ │ └── util │ │ │ │ ├── DBUtil.java │ │ │ │ └── JsonUtil.java │ │ │ └── resources │ │ │ └── db.properties │ │ └── webapp │ │ └── WEB-INF/web.xml │ └── pom.xml └── frontend ├── public ├── src │ ├── api │ ├── router │ ├── store │ ├── views │ └── App.vue ├── package.json └── vue.config.js这段目录结构对应了三个关键约定。第一servlet 包下所有类都继承 BaseServletBaseServlet 做的事情是统一读取 action 参数再反射调用子类的方法第二entity 里的每个类对应一张表字段名和表列名保持一致第三前端 src/api 里每个函数对应后端一个 Servlet 的 action例如login(user)对应UserServlet?actionlogin。这种“一个 action 一个方法”的设计让接口排查起来非常方便哪里出问题直接定位到方法。后端没有用 Spring所以事务管理和数据库连接都写在一个 DBUtil 里。db.properties 配置 MySQL 的连接地址、用户名和密码DBUtil 通过静态代码块加载驱动。前端也没有用大型 UI 框架而是用原生 VUE ElementUI 或者手写样式这也符合教学项目的定位——核心是讲请求链路不是讲组件库。2.3 数据层设计JDBC 与 JSON 的转换约定数据层是这个项目里最“原始”的部分但也是最值得看的部分。它用 JDBC 操作 MySQL没有 ORM意味着每一条 SQL 的查询结果都需要手动封装。我一般会把数据库连接和结果集处理拆成两个 Util这个项目也是这么做的。public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); Class.forName(com.mysql.cj.jdbc.Driver); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) {} } } } }这段代码的核心是静态代码块加载驱动所有数据库操作都会调用getConnection()获得连接。注意我用了 try-with-resources 读取配置文件并且把驱动类的加载放在静态块里避免每次连接时重复注册。很多新手会把Class.forName放在每个 DAO 方法里这样虽然不会报错但每次查询都会重新触发类加载性能上是浪费。实体类方面Dish 实体一般包含 id、name、price、image、categoryId、sales 等字段。DAO 层查询完 ResultSet 后要手动把rs.getInt(id)赋值给实体对象再通过 JsonUtil 序列化成 JSON。这里有个小约定所有实体类必须提供无参构造和 getter/setter否则 JSON 序列化库会直接抛异常。如果你在测试前端时发现返回的是{}或直接 500优先检查实体类是不是缺少 getter。2.4 前端如何调用这些接口VUE 组件 axios在浏览器里打开页面后真正发起请求的是 VUE 组件里的 JavaScript 代码。前端 api 目录里通常会封装一个request.js统一处理 axios 实例、超时时间和响应拦截器。下面这个例子展示了如何把后端接口封装成可复用的函数import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.response.use( response response.data, error Promise.reject(error) ); export const getDishList () request.get(/dish?actionlist); export const login (username, password) request.post(/user?actionlogin, { username, password });这样做的好处是组件里不需要关心 URL 拼接。调用getDishList()返回的是已经解包后的业务数据响应拦截器已经把 axios 的response.data剥出来了。注意 baseURL 用了/api所以在 dev 模式下必须配置代理否则请求会直接打到 Node 的前端服务上而 Node 上没有对应的 Servlet会出现 404。2.5 请求链路一次“查菜品”请求走了哪些代码我把一次完整的“查菜品”请求链路整理出来了。前端 VUE 组件在mounted里调用this.$axios.get(/api/dish?actionlist)前端 dev server 将/api前缀代理到http://localhost:8080/foodie。Tomcat 中的 DishServlet 收到请求后根据 action 参数进入list方法调用 service 层查询数据库最后把菜品列表转成 JSON 写回响应。WebServlet(/api/dish) public class DishServlet extends BaseServlet { private DishService dishService new DishService(); public void list(HttpServletRequest req, HttpServletResponse resp) throws Exception { ListDish dishList dishService.queryAll(); String json JsonUtil.toJson(dishList); resp.setContentType(application/json;charsetutf-8); resp.getWriter().write(json); } }这段代码只做了三件事调用 service、转 JSON、写响应。注意resp.setContentType必须写否则前端拿到的响应会被浏览器当成纯文本中文还会乱码。实际上 BaseServlet 里通常会统一设置编码这个细节后面的避坑章节会专门说。请求链路里最容易出问题的位置有三个一是前端代理是否指向正确 Tomcat 端口二是WebServlet的路径和前端接口地址是否一致三是 DAO 层 SQL 是否报错。排查时就从 Network 面板看请求状态如果是 404 查路径如果是 500 看 Tomcat 日志。3. 核心代码拆解登录鉴权、菜品列表和下单事务3.1 BaseServlet 抽象类把 AJAX 请求转发到方法由于每个 Servlet 都要处理不同的业务动作比如用户模块要有 login、register、info菜品模块要有 list、detail如果每个 Servlet 都写一遍 switch-case代码会非常臃肿。这个项目用了一个很常见的做法写一个 BaseServlet在service方法里读取action参数再通过反射调用子类里同名的方法。public abstract class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null || action.isEmpty()) { resp.setStatus(400); resp.getWriter().write({\msg\:\action is required\}); return; } try { Method method this.getClass().getMethod(action, HttpServletRequest.class, HttpServletResponse.class); method.invoke(this, req, resp); } catch (NoSuchMethodException e) { resp.setStatus(404); resp.getWriter().write({\msg\:\action not found\}); } catch (Exception e) { throw new ServletException(e); } } }这里把请求处理变成了“约定优于配置”子类只需要写public void login(HttpServletRequest req, HttpServletResponse resp)这样的方法前端请求UserServlet?actionlogin时就会自动进入该方法。这种反射实现有点类似 Spring MVC 里的RequestMapping但更透明也更容易理解。反射调用有几个注意点。第一方法必须是public否则getMethod找不到第二参数类型必须严格是HttpServletRequest、HttpServletResponse方法名和 action 值要完全一致第三action 参数不要带空格或大小写差异。我用这个 BaseServlet 时踩过坑明明方法名是对的但前端请求的 URL 里 action 被编码成了%20导致getMethod报找不到方法。所以前端封装 api 时最好统一成小写驼峰。3.2 用户登录Session 的生成与校验登录接口是用户模块的核心。前端把 username 和 password 通过 POST JSON 发送到UserServlet?actionlogin后端验证数据库里的记录成功后把用户对象放入 Session。后续的“获取当前用户”“退出登录”都依赖这个 Session。public void login(HttpServletRequest req, HttpServletResponse resp) throws Exception { BufferedReader reader req.getReader(); StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } String json sb.toString(); User user JsonUtil.fromJson(json, User.class); User loginUser userService.login(user.getUsername(), user.getPassword()); resp.setContentType(application/json;charsetutf-8); if (loginUser ! null) { req.getSession().setAttribute(user, loginUser); resp.getWriter().write(JsonUtil.toJson(Result.success(loginUser))); } else { resp.getWriter().write(JsonUtil.toJson(Result.error(用户名或密码错误))); } }这段代码的逻辑很清晰首先从请求体里读 JSON 字符串用JsonUtil.fromJson把它反序列化成 User 对象然后调用 service 层校验最后把结果包装成统一格式的 Result 返回。注意用户名密码是通过请求体发过来的而不是 URL 参数所以必须用getReader()读取。把用户对象放进 Session 之后其他接口怎么拿到当前用户呢常见做法是再写一个infoaction从 Session 里取出 user如果为空说明未登录。我在改这个项目时更习惯用 Filter 统一拦截除登录、注册以外的接口减少每个 Servlet 里重复的 Session 判断代码。但原始项目为了演示会在需要用户信息的接口里手动req.getSession().getAttribute(user)。3.3 菜品列表查询分页、分类与 JSON 序列化菜品列表是外卖 App 里最常刷的接口。这个项目的 DishServlet 除了list通常还有detail和listByCategory。参数可能包括categoryId和pageNum、pageSize。我们看一下带分类筛选的分页查询实现public void listByCategory(HttpServletRequest req, HttpServletResponse resp) throws Exception { int categoryId Integer.parseInt(req.getParameter(categoryId)); int pageNum Integer.parseInt(req.getParameter(pageNum)); int pageSize Integer.parseInt(req.getParameter(pageSize)); ListDish list dishService.queryByCategory(categoryId, pageNum, pageSize); int total dishService.countByCategory(categoryId); MapString, Object data new HashMap(); data.put(list, list); data.put(total, total); resp.setContentType(application/json;charsetutf-8); resp.getWriter().write(JsonUtil.toJson(Result.success(data))); }这里把分页参数直接通过req.getParameter获取前端通过 axios 的params传递。total是为了让前端分页组件知道总条数我一般建议再返回pageNum和pageSize方便前端回显当前页码。如果参数没有传全Integer.parseInt(null)会直接抛 NumberFormatException所以前端封装时必须保证这些字段都有默认值。在实际开发中分页查询最容易出现“查询列表成功但总条数不对”的问题。原因通常是count查询的 where 条件和列表查询的 where 条件不完全一致比如列表里加了分类过滤但 count 没加或者反过来了。因此我习惯把过滤条件封装成一个DishQuery对象让 list 和 count 都基于同一个对象构建 SQL而不是两处各写一遍。3.4 下单接口事务与数据库读写一致性问题订单模块是这个项目里最值得细看的因为一个下单接口涉及多个数据表订单主表、订单明细表、菜品库存表。如果第一步改库存成功第二步插入订单明细失败数据就不一致了。这个项目用 JDBC 的setAutoCommit(false)手动控制事务。public void createOrder(HttpServletRequest req, HttpServletResponse resp) throws Exception { Integer userId (Integer) req.getSession().getAttribute(userId); if (userId null) { resp.getWriter().write(JsonUtil.toJson(Result.error(未登录))); return; } Order order JsonUtil.fromJson(req.getReader().readLine(), Order.class); Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); OrderDAO orderDAO new OrderDAO(conn); int orderId orderDAO.insertOrder(order.getUserId(), order.getTotalPrice()); for (OrderItem item : order.getItems()) { orderDAO.insertOrderItem(orderId, item.getDishId(), item.getQuantity()); } conn.commit(); resp.getWriter().write(JsonUtil.toJson(Result.success(orderId))); } catch (Exception e) { conn.rollback(); resp.getWriter().write(JsonUtil.toJson(Result.error(下单失败: e.getMessage()))); } finally { DBUtil.close(conn); } }这段代码的关键在于conn.setAutoCommit(false)开启事务然后所有数据库操作都使用同一个 Connection 执行最后commit任何一步抛出异常就rollback。如果没有这个事务控制订单主表写入了但明细表写失败用户会看到“下单成功”却没有实际的商品记录属于典型的翻车现场。在用 JDBC 手写事务时我一般会直接把 Connection 传入 DAO 构造函数像new OrderDAO(conn)这样可以让同一个事务里的多个 DAO 操作复用连接。要注意不能让 DAO 内部自己调用DBUtil.getConnection()否则事务就失效了因为两个连接不在同一个事务内。这个项目之所以拿来做教学我觉得这种“能明显看到事务边界”的设计功不可没。4. 避坑指南跨域、中文乱码与 Session 失效排查4.1 跨域请求被浏览器拦截现象前端页面能打开但 Network 面板里请求标成红色控制台报CORS policy。请求没有发到后端点实际上 Servlet 可能已经处理了只是浏览器拒绝读取响应。原因Tomcat 默认端口是 8080前端 dev server 默认端口是 8081或 3000两个端口不同浏览器会认为是两个源。前端用 axios 直接请求http://localhost:8080/foodie就会触发跨域。解决最干净的方式不是在 Servlet 里加一堆Access-Control-Allow-*响应头而是配置前端代理。在vue.config.js里写const { defineConfig } require(vue/cli-service); module.exports defineConfig({ devServer: { proxy: { /api: { target: http://localhost:8080/foodie, changeOrigin: true, pathRewrite: { ^/api: } } } } });这样浏览器看到的请求是同源的/api/...代理转发到后端完全避开跨域。如果后端和前端部署在不同域名才需要 CORS 过滤器但开发阶段用代理最简单。4.2 POST 请求中文乱码现象前端登录时输入“张三”后端接收后打印出来是乱码或者数据库里存的是问号。原因Tomcat 默认请求体编码不是 UTF-8Servlet 读取 POST 参数时使用的解码字符集取决于容器配置。常见情况是前端发送Content-Type: application/json;charsetutf-8但 Servlet 没有设置req.setCharacterEncoding(utf-8)所以getParameter和getReader读中文就乱了。解决在 BaseServlet 的service方法开头加一行req.setCharacterEncoding(utf-8)同时响应设置resp.setContentType(application/json;charsetutf-8)。比较稳妥的做法是写一个 Filter对所有请求统一设置编码避免每个 Servlet 漏写。WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(utf-8); resp.setCharacterEncoding(utf-8); chain.doFilter(req, resp); } }加上这个 Filter 之后POST 中文就不再是玄学。注意resp.setCharacterEncoding只影响响应体字符编码你仍然需要resp.setContentType带上application/json两者不冲突。4.3 Session 拿不到用户信息现象用户登录成功后前端调用“获取当前用户”接口返回 401 或者变成未登录。原因常见原因有三个。第一后端写入 Session 的 key 和读取时的 key 不一致比如写入userId读取时用了user。第二前端请求没有携带 Cookieaxios 默认在跨域场景下不会主动带上 Cookie需要设置withCredentials: true。第三Session 过期Tomcat 默认 session 有效时间是 30 分钟长时间不操作就会丢失。解决先检查代码里写入和读取的 key 是否一致。再在 axios 实例里加上withCredentials: trueconst request axios.create({ baseURL: /api, timeout: 10000, withCredentials: true });如果用了代理changeOrigin: true已经保证了 Cookie 能够正常回传。对于“丢失”问题可以在后端增加一个 Filter 检查 Session如果为空返回 JSON{msg:未登录}这样前端能明确知道是会话过期而不是莫名其妙的 500。4.4 JSON 序列化循环引用导致接口超时现象查询订单列表时接口一直转圈最后返回 500Tomcat 日志里有StackOverflowError。原因Order 对象里持有 User 对象的有参引用User 对象里又持有 List 的有参引用。用 JSON 序列化库直接转换时遇到这种双向引用会无限递归。很多序列化库有循环引用检测但也可能直接抛出异常。解决设计 JSON 返回对象时避免直接把实体类整个序列化。我一般会写一个 VO视图对象只包含需要返回的字段比如订单 ID、商品名称、数量。这里的意思不是让你把所有实体都改成 VO而是在返回嵌套对象时用JsonIgnoreProperties或者其他序列化库的注解打断循环引用。public class OrderVO { private Integer orderId; private String dishName; private Integer quantity; private Double totalPrice; // getter/setter }改造时最容易犯的错误是只给 getter 加注解但实际上序列化库不一定走 getter 或者会直接访问字段所以最好还是新建 VO。这样做虽然多写几个类但接口响应结构更可控前端也不需要从深层嵌套对象里取数据。5. 本地跑起来Tomcat VUE Dev Server 联调5.1 准备工作数据库初始化与工程导入先把项目源码里的sql目录下脚本导入 MySQL通常包含建库、建表、插入菜品数据。我一般用 Navicat 或者命令行执行然后检查backend/src/main/resources/db.properties里的jdbc.url、jdbc.username、jdbc.password是否和本地一致。如果你是 Windows 环境密码配置成空字符串也可以但建议设置简单密码避免后面连接失败。然后后端工程用 IntelliJ IDEA 或 Eclipse 导入 Maven 项目。IDEA 里选择pom.xml作为 Maven 工程等待依赖下载完毕。项目依赖一般包括javax.servlet-api、mysql-connector-java、jackson-databind或fastjson、gson等。注意javax.servlet-api的 scope 必须设置为provided因为它由 Tomcat 提供否则打 war 包会重复。5.2 后端启动把 war 包发到 Tomcat在 IDEA 里配置 Tomcat 很直接。选择 Add Configuration添加 Tomcat Server Local然后在 Deployment 里添加 Artifact。如果工程是 Maven 结构通常会生成backend:war exploded这种 artifact。配置完成后直接启动Tomcat 会加载项目这里的上下文路径默认是/foodie也就是访问http://localhost:8080/foodie时会进入项目入口。Artifact: backend:war exploded Application context: /foodie Server port: 8080启动后如果报端口被占用可以用lsof -i:8080查看占用进程。如果没有报错浏览器访问http://localhost:8080/foodie如果能看到前端静态页说明后端环境正常。不过在前端工程跑起来之前这个地址可能只有接口没有页面。5.3 前端启动安装依赖并配置代理前端工程在frontend目录先执行npm install安装依赖。如果 npm 安装太慢可以设置镜像源但这属于网络环境问题和项目本身无关。安装完成后检查vue.config.js里的代理配置是否指向后端 Tomcat 地址。注意target要写成http://localhost:8080/foodie否则代理转发后路径里会少foodie导致 Servlet 匹配不到。cd frontend npm install npm run serve启动成功后命令行会显示Compiled Successfully。打开http://localhost:8081此时页面的 API 请求会通过 dev server 代理到 Tomcat。如果一切正常首页应该能直接看到菜品列表。5.4 验证接口是否打通打开浏览器控制台切到 Network 面板刷新页面。关注几个关键点Request URL: http://localhost:8081/api/dish?actionlist Status Code: 200 Response: [{id:1,name:宫保鸡丁,price:32.5}]如果看到 Status 200 且有 JSON 数据说明前后端接口已经打通。如果请求返回 404检查前端代理的target是否包含/foodie同时确认后端WebServlet(/api/dish)的路径配置和前端请求路径是否一致。如果返回 500去 IDEA 的 Tomcat 日志里看具体异常栈。验证接口还可以用命令行工具直接请求后端绕开前端代理这样可以快速区分是前端问题还是后端问题curl -X GET http://localhost:8080/foodie/api/dish?actionlist如果 curl 返回 JSON而后端跑在 Tomcat 上没问题那问题基本就出在前端代理或者请求参数的拼写上。6. 二次开发思路从仿写到能自主加功能6.1 加购物车模块先定接口契约这个项目目前可能只有“直接下单”的逻辑但真实外卖场景里要先加购物车。我改造时会先列出接口契约而不是直接动 SQLPOST /api/cart?actionadd 参数userId, dishId, quantity GET /api/cart?actionlist 参数userId POST /api/cart?actionupdate 参数cartItemId, quantity POST /api/cart?actionclear 参数userId接口契约表列清楚之后后端每一行代码都知道为什么写。购物车表不需要单独再写一个聚合查询join 菜品表拿价格和名称就行。注意更新数量时要做库存校验防止用户把数量改成 999而实际库存只有 5。6.2 订单状态机从“已下单”到“已完成”下单之后订单状态不能只用一个字符串表示。我一般定义一个枚举待支付、已支付、商家接单、配送中、已完成、已取消。每次状态变更都要校验合法流转例如“待支付”可以直接到“已取消”但不能跳到“配送中”。在 Servlet 架构里做状态机不复杂写一个 OrderStatus 枚举和一个 OrderService 里的 updateStatus 方法即可。public enum OrderStatus { PENDING_PAYMENT(0, 待支付), PAID(1, 已支付), ACCEPTED(2, 商家接单), DELIVERING(3, 配送中), COMPLETED(4, 已完成), CANCELLED(5, 已取消); public final int code; public final String text; OrderStatus(int code, String text) { this.code code; this.text text; } }枚举的好处是代码里不会出现魔法数字前端拿到状态码后再映射成文本展示。如果你把枚举序列化成 JSON需要告诉 Jackson 用 code 而不是 name 序列化我会在枚举字段上加上JsonValue指向 code这样前端处理更直观。6.3 用 Filter 统一做登录校验和编码我改造这类项目时第一件事会把“编码设置”和“登录校验”都放进 Filter而不是每个 Servlet 重复写。这样新加的接口只要没在排除列表里就必须登录后才能访问。WebFilter(/api/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getRequestURI(); if (path.endsWith(/user?actionlogin) || path.endsWith(/user?actionregister)) { chain.doFilter(req, resp); } else if (request.getSession().getAttribute(user) ! null) { chain.doFilter(req, resp); } else { response.setStatus(401); response.setContentType(application/json;charsetutf-8); response.getWriter().write({\msg\:\未登录\}); } } }注意这个 Filter 里的路径判断不能依赖request.getRequestURI()的完整结尾因为实际的请求路径可能是/api/user?actionlogin或者/foodie/api/user。更准确的做法是先拿request.getParameter(action)判断然后在白名单集合里过滤。我把这段代码列出来是为了展示思路实际改的时候一定要根据你的 URL 结构调整。6.4 我的习惯先把接口契约表列出来每次拿到这种教学项目我不会急着看源码细节而是先把项目里所有接口和字段列成一张表。这个项目大概有用户、菜品、订单三个模块每个模块三到五个 action列完之后整个项目的边界就清楚了。拿一张纸或者一个 Markdown 表格记下接口路径、action 参数、请求方法、是否需要登录、返回字段。例如UserServlet actionlogin POST 不需要 user_id, username UserServlet actioninfo GET 需要 user_id, username, avatar DishServlet actionlist GET 不需要 list, total OrderServlet actioncreate POST 需要 order_id后面做二次开发时比如加购物车就照着这个表继续补充一下而不是在代码里到处乱找。实践下来这个习惯帮我避开了不少“加了功能后不知道改坏了哪个旧接口”的问题。从那以后我每次写前后端分离项目都会强制先整理一遍接口清单再开始改代码希望这个习惯也能帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?