- 1WEB 基础HTTP / Browser / WebServer静态页面写死,人人看到一样
- 2Servlet手写 if-else 分发不可扩展,无生命周期/并发管理
- 3JSP:参数散字符串参数散成一堆 String,取值靠字段名硬记
- 3JSP:scriptlet 塞逻辑scriptlet 把逻辑塞进页面,代码与 HTML 糊在一起
- 3JavaBean:数据对象化对象全靠 new、层层硬依赖
- 4Spring IoCWeb 路由仍手写 web.xml/if-else、手动取参绑定,展示/业务/数据访问没分开
- 5MVC · SpringMVC数据在库里,访问是重复样板 + 拼串有注入风险
- 6MySQLJava 连库要手写五步,连接与资源管理全手动
- 7JDBCSQL 与 Java 混写、结果集逐行手映射、没有动态 SQL
- 8MyBatis四份配置 + web.xml 装配繁琐,部署要外部 Tomcat
- 9SpringBoot链尾 · 不考核
内置对象 · JSP · JavaBean
getWriter() 手拼 HTML,代码与页面糊在一起。本章用 JSP + JavaBean 把「展示」和「数据」分开。内置对象 · JSP · JavaBean
目标:掌握 JSP 页面五种组成元素、运行机制(JSP → Servlet → HTML)、内置对象与 JavaBean;能写「表单取值 → 封装 → 回显」的完整 JSP。
本模块把模块1 埋的线接上:浏览器请求一个
.jsp,Tomcat 把它编译成 Servlet、再执行生成 HTML——你写的<% %>最终都变成了 Java 代码。
一、JSP 运行机制:JSP → Servlet → HTML
JSP 不是「直接运行」的:首次请求某 .jsp 时,Tomcat 把它编译成一个 Servlet(.java → .class),之后每次请求都是执行这个 Servlet。所以:
- 编译期(写代码时):检查语法、解析指令;
- 运行期(浏览器请求时):Servlet 的
service()执行,脚本/表达式被拼进输出流,最终产出 HTML 返回浏览器。
一句话:JSP = 「披着 HTML 外衣的 Servlet」——这解释了为什么 JSP 里能用 request/response、为什么能访问数据库。
二、JSP 页面的五种组成元素
一个 JSP 页面由五类内容拼成,下面交互实验逐一点开看每类的写法与编译期/运行期怎么处理:
交互实验:JSP 五类元素拆解(JSPPageStructureExplorer)
同一个 .jsp 页面里混着 5 类元素。点某类,源码里属于它的行会高亮,右边说明服务器编译/运行期怎么处理它——这就是「JSP = HTML + Java 片段 + 标签」。
01<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>02<%! int visitCount = 0; %>03<html><head><title>JSP 示例</title></head>04<body>05<% visitCount = visitCount + 1; %>06<p>本页已被访问 <%= visitCount %> 次。</p>07<a href="hello.jsp">刷新再点一次</a>08</body></html>
点上面任一元素类别,看它在源码里的分布与服务器处理方式。
关键区别:<%! %> 声明 → Servlet 成员(一次性);<% %> 程序片 → service() 内(每次请求执行);<%= %> 表达式 → 求值后输出。三者都最终被编译成一个 Servlet。
| 元素 | 写法 | 作用 |
|---|---|---|
| 普通 HTML | <div>...</div> | 原样输出到响应 |
| 指令 | <%@ page ... %> | 编译期处理:设 contentType / import 等 |
| 声明 | <%! int i = 0; %> | 编译成 Servlet 的成员变量/方法(整个页面共享) |
| 脚本 | <% if(...){...} %> | 编译进 service() 的 Java 语句 |
| 表达式 | <%= x %> | 编译成 out.print(x)——把值写进响应 |
声明 vs 脚本的关键区别:
<%! %>里是成员级(变量/方法,跨请求存活、线程共享,慎用);<% %>里是方法内局部语句(每次请求执行)。这也是「声明变量」与「脚本变量」的差别。
三、page 指令
<%@ page ... %> 在编译期生效,最常见的属性:
contentType="text/html;charset=UTF-8":设响应类型与编码;language="java":脚本语言(默认 java);import="java.util.*":给 Servlet 加 import;errorPage/isErrorPage:错误页关联。
四、JSP 注释 vs HTML 注释
- JSP 注释
<%-- ... --%>:编译期就被去掉,不会出现在生成的 Servlet/HTML 里——真正的「源码级」隐藏; - HTML 注释
<!-- ... -->:会原样发到浏览器(查看源码可见)——只影响显示、不影响安全。
别用 HTML 注释藏敏感信息——它会被用户看到。要真正隐藏,用 JSP 注释。
五、内置对象与 request.getParameter()
JSP 提供 9 个内置对象(implicit objects),不用声明就能直接用。关键差异在作用域(活多久)与创建时机:
| 内置对象 | 类型 | 作用域 / 说明 |
|---|---|---|
request | HttpServletRequest | 一次请求;取参、存请求级数据 |
response | HttpServletResponse | 一次响应;写输出、设头/重定向 |
session | HttpSession | 一个会话(跨请求、同一用户);登录态 |
application | ServletContext | 整个应用(全局共享);如在线人数 |
out | JspWriter | 输出流;<%= %> 最终走它写响应 |
pageContext | PageContext | 当前页上下文;可访问上面所有作用域 |
config | ServletConfig | 该 Servlet/JSP 的配置 |
page | Object(this) | 当前页面对象本身 |
_page | 同上(内部) | 编译生成的 page 实例引用 |
下面交互实验对比它们的作用域与创建时机:
交互实验:JSP 内置对象对比(JSPImplicitObjectTable)
JSP 里这 5 个对象不用声明就能用。点行看它的类型 / 作用域 / 创建时机 / 典型用法; 勾选「按作用域排序」看谁活得更久——request/response/out(单次)< session < application。
| 内置对象 | Java 类型 | 作用域 | 创建时机 |
|---|---|---|---|
| request | HttpServletRequest | 单次请求(request 作用域) | 每次客户端发起请求时创建,请求结束即销毁 |
| response | HttpServletResponse | 单次响应(与 request 配对) | 每次请求处理时创建,用于构造返回给客户端的响应 |
| session | HttpSession | 单个会话(session 作用域) | 首次 getSession(true)/setAttribute 时创建;超时或被 invalidate() 销毁 |
| application | ServletContext | 整个 Web 应用(application 作用域) | Web 应用启动时创建,应用停止时销毁——全局唯一 |
| out | JspWriter | 当前页面输出(单次请求) | 每次请求处理该 JSP 页时创建 |
请求对象(request):类型 HttpServletRequest;作用域 单次请求(request 作用域);创建时机——每次客户端发起请求时创建,请求结束即销毁。
典型用法:getParameter() 取表单参数;setAttribute/getAttribute 在请求内传递数据
选型口诀:请求内传值用 request,跨请求(同一用户)用 session,全局共享用 application;response 管「回什么」,out 管「往响应写」。
表单取值的核心是 request.getParameter("name")——字段名 = 取参键。把参数装进 JavaBean、再回显的完整链路,看下面的全链路动画:
交互实验:表单 → request → JavaBean → 回显(JavaBeanFormFlow)
点「下一步」看一条留言从表单到页面回显的全链路:参数怎么被 request 取出、怎么装进WordBean、最后怎么显示出来。
① 表单提交:用户填好 title / content,点「提交」——浏览器把字段以 name=value 形式 POST 给服务器。
表单
request
JavaBean
页面回显
JavaBean = 私有字段 + getter/setter 的封装类(对照 WordBean.java)。表单字段名 = request 取参键 = bean 的属性——三者对齐,数据才流得通。
六、深入两个内置对象:session 与 application
九个内置对象里,request/response/out 都只活一次请求;session 和 application 是两个「跨请求」的对象,也是实际项目里用得最多的。它们的差别就一句话:数据归谁。下面的交互实验先让你亲手感受两个作用域的区别:
交互实验:session vs application 作用域(SessionAppScopeLab)
两个虚拟用户小明 / 小红轮流操作。每个用户的按钮把数据写进 TA 自己的 session;而「发请求」会让application 作用域的全站 PV +1—— 看事件日志里每条数据落在哪个作用域,就明白两个对象各管什么。
先让 TA「发一次请求」,容器才会创建 session
先让 TA「发一次请求」,容器才会创建 session
application 作用域(ServletContext)——整个应用所有用户共享,全局唯一
0当前在线人数(存活会话数) 0事件日志(按作用域着色)
- 容器事件Web 应用启动:application(ServletContext)创建——整个应用唯一,活到应用停止
结论:session 管「同一个用户跨请求」(登录态、购物车,凭 JSESSIONID 认人);application 管「所有用户共享」(PV、在线人数)。数据放错作用域 = 串数据或白存。
6.1 session(HttpSession):同一用户的跨请求状态
作用:HTTP 是「无状态」协议——每次请求都是一次独立连接,服务器自己认不出「第二次请求和第一次是同一个人」。session 解决的就是这个问题:用户首次请求时,容器为他创建一个 HttpSession,并把它的 id 写进响应头(Set-Cookie: JSESSIONID=...);之后的请求浏览器会自动带上这个 cookie,容器凭 id 找到同一个会话。session = 跟在一个具体用户身上、由他多次请求共享的一个属性袋。
生命周期(三阶段):
| 阶段 | 触发条件 | 发生了什么 |
|---|---|---|
| 创建 | 首次 getSession(true) 或 setAttribute | 容器生成 HttpSession,把 JSESSIONID 写进 Set-Cookie |
| 使用 | 后续请求带着 cookie 回来 | 命中同一个会话对象,getAttribute/setAttribute 读写;长时间不访问则「闲置时间」累计 |
| 销毁 | ① 闲置超过 maxInactiveInterval(默认 30 分钟)② 显式调 invalidate() ③ 服务器停止 / 应用卸载 | 属性清空;下次请求会创建新会话(登录态就此丢失) |
常用方法:
| 方法 | 用途 |
|---|---|
request.getSession(true/false) | 取当前会话;不存在时 true 顺手创建、false 返回 null |
setAttribute(name, value) / getAttribute(name) / removeAttribute(name) | 存 / 取 / 删属性(value 必须是对象) |
invalidate() | 立即使本会话失效——「退出登录」的标准动作 |
isNew() | 是否新建的会话 |
getId() | 取会话 id(即 JSESSIONID 的值) |
getMaxInactiveInterval() / setMaxInactiveInterval(秒) | 读 / 设超时时长 |
应用示例一:登录状态保持(session 最经典的用途)
// LoginServlet.java —— 登录成功后把用户存进 session
String name = request.getParameter("name");
String pwd = request.getParameter("pwd");
if ("admin".equals(name) && "123456".equals(pwd)) {
HttpSession session = request.getSession(); // 没有就创建
session.setAttribute("user", name); // 登录态:跨请求、只有本人可读
response.sendRedirect("welcome.jsp");
} else {
response.sendRedirect("login.jsp?error=1");
}
<%-- welcome.jsp —— 任何页面随时「查户口」:现在登录的是谁 --%>
<%
Object user = session.getAttribute("user");
%>
<% if (user != null) { %>
<p>你好,<%= user %>! <a href="logout.svc">退出</a></p>
<% } else { %>
<p>尚未登录。 <a href="login.jsp">请先登录</a></p>
<% } %>
// LogoutServlet.java —— 退出 = 销毁会话,属性全部清空
request.getSession().invalidate();
response.sendRedirect("login.jsp");
应用示例二:购物车(跨请求累积数据)
// AddToCartServlet.java —— 每次加购都要「先取出来、再加、再存回去」
HttpSession session = request.getSession();
@SuppressWarnings("unchecked")
List<String> cart = (List<String>) session.getAttribute("cart");
if (cart == null) {
cart = new ArrayList<>();
}
cart.add(request.getParameter("goods"));
session.setAttribute("cart", cart); // 购物车对象存进会话,跨请求共享
两个前提:① 用户浏览器要允许 cookie——否则 JSESSIONID 带不回来,每次请求都会创建新会话;② 存进 session 的对象建议实现
Serializable——服务器做会话持久化 / 集群迁移时需要序列化。
6.2 application(ServletContext):整个应用全局共享
作用:application 指的是整个 Web 应用的 ServletContext——应用启动时只创建一次、应用停止时销毁,被所有用户、所有会话、所有请求共享。存在这里的是「全站公共数据」:统计(访问次数、在线人数)、全局配置(数据库连接信息等)。
生命周期:Web 应用被容器加载时创建 → 应用整个存活期间一直可用 → 应用停止时销毁。全局唯一、活得比任何 session 都久——所以「全站共享」的数据只能放这里。
常用方法:
| 方法 | 用途 |
|---|---|
setAttribute(name, value) / getAttribute(name) / removeAttribute(name) | 存 / 取 / 删全局属性 |
getInitParameter(name) | 读 web.xml 里 <context-param> 的全局配置 |
getInitParameterNames() | 取所有 context-param 的名字 |
getRealPath(path) | 把应用内相对路径转成磁盘绝对路径(文件上传 / 下载常用) |
getResourceAsStream(path) | 以流方式读取应用内的资源文件 |
log(msg) | 向容器日志输出信息 |
应用示例一:在线人数统计(application + Listener 的经典组合)
// OnlineCountListener.java —— 旁听会话创建 / 销毁事件,在 application 里维护在线数
public class OnlineCountListener implements HttpSessionListener {
public void sessionCreated(HttpSessionEvent e) {
ServletContext app = e.getSession().getServletContext();
Integer n = (Integer) app.getAttribute("online");
app.setAttribute("online", (n == null ? 0 : n) + 1); // 有会话创建 → +1
}
public void sessionDestroyed(HttpSessionEvent e) {
ServletContext app = e.getSession().getServletContext();
Integer n = (Integer) app.getAttribute("online");
app.setAttribute("online", Math.max(0, (n == null ? 0 : n) - 1)); // 会话销毁 → -1
}
}
<!-- web.xml —— 注册监听器,事件发生时容器自动通知它(上一章讲过) -->
<listener>
<listener-class>examples.OnlineCountListener</listener-class>
</listener>
<%-- index.jsp —— 任何页面随时显示全站在线人数 --%>
<p>当前在线:<%= application.getAttribute("online") %> 人</p>
应用示例二:PV 统计 + 读 web.xml 全局配置
// IndexServlet.java —— 每次访问都在 application 里累加 PV
ServletContext app = request.getServletContext();
Integer pv = (Integer) app.getAttribute("pv");
app.setAttribute("pv", (pv == null ? 0 : pv) + 1);
// 读 web.xml 的全局配置——代码里不用硬编码
String siteName = app.getInitParameter("siteName");
<!-- web.xml —— <context-param> 是「全局配置」的标准存放处 -->
<context-param>
<param-name>siteName</param-name>
<param-value>Zmofun 在线教育</param-value>
</context-param>
6.3 选型对照:到底该用哪个?
| 场景 | 用谁 | 为什么 |
|---|---|---|
| 同一次请求内传参(Servlet 处理完转发给 JSP 显示) | request | 只活本次请求,用完自动清 |
| 同一用户跨页面保留登录态 / 购物车 | session | 跨请求、且只有该用户可见 |
| 全站在线人数 / 总访问次数 PV | application | 所有用户共享,不能存在某人的 session 里 |
| 数据库连接信息等全局配置 | application(context-param) | 全站读同一份,不硬编码 |
常见坑:把「在线人数」存进每个用户的 session——session 只装一个人的数据,永远汇总不到全站;把「登录状态」存进 application——全体用户会共享同一份登录态(串数据)。口诀再念一遍:请求内用 request,跨请求(同一用户)用 session,全局共享用 application。
七、JavaBean:带 getter/setter 的封装类
JavaBean 就是「私有字段 + getXxx/setXxx」的普通 Java 类(对照 GuestBook/src/examples/WordBean.java)。它把「一组相关数据 + 操作」打包成对象,JSP 用它承接表单数据、传给后续处理。
JavaBean 规范(满足才算标准 Bean):
- public 类,且提供无参构造器——容器/框架靠它
new出实例; - 属性私有(
private),通过 getter/setter 访问:getName()/setName(String),方法名与属性名驼峰对应; - 建议实现
Serializable——要存进 session 或跨进程序列化时必需; - 不暴露静态成员、不直接对外敞开内部状态。
选型口诀:请求内传值用 request,跨请求用 session,全局共享用 application。
八、JSP 与 Servlet 的关系
- Servlet:纯 Java,用
response.getWriter()手写 HTML——逻辑清晰但「HTML 和代码混在一起」很痛苦; - JSP:HTML 为主、Java 为辅——页面友好,但复杂逻辑塞进
<% %>会变「面条」。
实践中的分工:JSP 负责展示(View),Servlet 负责控制/业务(Controller)——这正是后面模块5 MVC 的雏形。
思考题
<%! int count = 0; %>和<% int count = 0; %>有什么区别?哪个会跨请求共享、为什么危险?- JSP 注释
<%-- --%>和 HTML 注释<!-- -->,哪个能被用户「查看源码」看到? - 一个留言板:提交用哪个内置对象取参?同一用户刷新后还想保留「已登录」状态,该用哪个对象存?
- 实现「退出登录」应该调用 session 的哪个方法?会话销毁后,该用户下一次请求会发生什么?
- 为什么「全站在线人数」不能存在每个用户的 session 里?正确的做法需要什么配合(提示:上一章的 Listener)?
学习小测验
学习小测验
JSP 页面里 <% if(x>0){...} %> 属于哪种元素?
<%! int i = 0; %> 与 <% int i = 0; %> 的区别是( )
要「真正隐藏」一段代码不让用户查看源码看到,应使用( )
要保存「同一用户多次访问都要保留」的数据(如登录状态),应使用哪个内置对象?
实现「退出登录」(清空登录态),标准做法是调用( )
调用 session.invalidate() 之后,该用户下一次请求会发生什么?
下列数据最适合存放在 application(ServletContext)里的是( )
<%= name %> 在编译后会变成什么?
JSP 与 Servlet 的关系最准确的描述是( )
JSP 里要把「整个应用共享」的数据(如在线人数)存起来,应使用哪个内置对象?
按 JavaBean 规范,一个标准 Bean 必须提供( )
new、层层硬依赖——改一处牵一片(下一章 Spring IoC)。