简介:这份JSP上机实习报告文档面向学习Java Web开发与JSP技术的在校学生及初学者,围绕课程实验场景整理了一套可直接参考的上机实践记录。内容覆盖在JSP页面中用Java代码完成1+1计算并输出结果、创建BookInfo JavaBean封装name、price、stock、author属性、编写DoString工具类转义字符串中的尖括号,以及基于JavaBean接收用户名和密码并完成admin/000校验与页面转发的登录程序,末尾附有环境搭建、页面跳转与调试心得。资源包共1个doc文件,约38KB,篇幅紧凑,适合作为课程作业对照、实验报告撰写模板与知识点复盘材料。目前已有102人浏览学习,可供需要快速梳理JSP基础语法、JavaBean规范与简单登录流程的读者参考。
1. JSP 上机实习报告:真正值钱的是那几处差点翻车的地方
翻完这份《JSP 上机实习报告》,最有意思的发现是:六道题里没有一道是死在业务逻辑上的。1+1 的算术没人算错,BookInfo 的四个属性也没人写漏,登录判断的 admin/000 更不至于记不住。真正把人卡住一下午的,是 pageEncoding 写成了 ISO-8859-1、包名和类名大小写对不上、转发路径少了一个斜杠、转义时先换了<后换了&。这几处踩坑点,才是这份文档资料里最该被抠出来的东西。
下面按报告里六道题的顺序重新走一遍:先讲 1+1 背后的三类 Java 代码块,再讲两道 JavaBean 题的规范,然后是登录那条收参—判断—转发的链路,中间插一章专门写排查,最后给一份交作业前十分钟能跑完的自检清单。新手能照着把六道题重新做对,熟手能看到每处写法背后的判断依据。
2. 从 1+1 到页面输出:JSP 里三类 Java 代码块的分工
第一题表面上是最简单的一道,实际上它逼着你把项目的目录结构、page 指令、三种脚本标签一次性搞清楚。这三件事任何一件没弄明白,后面五道题都会以各种奇怪的方式报错,而且报错信息往往指向别处,特别像玄学。
2.1 项目目录结构决定了你后面所有的路径写法
在 IDE 里新建动态 Web 项目之后,会得到一套固定骨架:页面文件放在 Web 内容根目录(不同 IDE 里叫 WebContent、webapp 或 WebRoot),编译产物落到WEB-INF/classes,第三方 jar 放WEB-INF/lib,而WEB-INF本身对浏览器是不可直接访问的。
这个“不可直接访问”不是限制,是设计。它意味着:凡是放在WEB-INF下的 JSP,浏览器敲地址访问不到,只能由服务端转发进去。后面第四题、第六题做页面跳转时,路径到底该相对谁、该不该带斜杠,全部由这套结构决定。我一般会建议在动手写第一行代码之前,先在脑子里过一遍这道题:这个 JSP 是要被浏览器直接敲地址打开的,还是只作为转发目标?答案不同,放置位置和跳转写法就不同。
判断依据很简单:能被直接访问的页面,路径相对“应用上下文根”;只能被转发到的页面,路径以/开头、相对上下文根来写,并且可以安全地藏在WEB-INF里。很多人第一题就随便把 JSP 丢在项目根下,后面做登录跳转时又照抄相对路径,结果一改目录就 404。
2.2 脚本片段、表达式、声明:三种标签的分工与边界
JSP 里嵌 Java 代码有三种写法,职责完全不同,混用就会出问题。
| 标签 | 名称 | 编译后的位置 | 是否直接输出 | 典型用途 |
|---|---|---|---|---|
<% %> | 脚本片段 | _jspService方法体内 | 否,需自己 out | 声明局部变量、写 if/for、调方法 |
<%= %> | 表达式 | _jspService内的out.print(...)参数 | 是,自动转字符串 | 输出变量、表达式结果 |
<%! %> | 声明 | Servlet 类的成员位置 | 否 | 定义成员变量或方法 |
第一题用的是表达式,因为它的目标就是把一个计算结果打到页面上。这里有两个高频错误:一是表达式后面加了分号,<%= sum; %>会直接编译失败,因为分号被当成表达式的一部分;二是在脚本片段里用out.print输出中文却没有设置响应编码,浏览器拿到的字节流按默认编码解析,就变成问号。
声明的坑更深。写在<%! %>里的变量是 Servlet 的成员变量,所有请求共享同一份。第一题只算 1+1 看不出问题,但如果换成累加器,两个浏览器同时刷新就会看到互相污染的结果。上机作业可以不管,但心里要清楚:<%! %>定义的是“应用级”状态,脚本片段里定义的才是“请求级”局部变量。这也是后面讲 JavaBean 作用域时的底层逻辑。
2.3 把 1+1 打到浏览器上:完整可跑的一版
把上面几件事合起来,第一题正确的写法大致是这样:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <% // 脚本片段:定义请求级局部变量,每个请求各自一份 int a = 1; int b = 1; int sum = a + b; %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>计算结果</title> </head> <body> <%-- 表达式:结果自动转字符串并输出 --%> <p>1 + 1 = <%= sum %></p> </body> </html>page 指令里那三个属性值得逐个说清楚。language指定脚本语言,实际只有 java 一个选项;contentType决定响应头里的Content-Type,浏览器靠它判断页面按什么编码渲染;pageEncoding决定容器用什么编码去读这个.jsp源文件本身。两者都写 UTF-8,才算是把“读文件”和“写响应”两头都堵住了。
报告里那段代码出现了pageEncoding="ISO-8859-1",还叠了好几个 DOCTYPE,明显是多段代码拼在一份文档里被格式吞掉了。这类文档资料的常见问题就是粘贴过程中标签被吃掉、引号变成全角、import=".*"这种无效导入残留下来。import=".*"是不合法的包名,正确写法要么省略,要么写import="java.util.*"这样的具体包。看到这一行就要意识到:这份报告里的代码不能逐字复制,得按语义重写一遍。
验证第一题是否真的跑通,不用猜。打开浏览器开发者工具看 Network,找到那个 JSP 请求,检查响应头里的Content-Type是否带charset=UTF-8。带了,说明编码链路是通的;没带或写的是别的编码,后面几道题只要出现中文就一定翻车。
3. BookInfo 与 DoString:两道 JavaBean 题里的规范与陷阱
第四题和第五题看着是两类东西——一个是数据载体,一个是工具方法——但它们被放在一起讲是有道理的:JavaBean 的“规范”两个字,一半靠命名约定,一半靠容器用反射去猜。猜错了不会报错,只会静默地拿不到值,这是最耗时间的一种失败。
3.1 值 JavaBean 的四条硬规范,缺一条反射就失灵
第四题要求 BookInfo 具备 name、price、stock、author 四个 String 属性。这份报告的代码里有几处典型问题:包名小写、类名小写、setter 里写成了= name少了this.。类名小写这件事在 Java 里能编译通过,但和规范不符,且和后面class="hbe.bookinfo"的写法强耦合,改一个地方就全线崩。
一个能被容器正确识别的 JavaBean,需要同时满足下面几条:
| 规范项 | 具体要求 | 不满足时的后果 |
|---|---|---|
| 类可见性 | public 类,且能被容器加载 | 反射实例化抛异常或找不到类 |
| 无参构造 | 提供一个 public 无参构造方法 | 容器无法 new 出实例 |
| 属性私有 | 成员变量 private | 不符合封装约定,且部分框架不识别 |
| 访问器成对 | getXxx / setXxx 命名成对出现 | 属性名推导失败,取到 null |
| 包名完整 | 类必须放在包内,且路径与包名一致 | 类加载失败 |
这里最容易被忽略的是“属性名由访问器推导,而不是由成员变量决定”。如果有成员变量bookName,但写成getName(),那么这个 Bean 的属性名就是name而不是bookName。后面用<jsp:setProperty property="name">才能对上,写property="bookName"就会抛找不到属性的异常。属性的首字母大小写规则也是固定的:getURL()推导出的属性名是URL而不是uRL。
另一处值得说道的是类型选择。price 和 stock 用 String,能省掉输入校验的麻烦,代价是所有比较和计算前都得手工转一次Integer.parseInt或new BigDecimal,而且要处理空串和非法字符。上机作业里图省事没问题,但心里要留个底:真做业务时这两个字段应该是数字类型,把校验前移到 setter 或独立的校验层,比后面满页面 try-catch 靠谱。
重写后的 BookInfo 大致如下:
package com.demo.bean; import java.io.Serializable; public class BookInfo implements Serializable { private static final long serialVersionUID = 1L; private String name; private String price; private String stock; private String author; // 容器用反射实例化时必须有无参构造 public BookInfo() { } public String getName() { return name; } public void setName(String name) { this.name = name; // 少了 this. 就是自己赋给自己 } public String getPrice() { return price; } public void setPrice(String price) { this.price = price; } public String getStock() { return stock; } public void setStock(String stock) { this.stock = stock; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } }implements Serializable这一条不是必须,但加上以后对象才能塞进 session 或走序列化复制,上机时用不到,工作里迟早用得到。serialVersionUID同理,属于“现在写一行,以后省一次排查”的东西。
3.2 工具 JavaBean 的转义顺序:DoString 这道题真正考的是什么
第五题的意图是把字符串里的尖括号换掉,防止内容被当成 HTML 标签解析。报告里的实现只有两行,而且写法上明显丢了方法名,大概率原本是str.replace(...)。这里有两个关键判断。
第一个是替换顺序。必须先替换&,再替换<和>。如果先换尖括号,换出来的结果里就包含&,再处理&时会把这部分又转义一遍,最终页面显示出来是一堆&lt;的字面量。这个错误在页面上看起来只是“多显示了一个 &”,很难第一眼联想到顺序问题,属于典型的血泪经验。
第二个是replace和replaceAll的区别。replace(CharSequence, CharSequence)是纯字面量替换,replaceAll走正则。转义场景里要用前者,因为一旦用了正则版本,待处理的内容里只要出现正则元字符就可能被误解释。
完整一点的做法是这样:
package com.demo.util; public final class DoString { // 工具类不允许实例化 private DoString() { } public static String escapeHtml(String str) { if (str == null) { return ""; } // 顺序不能反:先处理 &,再处理 < > 和引号 String s = str.replace("&", "&"); s = s.replace("<", "<"); s = s.replace(">", ">"); s = s.replace("\"", """); s = s.replace("'", "'"); return s; } }方法声明为 static,是因为这类工具没有状态,调用方不需要先 new 一个对象。声明为 final 类加私有构造,是防止被继承和误实例化。这两条都属于约定,不影响编译,但决定这段代码被别人复用时会不会被带偏。
需要提醒的是:把转义塞进 JSP 显示层,只是最粗的一层防护。真正落到生产,应该在输出到 HTML 上下文时统一转义,而不是靠某个页面记得调用一下工具类。上机作业做到这一层已经够了,但别把“我写了 DoString”当成安全措施。
3.3 用 jsp:useBean 把对象放进作用域
JavaBean 写完了,还得让 JSP 认识它。标准动作有三个:useBean负责查找或创建,setProperty负责赋值,getProperty负责取值。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%-- id 是作用域里的键名,class 必须是全限定类名 --%> <jsp:useBean id="book" class="com.demo.bean.BookInfo" scope="request" /> <%-- 手工赋值 --%> <jsp:setProperty name="book" property="name" value="某图书" /> <jsp:setProperty name="book" property="author" value="某作者" /> <p>书名:<jsp:getProperty name="book" property="name" /></p> <p>作者:<jsp:getProperty name="book" property="author" /></p>四个属性里class最容易写错:必须写全限定名,写成BookInfo或bean.BookInfo都会找不到类。scope决定这个对象的生命周期,四种取值的判断依据是“谁需要读到它”:只在当前页面用就 page,要在转发链路里传就 request,要跨多次请求记住就 session,全局共享就 application。上机时基本都是 request。
setProperty还有一个更省事的写法:property="*",它会把请求参数按名字自动匹配到所有同名属性上。第六题登录里的用户名密码,用这个动作可以省掉两行取值代码。代价是它会一次性把所有能匹配的参数都塞进去,参数名和属性名对不上时静默跳过,出错时不好定位。所以我更倾向在作业里手写getParameter,哪怕多两行,至少每一步都看得见。
4. 登录判断这条链:收参、比较、转发三步怎么落地
第六题是整份报告里唯一一道“有流程”的题:页面收参、代码判断、按结果去不同页面。它把前面几题的东西全串起来了——Bean 负责承载、脚本负责判断、转发负责跳转,中间任何一环写错,看到的现象都是“点了没反应”或者“跳到了奇怪的地址”。
4.1 收参的三个动作和编码设置的位置
接收表单参数只需要一句request.getParameter("username"),但要把中文正确收进来,还有两个前提动作。
第一个是编码设置的位置。request.setCharacterEncoding("UTF-8")必须放在第一次读取参数之前。容器在第一次getParameter时就把请求体解析成字符串并缓存了,之后再设编码不会重新解析,等于白设。这个顺序问题在代码里看不出任何异常,只会表现为“中文全变问号”,非常容易误判成页面编码没写对。
第二个是 GET 和 POST 的差别。表单默认 method 是 GET,参数拼在 URL 上,请求体为空,setCharacterEncoding对它们不起作用——查询串的解码依赖容器对 URI 的编码配置。所以做登录这类提交,表单要明确写method="post",让编码设置真正生效。这一步在报告原文里没有体现,但它是中文字段能不能正确收到的前提。
<form action="login.jsp" method="post"> 用户名:<input type="text" name="username"> 密码:<input type="password" name="password"> <button type="submit">登录</button> </form>表单里两个 input 的name值,必须和后面getParameter里的字符串一字不差,包括大小写。这是另一个查起来最费劲的地方:参数取到 null,代码看起来毫无问题,实际只是名字对不上。
4.2 判断逻辑写在 JSP 里到底合不合适
作业允许在 JSP 里直接写 if-else,因为题目就是这么要求的。但从工程角度看,这段逻辑有几个必须补的细节。
比较字符串要用equals而不是==,这一点报告里写对了。更稳的写法是把常量放左边,"admin".equals(username),这样即使 username 是 null 也不会抛空指针,只是返回 false。判断条件里如果两个都是变量,一旦有一个为 null,就会直接抛异常,页面变成 500。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <% request.setCharacterEncoding("UTF-8"); // 必须在取参数之前 String username = request.getParameter("username"); String password = request.getParameter("password"); // 常量在前,避免 username 为 null 时抛空指针 if ("admin".equals(username) && "000".equals(password)) { request.setAttribute("msg", "欢迎登录"); request.getRequestDispatcher("/ok.jsp").forward(request, response); } else { request.setAttribute("msg", "登录失败"); request.getRequestDispatcher("/fail.jsp").forward(request, response); } %>还有一点要说明:把判断逻辑放在 JSP 里,等于把控制逻辑和展示混在一起,页面越写越难维护。上机阶段没问题,但心里要清楚正规做法是放到 Servlet 或过滤器里,JSP 只负责渲染。这个迁移思路值得在报告的心得部分提一句,比单纯写“学会了页面跳转”有价值得多。
另外,登录判断这个例子里把用户名密码硬编码在页面中,只能作为练习题,不能理解为真实登录实现。真实系统里密码要存哈希、要防暴力尝试、要走会话管理,这几件事在这道题的范围之外,但别把作业写法当成可上线方案。
4.3 转发与重定向:路径、作用域、地址栏三处差异
第六题说“转发到页面显示”,用的是RequestDispatcher.forward。它和response.sendRedirect经常被混着用,因为两者在浏览器上都能看到页面变了。
| 对比项 | 转发 forward | 重定向 sendRedirect |
|---|---|---|
| 请求次数 | 1 次,服务端内部转交 | 2 次,浏览器重新发起 |
| 地址栏 | 不变 | 变为新地址 |
| request 作用域 | 保留,可以传 attribute | 丢失,需要靠 session 或参数 |
| 路径基准 | 以/开头时相对上下文根 | 相对当前请求路径,或写完整路径 |
| 能否到达 WEB-INF 下页面 | 可以 | 不可以 |
这张表里的前两行决定了:如果用了重定向,前面request.setAttribute("msg", ...)设的值在目标页面里一定是 null。这个现象非常迷惑人——代码没错,页面就是拿不到值。判断依据是:需要携带 request 级数据,就必须用转发。
路径那一行同样关键。转发里写getRequestDispatcher("ok.jsp")(不带斜杠)和写"/ok.jsp"(带斜杠)解析基准不同,前者相对当前请求的目录,后者相对应用根。目标页面在根目录时两种写法都能通,一旦页面挪到子目录,不带斜杠的写法就开始 404。我一般直接统一成带斜杠的绝对写法,省得以后挪文件时重新推理一遍。
还有一处容易被忽略:forward之后再往响应里写内容是没有意义的,因为响应已经提交了。所以转发语句之后不要跟out.print或者 HTML,那些内容不会按预期出现。
5. 避坑与排查:上机最常卡住的五个地方
这一章把六道题里反复出现的问题集中列一遍。每一条都按“看到什么现象—为什么会这样—怎么解决”来写,遇到卡壳时可以按顺序对照。
5.1 中文乱码:三处编码只要有一处不一致就出问题
现象:页面上的中文变成问号、方块,或者提交的中文参数在后台取出来是乱码,英文数字一切正常。
原因:整条链路上有三处编码需要一致——JSP 源文件的读取编码(pageEncoding)、响应的输出编码(contentType 里的 charset)、请求参数的解析编码(setCharacterEncoding 或容器 URI 配置)。报告原文里出现过 ISO-8859-1 和 utf-8 混用的情况,这种混搭在纯英文页面里看不出问题,一旦出现中文就必然翻车。
解决:page 指令统一写contentType="text/html; charset=UTF-8" pageEncoding="UTF-8";取参数前第一行写request.setCharacterEncoding("UTF-8");表单明确用 POST。三处都对齐后仍乱码,就去查容器的 URI 编码配置,那是 GET 请求的最后一道关口。
5.2 包名、类名与实例化对不上:ClassNotFound 和属性为 null
现象:<jsp:useBean>报找不到类,或者类找到了但页面显示 null,setter 明明写了却没生效。
原因:三种情况最常见。类名首字母小写(如bookinfo)、包名和目录结构不一致、class属性没写全限定名。另一种是属性名推导错位——成员变量叫bookName,访问器叫getName,属性名就变成了name。
解决:先把类名改成大驼峰、包名全小写,确认WEB-INF/classes下生成的 class 路径与包名逐级对应;再检查class属性里带没带包名;最后核对property的值和访问器推导出的属性名是否一致。这三步走完,反射相关的报错基本都能定位。
5.3 转义顺序反了:页面上多出一串 <
现象:本来只想把<转成实体,结果页面显示的是&lt;这种双层转义后的字面量。
原因:替换顺序写反了。先处理了尖括号,产生的结果里含&,再处理&时把新产生的实体又转义了一遍。
解决:固定顺序,先&,再<、>、"、'。另外确认用的是replace而不是replaceAll,后者按正则处理,遇到内容里带正则元字符时会给出意外结果。
5.4 转发之后 404 或白屏:路径基准和响应提交
现象:判断逻辑走对了分支,但浏览器显示 404,或者干脆一片空白。
原因:404 大多是转发路径的基准搞错了,不带斜杠时相对当前目录解析,页面一挪位置就断;白屏则通常是forward之后又写了输出,或者转发目标页面本身有编译错误被吞掉了。
解决:转发路径统一以/开头,相对上下文根;把forward放在分支的最后一行,后面不再写任何输出;如果目标页面白屏,直接单独访问一次那个页面,看它自己的报错信息,很多时候问题根本不在转发这一步。
5.5 改了代码没生效:class 没重编译或缓存还在
现象:改完 JavaBean 的代码,刷新浏览器,页面行为还是旧的,清浏览器缓存也没用。
原因:JSP 会自动重编译,但.java文件改完之后,编译产物不一定同步更新到WEB-INF/classes,尤其是手工放 class 的项目。另外容器对已加载的类有缓存,热部署没触发时旧版本会一直生效。
解决:先确认WEB-INF/classes下对应 class 的时间戳是不是最新的;不是就手工重新编译或让 IDE 重新发布。再不行就重启容器,别在浏览器层面反复刷新浪费时间。这一条属于典型的“看起来像代码问题,实际是构建问题”,排查时先把构建产物放在怀疑链的第一位。
6. 交报告之前:一份十分钟能跑完的自检清单
六道题做完,别急着交。按下面这张表走一遍,把能提前发现的错误在本地解决掉,比交上去被退回来重做强得多。
| 检查项 | 合格标准 | 出问题时先看哪儿 |
|---|---|---|
| 编码一致性 | 所有 JSP 的 page 指令都写 UTF-8,响应头带 charset | 响应头 Content-Type |
| 类加载 | WEB-INF/classes下 class 路径与包名逐级对应 | 目录结构与 package 声明 |
| Bean 规范 | 类名大驼峰、有无参构造、访问器成对 | 属性名推导结果 |
| 转义顺序 | 先 & 后尖括号,用 replace 不用 replaceAll | 页面输出内容 |
| 转发路径 | 统一以/开头,forward 后无输出 | 目标页面能否单独访问 |
| 请求方法 | 含中文的表单用 POST,取参前设编码 | 表单 method 与 setCharacterEncoding 位置 |
表里最后一行值得单独说一个技巧:判断编码问题到底出在读取还是输出,不用靠猜。打开浏览器开发者工具,看响应头里Content-Type后面带的 charset;如果这里是对的但页面还是乱码,问题就在源文件读取或请求解析那一侧。这个判断方法只需要十秒,比反复改代码试错快得多。
回过头看,这份上机报告里真正让我记住的不是那六段代码,而是几个“当时不知道为什么、后来才想明白”的瞬间:setCharacterEncoding 为什么必须写在取参之前、forward 之后为什么不能有输出、转义为什么有顺序。这些点在没有报错提示的情况下,只能靠理解机制去推。
从那以后我每次碰这种练习性质的项目,都会强制走一遍上面那张表,顺序固定、不跳步——因为它省下的是最难受的那部分时间:明明觉得自己写对了,却怎么都找不出错在哪。这份报告的价值也在这里,它把六个小题摆在一起,正好把 JSP 最基础的那条链路完整走了一遍,照着复现一次,比看十篇教程都实在。希望帮到你。
本文还有配套的精品资源,点击获取