系列文章目录
Java Servlet 学习笔记(版本3.1)(1)
Java Servlet 学习笔记(版本3.1)(2)
Java Servlet 学习笔记(版本3.1)(3)
Java Servlet 学习笔记(版本3.1)(4)
Java Servlet 学习笔记(版本3.1)(5)
Java Servlet 学习笔记之会话(版本3.1)(6)
文章目录
前言
超文本传输协议(HTTP)从一开始就被设计为无状态的协议。但是为了构建有效的Web应用程序,必须将来自特定客户端的请求彼此关联,比如区分是否同一个用户的请求。该规范定义了一个简单的HttpSession接口,该接口允许Servlet容器使用多种方法中的任何一种来跟踪用户的会话,而无需让应用开发人员感知任何一种方法的细微差别。
一、 session跟踪机制
通过以下几种机制可以实现session的跟踪
1.Cookies
通过Cookies进行session跟踪是使用最广泛的跟踪机制,而且要求所有的Servlet容器都必须实现。
Servlet容器发送一个Cookie到客户端,客户端将在随后的请求中带上这个Cookie,并明确的带上session信息。用于session跟踪的Cookie的标准名称必须为JSESSIONID
。当然了,容器应该允许特定配置修改这个名称。如果Web应用程序为其会话跟踪cookie配置了自定义名称,并且在URL中编码了会话ID(前提是已启用URL重写),则相同的自定义名称也将用作URI参数的名称(第3条)。
比如以下的Cookies中就包含了两个session信息。
比如以下为请求百度时返回的会话信息,此时使用的不是标准名称JSESSIONID
。
2.SSL会话
安全套接字层(HTTPS协议中使用的加密技术)具有内置机制,可以将来自客户端的多个请求明确地标识为会话的一部分。 Servlet容器可以轻松地使用此数据来定义会话。
3.URL重写
URL重写是最低等级的会话跟踪机制,如果客户端不能接收Cookie信息,服务器可以将URL重写用作会话跟踪的基础。URL重写涉及将数据(会话ID)添加到容器解释的URL路径,以将请求与会话相关联。
会话ID必须在URL字符串中编码为路径参数。 参数的名称必须为jsessionid。 这是包含编码路径信息的URL的示例:http://www.myserver.com/catalog/index.html;jsessionid=1234。
URL重写在日志,书签,引用标头,缓存的HTML和URL栏中公开会话标识符。所以是不安全的。URL重写不应用作支持和适合Cookie或SSL会话的会话跟踪机制。
为了保证会话的完整性。Web容器在处理来自不支持使用cookie的客户端的HTTP请求时,必须能够支持HTTP会话。 为了满足此要求,Web容器通常支持URL重写机制。
与每个会话相关联,有一个包含唯一标识符的字符串,该标识符称为会话ID。 会话ID的值可以通过调用javax.servlet.http.HttpSession.getId
获得,并且可以在创建后通过调用javax.servlet.http.HttpServletRequest.changeSessionId
进行更改。
二、 创建session
如果一个会话只是一个预期的会话而尚未建立,则认为该会话是“新的”。 因为HTTP是基于请求-响应的协议,所以HTTP会话被认为是新的,直到客户端“加入”它为止。 当会话跟踪信息已返回到服务器,表明已建立会话时,客户端将加入会话。 在客户端加入会话之前,不能假定客户端的下一个请求将被识别为会话的一部分。
在Servlet规范中,并没有直接的接口比如createSession,但是有以下这个接口
/**
* Returns the current <code>HttpSession</code>
* associated with this request or, if there is no
* current session and <code>create</code> is true, returns
* a new session.
*
* <p>If <code>create</code> is <code>false</code>
* and the request has no valid <code>HttpSession</code>,
* this method returns <code>null</code>.
*
* <p>To make sure the session is properly maintained,
* you must call this method before
* the response is committed. If the container is using cookies
* to maintain session integrity and is asked to create a new session
* when the response is committed, an IllegalStateException is thrown.
*
* @param create <code>true</code> to create
* a new session for this request if necessary;
* <code>false</code> to return <code>null</code>
* if there's no current session
*
* @return the <code>HttpSession</code> associated
* with this request or <code>null</code> if
* <code>create</code> is <code>false</code>
* and the request has no valid session
*
* @see #getSession()
*/
public HttpSession getSession(boolean create);
返回与当前请求相关量的HttpSession对象,如果不存在而且参数create
设置为true的话,则创建一个HttpSession对象对象。如果没有对应的HttpSession对象而且该参数设置为false,这个方法会返回null,也就是没法获取到HttpSession对象。如果直接调用org.apache.catalina.connector.RequestFacade#getSession()
方法的话,相当于调用create
参数为true的以上方法。
1.Tomcat中关于Session创建的实现
以下为Tomcat中的实现。
在Tomcat中的方法实现org.apache.catalina.connector.Request#doGetSession
/**
* The currently active session for this request.
*/
protected Session session = null;
protected Session doGetSession(boolean create) {
// There cannot be a session if no context has been assigned yet
// 获取对应的上下文 如果上下文都没有的话 当然不存在session了
Context context = getContext();
if (context == null) {
return (null);
}
// Return the current session if it exists and is valid
// 如果当前请求中存在session 如果已经失效 则该session做废
if ((session != null) && !session.isValid()) {
session = null;
}
if (session != null) {
// 请求中存在有效的session 比如在转发请求 同一个请求中多次调用getSession方法
return (session);
}
// Return the requested session if it exists and is valid
// requestedSessionId为从客户端带过来的JSESSIONID
Manager manager = context.getManager();
if (manager == null) {
return (null); // Sessions are not supported
}
if (requestedSessionId != null) {
try {
// 根据JSESSIONID获取session
session = manager.findSession(requestedSessionId);
} catch (IOException e) {
session = null;
}
// 再次检查失效时间
if ((session != null) && !session.isValid()) {
session = null;
}
// 记录访问时间
if (session != null) {
session.access();
return (session);
}
}
// Create a new session if requested and the response is not committed
if (!create) {
// 如果create属性设置为false 即使不存在session也不会创建
return (null);
}
// 当前响应不能是已经提交状态 否则抛出异常 Cannot create a session after the response has been committed
if (response != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)
&& response.getResponse().isCommitted()) {
throw new IllegalStateException(
sm.getString("coyoteRequest.sessionCreateCommitted"));
}
// Re-use session IDs provided by the client in very limited
// circumstances.
String sessionId = getRequestedSessionId();
if (requestedSessionSSL) {
// If the session ID has been obtained from the SSL handshake then
// use it.
} else if (("/".equals(context.getSessionCookiePath())
&& isRequestedSessionIdFromCookie())) {
/* This is the common(ish) use case: using the same session ID with
* multiple web applications on the same host. Typically this is
* used by Portlet implementations. It only works if sessions are
* tracked via cookies. The cookie must have a path of "/" else it
* won't be provided for requests to all web applications.
*
* Any session ID provided by the client should be for a session
* that already exists somewhere on the host. Check if the context
* is configured for this to be confirmed.
*/
if (context.getValidateClientProvidedNewSessionId()) {
boolean found = false;
for (Container container : getHost().findChildren()) {
Manager m = ((Context) container).getManager();
if (m != null) {
try {
if (m.findSession(sessionId) != null) {
found = true;
break;
}
} catch (IOException e) {
// Ignore. Problems with this manager will be
// handled elsewhere.
}
}
}
if (!found) {
sessionId = null;
}
}
} else {
sessionId = null;
}
// 创建一个session
session = manager.createSession(sessionId);
// 添加Cookie信息
// Creating a new session cookie based on that session
if (session != null
&& context.getServletContext()
.getEffectiveSessionTrackingModes()
.contains(SessionTrackingMode.COOKIE)) {
Cookie cookie =
ApplicationSessionCookieConfig.createSessionCookie(
context, session.getIdInternal(), isSecure());
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
// 记录访问时间
session.access();
return session;
}
- 如果是第一次请求
- 如果是在同一个Request对象或者转发的Request对象时
- 客户端再次请求时,此时请求对象中的session属性是不存在的(新的请求)
2.案例讲解
假如我们在com.example.web.HelloServlet#doGet中编写如下的代码:
// 获取session
HttpSession session = request.getSession(true);
// 获取请求创建时间
Date created = new Date(session.getCreationTime());
// 获取上一次请求时间
Date accessed = new Date(session.getLastAccessedTime());
String info = " ID " + session.getId() +
" Created: " + created +
" Last Accessed: " + accessed +
" maxInactiveInterval: " + session.getMaxInactiveInterval() + "s";
// 打印信息
System.out.println(info);
// 修改session最大有效时间 单位为秒
session.setMaxInactiveInterval(10);
然后我们在浏览器先后发起两次请求,这个时候页面打印如下信息
--------------HelloServlet----------ServletContext------org.apache.catalina.core.ApplicationContextFacade@67e66bd
ID BC82D820EC92698F40D2CCB839DE96D9 Created: Wed Nov 11 15:54:26 CST 2020 Last Accessed: Wed Nov 11 15:54:26 CST 2020 maxInactiveInterval: 1800s
--------------HelloServlet----------ServletContext------org.apache.catalina.core.ApplicationContextFacade@67e66bd
ID BC82D820EC92698F40D2CCB839DE96D9 Created: Wed Nov 11 15:54:26 CST 2020 Last Accessed: Wed Nov 11 15:54:26 CST 2020 maxInactiveInterval: 10s
从结果不难看出两点,session的ID是一致的,而且第二次请求的session的最大活跃时间变为了10s,恰好为第一次请求中修改的值,这也证明了这两次Servlet请求共享同一个session。第一次请求服务端分配了一个session,然后发送回客户端,然后客户端再将这个session通过Cookies发送到服务端,这样就完成了通过session完成了跟踪了。在上面这个session创建到发送回客户端的这段时间里,其实session其实是非共享的,也就是说客户端其实还是不知道的,上面所说的“新”会话指的就是这段时间的会话,而一旦客户端接收到了,然后建立连接并且再次发送到服务器, 称为会话的建立(服务器->客户端->服务器
)。如果在新会话期间客户端发起了请求,其实是无法判断两个请求是不是同一个客户端的,比如修改以上的代码:
package com.example.web;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
public class HelloServlet extends HttpServlet {
private static AtomicInteger integer = new AtomicInteger();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
System.out.println("--------------" + this.getClass().getSimpleName() + "----------ServletContext------" + request.getServletContext());
HttpSession session = request.getSession(true);
// 获取session的创建时间
Date created = new Date(session.getCreationTime());
// 上一次session访问时间
Date accessed = new Date(session.getLastAccessedTime());
String info = " ID " + session.getId() +
" Created: " + created +
" Last Accessed: " + accessed +
" maxInactiveInterval: " + session.getMaxInactiveInterval() + "s";
System.out.println(info);
int i = integer.incrementAndGet();
try {
Thread.sleep(2000 / i);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 单位为秒
session.setMaxInactiveInterval(10);
// 返回信息
response.getWriter().write(this.getClass().getSimpleName());
}
}
通过一个全局的计数器,注意必须线程安全,所以用了AtomicInteger。第一个请求的线程会休眠2秒,而第二个请求的线程休眠一秒,这样就能在第一个请求还未返回发起第二次请求了。
测试结果(这里要看手速的哦!要不然就把睡眠时间加大!)如下所示
可以看到此时是两个完全不同的session信息,而且我们查看客户端的Cookie信息
ID为57AC843C1622A951B0E1A45A1FD4B409
(后一次请求)的session信息没有了。只有第一次请求的session信息存在,这也说明了后一个session信息被第一个session信息覆盖了。上面我为啥说哪个是第一个,哪个是第二个呢,在以上的日志信息中其实通过session.getCreationTime()
获取了session的创建时间了。
在以下场景中,我们认为会话还是新会话
- 客户端还不知道这个会话(请求还未响应)
- 客户端选择不加入这个会话
其实session有一个isNew的方法可以说明是不是新会话。
Date created = new Date(session.getCreationTime());
Date accessed = new Date(session.getLastAccessedTime());
String info = " ID " + session.getId() +
" Created: " + created +
" Last Accessed: " + accessed +
" isNew: " + session.isNew() +
" maxInactiveInterval: " + session.getMaxInactiveInterval() + "s";
System.out.println(info);
这些条件定义了Servlet容器不具有将请求与先前的请求相关联的机制的情况。Servlet开发人员必须设计其应用程序以处理客户端没有,不能或不会加入会话的情况。
三、 session作用域
HttpSession对象的作用域必须在应用程序(或Servlet上下文)级别。 底层机制(例如用于建立会话的cookie)对于不同的上下文可以相同,但是容器绝不能在上下文之间共享引用的对象(包括该对象中的属性)。
修改我们的嵌入式tomcat启动程序,再添加一个上下文StandardContext
,同时取消HelloServlet中的睡眠时间和最大活跃时间。
package com.example.web;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.webresources.DirResourceSet;
import org.apache.catalina.webresources.StandardRoot;
import javax.servlet.ServletException;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
public class TomcatStartMain {
public static void main(String[] args) throws LifecycleException, ServletException {
// 创建Tomcat容器
Tomcat tomcatServer = new Tomcat();
// 端口号设置
tomcatServer.setPort(8082);
// 读取项目路径 加载静态资源
String basePath = System.getProperty("user.dir") + File.separator;
tomcatServer.getHost().setAppBase(basePath);
{
//改变文件读取路径,从resources目录下去取文件
StandardContext ctx = (StandardContext) tomcatServer.addWebapp("/app1", basePath + "src" + File.separator + "main" + File.separator + "webapp");
// 禁止重新载入
ctx.setReloadable(false);
// class文件读取地址
File additionWebInfClasses = new File("embrace/target/classes");
// 创建WebRoot
WebResourceRoot resources = new StandardRoot(ctx);
// tomcat内部读取Class执行
resources.addPreResources(
new DirResourceSet(resources, "/embrace/WEB-INF/classes", additionWebInfClasses.getAbsolutePath(), "/"));
}
{
//改变文件读取路径,从resources目录下去取文件
StandardContext ctx = (StandardContext) tomcatServer.addWebapp("/app2", basePath + "src" + File.separator + "main" + File.separator + "webapp");
// 禁止重新载入
ctx.setReloadable(false);
// class文件读取地址
File additionWebInfClasses = new File("embrace/target/classes");
// 创建WebRoot
WebResourceRoot resources = new StandardRoot(ctx);
// tomcat内部读取Class执行
resources.addPreResources(
new DirResourceSet(resources, "/embrace/WEB-INF/classes", additionWebInfClasses.getAbsolutePath(), "/"));
}
tomcatServer.start();
// 异步等待请求执行
tomcatServer.getServer().await();
}
}
分别从页面请求http://localhost:8082/app1/hello
和http://localhost:8082/app2/hello
两次
从以上的结果可以看出,这里有两个ServletContext
对象,每个对对应不同的会话。
四、 session绑定参数
可以通过HttpSession绑定属性而且同一个ServletContext上下文共享。在上面我们已经看到在同一个Servelt当中同一个客户端请求时session是共享的,那么不同的客户端呢?以及分发的请求呢?
修改HelloServlet添加一下逻辑
// 设置session共享属性
session.setAttribute("user name", "Tony Stark");
// 输出内容
response.getWriter().write(this.getClass().getSimpleName());
// 请求转发
request.getRequestDispatcher("/reg").forward(request, response);
修改com.example.web.RegisterServlet#doGet方法
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
System.out.println("--------------" + this.getClass().getSimpleName() + "----------ServletContext------" + request.getServletContext());
HttpSession session = request.getSession(true);
Date created = new Date(session.getCreationTime());
Date accessed = new Date(session.getLastAccessedTime());
String info = " ID " + session.getId() +
" Created: " + created +
" Last Accessed: " + accessed +
" maxInactiveInterval: " + session.getMaxInactiveInterval() + "s";
System.out.println(info);
// print session contents
Enumeration e = session.getAttributeNames();
while (e.hasMoreElements()) {
String name = (String) e.nextElement();
String value = session.getAttribute(name).toString();
System.out.println(name + " = " + value);
}
response.getWriter().write(this.getClass().getSimpleName());
}
首先在页面请求:http://localhost:8082/app1/hello
转发后的请求属于同一个ServeltContext,也是同一个Session,而且可以共享属性。
再次在页面请求:http://localhost:8082/app1/reg,这已经是另一次请求了,但是同样属于同一个会话。这就是session的属性共享机制。
某些对象在放入或者移除一个session的需要通知,可以通过实现接口HttpSessionBindingListener
。这个结果包含了两个方法,一个valueBound
,一个valueUnbound
,前者在往session中设置这个对象属性的时候触发,后者是在session中移除这个属性的时候触发(如果session失效的话,当然会触发,因为所有的属性都随着session的失效而失效了)。创建实现类
package com.example.web;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import java.util.Date;
public class MyHttpSessionBindingListener implements HttpSessionBindingListener {
@Override
public void valueBound(HttpSessionBindingEvent event) {
String id = event.getSession().getId();
Date created = new Date(event.getSession().getCreationTime());
System.out.println("sessonId = " + id + " created " + created);
}
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
String id = event.getSession().getId();
Date created = new Date();
System.out.println("sessonId = " + id + " invalidate " + created);
}
}
修改HelloServlet,在执行doGet方法之前设置一个MyHttpSessionBindingListener属性值到session当中,然后在doGet方法执行完之后再移除这个属性。
通过页面请求这个Servlet
从以上结果可以看出,在设置属性和移除属性的时候触发了事件。
这里需要注意的是,不是针对session设置任何的值都会触发这个事件,只是针对部分类型的对象,也就是javax.servlet.http.HttpSessionBindingListener类型的对象。
五、 session有效时间
在HTTP协议当中,如果客户端不再活跃的话不会明确通知到服务端,这也意味着对于服务端而言唯一的机制用来表名客户端不再活跃的方式就是过期时间了。也可以称为活跃时间。每个Session都有一个最大活跃时间的属性,通过javax.servlet.http.HttpSession#getMaxInactiveInterval
来获取,上面我们已经使用过多次了,包括通过setMaxInactiveInterval
来修改最大活跃时间。
/**
* Specifies the time, in seconds, between client requests before the
* servlet container will invalidate this session.
*
* <p>An <tt>interval</tt> value of zero or less indicates that the
* session should never timeout.
*
* @param interval An integer specifying the number
* of seconds
*/
public void setMaxInactiveInterval(int interval);
/**
* Returns the maximum time interval, in seconds, that
* the servlet container will keep this session open between
* client accesses. After this interval, the servlet container
* will invalidate the session. The maximum time interval can be set
* with the <code>setMaxInactiveInterval</code> method.
*
* <p>A return value of zero or less indicates that the
* session will never timeout.
*
* @return an integer specifying the number of
* seconds this session remains open
* between client requests
*
* @see #setMaxInactiveInterval
*/
public int getMaxInactiveInterval();
需要额外注意的是,这里如果将最大活跃时间设置为0或者负数,并不是让立即失效,而是永久不过期。
其实通过服务端除了手动修改最大活跃时间,还可以直接调用javax.servlet.http.HttpSession#invalidate
方法直接让session失效。一旦调用这个结果让session失效之后,客户端再次请求将无法使用这个session值了,另外需要注意的是,如果这个会话的其他请求还未结束,调用这个接口并不能使session失效。
修改有效时间为10s ,根据触发的事件来看,实际上为30s
直接调用session的invalidate方法,可以看到属性移除事件很快就发生了
六、 session语义探究
1.线程安全问题
由于sesson在不同的Servlet请求中共享,所以必须保证线程的安全性。执行请求线程的多个servlet可以同时主动访问同一会话对象。 容器必须确保以线程安全的方式对表示会话属性的内部数据结构进行操作。 开发人员负责线程安全地访问属性对象本身。 这将保护HttpSession对象内部的属性集合免受并发访问,从而消除了应用程序导致并发问题的机会。比如在Tomcat中存放session属性的容器就是并发安全的
/**
* The collection of user data attributes associated with this Session.
*/
protected ConcurrentMap<String, Object> attributes = new ConcurrentHashMap<>();
同时在org.apache.catalina.session.StandardSessionFacade
类中HttpSession
属性使用了final
来修饰
/**
* Wrapped session object.
*/
private final HttpSession session;
2. 分布式共享问题
在分布式web应用程序中,作为会话一部分的所有请求必须一次由一个JVM处理。容器必须能够使用setAttribute或putValue方法适当地处理放置在HttpSession类实例中的所有对象。为了满足这些条件,施加了以下限制:
- 容器必须接受实现Serializable接口的对象。
- 容器可以选择支持在HttpSession中存储其他指定的对象,例如对Enterprise JavaBeans组件和事务的引用。
- 会话共享将由特定于容器的设备处理。
分布式Servlet容器必须为存储在session中的对象抛出IllegalArgumentException异常如果容器不支持必须要的session共享机制。
分布式Servlet容器必须支持迁移实现Serializable的对象所必需的机制。
这些限制意味着开发人员可以确保除了在非分布式容器中遇到的并发问题之外,没有其他并发问题。
容器提供程序可以通过将会话对象及其内容从分布式系统的任何活动节点移动到系统的其他节点,来确保可伸缩性和服务质量功能(例如负载平衡和故障转移)。
如果分布式容器保留或迁移会话以提供服务质量功能,则它们不限于使用本机JVM序列化机制来序列化HttpSession及其属性。如果开发人员实现了容器,则不能保证容器将在会话属性上调用readObject和writeObject方法,但是可以保证保留其属性的Serializable关闭。
在会话迁移期间,容器必须通知实现HttpSessionActivationListener
接口的所有会话属性。它们必须在会话序列化之前调用sessionWillPassivate
方法,并在会话反序列化之后将调用sessionDidActivate
方法。
编写分布式应用程序的应用程序开发人员应注意,由于容器可以在多个Java虚拟机中运行,因此开发人员不能依赖静态变量来存储应用程序状态。他们应该使用企业Bean或数据库
存储此类状态。
在Tomcat中,org.apache.catalina.session.StandardSession
不但实现了java.io.Serializable
接口,并实现了org.apache.catalina.session.StandardSession#doReadObject
和org.apache.catalina.session.StandardSession#doWriteObject
方法。
总结
javax.servlet.http.HttpSession
提供一种在多个页面请求或访问网站中识别用户的方法,并存储有关该用户的信息。
Servlet容器使用此接口在HTTP客户端和HTTP服务器之间创建会话。该会话在用户的多个连接或页面请求中持续指定的时间段。一个会话通常对应一个用户,该用户可能会多次访问该站点。服务器可以通过多种方式维护会话,例如使用cookie或重写URL。
该接口允许Servlet查看和处理有关会话的信息,例如会话标识符,创建时间和上次访问时间。
将对象绑定到会话,允许用户信息在多个用户连接之间持久化。当应用程序将对象存储在会话中或从会话中删除对象时,会话将检查该对象是否实现HttpSessionBindingListener。如果是这样,则servlet通知对象它已绑定到会话或从会话解除绑定。绑定方法完成后发送通知。对于无效或过期的会话,在会话无效或过期后发送通知。
当容器在分布式容器设置中的VM之间迁移会话时,将通知实现HttpSessionActivationListener接口的所有会话属性。
Servlet应该能够处理客户端不选择加入会话的情况,例如故意关闭cookie的情况。在客户端加入会话之前,isNew返回true。如果客户端选择不加入会话,则getSession将针对每个请求返回不同的会话,而isNew将始终返回true。
会话信息仅适用于当前Web应用程序(ServletContext),因此存储在一个上下文中的信息在另一个上下文中将不直接可见