简介:这是一份面向大二小学期的Java程序设计实践项目,主题为校园二手交易市场,适合计算机相关专业学生用于课程设计、小学期实训或毕业设计参考。资源包共223个文件,包含37个Java源文件、24个HTML页面、21个XML配置、CSS样式与JavaScript脚本,以及大量jpg/png图片素材,压缩包约9.97MB,目录结构清晰,便于直接导入IDE运行。同时附带系统设计文档和使用须知,可帮助读者快速梳理用户登录、商品发布、浏览下单、信息管理等核心模块的业务流程与实现思路;所有源码均经过严格测试,可直接运行。目前已有61人学习下载,对需要完整Web项目案例或二手交易系统开发参考的同学具有较高参考价值。
1. 大二小学期Java程序设计实践:校园二手交易市场.zip 到底是什么、在考什么
每年小学期最常被问的一句话就是“别人都把项目跑起来了,我还在配环境”。而这个校园二手交易市场.zip,正是大二Java程序设计实践课最常见的交付物:一份包含完整源码、数据库脚本、文档和演示路径的压缩包。课程老师不会只看界面漂不漂亮,真正决定成绩的是你拿到zip后能不能当场演示、能不能说清每个类的职责。
做这个项目能解决一个很实际的问题:把Java基础语法、集合、JDBC、Servlet、JSP、Session和MySQL这些零散知识点串成一条完整的Web开发链路。适合正在做大二课程设计、Java基础刚过关、想交出一份不丢人作业的同学。看完这篇,你能从建表一路写到打包交付,中间那些让你半夜翻车的坑,我会按踩过的顺序一个个点名。
2. 技术选型先定死这三件事:Servlet+JSP 还是 Spring Boot,课程设计选哪条路最稳
2.1 课程设计评审到底看什么:先按验收项倒推工期
小学期的项目不是企业项目,老师验收时手上有几张固定的检查表。第一项是“能不能跑”,签到时现场打开浏览器,注册一个账号、发一件商品、再买一件商品,五秒钟之内见真章;第二项是“代码是不是你自己写的”,老师会随机点开一个类问你“这个接口为什么这么设计”;第三项是“表结构是否合理”,一把拉出数据库里的表,看有没有主外键关系、金额字段用没用对类型。
按这三条倒推,你真正需要投入的时间其实是:建模半天、核心功能两天、页面打磨一天半、文档和打包一天,总共五天左右就能达到中等偏上水平。如果选错技术栈,光环境问题就能耗掉两天。所以选型这件事不是看哪个框架更现代,而是看哪个方案能在最短时间内让你把每个模块都讲得清楚。
2.2 三套选型放在一起比:上手速度、答辩追问风险、简历成色
大二课设常见的路线就三条:Servlet+JSP 手工搭建、Spring Boot+Thymeleaf、以及现成的若依之类的快速脚手架。对Java程序设计实践这门课来说,我推荐第一条。
| 选型方案 | 上手周期 | 答辩被追问的风险 | 简历成色 |
|---|---|---|---|
| Servlet+JSP+JDBC | 约2~3天 | 低,每个类自己写的 | 能讲清Java Web基础,适合校招前期 |
| Spring Boot+Thymeleaf | 约3~5天 | 中,容易被问自动配置原理 | 更贴近生产环境,但课设深度难体现 |
| 快速脚手架(若依等) | 半天起 | 高,一问代码来源就露馅 | 不建议写进简历 |
这里有个反直觉的结论:Spring Boot本身不复杂,复杂的是它帮你自动干完的那些事。课程设计答辩时老师最爱问“这个配置是怎么生效的”“为什么加一个依赖就可以用了”,你要是没读过自动配置源码,回答时就会变成“好像就可以了”,这一下就把印象分拉下来了。而Servlet+JSP这套方案里,过滤器、监听器、连接池、DAO都是你自己写的,被问到“继承和接口在哪个类里面有体现”,你能直接指出来,这就是面向对象编程java这门课最想要的答卷。
2.3 环境准备一条命令都别省:JDK、MySQL 8.0 zip 版、驱动版本
环境问题占了课设踩坑的一半。先说JDK,安装完成后一定在命令行验证一下,IDE里能跑不等于命令行能跑,因为Tomcat启动脚本走的是系统环境变量。然后是MySQL,很多同学装MySQL图省事用exe向导,结果密码忘了、服务没启动还找不到日志。其实MySQL官方提供zip免安装版,解压就能用,步骤反而比向导更可控,网上搜“mysql8.0 zip windows10 安装教程”已经是一套成熟流程了。
# 验证JDK,javac存在才能编译Servlet java -version # 如果你的mysql是zip版,先解压到D:/mysql-8.0.x-winx64 # 管理员身份打开CMD,先初始化数据目录 mysqld --initialize-insecure --basedir="D:/mysql-8.0.x-winx64" --datadir="D:/mysql-8.0.x-winx64/data" # 注册成Windows服务,注意服务名别默认,防止和已有MySQL冲突 mysqld --install MySQL8 --defaults-file="D:/mysql-8.0.x-winx64/my.ini" # 启动服务 net start MySQL8--initialize-insecure的意思是初始化一个root密码为空的实例,后续自己再改密码,免得你在安装向导里被密码规则卡住。《--defaults-file` 参数指定配置文件位置,my.ini里必须写清楚basedir和datadir,否则服务起来后找不到数据目录。
驱动版本是个血泪经验。连接MySQL 8.0必须用com.mysql.cj.jdbc.Driver,而不是老教程里的com.mysql.jdbc.Driver。如果用的是课程提供的旧版mysql-connector-java 5.x,初始化时能加载驱动,一执行查询就报Unable to load authentication plugin 'caching_sha2_password'。这个坑后面第5章专门展开,先在pom或lib目录确认jar包版本在8.0以上。
3. 从建库到跑通:核心表结构、登录注册、商品发布与图片上传
3.1 数据模型:核心表字段设计,别把状态写成字符串
校园二手交易市场本质上是一个“商品+用户+订单”的三元模型。很多同学一上来就建七八张表,最后关联查询把自己绕晕。我的习惯是只建六张必要的表:用户表、商品表、分类表、订单表、收藏表、留言表。后面的收藏和留言是加分项,前面四张是必选项。
| 表名 | 职责 | 关键字段 |
|---|---|---|
| t_user | 买家卖家一体 | id, username, password, email, create_time |
| t_goods | 二手商品 | id, seller_id, category_id, title, price, image_url, status |
| t_category | 商品分类 | id, name |
| t_order | 交易订单 | id, goods_id, buyer_id, price, status, create_time |
| t_favorite | 收藏 | id, user_id, goods_id, create_time |
| t_message | 商品留言 | id, goods_id, user_id, content, create_time |
建表时有两个习惯是加分项。第一,表名不要叫user或order,这两个词在MySQL里容易撞保留字,统一加t_前缀省心;第二,状态字段用TINYINT不用VARCHAR,“在售/下架/已售”这种状态值本质是枚举,用数字存能省空间还避免中文编码问题。
CREATE TABLE t_goods ( id INT AUTO_INCREMENT PRIMARY KEY, seller_id INT NOT NULL COMMENT '发布者ID,关联t_user.id', category_id INT NOT NULL COMMENT '分类ID,关联t_category.id', title VARCHAR(100) NOT NULL COMMENT '商品标题', description TEXT COMMENT '详细描述,TEXT类型足够存放一段话', price DECIMAL(10,2) NOT NULL COMMENT '价格,带两位小数', image_url VARCHAR(255) DEFAULT '' COMMENT '商品图片的相对访问路径', status TINYINT NOT NULL DEFAULT 1 COMMENT '0下架 1在售 2已售', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_seller (seller_id), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';价格字段是新手最容易翻车的地方。浮点数float和double在二进制里无法精确表示,定价59.9元的商品经过计算可能变成59.899999,这在订单对账时能算出差一分钱的问题。所以价格一律用DECIMAL(10,2),第一位参数10表示总位数,2表示小数位数,课程设计里撑到上亿价格足够。索引方面,给seller_id和status分别建普通索引就够了,商品列表页的查询条件无非是“某个人发布的所有商品”和“状态为1的在售商品”,这两个索引正好命中。
初始化数据时顺手把分类表填上,后面页面下拉框直接查这张表。
INSERT INTO t_category (name) VALUES ('教材教辅'), ('电子产品'), ('生活用品'), ('运动户外'), ('其他');3.2 注册、登录与 Session 拦截:完整代码与参数说明
登录注册是项目门面,也是老师最喜欢考核的一个模块。先写数据库访问层,连接信息集中放在DBUtil里,不要在十个类里各写一份DriverManager.getConnection。
public class DBUtil { // serverTimezone=Asia/Shanghai 必须写,MySQL 8.0 默认时区与JDBC不一致 private static final String URL = "jdbc:mysql://localhost:3306/campus_market?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动加载失败,检查jar包版本", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }URL里那串参数不是玄学。characterEncoding=utf8解决中文写入乱码;useSSL=false避免本地连接时SSL握手告警刷屏;serverTimezone=Asia/Shanghai如果不写,MySQL 8.0会报连接失败的时区错误,这是所有MySQL 8新手的第一道坎。把连接参数都写在常量里,之后换机器只需改这四个参数,课设文档里也好解释。
注册Servlet的写法决定了答辩时老师怎么评价你。这里有个细节:表单校验失败时用请求转发而不是重定向,因为转发是服务器内部跳转,request里设置的错误提示能带到JSP页面显示。
@WebServlet("/register") public class RegisterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); String email = req.getParameter("email"); // 基础校验:用户名3-20位,密码至少6位 if (username == null || username.trim().length() < 3 || password == null || password.length() < 6) { req.setAttribute("error", "用户名需3-20位,密码至少6位"); req.getRequestDispatcher("/WEB-INF/register.jsp").forward(req, resp); return; } UserDao dao = new UserDao(); boolean ok = dao.register(username, Md5Util.md5(password), email); if (ok) { resp.sendRedirect(req.getContextPath() + "/login"); } else { req.setAttribute("error", "用户名已存在,换一个试试"); req.getRequestDispatcher("/WEB-INF/register.jsp").forward(req, resp); } } }密码加密这块是答辩高频问题。明文存密码一旦被质疑你就很难圆场,直接说自己做了哈希处理就能过关。MD5虽然按现在的标准不算强加密,但在课程设计里足够;如果能补一句“生产环境应该用BCrypt这类加盐哈希”,这个回答就是加分项。Md5Util.md5()内部用MessageDigest.getInstance("MD5")生成32位十六进制字符串,正好对应数据库里CHAR(32)的长度。
登录成功后把用户对象放进Session,注意只放id、用户名这类的必要信息,不要把密码字段也塞进对象。一个简单的做法是session.setAttribute("currentUser", user),同时在User对象里把password字段标成@JsonIgnore或干脆用VO类。
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); // 先查一次,再比对,避免登录失败和请求错误混在一起 User user = new UserDao().findByUsernameAndPassword( username, Md5Util.md5(password)); if (user == null) { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/WEB-INF/login.jsp").forward(req, resp); return; } HttpSession session = req.getSession(); session.setAttribute("currentUser", user); resp.sendRedirect(req.getContextPath() + "/index"); } }这里特意用“先查库再比对”而不是“拉出密码用equals比对”,是为了让DAO层更简洁。SQL写成WHERE username=? AND password=?参数化查询,既防SQL注入,也避免把用户不存在和密码错误两种情况分开枚举。
3.3 商品发布与图片上传:multipart 配置和文件落盘路径
商品发布是二手交易市场的核心动作,比登录注册复杂在它要处理图片上传。表单必须声明enctype="multipart/form-data",否则浏览器只会把文件名作为普通字符串传过来,后台拿不到文件流。
图片上传的Servlet要加@MultipartConfig注解,三个参数的含义必须能答上来。maxFileSize限制单张图片大小,防止有人传几个G的文件把服务器撑爆;maxRequestSize限制整个表单请求体大小;location是文件落盘的根目录,默认走Tomcat临时目录会丢文件,必须指定一个稳定的绝对路径。
@WebServlet("/publish") @MultipartConfig( location = "D:/campus_upload/", maxFileSize = 5 * 1024 * 1024, maxRequestSize = 50 * 1024 * 1024 ) public class PublishServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String title = req.getParameter("title"); BigDecimal price = new BigDecimal(req.getParameter("price")); int categoryId = Integer.parseInt(req.getParameter("categoryId")); HttpSession session = req.getSession(false); // 发布本来就是登录后才能做的动作,这里再校验一次更严谨 if (session == null || session.getAttribute("currentUser") == null) { resp.sendRedirect(req.getContextPath() + "/login"); return; } Part part = req.getPart("image"); String fileName = extractFileName(part); // 文件名加时间戳,避免两个用户传同名文件互相覆盖 String savedName = System.currentTimeMillis() + "_" + fileName; String webPath = "/upload/" + savedName; part.write("D:/campus_upload/" + savedName); Goods goods = new Goods(); goods.setTitle(title); goods.setPrice(price); goods.setCategoryId(categoryId); goods.setImageUrl(webPath); // 数据库存访问路径,不存磁盘绝对路径 goods.setStatus(1); // 1=在售,发布默认直接上架 new GoodsDao().insert(goods, (Integer) session.getAttribute("currentUserId")); resp.sendRedirect(req.getContextPath() + "/index"); } }extractFileName这个工具方法要从Part的header里抠文件名,因为不同浏览器传上来的header格式有差异。核心逻辑是先取"content-disposition"头,再按filename=切割,然后处理路径分隔符取最后一段。写完后最好在Chrome和Edge各测一次上传。
数据库里存image_url这一列,我存的是/upload/xxx.jpg这种相对访问路径,而不是D:/campus_upload/xxx.jpg。原因是页面渲染时直接用项目根拼上相对路径就能访问,而如果存绝对路径,一旦把项目拷到别人电脑上,路径瞬间失效。要把磁盘路径对外暴露成一个URL,还需要在Tomcat的server.xml里配一个虚拟目录映射,这一步放到第5章踩坑清单里细说。
4. 把订单状态流转和权限边界写清楚:让老师答辩时放心给你加分
4.1 订单状态机:四个状态的合法流转,拒绝乱改状态
订单状态是二手交易市场里最容易被写乱的部分。很多同学用一堆if else在Servlet里直接改状态,导致数据库里出现“已付款的商品还在售”这种逻辑漏洞。正确做法是先定义一张状态流转表,代码里只允许合法跳转。
| 状态值 | 含义 | 允许流转到 |
|---|---|---|
| 0 | 待付款 | 1(付款)、3(取消) |
| 1 | 已付款待发货 | 2(确认收货)、3(退款取消) |
| 2 | 已完成 | 无 |
| 3 | 已取消 | 无 |
商品状态和订单状态是两套独立的状态,但两者要联动。下单成功后商品状态从1(在售)改成2(已售),订单取消后商品状态要回滚成1继续可售。这个联动逻辑放在Service层里,用事务包起来,避免中间某一步失败导致状态不一致。
@WebServlet("/order/create") public class CreateOrderServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); HttpSession session = req.getSession(false); User currentUser = (User) session.getAttribute("currentUser"); // 价格不能从页面传,要重新从数据库查,防止客户端改价 int goodsId = Integer.parseInt(req.getParameter("goodsId")); GoodsDao goodsDao = new GoodsDao(); Goods goods = goodsDao.findById(goodsId); // 校验商品必须存在且在售,这是最关键的一步 if (goods == null || goods.getStatus() != 1) { req.setAttribute("error", "商品不存在或已下架"); req.getRequestDispatcher("/goods/detail?goodsId=" + goodsId).forward(req, resp); return; } OrderDao orderDao = new OrderDao(); // 事务方法:创建订单 + 改商品状态 boolean ok = orderDao.createWithLock(goods, currentUser.getId()); if (ok) { resp.sendRedirect(req.getContextPath() + "/orders/list"); } else { req.setAttribute("error", "下单失败,请重试"); req.getRequestDispatcher("/goods/detail?goodsId=" + goodsId).forward(req, resp); } } }注意一个细节:页面表单里不要出现price参数,下单时价格一律重新从商品表查。这是面试里常被问的“数据一致性”问题的课设版体现。createWithLock内部先执行SELECT ... FOR UPDATE锁定商品行,再插入订单记录并更新商品状态。这么写不仅解决了超卖,答辩时还能顺势讲一句“这里用了行级锁防止并发下单”,老师对你的代码评估会明显上一个档次。
4.2 用Filter统一做权限控制,Session为空直接打回
权限控制应该是项目里重复代码最少的地方。小学期项目常见的错误是每个Servlet开头都粘一段“判断Session是否为空”的代码,七八个类里复制粘贴,改一处漏一处。Java Web的标准答案是写一个过滤器,把需要登录才能访问的URL全部拦截。
@WebFilter(urlPatterns = {"/publish", "/orders/*", "/favorite/*", "/message/*"}) public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // getSession(false) 不创建新会话,避免给未登录用户制造空Session HttpSession session = req.getSession(false); if (session == null || session.getAttribute("currentUser") == null) { // 未登录的打回登录页,并记录原始路径,登录后跳回 resp.sendRedirect(req.getContextPath() + "/login"); return; } // 已登录则放行,进入后面的Servlet或JSP chain.doFilter(req, resp); } }urlPatterns里用通配符*批量拦截一组路径。不用把所有路径都列进来,登录和注册页面本来就要匿名访问,首页和商品详情页也不用拦——浏览商品是游客就该有的权限。这里体现出来的边界感,答辩时是能讲出东西的:哪些放行、哪些拦截、为什么这样划分。
如果你的项目里还有“卖家不能购买自己商品”这类业务规则,不要放进Filter。Filter只解决“有没有登录”这一层问题;具体到“是不是本人”要在Service层里判断。把这两层权限分开,代码职责就清晰了,老师问起“权限是怎么控制的”你可以分两级回答。
4.3 分页和搜索的SQL写法:列表页不卡、答辩有话聊
商品列表页如果一次把所有商品都查出来,数据量小的时候看不出问题,但老师一句“如果有一万条商品呢”就能把你问住。分页是课程设计的必考点,也是以后javascript和java面试八股文里反复出现的基础能力。
public List<Goods> findPage(int page, int pageSize) { // LIMIT 第一个参数是偏移量,第二个参数是返回条数 String sql = "SELECT * FROM t_goods WHERE status = 1 " + "ORDER BY create_time DESC LIMIT ?, ?"; List<Object> params = new ArrayList<>(); params.add((page - 1) * pageSize); // 第1页偏移0,第2页偏移pageSize params.add(pageSize); return queryList(sql, params); } public int countOnSale() { String sql = "SELECT COUNT(*) FROM t_goods WHERE status = 1"; // 总页数 = (总条数 + pageSize - 1) / pageSize }分页参数设计的常见问题是页数用page和pageSize两个变量还是用offset。我建议Controller层接收页码和每页条数,在DAO层换算成偏移量。这样页面和用户交互更友好,地址栏显示的是?page=3而不是?offset=20。每页条数不要超过20,二手商品列表最重要的是在“首屏”内给出有效信息。
搜索功能在分页基础上加一个LIKE条件就行。关键点是只查“在售”状态的商品,并且对搜索词做合理的字符串处理:去首尾空格、限制最短长度。SQL里用LIKE CONCAT('%', ?, '%')而不是直接拼接,否则搜索词里带%会把整表扫出来,这也是一个能讲半天的细节。
5. 校园二手交易项目最常翻车的五类问题:踩坑排查清单
5.1 中文乱码:请求和响应编码分开设,Filter别只设一半
现象:注册时用户名填“小明”,存进数据库变成“灏忔槑”,页面显示一堆问号。
原因:客户端提交表单用的UTF-8,服务器端默认用ISO-8859-1解码,两端字符集不一致导致乱码。更隐蔽的是只设置了请求编码没设置响应编码,JSP页面输出的标题正常,但动态内容全是问号。
解决:写一个全局编码Filter,放在所有业务Filter的最前面,同时处理请求和响应两端。
@WebFilter(urlPatterns = "/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }另外JSP页面顶部的pageEncoding也必须是UTF-8,数据库连接串里的characterEncoding=utf8和建表时的CHARSET=utf8mb4三层都对齐,一个字符都不会乱。
5.2 MySQL 8.0连不上:驱动jar太旧和serverTimezone缺失
现象:启动Tomcat后,一访问商品列表页就报Unable to load authentication plugin 'caching_sha2_password',或者直接抛Connection refused。
原因:MySQL 8.0默认认证插件换成caching_sha2_password,旧版mysql-connector-java 5.x不支持。另一个常见理由是驱动类加载了但连接串里没写时区,MySQL 8.0对时区要求比5.x严格得多。
解决:把lib目录下的驱动jar换成mysql-connector-java-8.0.x.jar,连接串改成完整版。
jdbc.url=jdbc:mysql://localhost:3306/campus_market?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=123456如果确认都改了还连不上,检查MySQL服务是否真的启动了。用net start | findstr MySQL看服务列表,到服务窗口把MySQL8启动类型改成“自动”,免得重启电脑后又白屏。
5.3 图片上传后前端打不开:本地路径和虚拟路径不一致
现象:上传图片时提示成功,数据库里也能看到一条image_url记录,但浏览器访问商品的图片链接返回404。
原因:数据库里存的是D:/campus_upload/xxx.jpg,而Tomcat能访问的路径必须是项目根目录下的资源或者配置文件里声明的虚拟路径。浏览器请求/upload/xxx.jpg时Tomcat根本不知道这个路径映射到磁盘哪里。
解决:路径存储和对外访问分开——数据库只存访问路径/upload/xxx.jpg,磁盘文件放在D:/campus_upload/,然后在Tomcat的conf/server.xml的Host节点下加一个Context映射。
<Context docBase="D:/campus_upload" path="/upload" reloadable="false"/>这个配置的意思是:用户访问/upload/1.jpg时,Tomcat从D:/campus_upload/1.jpg读取文件。注意给目录设置可读权限,Windows下默认没问题,Linux下要chmod 755。如果用的是IDEA内置Tomcat,改server.xml之后必须重启才能生效。
5.4 zip包里的文件解压失败或要密码:交付前先解压一遍
现象:压缩包交上去之后,老师解压时提示文件损坏,或者双击zip弹出来一个密码输入框。明明在自己电脑上压缩得好好的。
原因:压缩工具版本和zip格式兼容性问题。用旧版WinRAR压出来的zip如果用7-Zip解压可能报头损坏;更常见的是为了“保护源码”给zip设置了伪加密,这种标记在部分解压软件上会触发密码框,实际上文件并没有真正加密,但老师已经没心情研究这个了。
解决:用zip格式压缩时统一选标准zip,不要选rar或7z;不需要加密,课程设计的代码就是要给人看的。压缩完成后,用系统自带的资源管理器解压到一个全新目录,逐一遍历文件确认大小不为0、图片能打开。交付前在另一台电脑上做一次完整的“解压→导入数据库→启动Tomcat→演示”流程,这一遍做完基本就稳了。
5.5 Tomcat端口被占用:项目明明没改,页面却打不开
现象:上次还能正常启动的项目,第二天一跑控制台报Port 8080 required by Tomcat ... is already in use。
原因:后台残留了前一次的Tomcat进程,或者其它Java进程占用了8080端口。这种情况在IDEA里特别常见,非正常关闭后进程还在,端口被锁。
解决:Windows下用命令行查出占用8080的进程PID,然后强制结束。
netstat -ano | findstr 8080 taskkill /PID 这里填查到的PID /F更优雅的方案是把Tomcat端口改成避免冲突的值,比如8090。改Tomcat的conf/server.xml里<Connector port="8080">这一行,顺带把项目启动的URL习惯改掉。答辩现场遇到端口被占用,在老师的注视下手动kill进程再重启,这比没有任何处理措施给人的印象好很多。
6. 交付前按这份验证清单走一遍,zip交出去不被打回
6.1 一份能打分的zip内部结构
课程设计项目的zip包结构最好一眼能看明白。三层目录:源码、数据库脚本、文档和SQL放根目录,不要套三层文件夹。
| 目录/文件 | 作用 |
|---|---|
| /src | Java源码,按servlet、service、dao、util分包 |
| /WebContent | JSP页面、WEB-INF、css、js |
| /sql | 建表脚本和初始化数据,命名为init.sql |
| /README.md | 运行环境、启动步骤、测试账号 |
| /答辩PPT | 页面截图和功能列表 |
源码分包时按controller / service / dao / entity / util五个包放,一眼就能看出三层架构。README里写清楚JDK版本、MySQL版本、Tomcat版本和启动顺序,这份文档能省掉老师一半的排查时间。
6.2 换机跑通再压缩:环境越接近“裸机”越可靠
自己电脑上能跑不代表交上去能跑。我自己的习惯是压缩前最后一步,把项目复制到一台没有装IDE的机器上,用命令行启动Tomcat验证一遍。重点看两处:数据库密码和连接串是不是写死在代码里、图片上传目录在目标机器上是否存在。如果连接信息写在db.properties里而不是Java类里,换机器时改一个文件就够了。
6.3 端口清理与路径检查:两句命令能解决的绝不靠重装
最后过一遍检查清单:Tomcat能正常启动、8080端口未被占用、MySQL服务已启动、t_goods表里有种子数据、测试账号能登录。把项目从源目录复制出来时,注意删掉IDEA生成的那些.idea和out目录,压缩包大小会小一大截,老师解压也不会被干扰。
课程设计做到这个程度,重点已经不是“能不能跑”而是“能不能稳定地跑给别人看”。把zip当产品交付,先解压后启动,所有路径都改成相对路径,数据库脚本在全新实例上跑一遍。这个习惯我从大学带到现在,工作后交付运维脚本依然复用的是同一套验证思路——你提交的不是一个压缩包,而是一条能重复落地的路径。希望这篇能帮你在小学期少熬几个夜,祝顺利。
本文还有配套的精品资源,点击获取