2.2 四大作用域与九大隐式对象


2.2 四大作用域与九大隐式对象

本节摘要:九大隐式对象是容器在翻译页面时自动注入的九个现成变量,分别对应请求、响应、会话、全局上下文等核心接口;四大作用域则规定了数据从"当前页面"到"全站共享"的四级可见范围与存续时长。上一节看清了请求链路的骨架,本节回答链路上数据放哪、活多久、谁看得见——这三个问题的答案,直接决定购物车这类跨请求数据的正确性。

九个不用声明的对象从哪来

上节末尾留了一根引信:服务方法是多线程并发执行的。要理解并发之下什么会出事,得先盘点方法体内到底有些什么。你在 1.1 节见过翻译产物的示意代码——方法开头有一行 JspWriter out 的准备语句。容器翻译每个页面时,都会在服务方法开头生成一大段类似的准备代码:把九个现成变量依次就位。这就是"隐式"的全部秘密:不是没有声明,是容器替你声明了

九个对象各司其职,用一张表盘清家底。读表时重点看最后一列——它决定了你能不能放心往里面放数据。

隐式对象 对应的角色 管什么 线程维度
request 请求的化身 参数、请求头、本次请求携带的一切 每请求一份,天然安全
response 响应的信使 状态码、响应头、输出流控制 每请求一份,天然安全
out 输出流 把内容写回浏览器,带缓冲 每请求一份,天然安全
session 会话档案 同一用户跨请求共享的数据 按用户隔离,同用户并发仍共享
application 全站公告栏 所有用户共享的全局数据 全站一份,处处共享
pageContext 页面总管 统一存取四大作用域的中枢 每请求一份,天然安全
page 页面自身 当前类实例的自引用 全站一份
config 初始化配置 页面对应类的初始化参数 只读,安全
exception 异常现场 仅在错误页里可用的异常对象 每请求一份,天然安全

看出规律了吗:九个对象里真正需要你操心的只有 session 和 application 两个——它们的数据跨请求存活,于是必然面临"上一个请求写、下一个请求读"的共享问题。其余七个随请求生灭,各线程人手一份,想出事都没机会。老站故障里千奇百怪的"偶发串数据",排查范围可以先用这条规律砍掉四分之三。

四大作用域:数据活多久、走多远

隐式对象负责"存",作用域负责"存多久、给谁看"。四级作用域是严格的包含关系,画成同心层最好理解。

图2-2 四大作用域的同心边界

图2-2 四大作用域的同心边界

四级边界对应四条工程纪律。购物车数据放 session:它是"同一用户跨请求"的典型,放窄了(request)一跳转就丢,放宽了(application)全站顾客共享一辆车——真有老代码这么写过,那就是事故本身。控制器传给视图的数据放 request:第 4 章 MVC 落地时的标准动作,一次请求用完即弃,不占会话内存。全局配置放 application:全站只读,一份就够。临时渲染变量放 page:出了页面就没有意义。

还有一个隐蔽规则值得单独点名:EL 表达式取值时按 page、request、session、application 的顺序就近查找。同名属性放在不同作用域,窄的会遮住宽的。青梧书肆就出过一次事故:某页面在 session 里放了个旧版本的购物车,又在 request 里放了新版本,页面上 ${cart} 永远取到新的,另一处用脚本片段从 session 里取的辅助逻辑却读到旧的——两处数据对不上,排查了一下午。名字冲突时,作用域就是嫌疑顺序。

共享状态的暗礁:两个必踩的坑

坑都在 session 与 application 的写操作上。第一个坑,非原子的读改写

<% Integer hits = (Integer) application.getAttribute("visitCount"); if (hits == null) hits = 0; application.setAttribute("visitCount", hits + 1); %>

单线程看毫无破绽。并发下,两个线程可能同时读到同一个旧值、各自加一、先后写回——两次访问只涨了一次。修复思路有二:一是把读改写整体加锁;二是换线程安全的原子类,读改写一步完成。后者更优雅:

<% java.util.concurrent.atomic.AtomicInteger hits = (java.util.concurrent.atomic.AtomicInteger) application.getAttribute("visitCount"); if (hits == null) { hits = new java.util.concurrent.atomic.AtomicInteger(0); application.setAttribute("visitCount", hits); } hits.incrementAndGet(); %>

第二个坑更阴险,藏在页面声明区里:

<%! private Cart userCart = new Cart(); // 危险:这是类的成员变量 %> <% userCart.add(book); // 所有用户共享同一辆"购物车" %>

声明区里的变量翻译后是类的成员变量,而类是单例——于是这个名为 userCart 的"购物车"成了全站顾客的公共车。这就是 2.1 节那条引信的爆点。纪律只有一条:请求级数据一律放在脚本片段里,让它们成为服务方法的局部变量,随请求生灭。

背景:把两坑合并成一次真实事故看。青梧书肆上线初期,运维群反馈"顾客 A 的购物车里偶尔出现顾客 B 加的书",频率低到无法复现。

操作:按本节规律缩小包围圈——串的数据是购物车,跨请求存在,重点查 session 的读写路径;逐页排查后发现详情页的加入动作用了声明区变量持有购物车对象。

结果:把声明区变量改写为脚本片段内的局部变量,从 session 取放,事故消失。

解读:低频偶发是共享状态缺陷的典型面貌——冲突窗口越小,越难复现,但数学上必然发生。这类问题不能靠"再观察观察"结案,只能靠识别共享模式根除。

变式:如果你接手的站点不能立刻改代码,临时止血可以把该页面的并发度降下来(容器里限制单页面并发线程),但这只是稀释冲突概率,不解决问题。根治永远是消灭"单例里的可变请求级状态"。

⚠️ 常见坑:把本该放 request 的数据图省事放进 session,短期看功能正常,长期看是内存缓慢上涨与用户间数据干扰两颗雷——老站内存泄漏的头号来源。

本节要点回顾

  • 隐式即代劳:九大对象由容器翻译时注入,其中只有 session 与 application 是跨请求的共享对象;
  • 作用域四级同心:page、request、session、application 逐级放宽,放数据能窄不宽;
  • 就近查找规则:EL 按窄到宽顺序取值,同名属性窄者遮宽者,排查取值异常先查重名;
  • 读改写不原子:共享计数类缺陷用原子类或加锁修复;
  • 声明区是雷区:声明区变量是单例的成员变量,请求级数据必须放脚本片段内。

数据放哪、活多久已经清楚了,还剩旅程的最后一帧:页面之间的跳转。转发与重定向这两条路线的差别,是参数丢失、重复提交这类工单的常客——下一节用一次真实排障把它们讲透。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U