这篇文章挖掘Session的原理和tomcat实现机制。
由于HTTP是无状态的协议,客户程序每次都去web页面,都打开到web服务器的单独的连接,并且不维护客户的上下文信息。如果需要维护上下文信息,比如用户登录系统后,每次都能够知道操作的是此登录用户,而不是其他用户。对于这个问题,存在三种解决方案:cookie,url重写和隐藏表单域。
1、cookie
cookie是一个服务器和客户端相结合的技术,服务器可以将会话ID发送到浏览器,浏览器将此cookie信息保存起来,后面再访问网页时,服务器又能够从浏览器中读到此会话ID,通过这种方式判断是否是同一用户。
生成响应的时候,服务器向客户端发送cookie。cookie的属性是JSESSIONID,值是267A671BFEAA147A2383B7E083D4G7E。以后每次客户端请求时,都会附上此cookie,服务器端就可以读取到。
服务器端根据读取到的JSESSIONID,在一个map里面查找其对应的session对象,这个map的key是jsessionid的值,value是session对象。
2、URL重写
重写这种方式,客户端程序在每个URL的尾部自动添加一些额外数据,这些数据以表示这个会话,比如
http://192.168.1.20:8080/crm/getuserprofile.html;jsessionid=abc123。URL重写的额外数据是服务器自动添加的,那么服务器是怎么添加的呢?Tomcat在返回Response的时候,检查JSP页面中所有的URL,包括所有的链接,和 Form的Action属性,在这些URL后面加上“;jsessionid=xxxxxx”。 添加url后缀的代码片段如下:
org.apache.coyote.tomcat5.CoyoteResponse类的toEncoded()方法支持URL重写。
从上面URL的实现原理可知,URL重写有一个缺点:在你的站点上不能有任何静态的HTML页面(至少静态页面中不能有任何链接到站点动态页面的链接)。因此,每个页面都必须使用servlet或 JSP动态生成。即使所有的页面都动态生成,如果用户离开了会话并通过书签或链接再次回来,会话的信息都会丢失,因为存储下来的链接含有错误的标识信息- 该URL后面的SESSION ID已经过期了。
3、隐藏表单域
这种方式借助html表单中的hidden来实现,适用特定的一个流程,但是不适用于通常意义的会话跟踪。
综上所述,session实现会话跟踪通常是cookie和url重写,如果浏览器不禁止cookie的话,tomcat优先使用cookie实现。
服务器端实现原理
Session在服务器端具体是怎么实现的呢?我们使用session的时候一般都是这么使用的:
request.getSession()或者request.getSession(true)。
这个时候,服务器就检查是不是已经存在对应的Session对象,见HttpRequestBase类
doGetSession(boolean create)方法:
requestSessionId从哪里来呢?这个肯定是通过Session实现机制的cookie或URL重写来设置的。见HttpProcessor类中的parseHeaders(SocketInputStream input):
或者HttpOrocessor类中的parseRequest(SocketInputStream input, OutputStream output)
里面的manager.findSession(requestSessionId)用于查找此会话ID对应的session对象。Tomcat实现
是通过一个HashMap实现,见ManagerBase.java的findSession(String id):
Session本身也是实现为一个HashMap,因为Session设计为存放key-value键值对,Tomcat里面Session实现类是StandardSession,里面一个attributes属性:
所有会话信息的存取都是通过这个属性来实现的。Session会话信息不会一直在服务器端保存,超过一定的时间期限就会被删除,这个时间期限可以在web.xml中进行设置,不设置的话会有一个默认值,Tomcat的默认值是60。那么服务器端是怎么判断会话过期的呢?原理服务器会启动一个线程,一直查询所有的Session对象,检查不活动的时间是否超过设定值,如果超过就将其删除。见StandardManager类,它实现了Runnable接口,里面的run方法如下:
Session信息在create,expire等事情的时候都会触发相应的Listener事件,从而可以对session信息进行监控,这些Listener只需要继承HttpSessionListener,并配置在web.xml文件中。如下是一个监控在线会话数的Listerner:
由于HTTP是无状态的协议,客户程序每次都去web页面,都打开到web服务器的单独的连接,并且不维护客户的上下文信息。如果需要维护上下文信息,比如用户登录系统后,每次都能够知道操作的是此登录用户,而不是其他用户。对于这个问题,存在三种解决方案:cookie,url重写和隐藏表单域。
1、cookie
cookie是一个服务器和客户端相结合的技术,服务器可以将会话ID发送到浏览器,浏览器将此cookie信息保存起来,后面再访问网页时,服务器又能够从浏览器中读到此会话ID,通过这种方式判断是否是同一用户。
1 请求:
2 POST /ibsm/LoginAction.do HTTP/1.1
3 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
4 Referer: http://192.168.1.20:8080/crm/
5 Accept-Language: zh-cn
6 Content-Type: application/x-www-form-urlencoded
7 UA-CPU: x86
8 Accept-Encoding: gzip, deflate
9 User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)
10 Host: 192.168.1.20:8080
11 Content-Length: 13
12 Connection: Keep-Alive
13 Cache-Control: no-cache
14
15 username=jack
16
17 响应:
18 HTTP/1.1 200 OK
19 Server: Apache-Coyote/1.1
20 Set-Cookie: JSESSIONID=3267A671BFEAA147A2383B7E083D4G7E; Path=/crm
21 Content-Type: text/html;charset=GBK
22 Content-Length: 436
23 Date: Sat, 10 June 2009 12:43:26 GMT
2 POST /ibsm/LoginAction.do HTTP/1.1
3 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
4 Referer: http://192.168.1.20:8080/crm/
5 Accept-Language: zh-cn
6 Content-Type: application/x-www-form-urlencoded
7 UA-CPU: x86
8 Accept-Encoding: gzip, deflate
9 User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)
10 Host: 192.168.1.20:8080
11 Content-Length: 13
12 Connection: Keep-Alive
13 Cache-Control: no-cache
14
15 username=jack
16
17 响应:
18 HTTP/1.1 200 OK
19 Server: Apache-Coyote/1.1
20 Set-Cookie: JSESSIONID=3267A671BFEAA147A2383B7E083D4G7E; Path=/crm
21 Content-Type: text/html;charset=GBK
22 Content-Length: 436
23 Date: Sat, 10 June 2009 12:43:26 GMT
生成响应的时候,服务器向客户端发送cookie。cookie的属性是JSESSIONID,值是267A671BFEAA147A2383B7E083D4G7E。以后每次客户端请求时,都会附上此cookie,服务器端就可以读取到。
1 1. GET /ibsm/ApplicationFrame.frame HTTP/1.1
2 2. Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
3 3. Accept-Language: zh-cn
4 4. UA-CPU: x86
5 5. Accept-Encoding: gzip, deflate
6 6. User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)
7 7. Host: 192.168.1.20:8080
8 8. Connection: Keep-Alive
9 9. Cookie: JSESSIONID=267A671BFEAA147A2383B7E083D4G7E
2 2. Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
3 3. Accept-Language: zh-cn
4 4. UA-CPU: x86
5 5. Accept-Encoding: gzip, deflate
6 6. User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)
7 7. Host: 192.168.1.20:8080
8 8. Connection: Keep-Alive
9 9. Cookie: JSESSIONID=267A671BFEAA147A2383B7E083D4G7E
服务器端根据读取到的JSESSIONID,在一个map里面查找其对应的session对象,这个map的key是jsessionid的值,value是session对象。
2、URL重写
重写这种方式,客户端程序在每个URL的尾部自动添加一些额外数据,这些数据以表示这个会话,比如
http://192.168.1.20:8080/crm/getuserprofile.html;jsessionid=abc123。URL重写的额外数据是服务器自动添加的,那么服务器是怎么添加的呢?Tomcat在返回Response的时候,检查JSP页面中所有的URL,包括所有的链接,和 Form的Action属性,在这些URL后面加上“;jsessionid=xxxxxx”。 添加url后缀的代码片段如下:
org.apache.coyote.tomcat5.CoyoteResponse类的toEncoded()方法支持URL重写。
1 StringBuffer sb = new StringBuffer(path);
2 if( sb.length() > 0 ) { // jsessionid can't be first.
3 sb.append(";jsessionid=");
4 sb.append(sessionId);
5 }
6 sb.append(anchor);
7 sb.append(query);
8 return (sb.toString());
2 if( sb.length() > 0 ) { // jsessionid can't be first.
3 sb.append(";jsessionid=");
4 sb.append(sessionId);
5 }
6 sb.append(anchor);
7 sb.append(query);
8 return (sb.toString());
从上面URL的实现原理可知,URL重写有一个缺点:在你的站点上不能有任何静态的HTML页面(至少静态页面中不能有任何链接到站点动态页面的链接)。因此,每个页面都必须使用servlet或 JSP动态生成。即使所有的页面都动态生成,如果用户离开了会话并通过书签或链接再次回来,会话的信息都会丢失,因为存储下来的链接含有错误的标识信息- 该URL后面的SESSION ID已经过期了。
3、隐藏表单域
这种方式借助html表单中的hidden来实现,适用特定的一个流程,但是不适用于通常意义的会话跟踪。
综上所述,session实现会话跟踪通常是cookie和url重写,如果浏览器不禁止cookie的话,tomcat优先使用cookie实现。
服务器端实现原理
Session在服务器端具体是怎么实现的呢?我们使用session的时候一般都是这么使用的:
request.getSession()或者request.getSession(true)。
这个时候,服务器就检查是不是已经存在对应的Session对象,见HttpRequestBase类
doGetSession(boolean create)方法:
1 if ((session != null) && !session.isValid())
2 session = null;
3 if (session != null)
4 return (session.getSession());
5
6
7 // Return the requested session if it exists and is valid
8 Manager manager = null;
9 if (context != null)
10 manager = context.getManager();
11 if (manager == null)
12 return (null); // Sessions are not supported
13 if (requestedSessionId != null) {
14 try {
15 session = manager.findSession(requestedSessionId);
16 } catch (IOException e) {
17 session = null;
18 }
19 if ((session != null) && !session.isValid())
20 session = null;
21 if (session != null) {
22 return (session.getSession());
23 }
24 }
2 session = null;
3 if (session != null)
4 return (session.getSession());
5
6
7 // Return the requested session if it exists and is valid
8 Manager manager = null;
9 if (context != null)
10 manager = context.getManager();
11 if (manager == null)
12 return (null); // Sessions are not supported
13 if (requestedSessionId != null) {
14 try {
15 session = manager.findSession(requestedSessionId);
16 } catch (IOException e) {
17 session = null;
18 }
19 if ((session != null) && !session.isValid())
20 session = null;
21 if (session != null) {
22 return (session.getSession());
23 }
24 }
requestSessionId从哪里来呢?这个肯定是通过Session实现机制的cookie或URL重写来设置的。见HttpProcessor类中的parseHeaders(SocketInputStream input):
1 for (int i = 0; i < cookies.length; i++) {
2 if (cookies[i].getName().equals
3 (Globals.SESSION_COOKIE_NAME)) {
4 // Override anything requested in the URL
5 if (!request.isRequestedSessionIdFromCookie()) {
6 // Accept only the first session id cookie
7 request.setRequestedSessionId
8 (cookies[i].getValue());
9 request.setRequestedSessionCookie(true);
10 request.setRequestedSessionURL(false);
11
12 }
13 }
14 }
2 if (cookies[i].getName().equals
3 (Globals.SESSION_COOKIE_NAME)) {
4 // Override anything requested in the URL
5 if (!request.isRequestedSessionIdFromCookie()) {
6 // Accept only the first session id cookie
7 request.setRequestedSessionId
8 (cookies[i].getValue());
9 request.setRequestedSessionCookie(true);
10 request.setRequestedSessionURL(false);
11
12 }
13 }
14 }
或者HttpOrocessor类中的parseRequest(SocketInputStream input, OutputStream output)
1 // Parse any requested session ID out of the request URI
2 int semicolon = uri.indexOf(match); //match 是";jsessionid="字符串
3 if (semicolon >= 0) {
4 String rest = uri.substring(semicolon + match.length());
5 int semicolon2 = rest.indexOf(';');
6 if (semicolon2 >= 0) {
7 request.setRequestedSessionId(rest.substring(0, semicolon2));
8 rest = rest.substring(semicolon2);
9 } else {
10 request.setRequestedSessionId(rest);
11 rest = "";
12 }
13 request.setRequestedSessionURL(true);
14 uri = uri.substring(0, semicolon) + rest;
15 if (debug >= 1)
16 log(" Requested URL session id is " +
17 ((HttpServletRequest) request.getRequest())
18 .getRequestedSessionId());
19 } else {
20 request.setRequestedSessionId(null);
21 request.setRequestedSessionURL(false);
22 }
23
2 int semicolon = uri.indexOf(match); //match 是";jsessionid="字符串
3 if (semicolon >= 0) {
4 String rest = uri.substring(semicolon + match.length());
5 int semicolon2 = rest.indexOf(';');
6 if (semicolon2 >= 0) {
7 request.setRequestedSessionId(rest.substring(0, semicolon2));
8 rest = rest.substring(semicolon2);
9 } else {
10 request.setRequestedSessionId(rest);
11 rest = "";
12 }
13 request.setRequestedSessionURL(true);
14 uri = uri.substring(0, semicolon) + rest;
15 if (debug >= 1)
16 log(" Requested URL session id is " +
17 ((HttpServletRequest) request.getRequest())
18 .getRequestedSessionId());
19 } else {
20 request.setRequestedSessionId(null);
21 request.setRequestedSessionURL(false);
22 }
23
里面的manager.findSession(requestSessionId)用于查找此会话ID对应的session对象。Tomcat实现
是通过一个HashMap实现,见ManagerBase.java的findSession(String id):
1 if (id == null)
2 return (null);
3 synchronized (sessions) {
4 Session session = (Session) sessions.get(id);
5 return (session);
6 }
2 return (null);
3 synchronized (sessions) {
4 Session session = (Session) sessions.get(id);
5 return (session);
6 }
Session本身也是实现为一个HashMap,因为Session设计为存放key-value键值对,Tomcat里面Session实现类是StandardSession,里面一个attributes属性:
1 /**
2 * The collection of user data attributes associated with this Session.
3 */
4 private HashMap attributes = new HashMap();
2 * The collection of user data attributes associated with this Session.
3 */
4 private HashMap attributes = new HashMap();
所有会话信息的存取都是通过这个属性来实现的。Session会话信息不会一直在服务器端保存,超过一定的时间期限就会被删除,这个时间期限可以在web.xml中进行设置,不设置的话会有一个默认值,Tomcat的默认值是60。那么服务器端是怎么判断会话过期的呢?原理服务器会启动一个线程,一直查询所有的Session对象,检查不活动的时间是否超过设定值,如果超过就将其删除。见StandardManager类,它实现了Runnable接口,里面的run方法如下:
1 /**
2 * The background thread that checks for session timeouts and shutdown.
3 */
4 public void run() {
5
6 // Loop until the termination semaphore is set
7 while (!threadDone) {
8 threadSleep();
9 processExpires();
10 }
11
12 }
13
14 /**
15 * Invalidate all sessions that have expired.
16 */
17 private void processExpires() {
18
19 long timeNow = System.currentTimeMillis();
20 Session sessions[] = findSessions();
21
22 for (int i = 0; i < sessions.length; i++) {
23 StandardSession session = (StandardSession) sessions[i];
24 if (!session.isValid())
25 continue;
26 int maxInactiveInterval = session.getMaxInactiveInterval();
27 if (maxInactiveInterval < 0)
28 continue;
29 int timeIdle = // Truncate, do not round up
30 (int) ((timeNow - session.getLastUsedTime()) / 1000L);
31 if (timeIdle >= maxInactiveInterval) {
32 try {
33 expiredSessions++;
34 session.expire();
35 } catch (Throwable t) {
36 log(sm.getString("standardManager.expireException"), t);
37 }
38 }
39 }
40
41 }
2 * The background thread that checks for session timeouts and shutdown.
3 */
4 public void run() {
5
6 // Loop until the termination semaphore is set
7 while (!threadDone) {
8 threadSleep();
9 processExpires();
10 }
11
12 }
13
14 /**
15 * Invalidate all sessions that have expired.
16 */
17 private void processExpires() {
18
19 long timeNow = System.currentTimeMillis();
20 Session sessions[] = findSessions();
21
22 for (int i = 0; i < sessions.length; i++) {
23 StandardSession session = (StandardSession) sessions[i];
24 if (!session.isValid())
25 continue;
26 int maxInactiveInterval = session.getMaxInactiveInterval();
27 if (maxInactiveInterval < 0)
28 continue;
29 int timeIdle = // Truncate, do not round up
30 (int) ((timeNow - session.getLastUsedTime()) / 1000L);
31 if (timeIdle >= maxInactiveInterval) {
32 try {
33 expiredSessions++;
34 session.expire();
35 } catch (Throwable t) {
36 log(sm.getString("standardManager.expireException"), t);
37 }
38 }
39 }
40
41 }
Session信息在create,expire等事情的时候都会触发相应的Listener事件,从而可以对session信息进行监控,这些Listener只需要继承HttpSessionListener,并配置在web.xml文件中。如下是一个监控在线会话数的Listerner:
import java.util.HashSet;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class MySessionListener implements HttpSessionListener {
public void sessionCreated(HttpSessionEvent event) {
HttpSession session = event.getSession();
ServletContext application = session.getServletContext();
// 在application范围由一个HashSet集保存所有的session
HashSet sessions = (HashSet) application.getAttribute("sessions");
if (sessions == null) {
sessions = new HashSet();
application.setAttribute("sessions", sessions);
}
// 新创建的session均添加到HashSet集中
sessions.add(session);
// 可以在别处从application范围中取出sessions集合
// 然后使用sessions.size()获取当前活动的session数,即为“在线人数”
}
public void sessionDestroyed(HttpSessionEvent event) {
HttpSession session = event.getSession();
ServletContext application = session.getServletContext();
HashSet sessions = (HashSet) application.getAttribute("sessions");
// 销毁的session均从HashSet集中移除
sessions.remove(session);
}
}
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class MySessionListener implements HttpSessionListener {
public void sessionCreated(HttpSessionEvent event) {
HttpSession session = event.getSession();
ServletContext application = session.getServletContext();
// 在application范围由一个HashSet集保存所有的session
HashSet sessions = (HashSet) application.getAttribute("sessions");
if (sessions == null) {
sessions = new HashSet();
application.setAttribute("sessions", sessions);
}
// 新创建的session均添加到HashSet集中
sessions.add(session);
// 可以在别处从application范围中取出sessions集合
// 然后使用sessions.size()获取当前活动的session数,即为“在线人数”
}
public void sessionDestroyed(HttpSessionEvent event) {
HttpSession session = event.getSession();
ServletContext application = session.getServletContext();
HashSet sessions = (HashSet) application.getAttribute("sessions");
// 销毁的session均从HashSet集中移除
sessions.remove(session);
}
}
相关推荐
### JavaWeb Session原理分析 #### 一、Session与Cookie的关系及Session的工作原理 在JavaWeb开发中,Session机制被广泛用于实现用户会话状态的跟踪。Session与Cookie有着紧密的联系,但它们各自承担着不同的角色...
总结,memcached-session-manager 是解决分布式环境中 Tomcat Session 共享的有效工具,通过与 Memcached 结合,实现了跨服务器的 Session 同步,提高了系统的可扩展性和可用性。深入理解和熟练使用这个工具,对于...
总结来说,Tomcat中的Session是通过Session ID(JSESSIONID)作为标识,结合Cookie在客户端和服务器之间传递,实现对用户会话状态的跟踪。了解这些原理对于优化Web应用性能、处理会话管理问题以及确保用户安全性至关...
总结,Tomcat6源码分析是深入了解Web服务器运行机制的重要途径,通过对源码的学习,我们可以掌握其内部的工作原理,从而在实际开发中实现更高效、更稳定的应用部署和维护。这是一份宝贵的资源,值得我们深入研究和...
通过本分析,读者将能更好地理解Tomcat的工作原理及其各组成部分的功能。 #### 二、Tomcat各个组件详解 ##### 1. Server组件 - **定义**:`Server`组件是Tomcat实例的顶层元素,代表整个容器,由`org.apache....
这使得session可以在多台Tomcat服务器之间共享,实现了session复制和故障转移,增强了系统的可扩展性和可靠性。 1. **Redis作为Session存储** Redis是一个高性能的键值数据库,以其高速读写性能和丰富的数据结构,...
Tomcat的核心设计基于模块化,主要由连接器(Connector)和容器(Container)两大部分组成,这两部分共同构成了Tomcat的核心工作原理。 **1. 原理** - **概述Tomcat** Tomcat作为Servlet容器,它的主要任务是接收...
本文将深入探讨如何在Tomcat中实现Session的共享,并分析其背后的原理和配置方法。 首先,我们要理解Session的基本概念。Session是Web服务器为用户创建的一段持久性存储空间,用于存储用户在访问网站期间产生的状态...
- **Session管理**: Tomcat提供了基于内存、文件和数据库的Session持久化策略,以保持用户会话状态。 4. **源码分析** - 深入阅读Tomcat源码有助于理解其工作原理,例如`org.apache.catalina.connector.Request`...
模拟Tomcat实现项目发布的过程,主要是理解Tomcat如何加载和运行Web应用。这涉及到以下几个关键步骤: 1. **配置Context**: Tomcat通过`context.xml`文件来配置每个Web应用。在这个文件中,你可以定义应用的路径、...
本篇将深入探讨如何使用Multicast Session Manager (MSM)来实现在Tomcat集群中的Session共享。 【描述】:由于描述为空,我们直接进入主题。在Tomcat集群中,当用户请求被负载均衡到不同的服务器时,保持用户的...
【标题】:“Tomcat实现推送技术” 在Web开发中,传统的HTTP协议是基于请求-响应模型的,服务器只会在客户端发起请求时返回数据。然而,实时性需求的提升催生了服务器推送技术,使得服务器能够主动将数据推送给...
- **Cluster**: 支持Tomcat集群,实现负载均衡和session复制。 ### 4. 配置与优化 Tomcat的配置主要在`conf`目录下的文件中进行,如`server.xml`、`web.xml`等。通过调整配置,可以定制Tomcat的行为,例如设置最大...
【深入浅析TomCat Session管理分析】 Session在Java Web开发中扮演着至关重要的角色,它允许服务器跟踪...通过源码分析,我们可以更深入地了解Session的内部工作原理,从而更好地利用和定制Tomcat的Session管理功能。
Tomcat的log模块负责日志记录,session管理模块处理用户会话,jasper模块用于JSP编译,naming模块实现了JNDI(Java Naming and Directory Interface)服务,JMX模块支持远程管理和监控,权限控制模块确保了安全性的...
3. **会话管理**:Tomcat如何实现Session跟踪,包括Cookie、URL重写和基于数据库的持久化策略。 4. **连接器与协议处理**:Coyote如何处理HTTP请求,包括解析请求头、读取请求体、构建响应,以及处理Keep-Alive和...
1. Tomcat集群:通过复制session数据和负载均衡策略,实现多台Tomcat间的session共享和负载分发。 2. 使用负载均衡器:如Nginx、Apache HTTP Server,实现对多台Tomcat的智能调度。 总结,Apache Tomcat是Java Web...
6. **Session管理**:Tomcat如何实现session的创建、持久化、复制和过期策略。 7. **国际化与本地化**:Tomcat如何处理不同语言和地区的资源文件,提供多语言支持。 8. **WebSocket支持**:从Tomcat 7开始,Tomcat...