
第一次拿到WEB大作业的题目时我以为要做一个“网站”——画面感很足的那种轮播图、跑马灯、满屏渐变最好还能弹个窗欢迎用户。结果老师只淡淡说了一句做一个完整的Web项目能注册能登录有数据展示逻辑闭环。那一刻大部分同学和我一样脑子里全是问号。这篇博客就来聊聊我第一次完成WEB大作业的全过程。项目本身不算难但它覆盖了web前端开发、后端接口、数据库操作、部署检查这条完整链路认真做完之后你会发现自己突然把之前零散的HTML、CSS、JS、Servlet知识全串起来了。如果你也在准备自己的第一个Web工程或者正在为验收焦头烂额这篇东西应该能给你一条可以直接照着走的路径。为什么我要把“第一次”单独拿出来讲因为第一次大作业和平时的小练习完全是两码事。小练习只需要做一个静态网页老师只看好不好看大作业则默认你已经具备“自己搭一个完整功能”的能力。这也意味着光靠“好看”是拿不到高分的你得让整个系统真的跑起来。1. 先把题目读薄第一次大作业到底在考什么1.1 表面是“做一个系统”实际考三件事很多同学看到“做一个XX管理系统”“做一个XX展示网站”就慌觉得功能越多越好。我第一次也这样恨不得加上购物车、评论、点赞、后台管理。后来跟老师聊了聊才明白第一次大作业真正考的是三件事。第一页面架构与用户流程是否完整。也就是用户从首页能不能顺利找到注册入口、登录之后能不能回到首页、数据列表能不能点进详情页。这个叫“信息架构”页面之间如果跳转关系混乱老师点两下就迷路了分数基本不会高。第二前后端是否真正打通。一个前端页面哪怕做得再漂亮如果表单提交之后没有后续数据没有落到数据库那它本质上还是一个静态网页。第一次大作业最核心的验收点就在这里注册的信息能不能存进去登录的时候能不能查出来列表页展示的是不是数据库里的真实数据。第三边界情况有没有基本的防御。比如用户不填任何内容直接点提交会怎样用户输入了超长字符会怎样密码错了有没有提示这些不一定写在题目里但老师验收时会专门去点。理解了这三点我的作业范围一下子就清晰了不需要做十个功能模块只需要把“注册→登录→数据展示→退出”这条链路做到滴水不漏就足以应付绝大多数第一次大作业。1.2 技术栈选型第一次作业真的没必要上全家桶在动手之前我先梳理了一下自己手里能用的牌。我们课程是Java方向所以后端候选主要是Servlet/JSP、Spring Boot、或者JSPJavaBean。班里也有同学用Python Flask写只要答辩能讲清楚其实都行但考虑到老师一眼就能看懂的才是好方案我最后选了Servlet JSP JDBC MySQL。这里我列了一张对比表大家可以根据自己的情况选技术方案优点缺点适合什么情况纯静态 HTMLCSSJS上手快界面容易出效果没有数据持久化难满足“注册登录”要求题目只要求做页面展示ServletJSPJDBC课程同步、原理透明答辩能讲清楚每个环节代码繁琐配置需要手工处理绝大多数第一次大作业Spring Boot开发效率高、不用手工配Tomcat概念多答不好“为什么这么配”容易翻车已经学完SSM的进阶同学Python Flask/Node Express代码短开发快和Java课程可能脱节自选方向且老师允许我见过一个同学用Spring Boot两天就写完了一个管理系统但是答辩的时候老师问了一句“Spring Boot是怎么帮你自动配置Tomcat的”他完全答不上来场面一度尴尬。所以我的建议很直接第一次大作业用你最能讲清楚原理的技术栈而不是最热门的技术栈。如果你连Spring Boot的starter是怎么拉起内嵌服务器都讲不明白那这个“超高效率”反而会成为你的扣分项。顺带说一句我见过不少同学在技术选型上犯的另一个毛病为了用热门技术强行给自己加戏。比如明明课程没讲WebSocket非要做一个实时聊天室明明不会Linux非要申请服务器部署。第一次大作业最重要的目标是形成闭环不是展示你追新了多少。我这次就只做了该做的功能后面在加分项里才加了两个小而精的交互。2. 用 IDEA 2024 搭起第一个 Web 工程2.1 创建工程的两种路径别踩“空项目”的坑我用的IDE是IntelliJ IDEA 2024版本。这版本的New Project向导和之前变化很大第一次在建Web项目时如果选错模板后面会非常痛苦。核心有两条路径。路径一Maven原型创建Web工程。打开IDEA依次选择 File → New → Project左侧选 Maven然后勾选 Create from archetype在原型列表里找到org.apache.maven.archetypes:maven-archetype-webapp选中后直接下一步。这个骨架会生成标准的src目录、webapp目录和一个简陋的index.jsp相当于给你搭好了Web工程的脚手架。路径二Java项目 手动添加Web支持。先创建普通Java项目然后右键模块 → Add Framework Support → 勾选 Web ApplicationIDEA会自动创建一个webapp目录。这种方式适合你不想要Maven就想用最原始的方式写Servlet。但我不太推荐因为你后面要引MySQL驱动、JSTL这些依赖没有Maven就只能手动下载jar包放到WEB-INF/lib里折腾不说还容易因为jar版本问题踩坑。我最终用的是路径一。创建完之后需要在pom.xml里补上Servlet依赖和MySQL驱动依赖。这一步很多教程不会说因为骨架生成的pom.xml非常简陋不补依赖的话你写Servlet时连HttpServlet这个类都import不到。我加的依赖大概长这样dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency注意Servlet依赖的scope是provided意思是打包的时候不把它打进去因为Tomcat本身已经带了Servlet运行环境。如果你把它设置成默认的compile后期打war包的时候会产生奇怪冲突。2.2 目录结构到底该怎么摆别再全塞webapp里第一次写Web工程的人最常犯的错误就是把所有文件都堆在webapp根目录下面。index.jsp放这没问题但如果你把Java类、CSS、JS、图片也一起放在webapp里后期维护和答辩展示都会很难受。我当时的目录结构是这样的src/main/java/com/example/ ├── servlet/ │ ├── RegisterServlet.java │ ├── LoginServlet.java │ ├── ListServlet.java │ └── LogoutServlet.java ├── dao/ │ └── UserDao.java ├── model/ │ └── User.java └── util/ └── DBUtil.java src/main/resources/ └── db.properties src/main/webapp/ ├── WEB-INF/ │ └── web.xml ├── index.jsp ├── login.jsp ├── register.jsp ├── list.jsp ├── css/ │ └── style.css ├── js/ │ └── main.js └── images/把Java类按servlet、dao、model、util分包好处是查问题的时候能快速定位。JSP页面我放在了webapp根目录因为第一次作业不需要把JSP藏到WEB-INF下面做访问控制根目录访问路径短演示时输入URL比较方便。WEB-INF目录是Web容器保护起来的浏览器直接访问不到里面的文件所以web.xml放这里是标准做法。一个很容易被忽略的细节webapp下的css/js路径是跟随URL的不是跟随JSP文件位置的。刚开始我把CSS这样引用link relstylesheet hrefcss/style.css在首页好好的结果点进一级目录页面之后样式全没了因为浏览器相对的是当前URL路径不是JSP所在目录。这个坑后面会专门讲。2.3 配置Tomcat并启动本地服务器IDEA跑Web项目不像直接跑main方法那么简单。我用的老版本Tomcat 9配置步骤大概是点击菜单 Run → Edit Configurations左上角加号选择 Tomcat Server → Local。在 Application server 一栏点击 Configure选择Tomcat安装目录不是bin目录是Tomcat的根目录。切到 Deployment 标签页点加号选择 Artifact把你的项目以 war exploded 的形式部署上去。设置 Application context。我直接设成/这样访问地址就是http://localhost:8080/不用再多敲项目名。这里有两个新手最容易踩的坑。第一个是端口占用。Tomcat默认8080端口如果你之前开过一个Tomcat实例或者别的程序占用了8080启动会直接报Port already in use: 8080。Windows下我习惯用netstat -ano | findstr 8080找到占用进程的PID然后在任务管理器里结束它Mac/Linux用lsof -i:8080。第二个坑是启动报错No artifacts marked to deploy这是因为你还没把项目构建产物部署到Tomcat上。回到Deployment标签页把Artifact加上去就行。我当时卡了好一会儿后来才发现是IDEA 2024创建Maven骨架项目之后没有自动生成Artifact需要手动点一下。配置好之后运行浏览器访问本地地址看到Tomcat默认页面或者你项目里的index.jsp说明Web服务器已经正常工作了。到这里你的Web工程算是真正“活”了。3. 页面设计从线框图到能看的界面3.1 先花半小时画线框图别急着写代码我知道很多同学一拿到作业就打开编辑器噼里啪啦开始写HTML写完才发现页面之间跳转关系混乱导航栏漏了链接整个站点像一盘散沙。这是因为跳过了“设计”这一步。我用的办法是拿一张A4纸把自己要做的页面列表写下来。我这次的作业是一个“校园学习资源分享”主题站点页面不多就四个首页index、资源列表页list、个人注册/登录页register/login、内容详情页detail。然后在纸上画出每个页面的布局草图顶部是统一的导航栏中间是内容区底部是页脚再标注导航里的每个链接指向哪里。这一步花不了多少时间但它能帮你提前发现很多问题。比如绘制完成后我发现注册成功之后用户不知道该去哪里于是我在注册页面写了一句提示“注册成功3秒后自动跳转到登录页”这个细节后期在演示时非常加分因为说明你考虑到了用户路径。线框图的另一个价值是它可以帮你确定CSS布局方案。我画完全部页面后发现大部分页面的骨架结构都是“顶部导航中间内容底部页脚”只是中间内容区的列数不一样这意味着我可以抽一套公共CSS来统一风格不用每个页面从头写样式。3.2 用语义化标签写HTML而不是满屏div第一次写页面的人结构往往长这样div classheaderdiv classnavdiv classcontent。能用但不好。我推荐用HTML5的语义化标签原因有三个代码可读性高答辩时老师扫一眼就知道每个区域是干嘛的CSS选择器写起来更短同时有利于屏幕阅读器辅助访问。我的页面骨架大概是这样body header nav ul lia hrefindex.jsp首页/a/li lia hrefListServlet资源列表/a/li lia hreflogin.jsp登录/a/li lia hrefregister.jsp注册/a/li /ul /nav /header main !-- 页面核心内容 -- /main footer p© 2024 学习资源分享站/p /footer /body表单部分也有讲究。很多新手写完表单label和input之间根本没有关联鼠标点一下“用户名”三个字不会聚焦到输入框。正确做法是让label的for属性指向input的idlabel forusername用户名/label input typetext idusername nameusername required placeholder请输入用户名name属性是提交数据时传给后端的参数名id是用来关联label和前端操作的两者各司其职。required属性可以让浏览器在提交前先做一轮非空校验虽然这个不可靠但能拦截大部分误操作。这些细节单独看不起眼但组合在一起就是你页面比同学“显得专业”的关键。3.3 CSS布局flex管内部grid管整体第一次大作业的CSS布局我把核心思路总结成一句话页面整体骨架用Grid局部元素排列用Flex。这两个现代布局方式能解决我见过90%以上的布局需求而且比浮动清晰一百倍。页面整体骨架我用Grid定义body { display: grid; grid-template-rows: auto 1fr auto; min-height: 100vh; margin: 0; }这个写法的好处是即使内容区内容很少页脚也会被牢牢钉在页面底部不会出现“页面太短、页脚飘在中间”的尴尬。内容区如果是两栏布局继续用gridmain { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; max-width: 1200px; margin: 0 auto; padding: 20px; }而在导航栏、卡片内部的排列、按钮组这些“一行内多个元素”的场景用flex更合适nav ul { display: flex; gap: 24px; list-style: none; } .card-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }repeat(auto-fill, minmax(260px, 1fr))这套组合是卡片列表的万能写法卡片宽度小于260px时自动换行等于做了一版简易响应式。我第一次做的时候踩了一个很经典的坑所有宽度都用固定像素。导航栏写死980px内容区写死800px在自己屏幕上看着挺整齐结果老师用笔记本一打开竖直滚动条出现了页面被挤得变形。后来我统一改成max-width 百分比/margin: 0 auto页面在不同分辨率下都能保持居中。所以记得在CSS最开头加上这行* { box-sizing: border-box; }不加这行的话padding和border会把盒子实际宽度撑大你在grid里算好的列宽很容易溢出换行。3.4 用原生JS做交互别急着引框架作业里的前端交互有人一上来就引jQuery或者Vue我建议第一次作业先用原生JS把这些小功能写明白因为你答辩时需要回答“代码为什么这么写”原生JS是最能讲清楚原理的。我这次主要写了几个交互注册页面的前端校验、登录成功后的提示、列表页的确认删除按钮、导航栏高亮当前项。前端校验的大概逻辑是document.getElementById(registerForm).addEventListener(submit, function (e) { const username document.getElementById(username).value.trim(); const password document.getElementById(password).value.trim(); if (username.length 3) { e.preventDefault(); document.getElementById(usernameError).textContent 用户名至少3个字符; return; } if (password.length 6) { e.preventDefault(); document.getElementById(passwordError).textContent 密码至少6个字符; return; } });这里有一个很重要的点前端校验只是用户体验不是安全边界。因为用户可以通过禁用JS、或者直接向接口发请求来绕过这些校验所以后端Servlet里必须再做一次同样的校验。前端校验的意义是让正常用户不要被无谓的请求折腾后端校验才是真正守住数据合法性的底线。我最初的版本只做了前端校验后来用工具绕过JS直接提交了脏数据数据库里就出现了空用户名的记录这个教训让我老老实实补了后端校验。事件委托也是一个好用的小技巧。如果你的列表页有很多“删除”按钮给每个按钮都绑一个监听器会很浪费。把监听器绑在列表容器上用事件对象的target去判断document.querySelector(.card-list).addEventListener(click, function (e) { const btn e.target.closest(.delete-btn); if (btn) { if (confirm(确定要删除这条记录吗)) { location.href DeleteServlet?id btn.dataset.id; } } });closest方法会从当前元素一直向上查找匹配的祖先元素这样就算你点的是按钮里的文字或图标也能正确找到按钮本身。4. 打通前后端Servlet、编码与数据库4.1 从 doGet 到 doPost表单到底该往哪发前端页面做完之后就要开始写后端了。我第一次写Servlet时连doGet和doPost都分不太清楚。简单理解就是doGet处理URL上的参数doPost处理请求体里的参数。URL上带参数会暴露给用户看到比如?password123所以登录、注册、修改数据这类操作一律用POST查询类操作比如搜索、分页、筛选可以用GET。我的Servlet用的是注解配置不用在web.xml里一条条写映射WebServlet(/register) public class RegisterServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); // 业务处理... 跳转... } }WebServlet(/register)的意思是前端表单的action写成register或者register.jsp对应的提交地址请求就会被这个Servlet接住。这里字符串要写对我第一次就把注解写成了/RegisterServlet表单提交的是/register结果老返回404排查了半天才发现大小写不一致。URL路径是大小写敏感的这一点务必注意。表单提交成功后用转发还是重定向也是第一次作业必问的考点。转发是服务器内部跳转浏览器URL不变重定向是服务器告诉浏览器“你去访问另一个地址”浏览器URL会变。注册登录成功后我推荐用重定向因为如果用了转发用户按F5刷新表单数据会被再次提交数据库里就会出现重复记录。重定向的写法是response.sendRedirect(index.jsp)配合Session把用户信息带过去。4.2 JDBC连接数据库PreparedStatement是底线数据库操作我用的是最朴素的JDBC。第一次作业没必要上MyBatis、JPA这些框架因为JDBC能让你看清楚SQL是怎么执行的这就是课程想教你的东西。我的连接工具类大概是这样的public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/web_homework?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }注意连接串里的几个参数serverTimezoneAsia/Shanghai是解决时区报错用的characterEncodingutf8是保证数据库读写中文不乱码的关键。如果你用的是MySQL 8以上版本驱动类名是com.mysql.cj.jdbc.Driver老版本的com.mysql.jdbc.Driver在新驱动里已经不推荐了。数据库查询我坚持用PreparedStatement这是第一次大作业安全方面最值得强调的一点。先看反面教材String sql SELECT * FROM user WHERE username username AND password password ;这种字符串拼接SQL的方式在代码评审时会被直接判为“安全隐患”因为它存在SQL注入漏洞。假如用户输入的用户名是 OR 11拼出来的SQL就变成了SELECT * FROM user WHERE username OR 11 AND password...条件恒为真等于绕过了密码校验。用PreparedStatement可以很好地避免这个问题String sql SELECT * FROM user WHERE username? AND password?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery();问号是参数占位符通过setString方法传值时驱动会自动把特殊字符转义用户无论输入什么都会被当成一个普通字符串而不是SQL的一部分。这是所有Web开发人员都必须掌握的底线知识。4.3 中文乱码不是每个地方都写UTF-8就行中文乱码是我在联调阶段最崩溃的问题。明明数据库里显示正常页面一显示就变成问号明明页面提交的是中文数据库里却存成了乱码。这背后其实有四个环节任何一个环节编码不对全链路就断了。第一项目文件编码。在IDEA里确认 File → Settings → Editor → File Encodings将Global Encoding和Project Encoding都设为UTF-8Properties Files也设为UTF-8。否则你编译出来的字节码和源码字节码就不一致。第二请求编码。在Servlet的doPost方法第一行加上request.setCharacterEncoding(UTF-8)这句要在读取任何参数之前执行否则POST请求体里的参数会被按ISO-8859-1解码中文变乱码。第三响应编码。输出HTML或写JSON时要告诉浏览器这次响应是UTF-8response.setContentType(text/html;charsetUTF-8)。第四数据库连接和建表。JDBC连接串带上characterEncodingutf8建表时指定CHARSETutf8mb4CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL ) DEFAULT CHARSETutf8mb4;utf8mb4是MySQL对UTF-8的完整实现能存下emoji等四字节字符比utf8适用范围更广。这几个环节都设了UTF-8之后乱码问题基本就解决了。如果还乱码就用SELECT username, HEX(password) FROM user去看数据库里的原始字节定位到底哪个环节出了问题。4.4 用Session做一个最简单的登录态用户登录之后怎么让其他页面知道“当前已经登录”最基础的方案就是Session。登录验证通过后执行HttpSession session request.getSession(); session.setAttribute(user, user); response.sendRedirect(index.jsp);在需要进行登录校验的页面比如列表页、详情页在一开始检查SessionUser user (User) session.getAttribute(user); if (user null) { response.sendRedirect(login.jsp); return; }退出登录就调一次session.invalidate()把整个会话清除掉。JSP页面里也可以用JSTL或者Scriptlet读取Session值。这里我要提一个第一次作业很容易犯的错误Session里存的对象必须可以被序列化或者至少是简单的Java对象不要直接塞一个Map在里面。如果你存的是一个字段很复杂的对象后期如果要存到Redis或者做分布式会话会踩大坑。第一次作业阶段用简单的User实体类就足够了。5. 联调排错与那些会被扣分的隐藏细节5.1 F12是你的第一排查工具别靠猜前后端打通之后最痛苦的是报错。我的经历是页面一片白控制台也不报错眼睛盯着代码看不出问题整个人处于一种“乱枪打鸟”的状态。后来养成一个习惯打开浏览器按F12先看Network面板再猜。Network面板里能看到每个请求的状态码、耗时、请求头、响应体。我总结了一个速查表联调时遇到问题对着看状态码含义最常出现的原因200请求成功正常302重定向登录后再访问其他页面被服务器踢回登录页404资源不存在URL路径写错、Servlet注解路径不匹配、单词拼错405方法不允许表单POST到Servlet但Servlet只重写了doGet500服务器内部错误后端代码或SQL抛异常需要看服务器日志比如页面按钮点下去没反应先看Network里那个请求是不是404。如果404问题通常在前端请求的URL和后端接口路径不一致如果500直接切到Tomcat控制台看堆栈信息报错会精确到哪一行。记住靠日志和Network定位别靠肉眼扫代码。还有一个容易被忽视的小技巧勾选Network面板的Preserve log保留日志这样页面刷新或重定向之后之前的请求日志不会被清空你能看到完整的请求跳转链条。5.2 老师不会明说但一定会扣分的五个细节我做完主流程后自己当了一回“验收老师”挑出了一堆处女座看了会抓狂的小问题。整理成五个痛点每条都是我踩过的。第一按钮被重复点击。注册页面网络慢的时候用户狂点“提交”按钮同一个用户被插进数据库好几条记录。解决办法很简单JS里监听按钮提交后立即设置disabled并且给按钮加上“提交中...”之类的反馈。后端层面再给用户名字段加唯一索引做一层兜底。第二表单没填完也能提交。我在前端做了校验但前面说过前端校验可以被绕过所以后端Servlet里必须重新判断。比如username或password为null或空字符串就直接返回错误提示绝不往下执行。第三没有任何操作反馈。用户点注册页面纹丝不动等两分钟没反应他会以为网站坏了。我的做法是不管成功还是失败要么用alert弹窗要么在当前页面上显示一行红色的错误提示或绿色的成功提示。答辩时演示这些反馈老师会认为你站在用户角度考虑问题。第四页面刷新导致重复提交。注册完成后如果使用请求转发F5刷新就会重新提交。前面说过的sendRedirect就是为此存在的表单处理成功之后永远用重定向。第五异常信息直接展示给用户。我第一次写Servlet时catch块里直接response.getWriter().write(e.toString())把数据库连接失败的一大长串异常打到了页面上。这既不美观也暴露了系统内部细节严重的话会被认为是不安全。正确做法是把异常信息打印到服务器日志页面上只给用户一句模糊的“操作失败请稍后再试”。5.3 路径与静态资源相对路径、绝对路径和上下文路径这一节单独拿出来说因为路径问题是我见过的大作业翻车率最高的点同时也是最好修的。页面里的CSS、JS、图片引用我推荐用以web应用上下文为根的绝对路径。什么意思呢在JSP页面里页面开头写% String path request.getContextPath(); %然后引用资源时写成link relstylesheet href%path%/css/style.css script src%path%/js/main.js/script这样无论你当前在根目录页面还是在list/detail.jsp这种子路径页面样式资源都能被正确找到。如果你用相对路径css/style.css在根目录页面没问题但进入需要转发到子目录的页面后浏览器会把路径解析成css/style.css相对于子目录的位置结果就是样式丢失。如果用的是JSTL可以直接用${pageContext.request.contextPath}写法更简洁link relstylesheet href${pageContext.request.contextPath}/css/style.css超链接、表单action也是一样的道理。凡是涉及到页面跳转和资源引用的地方统一采用上下文路径开头可以一次性消灭所有“换个页面就找不到文件”的坑。5.4 基础安全习惯SQL注入、明文密码和HTML转义第一次大作业虽然只是一个课程项目但代码里体现的安全意识是老师区分“死记硬背”和“真正理解”的试金石。我觉得有三个安全习惯是最起码的。第一所有SQL都使用PreparedStatement这是防止SQL注入的地基。这一点前面已经详细说了不再重复。第二密码不要明文存储。很多第一次作业的代码里数据库密码字段直接存的是明文的“123456”一旦数据库泄露所有用户密码就全暴露了。第一次作业阶段不需要上很复杂的加密算法但至少应该做个单向哈希。我用的方法是加盐的SHA-256String salt UUID.randomUUID().toString(); String hashed DigestUtils.sha256Hex(salt password);存到数据库的是salt和hashed两列登录校验时再对salt 用户输入密码重新做同样的哈希比对是否一致。这样即使数据库被拖走攻击者拿到的也只是无法还原原始密码的哈希串。第三用户提交的内容在JSP页面输出时要转义。比如用户在留言板里提交了script.../script如果答案原样输出到页面浏览器会把它当成脚本执行这就是存储型XSS漏洞。最简单的防御是在JSP输出时用JSTL的c:out标签它默认会转义HTML特殊字符。如果你用Java直接输出就用StringEscapeUtils.escapeHtml4之类的方法处理一下。这三点不是“给作业加分”的噱头而是你之后不管做毕业设计还是实习项目都会被反复要求的基础。第一次作业就能有意识地用上答辩时把它们讲出来效果会非常打动人。6. 加分项让页面“卷”一点但要克制6.1 做一个对打印友好的页面轻松解锁隐藏好感我在项目做完主流程之后想加点不喧宾夺主的小功能。这时候想到资料详情页也许有人会想把内容打印出来带回家看就顺手加了一个“打印/保存为PDF”的功能。实现思路其实很简单。先放一个打印按钮button onclickwindow.print()打印本页/button然后专门写打印样式。普通样式和打印样式分开的诀窍是media printmedia print { header { display: none; } footer { display: none; } .sidebar { display: none; } main { width: 100%; } }再加上两个细节打印时页面背景色会被去掉所以尽量不用依赖背景色的文字表格和卡片加page-break-inside: avoid;防止分页把一行内容劈成两半。用户点一下按钮浏览器就会弹出自带的打印对话框在这里选择“另存为PDF”就可以直接导出PDF文件。这个功能代码量不超过20行但在演示时非常讨巧。因为大部分同学的页面根本没法打印你却能现场导出一份干净整洁的PDF老师会有一种“这小子想得挺周到”的印象。6.2 两个小而实用的前端加分交互除了打印我还加了两个不抢主流程风头的小交互。第一个是登录页记住用户名。用户登录成功后在本地存储里存一份用户名localStorage.setItem(rememberedUser, username);页面加载时读取存储并填进输入框const remembered localStorage.getItem(rememberedUser); if (remembered) { document.getElementById(username).value remembered; }选了“记住我”才存没选就不存这样只要十几行代码就在用户体验上增加了一个贴心细节。第二个是列表数据加载失败时给用户提示。很多同学在Ajax请求失败时什么都不做页面看起来就像“互联网断联的空白地带”。我的做法是无论成功还是失败都在页面停留区域显示一行状态信息“数据加载成功共N条”“数据加载失败请检查网络后刷新重试”。这些小交互的核心原则是不引入额外的库不做复杂逻辑但每一处都在告诉用户“系统知道发生了什么”。老师验收时最怕的不是功能少而是功能有但用起来像半成品。6.3 答辩前的自检清单照着过一遍交作业前我给自己列了一张排查清单现在分享出来你一定用得上。功能链路自检注册一个新用户 → 数据库能查到记录 → 用这个账号能登录登录后访问受保护页面 → 不登录直接用URL访问会被踢回登录页退出登录 → 再访问受保护页面需要重新登录表单不填直接提交 → 前端有拦截后端有二次校验前端体验自检在不同窗口宽度下浏览页面不会横向滚动导航栏在当前页面时高亮正确点击链接都能跳转所有CSS和图片通过上下文路径加载没有资源404中文显示正常无乱码代码自查所有SQL使用PreparedStatement数据库密码和连接串不散落在JSP页面中页面输出用户内容时做了HTML转义没有把异常堆栈直接打印到浏览器如果这些检查项都通过了再看一眼演示时候的“台词”——先介绍技术架构再演示注册登录然后展示列表详情最后提一句你做的安全处理和打印功能。一套流畅的演示流程比堆一堆没讲明白的功能更容易拿高分。最后再分享一个我这次作业里最真实的体会。整个项目前前后后写了差不多一周大部分时间不是在写新功能而是在修各种路径、编码、校验上的小问题。一开始我觉得这些“破事”纯属浪费时间后来才意识到第一次大作业真正的收获不是那个网站本身而是你终于开始学会系统地排查问题。遇到报错先看日志、再复现路径、再定位原因这个思路不只是Web开发要用做任何事情都用得上。你的第一次大作业可能进度不顺可能最后功能砍了又砍但只要把一条核心链路做扎实了你交出去的就不再是“作业”而是一个站得住脚的小项目。