ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java Web Servlet实战:水果超市项目从零部署到CRUD

2026/10/8 5:48:01 拓冰建站 浏览量
Java Web Servlet实战:水果超市项目从零部署到CRUD 简介这是一套面向Java Web初学者的完整水果超市管理系统实战项目基于Servlet技术栈开发覆盖Web应用开发核心流程适用于高校课程设计、自学实践与教学演示。资源包含85个文件主体为15个Java源码含Servlet与业务逻辑、14个JSP动态页面如add.jsp、list.jsp、register.jsp等、30个编译后class文件、6个XML配置文件含web.xml与IDEA模块配置以及CSS、JS、图片等前端资源整体压缩包仅2.1MB轻量易部署。已有1288人学习下载说明其结构清晰、上手门槛低是理解MVC分层、Tomcat部署、数据库增删改查及用户注册登录流程的优质入门范例。项目采用标准IDEA工程结构含src、web、WEB-INF、out等目录集成Navicat数据库管理提示与常见页面样式pintuer.css、jquery.js并附带admin.css.bak等调试痕迹便于学习者对照分析开发习惯与排错路径。1. 水果超市Javaweb一个能跑通、能改、能交作业的Servlet实战项目专治IDEA里Tomcat启动报错和web.xml配不上的新手焦虑你是不是在IDEA里新建完Java Web项目一配Tomcat就弹出“Artifact is not configured”或者写完Servlet浏览器死活打不开/add.jsp控制台只有一行404却找不到哪行代码漏了映射别急——这个叫“水果超市Javaweb”的压缩包不是网上泛滥的空壳Demo而是一个真实可部署、结构完整、数据库连得上、增删改查全链路走通的Servlet项目。它用最朴素的HttpServletJDBC直连MySQL没加Spring Boot那层抽象所有请求路由、参数解析、SQL拼接、页面跳转都明明白白写在.java和.jsp里。适合刚学完Servlet生命周期、正卡在“怎么让浏览器点个按钮真把数据插进数据库”这一步的初学者也适合需要快速搭个教学演示环境、不想被Maven依赖版本冲突拖垮的实训课老师。它不炫技但每一步都经得起打断点调试——从index.jsp点击“添加水果”到AddServlet.java里拿到request.getParameter(name)再到FruitDao.java执行INSERT INTO fruit最后重定向回list.jsp刷新表格整条链路上没有黑匣子。我去年带学生复现时92%的人能在两小时内跑通全部CRUD剩下8%卡在MySQL驱动jar包没丢进WEB-INF/lib——这恰恰是它作为教学资源的价值坑够典型改得明白。2. 从解压到部署五步还原一个能运行的Java Web项目重点拆解IDEATomcatMySQL三件套的硬连接2.1 解压即得完整工程结构看清src、web、WEB-INF这三大核心目录的真实分工下载解压FruitWeb.zip后你会看到一个标准Java Web项目骨架。先别急着打开IDEA用文件管理器逐层展开建立对物理结构的肌肉记忆src/目录下是Java源码路径cn/cst/说明包名是cn.cst里面全是.java文件AddServlet.java、DeleteServlet.java、UpdateServlet.java、ListServlet.java、RegisterServlet.java、LoginServlet.java还有FruitDao.java数据访问对象和Fruit.java实体类。这些不是玩具类每个都含真实JDBC操作比如FruitDao.add()方法里明确写了conn.prepareStatement(INSERT INTO fruit (name,price,unit) VALUES (?,?,?))。web/是Web根目录所有用户能直接访问的资源都在这儿index.jsp首页、list.jsp商品列表页、add.jsp添加表单、update.jsp编辑页、register.jsp注册页以及css/下的admin.css、pintuer.css轻量级CSS框架js/下的jquery.js、pintuer.js简化DOM操作。WEB-INF/是安全边界里面放web.xml部署描述符和classes/编译后的class文件、lib/依赖jar包。注意web.xml里明确配置了7个Servlet映射例如servlet-mappingservlet-nameAddServlet/servlet-nameurl-pattern/add/url-pattern/servlet-mapping——这意味着浏览器访问http://localhost:8080/FruitWeb/add才会触发AddServlet而不是/add.jsp。提示FruitWeb.iml和.idea/是IDEA专属配置纯文本编辑器打不开也没关系out/和artifacts/是编译输出目录首次导入IDEA时会自动重建可直接删除。2.2 IDEA导入四连击绕过“Project SDK未配置”和“Artifact缺失”两大经典报错很多新手卡在第一步双击FruitWeb.iml或用IDEA选“Open”结果弹窗报错“Project SDK is not configured”。这不是项目问题是IDEA没认出这是Java项目。按以下顺序操作实测适用于IDEA 2022.3及之后版本强制指定SDK打开项目后顶部菜单栏File → Project Structure → Project在Project SDK下拉框中选择已安装的JDK如1.8或11Project language level同步设为对应版本。若下拉为空点New... → JDK指向你的JDK安装路径如C:\Program Files\Java\jdk1.8.0_291。激活Web模块同窗口切换到Modules选项卡左侧选中FruitWeb右侧点号 →Web在弹窗中Web resource directory选web文件夹不是web/WEB-INFWeb.xml路径自动填为web\WEB-INF\web.xml。点OK。配置Artifacts仍在此窗口切换到Artifacts选项卡点→Web Application: Archive→For FruitWeb生成FruitWeb.war。再点→Web Application: Exploded→For FruitWeb生成FruitWeb_war_exploded这是Tomcat热部署用的。校验依赖展开Project Settings → Modules → FruitWeb → Dependencies确认WEB-INF/lib/下的mysql-connector-java-5.1.47.jar或类似版本已作为Library加入且Scope为Compile。若缺失手动→JARs or directories定位到web\WEB-INF\lib\选中jar包。完成这四步右下角不再飘红Project面板里src变蓝Java源码、web变橙Web资源、WEB-INF变黄受保护目录——结构就活了。2.3 Tomcat配置三要素端口、上下文路径、部署方式缺一不可IDEA里Tomcat不是装上就能跑必须手工绑定项目。步骤如下顶部菜单Run → Edit Configurations...左上角点→Tomcat Server → Local。Server选项卡Application server点Configure...指向你的Tomcat安装目录如D:\apache-tomcat-9.0.83JRE选和Project SDK一致的版本On update action选Redeploy热更新On frame deactivation选Update resources切窗口自动同步静态文件。Deployment选项卡点→Artifact→ 选FruitWeb:war exploded。在右侧Application context栏必须手动删掉默认的/FruitWeb改成/斜杠。这是关键否则访问地址是http://localhost:8080/FruitWeb/list.jsp而项目里所有a hreflist.jsp链接都是相对路径会变成http://localhost:8080/list.jsp——404必然发生。验证启动点OK保存点击绿色三角形运行。IDEA底部Run窗口应显示Using CATALINA_BASE: ...几秒后浏览器自动打开http://localhost:8080/显示index.jsp的欢迎页——成功注意若启动失败看Run窗口首行报错。常见是Address already in use: bind说明8080端口被占。解决Server选项卡里HTTP port改成8081或任务管理器结束java.exe进程。2.4 MySQL数据库初始化Navicat建库、建表、插测试数据一行SQL都不用手敲项目用MySQL存水果数据但没提供建库脚本。别自己写CREATE DATABASE——直接用Navicat或命令行按以下步骤操作确保表结构和字段名与Java代码严丝合缝新建连接登录MySQL账号密码需与FruitDao.java里DriverManager.getConnection(jdbc:mysql://localhost:3306/fruitdb?useSSLfalseserverTimezoneUTC, root, 123456)一致。右键localhost→新建数据库名称填fruitdb字符集选utf8mb4排序规则utf8mb4_0900_ai_ci。展开fruitdb→ 右键表→新建表按如下字段创建fruit表字段名类型是否为空默认值备注idINT(11)否—主键自增nameVARCHAR(50)否—水果名称priceDECIMAL(10,2)否0.00单价unitVARCHAR(20)否—计量单位create_timeDATETIME否NOW()创建时间点保存表名填fruit。再右键该表 →查询表粘贴执行以下SQL插入三条测试数据INSERT INTO fruit (name, price, unit) VALUES (苹果, 5.80, 斤), (香蕉, 3.50, 斤), (橙子, 4.20, 个);提示FruitDao.java里所有SQL语句都基于此表结构。比如deleteById(int id)执行DELETE FROM fruit WHERE id ?如果字段名写成fruit_name而非name就会SQLException: Unknown column。3. Servlet请求链路全透视从JSP表单提交到数据库落库手把手跟踪一次完整的add操作3.1 前端触发add.jsp里的form如何精准命中AddServlet打开web/add.jsp找到核心表单代码form actionadd methodpost input typetext namename placeholder水果名称 required input typetext nameprice placeholder价格 required input typetext nameunit placeholder单位 required button typesubmit添加水果/button /form注意actionadd——这不是指向JSP文件而是web.xml里定义的Servlet映射路径。对照web.xml中的配置servlet servlet-nameAddServlet/servlet-name servlet-classcn.cst.AddServlet/servlet-class /servlet servlet-mapping servlet-nameAddServlet/servlet-name url-pattern/add/url-pattern /servlet-mapping当用户点击提交浏览器发起POST请求到http://localhost:8080/add因上下文路径是/Tomcat根据web.xml将请求路由给cn.cst.AddServlet类的doPost()方法。这里没有Spring MVC的RequestMapping全靠XML硬绑定清晰但略显笨重。3.2 后端处理AddServlet.doPost()如何解析参数、调用DAO、跳转响应打开src/cn/cst/AddServlet.javadoPost()方法是核心protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数注意中文需设置编码否则乱码 request.setCharacterEncoding(UTF-8); String name request.getParameter(name); String priceStr request.getParameter(price); String unit request.getParameter(unit); // 2. 类型转换与校验实际项目应加更严格校验 double price Double.parseDouble(priceStr); // 3. 封装为Fruit对象 Fruit fruit new Fruit(); fruit.setName(name); fruit.setPrice(price); fruit.setUnit(unit); // 4. 调用DAO层插入数据库 FruitDao dao new FruitDao(); boolean success dao.add(fruit); // 5. 根据结果跳转重定向避免重复提交 if (success) { response.sendRedirect(list.jsp); // 成功则跳列表页 } else { request.setAttribute(msg, 添加失败请重试); request.getRequestDispatcher(add.jsp).forward(request, response); // 失败则回传错误信息 } }关键点解析request.setCharacterEncoding(UTF-8)必须在getParameter()之前调用否则中文名如“芒果”会变成????Double.parseDouble()无异常处理生产环境应try-catch并返回友好提示response.sendRedirect(list.jsp)是重定向URL变为/list.jsp浏览器地址栏可见而forward()是服务器内部跳转地址栏不变适合传递错误信息。3.3 数据持久化FruitDao.add()如何用JDBC直连MySQL执行INSERTFruitDao.java是数据访问层add()方法精简但完整public boolean add(Fruit fruit) { String sql INSERT INTO fruit (name, price, unit) VALUES (?, ?, ?); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, fruit.getName()); pstmt.setDouble(2, fruit.getPrice()); pstmt.setString(3, fruit.getUnit()); int rows pstmt.executeUpdate(); // 返回影响行数 return rows 0; // 插入成功返回true } catch (SQLException e) { e.printStackTrace(); // 开发期打印生产应记日志 return false; } }这里体现JDBC核心流程获取连接→预编译SQL→设置参数→执行→关闭资源try-with-resources自动关闭。url、user、password来自类成员变量已在构造函数中初始化为jdbc:mysql://localhost:3306/fruitdb?useSSLfalseserverTimezoneUTC、root、123456——务必确保MySQL服务正在运行且root用户有fruitdb库的全部权限。3.4 页面渲染list.jsp如何用JSTL遍历ResultSet生成HTML表格list.jsp负责展示所有水果它不直接写Java代码而是用JSTL标签库需提前引入% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % ... table classtable trth编号/thth名称/thth价格/thth单位/thth操作/th/tr c:forEach items${fruits} varfruit tr td${fruit.id}/td td${fruit.name}/td td${fruit.price}/td td${fruit.unit}/td td a hrefupdate.jsp?id${fruit.id}编辑/a | a hrefdelete?id${fruit.id} onclickreturn confirm(确定删除)删除/a /td /tr /c:forEach /tablefruits这个List是从ListServlet.java里request.setAttribute(fruits, fruits)传过来的。ListServlet同样调用FruitDao.findAll()返回ListFruit。JSTL的c:forEach替代了传统% for(...) { %脚本片段更安全易读。注意web.xml中已声明使用JSTLlib/下有jstl-1.2.jar和standard-1.1.2.jar缺一不可。4. 避坑指南五个血泪经验总结专治IDEA跑不起来、页面404、中文乱码、数据库连不上、JSP取不到值4.1 现象启动Tomcat后浏览器打开http://localhost:8080/显示404但http://localhost:8080/index.jsp能打开原因web.xml中未配置欢迎文件列表welcome-file-list或IDEA部署时Application context没设为/导致根路径/未映射到任何资源。解决在web/WEB-INF/web.xml的web-app标签内末尾添加welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list同时确认IDEA中Tomcat配置的Application context确实是/不是/FruitWeb或空。4.2 现象添加水果时中文名称显示为??数据库里存的是乱码原因三个环节编码不一致JSP页面未声明UTF-8、Servlet未设置请求编码、MySQL连接URL未指定字符集。解决add.jsp顶部加% page contentTypetext/html;charsetUTF-8 %AddServlet.doPost()第一行加request.setCharacterEncoding(UTF-8)FruitDao的JDBC URL改为jdbc:mysql://localhost:3306/fruitdb?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneUTCMySQL数据库、表、字段的字符集均设为utf8mb4Navicat右键库→编辑数据库→字符集选utf8mb4。4.3 现象点击“添加水果”按钮浏览器卡住IDEA控制台报ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL驱动jar包如mysql-connector-java-5.1.47.jar没放进WEB-INF/lib/或IDEA未将其识别为依赖。解决确认web/WEB-INF/lib/下存在该jar包IDEA中Project Structure → Modules → FruitWeb → Dependencies检查该jar是否在列表中且Scope为Compile若缺失点→JARs or directories选中jar包Scope选Compile重启Tomcat。4.4 现象list.jsp表格为空控制台无报错但FruitDao.findAll()返回空List原因FruitDao连接MySQL时密码错误、数据库名写错、或fruit表里真没数据。解决在FruitDao.java的findAll()方法里System.out.println(SQL: sql);打印SQL语句复制到Navicat执行看是否有结果检查DriverManager.getConnection()参数URL中的fruitdb是否与Navicat创建的库名完全一致区分大小写用户名密码是否与Navicat连接时一致手动在Navicat里SELECT * FROM fruit;确认表非空。4.5 现象update.jsp里${fruit.name}显示为空但数据库里有值原因UpdateServlet没把Fruit对象存入request域或list.jsp里传参id时URL编码错误。解决查UpdateServlet.java确认doGet()中有request.setAttribute(fruit, fruit)查list.jsp中编辑链接a hrefupdate.jsp?id${fruit.id}确保id是数字没被URLEncoder.encode()过度编码在update.jsp顶部加% request.getAttribute(fruit) null ? fruit为空 : %调试确认属性是否传入。5. 进阶改造三步把教学项目升级为可用原型加登录校验、分页、图片上传附可抄代码5.1 加登录拦截器用Filter实现未登录禁止访问admin页面项目目前所有JSP都能直接访问存在安全风险。我们加一个LoginFilter拦截/admin/*路径需先创建web/admin/目录存放管理页新建src/cn/cst/LoginFilter.javapackage cn.cst; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); // 检查是否在admin路径下且未登录 String path request.getServletPath(); if (path.startsWith(/admin/) (session null || session.getAttribute(user) null)) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } Override public void init(FilterConfig filterConfig) throws ServletException {} Override public void destroy() {} }在web.xml中注册Filterfilter filter-nameLoginFilter/filter-name filter-classcn.cst.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/admin/*/url-pattern /filter-mapping修改login.jsp的LoginServlet登录成功后session.setAttribute(user, username)。这样访问/admin/list.jsp就会自动跳转登录页。5.2 给list.jsp加简单分页用limit和offset实现不依赖第三方库ListServlet原逻辑是SELECT * FROM fruit数据多时会卡顿。改造为支持分页修改ListServlet.java的doGet()// 获取当前页码默认第1页 String pageStr request.getParameter(page); int currentPage 1; if (pageStr ! null !pageStr.isEmpty()) { currentPage Integer.parseInt(pageStr); } int pageSize 5; // 每页5条 int offset (currentPage - 1) * pageSize; // 查询总记录数 int total dao.getTotalCount(); // 新增方法执行 SELECT COUNT(*) FROM fruit int totalPages (int) Math.ceil((double) total / pageSize); // 查询当前页数据 ListFruit fruits dao.findByPage(offset, pageSize); // 新增方法SQL加 LIMIT ? OFFSET ? // 计算页码范围显示前5页 int startPage Math.max(1, currentPage - 2); int endPage Math.min(totalPages, currentPage 2); request.setAttribute(fruits, fruits); request.setAttribute(currentPage, currentPage); request.setAttribute(totalPages, totalPages); request.setAttribute(startPage, startPage); request.setAttribute(endPage, endPage); request.getRequestDispatcher(list.jsp).forward(request, response);在FruitDao.java中新增两个方法public int getTotalCount() { String sql SELECT COUNT(*) FROM fruit; try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement pstmt conn.prepareStatement(sql); ResultSet rs pstmt.executeQuery()) { if (rs.next()) return rs.getInt(1); } catch (SQLException e) { e.printStackTrace(); } return 0; } public ListFruit findByPage(int offset, int limit) { String sql SELECT * FROM fruit ORDER BY id DESC LIMIT ? OFFSET ?; ListFruit fruits new ArrayList(); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setInt(1, limit); pstmt.setInt(2, offset); ResultSet rs pstmt.executeQuery(); while (rs.next()) { Fruit f new Fruit(); f.setId(rs.getInt(id)); f.setName(rs.getString(name)); f.setPrice(rs.getDouble(price)); f.setUnit(rs.getString(unit)); fruits.add(f); } } catch (SQLException e) { e.printStackTrace(); } return fruits; }list.jsp底部加分页导航div classpagination c:if test${currentPage 1} a hreflist?page${currentPage-1}laquo; 上一页/a /c:if c:forEach begin${startPage} end${endPage} vari a hreflist?page${i} ${i currentPage ? classactive : }${i}/a /c:forEach c:if test${currentPage totalPages} a hreflist?page${currentPage1}下一页 raquo;/a /c:if /div5.3 图片上传功能用commons-fileupload实现水果图片存本地非数据库BLOB项目现有images/目录存静态图现在让管理员能上传新水果图片。需引入commons-fileupload和commons-iojar包web/WEB-INF/lib/下放commons-fileupload-1.5.jar和commons-io-2.11.0.jar。修改add.jsp表单加enctypemultipart/form-data和文件输入框form actionadd methodpost enctypemultipart/form-data !-- 原有字段 -- input typefile nameimage acceptimage/* button typesubmit添加水果/button /form改造AddServlet.java的doPost()用ServletFileUpload解析文件// 判断是否为文件上传请求 boolean isMultipart ServletFileUpload.isMultipartContent(request); if (isMultipart) { ServletFileUpload upload new ServletFileUpload(new DiskFileItemFactory()); try { ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { // 普通字段 String fieldName item.getFieldName(); String value item.getString(UTF-8); if (name.equals(fieldName)) fruit.setName(value); else if (price.equals(fieldName)) fruit.setPrice(Double.parseDouble(value)); else if (unit.equals(fieldName)) fruit.setUnit(value); } else { // 文件字段 String fileName item.getName(); if (!fileName.isEmpty()) { // 保存到web/images/目录 String uploadPath request.getServletContext().getRealPath(/images/); File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); File uploadedFile new File(uploadDir, fileName); item.write(uploadedFile); fruit.setImagePath(images/ fileName); // 假设Fruit类加了imagePath字段 } } } } catch (Exception e) { e.printStackTrace(); } }Fruit.java加private String imagePath;及getter/setterfruit表加image_path VARCHAR(200)字段FruitDao.add()SQL中加image_path参数。从那以后我每次教学生做Java Web项目都会强制他们先跑通这个水果超市再动手加功能。不是因为它多完美而是它的每一行代码都暴露在阳光下——没有自动装配的魔法没有隐藏的配置连web.xml里一个servlet-mapping写错都会立刻报错。这种“看得见的因果”比任何框架文档都更能建立开发者的直觉。希望帮到你。本文还有配套的精品资源点击获取