JavaEE 环境与程序设计 · 交互课程
技术演进接力链(M1 → M9)
  1. 1WEB 基础
    HTTP / Browser / WebServer
    静态页面写死,人人看到一样
  2. 2Servlet
    手写 if-else 分发不可扩展,无生命周期/并发管理
  3. 3JSP:参数散字符串
    参数散成一堆 String,取值靠字段名硬记
  4. 3JSP:scriptlet 塞逻辑
    scriptlet 把逻辑塞进页面,代码与 HTML 糊在一起
  5. 3JavaBean:数据对象化
    对象全靠 new、层层硬依赖
  6. 4Spring IoC
    Web 路由仍手写 web.xml/if-else、手动取参绑定,展示/业务/数据访问没分开
  7. 5MVC · SpringMVC
    数据在库里,访问是重复样板 + 拼串有注入风险
  8. 6MySQL
    Java 连库要手写五步,连接与资源管理全手动
  9. 7JDBC
    SQL 与 Java 混写、结果集逐行手映射、没有动态 SQL
  10. 8MyBatis
    四份配置 + web.xml 装配繁琐,部署要外部 Tomcat
  11. 9SpringBoot
    链尾 · 不考核

内置对象 · JSP · JavaBean

承接上一章的死结
上一章(Servlet)的死结:手写 if-else 分发不可扩展、无生命周期/并发管理;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。所以:

一句话: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 ... %> 在编译期生效,最常见的属性:

四、JSP 注释 vs HTML 注释

别用 HTML 注释藏敏感信息——它会被用户看到。要真正隐藏,用 JSP 注释。

五、内置对象与 request.getParameter()

JSP 提供 9 个内置对象(implicit objects),不用声明就能直接用。关键差异在作用域(活多久)与创建时机:

内置对象类型作用域 / 说明
requestHttpServletRequest一次请求;取参、存请求级数据
responseHttpServletResponse一次响应;写输出、设头/重定向
sessionHttpSession一个会话(跨请求、同一用户);登录态
applicationServletContext整个应用(全局共享);如在线人数
outJspWriter输出流;<%= %> 最终走它写响应
pageContextPageContext当前页上下文;可访问上面所有作用域
configServletConfig该 Servlet/JSP 的配置
pageObject(this)当前页面对象本身
_page同上(内部)编译生成的 page 实例引用

下面交互实验对比它们的作用域与创建时机:

交互实验:JSP 内置对象对比(JSPImplicitObjectTable)

JSP 里这 5 个对象不用声明就能用。点行看它的类型 / 作用域 / 创建时机 / 典型用法; 勾选「按作用域排序」看谁活得更久——request/response/out(单次)< session < application。

内置对象Java 类型作用域创建时机
requestHttpServletRequest单次请求(request 作用域)每次客户端发起请求时创建,请求结束即销毁
responseHttpServletResponse单次响应(与 request 配对)每次请求处理时创建,用于构造返回给客户端的响应
sessionHttpSession单个会话(session 作用域)首次 getSession(true)/setAttribute 时创建;超时或被 invalidate() 销毁
applicationServletContext整个 Web 应用(application 作用域)Web 应用启动时创建,应用停止时销毁——全局唯一
outJspWriter当前页面输出(单次请求)每次请求处理该 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 给服务器。

表单

title=第一条留言
content=你好,JavaEE!

request

request.getParameter("title")
request.getParameter("content")

JavaBean

wordBean.setWordTitle(title)
wordBean.setWordContent(content)

页面回显

getWordTitle() → 显示
getWordContent() → 显示
第 1 / 4 步

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

事件日志(按作用域着色)

  1. 容器事件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跨请求、且只有该用户可见
全站在线人数 / 总访问次数 PVapplication所有用户共享,不能存在某人的 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):

  1. public 类,且提供无参构造器——容器/框架靠它 new 出实例;
  2. 属性私有(private),通过 getter/setter 访问:getName() / setName(String),方法名与属性名驼峰对应;
  3. 建议实现 Serializable——要存进 session 或跨进程序列化时必需;
  4. 不暴露静态成员、不直接对外敞开内部状态。

选型口诀:请求内传值用 request,跨请求用 session,全局共享用 application。

八、JSP 与 Servlet 的关系

实践中的分工:JSP 负责展示(View),Servlet 负责控制/业务(Controller)——这正是后面模块5 MVC 的雏形。

思考题

  1. <%! int count = 0; %> 和 <% int count = 0; %> 有什么区别?哪个会跨请求共享、为什么危险?
  2. JSP 注释 <%-- --%> 和 HTML 注释 <!-- -->,哪个能被用户「查看源码」看到?
  3. 一个留言板:提交用哪个内置对象取参?同一用户刷新后还想保留「已登录」状态,该用哪个对象存?
  4. 实现「退出登录」应该调用 session 的哪个方法?会话销毁后,该用户下一次请求会发生什么?
  5. 为什么「全站在线人数」不能存在每个用户的 session 里?正确的做法需要什么配合(提示:上一章的 Listener)?

学习小测验

学习小测验

Q1

JSP 页面里 <% if(x>0){...} %> 属于哪种元素?

Q2

<%! int i = 0; %> 与 <% int i = 0; %> 的区别是( )

Q3

要「真正隐藏」一段代码不让用户查看源码看到,应使用( )

Q4

要保存「同一用户多次访问都要保留」的数据(如登录状态),应使用哪个内置对象?

Q5

实现「退出登录」(清空登录态),标准做法是调用( )

Q6

调用 session.invalidate() 之后,该用户下一次请求会发生什么?

Q7

下列数据最适合存放在 application(ServletContext)里的是( )

Q8

<%= name %> 在编译后会变成什么?

Q9

JSP 与 Servlet 的关系最准确的描述是( )

Q10

JSP 里要把「整个应用共享」的数据(如在线人数)存起来,应使用哪个内置对象?

Q11

按 JavaBean 规范,一个标准 Bean 必须提供( )

本章留下的死结
对象全靠 new、层层硬依赖——改一处牵一片(下一章 Spring IoC)。