How Tomcat works阅读笔记
知其然更要知其所以然,为了更好的理解Tomcat的运行逻辑和内部结构,这本《深入剖析Tomcat》是不错的选择。
How Tomcat works
第一章:一个简单的Web服务器
Socket类
套接字socket相当于应用程序从网络获取、写入数据的接口。
ServerSocket
Socket更像是一个客户端的套接字(当然,服务端从网络中获取数据也要使用Socket)。对于服务器来说,需要时刻保持警惕。因为客户端的请求会不定时的传来,ServerSocket的存在就是为了解决这个问题。它的accept()方法等待连接请求,创建一个Socket来与客户端通信。
Socket与ServerSocket
// An highlighted block
public void await() {
ServerSocket serverSocket = null;
int port = 8080;
serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
// Loop waiting for a request
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
socket = serverSocket.accept();
//构建request与response
input = socket.getInputStream();
output = socket.getOutputStream();
// create Request object and parse
Request request = new Request(input);
request.parse();
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
// Close the socket
socket.close();
//check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}
catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
程序调用流程:
第二章:一个简单的servlet容器
本章节通过两个简单的应用程序说明了一下servlet容器的大致运行过程。
应用程序1
在HttpServer中添加了对于请求的判断,servlet or 静态文件,确定了请求的类型后,交由ServletProcessor或StaticResourceProcessor的process()方法。
servlet or 静态文件
public class HttpServer1 {
public static void main(String[] args) {
HttpServer1 server = new HttpServer1();
server.await();
}
public void await() {
......
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor1 processor = new ServletProcessor1();
processor.process(request, response);
}
else {
StaticResourceProcessor processor = new StaticResourceProcessor();
processor.process(request, response);
}
......
}
}
servletProcessor的process方法
public class ServletProcessor1 {
public void process(Request request, Response response) {
URLClassLoader loader = null;
try {
// 构造一个URL类载入器
......
} catch (IOException e) {
System.out.println(e.toString());
}
Class myClass = null;
try {
//在仓库repository中获取目标servlet类
myClass = loader.loadClass(servletName);
} catch (ClassNotFoundException e) {
System.out.println(e.toString());
}
Servlet servlet = null;
try {
//实例化servlet
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
} catch (Exception e) {
System.out.println(e.toString());
} catch (Throwable e) {
System.out.println(e.toString());
}
}
}
程序调用流程:
应用程序2
在应用程序1中出现了一些不安全的代码:
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
}
出现了Request向上转型为ServletRequest的情况,Request的parse()方法在servlet中是不应该出现的。居心不良的人可以将构造servlet的ServletRequest向下转型为Request。于是parse()方法就可以使用了,所以需要修改相关代码。
RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
}
第三章:连接器
连接器的作用就是接收并解析Http请求,根据收到的Http请求来包装request,并将request和response传给Servlet容器。
StringManager类
此类用来处理Tomcat中的错误信息。
应用程序
在拥有了连接器后,request以及response的创建都留在HttpProcess类的process()方法中创建。
Bootstrap类
该类用来启动应用程序
public final class Bootstrap {
public static void main(String[] args) {
HttpConnector connector = new HttpConnector();
connector.start();
}
}
HttpConnector类
该类的作用是等待HTTP请求,创建Socket套接字。将套接字作为参数传入HttpProcess类的process()方法中。
public class HttpConnector implements Runnable {
public void run() {
......
socket = serverSocket.accept();
HttpProcessor processor = new HttpProcessor(this);
processor.process(socket);
......
}
}
HttpProcessor类
该类的主要方法是process()方法,该方法的工作是创建HttpRequest和HttpResponse;判断请求的是静态文件还是servlet后,将request和response传给StaticResourceProcessor或者servletProcessor的process()方法(两个process()方法)
servletProcessor的process()方法在上一章有介绍。
程序调用流程:
当然,生成request和response需要解析HTTP请求。对HTTP请求进行解析的方法parseHeaders()、parseRequest()也全部放在HttpProcessor类中。
public class HttpProcessor {
public void process(Socket socket) {
......
parseRequest(input, output);
parseHeaders(input);
......
}
private void parseHeaders(SocketInputStream input){
......
}
private void parseRequest(SocketInputStream input, OutputStream output){
......
}
}
第四章:Tomcat默认连接器
本章使用的默认连接器与上一章的连接器工作原理类似,但是与第三章相比加入了一些优化。首先,第三章中的HttpConnection必须等待当前正在处理的Http请求返回才能接收下一个Http请求。
对第三章连接器的升级
第三章Connector的run方法
public class HttpConnector implements Runnable {
public void run() {
...
while (!stopped) {
...
socket = serverSocket.accept();
HttpProcessor processor = new HttpProcessor(this);
//这里可以看出,想要接收下一个http请求必须等待process()方法执行完
processor.process(socket);
...
}
...
}
}
需要知道的是start()方法会使得该线程开始执行;java虚拟机会去调用该线程的run()方法。
具体原因是,HttpConnector中的start()方法中执行了threadStart()方法,在此方法中执行了thread.start()方法,而进一步看thread.start()方法,最关键的一步是start0()方法,此方法表示若实现了Runnable接口则执行器run()方法。这就是上面标黄部分的解释。
本章中,HttpProcessor类也实现了Runnable接口。也就是说,当HttpConnector新建HttpProcessor实例时,会自动调用其start()方法,HttpProcessor就可以在自己的线程中执行了。
HttpConnector连接器使HttpProcessor执行在自己线程的过程
public final class HttpConnector
implements Connector, Lifecycle, Runnable {
public void start(){
......
HttpProcessor processor = newProcessor();
//processor入栈
recycle(processor);
......
}
private HttpProcessor newProcessor() {
......
((Lifecycle) processor).start();
......
}
public void run() {
......
HttpProcessor processor = createProcessor();
//重要方法,用来配合processor的await()方法
processor.assign(socket);
......
}
private HttpProcessor createProcessor() {
if(......){
return ((HttpProcessor) processors.pop());
}
else if(......){
return (newProcessor());
}
}
}
HttpProcessor在连接器中创建时会触发start()方法,但是在执行run()方法时,await()方法需要等待连接器HttpConnector的run()方法中的assign()。
public final class HttpConnector{
......
public void run() {
......
//重要方法,用来配合processor的await()方法
processor.assign(socket);
......
}
......
}
final class HttpProcessor{
public void run() {
......
Socket socket = await();
process(socket);
connector.recycle(this);
......
}
}
处理请求
就是解析Http请求的各个部分,当遇到问题的时候再具体看,目前对于理解Tomcat的运行机制没太大帮助。
应用程序
因为本章的主题是Tomcat默认连接器,但是一个应用程序需要servlet容器。所以提供了一个简单的servlet容器SimpleContainer。
其中,invoke()方法的作用是接收HttpProcessor的process()方法传递的request、response;创建一个类载入器;载入相关servlet的service()方法。
值得一提的是,invoke()方法与第三章ServletProcessor类的process()方法类似
public class SimpleContainer implements Container {
public void invoke(Request request, Response response){
......
URLClassLoader loader = new URLClassLoader(urls);
Class myClass = loader.loadClass(servletName);
Servlet servlet = (Servlet) myClass.newInstance();
servlet.service((HttpServletRequest) request, (HttpServletResponse) response);
......
}
}
第五章:Servlet容器
Servlet容器的作用就是接收到连接器传来的request和response。根据request中的请求来调用servlet对象,为客户端填充response对象。
Container接口
各级Container接口的关系
其中Engine表示整个Catalina servlet引擎,
Host表示包含若干个Context容器的虚拟主机,
Context表示一个Web应用程序,可能包含多个Wrapper,
Wrapper表示一个独立的Servlet。
管道任务
管道相当于Servlet容器需要执行的任务列表,管道中的阈就是一个个任务。还记得上一章中HttpProcessor将request和response传入servlet容器的invoke()方法吗?它在管道任务的调用上发挥重要作用。
对于一个Context容器,执行过程如下所示:
上图中值得注意的是在具体的阈中实现遍历靠的是ValveContext,具体的执行原理可以查看从SimpleContext类的invoke()方法开始的调用过程。
Context容器查找Wrapper使用的是Mapper映射器;Wrapper容器查找Servlet使用的是Loader载入器。
第六章:生命周期
Lifecycle接口
Catalina允许一种父组件包含子组件的的结构。例如,Context容器包含Wrapper,映射器等子组件。子组件时刻被其父组件“监管”,当父组件启动时,所有的子组件会自动启动。
Lifecycle接口的作用就是实现这种单一启动/关闭机制。大概有下面三类方法。
public interface Lifecycle {
//组件在运行过程中可能会触发的事件
public static final String START_EVENT = "start";
public static final String BEFORE_START_EVENT = "before_start";
public static final String AFTER_START_EVENT = "after_start";
public static final String STOP_EVENT = "stop";
public static final String BEFORE_STOP_EVENT = "before_stop";
public static final String AFTER_STOP_EVENT = "after_stop";
//启动和终止的方法(最重要)
public void start() throws LifecycleException;
public void stop() throws LifecycleException;
//与监听器相关的方法
public void addLifecycleListener(LifecycleListener listener);
public LifecycleListener[] findLifecycleListeners();
public void removeLifecycleListener(LifecycleListener listener);
}
LifecycleEvent类
该类的实例就是之前所说的六个事件:
- START_EVENT
- BEFORE_START_EVENT
- AFTER_START_EVENT
- STOP_EVENT
- BEFORE_STOP_EVENT
- AFTER_STOP_EVENT
LifecycleListener接口
LifecycleListener接口只有一个方法lifecycleEvent(),当监听器监听到相关事件发生时,会调用该方法。
public interface LifecycleListener {
public void lifecycleEvent(LifecycleEvent event);
}
}
LifecycleSupport类
该类的作用是帮助组件管理监听器、触发相应的生命周期事件。
public final class LifecycleSupport {
......
private Lifecycle lifecycle = null;
private LifecycleListener listeners[] = new LifecycleListener[0];
//下面是方法
public void addLifecycleListener(LifecycleListener listener) {
......
}
public void removeLifecycleListener(LifecycleListener listener) {
......
}
public LifecycleListener[] findLifecycleListeners() {
......
}
public void fireLifecycleEvent(String type, Object data) {
......
}
......
}
所有实现了Lifecycle接口的类通过引入LifecycleSupport类来对监听器进行管理。
public final class HttpConnector
implements Connector, Lifecycle, Runnable {
......
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
......
}
应用程序
本章的应用程序使用了第五章应用程序的主体,包含了一个Context实例、两个Wrapper实例、一个载入器、一个映射器。但是为了让读者更好的理解Lifecycle机制,应用程序中的组件都实现了Lifecycle接口,其中Context实例中使用了事件监听器。
值得一提的是,虽然本章使用的连接器Connector也实现了Lifecycle接口且使用了类似((Lifecycle) connector).start()来执行程序,但是在其start()方法中并见将其子组件一同启动起来的代码。
SimpleContext类
public class SimpleContext implements Context, Pipeline, Lifecycle {
......
//LifecycleSupport帮助管理监听器
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
......
public synchronized void start() throws LifecycleException {
//判断是否已经开始执行
if (started)
throw new LifecycleException("SimpleContext has already started");
//触发事件BEFORE_START_EVENT
lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
......
//启动子组件
if ((loader != null) && (loader instanceof Lifecycle))
((Lifecycle) loader).start();
//启动子容器
Container children[] = findChildren();
for (int i = 0; i < children.length; i++) {
if (children[i] instanceof Lifecycle)
((Lifecycle) children[i]).start();
}
//启动管道中的任务
if (pipeline instanceof Lifecycle)
((Lifecycle) pipeline).start();
//触发START_EVENT事件
lifecycle.fireLifecycleEvent(START_EVENT, null);
}
......
}
在start()方法执行的过程中,触发了BEFORE_START_EVENT、START_EVENT、AFTER_START_EVENT事件,在stop()方法中还会触发BEFORE_STOP_EVENT、STOP_EVENT、AFTER_STOP_EVENT事件。
SimpleContextLifecycleListener类
为什么触发了某些事件,lifecycleEvent()方法就会被执行?我们需要了解一下“触发事件”的方法fireLifecycleEvent()具体是怎么执行的。
public final class LifecycleSupport {
......
private LifecycleListener listeners[] = new LifecycleListener[0];
private Lifecycle lifecycle = null;
public void addLifecycleListener(LifecycleListener listener) {
......
}
public void removeLifecycleListener(LifecycleListener listener) {
......
}
//下面是关键方法,拷贝当前所有监听器并执行lifecycleEvent()方法
public void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(lifecycle, type, data);
LifecycleListener interested[] = null;
synchronized (listeners) {
interested = (LifecycleListener[]) listeners.clone();
}
for (int i = 0; i < interested.length; i++)
interested[i].lifecycleEvent(event);
}
......
}
监听器类中的lifecycleEvent()就是在触发后需要执行的方法。这些监听器实例化以后使用LifecycleSupport类的addLifecycleListener()方法加入监听器数组中。在fireLifecycleEvent()方法中会被重新调出来。
public class SimpleContextLifecycleListener implements LifecycleListener {
public void lifecycleEvent(LifecycleEvent event) {
Lifecycle lifecycle = event.getLifecycle();
System.out.println("SimpleContextLifecycleListener's event " +
event.getType().toString());
if (Lifecycle.START_EVENT.equals(event.getType())) {
System.out.println("Starting context.");
}
else if (Lifecycle.STOP_EVENT.equals(event.getType())) {
System.out.println("Stopping context.");
}
}
}
SimpleWrapper类
该类的实现与SimpleContext类类似,都是将其子组件一起运行并触发事件。但是在stop()方法中略有不同,它需要destory()servlet实例。
第七章:日志记录器
本章的内容较简单,大致就是如何记录servlet提供的日志信息的问题。等有实际应用时在添加内容。
第八章:载入器
前几章的载入器属于系统的载入器,使用系统的载入器就相当于servlet容器完全信任它正在运行的servlet,这就会导致该servlet可以访问所有的类—包括Java虚拟机中环境变量CLASSPATH指明的路径下所有的类和库,不妥。所以servlet容器需要一个自定义的载入器,使其运行的servlet只能访问WEB-INF/classes目录下的文件。
- 仓库:类载入器在哪里搜索要载入的类
- 资源:类载入器中的DirContext对象,它的文件根路径就是指上下文的文件根路径
还有一个使用自定义载入器的原因—为了实现自动重载功能。当WEB-INF/classes目录下的类发生变化时,,Web应用能重新载入这些类。类载入器使用一个额外的线程来检查目录下的变化。
Java的类载入器
简单的介绍了一下Java类载入器的结构以及其代理模式。举例介绍了代理模式下是如何解决类载入过程中的安全问题的。
Loader接口
这里的载入器是指Web应用程序载入器,应用程序载入器的实现中需要:
- 一个自定义的类载入器(WebappClassLoader类)
- 与Servlet容器Context相关联
- 仓库操作
- 重载问题
- 应用程序中的servlet只能引用部署在WEB-INF/classes目录及其子目录下的类
- servlet类只能访问WEB-INF/lib目录下的库
- WEB-INF/classes和WEB-INF/lib作为仓库添加到载入器中
public interface Loader {
//获取类载入器
public ClassLoader getClassLoader();
//与容器的关联
public Container getContainer();
public void setContainer(Container container);
//关于重载问题
public boolean getReloadable();
public void setReloadable(boolean reloadable);
public boolean modified();
//仓库相关操作
public void addRepository(String repository);
public String[] findRepositories();
......
}
Reloader接口
支持自动重载功能的类载入器需要实现Reloader接口。
WebappLoader类
该类就是Web应用程序的载入器,实现了Loader接口。它的作用就是载入Web应用程序中所使用的类。还实现了Runnable、Lifecycle接口。当调用WebappLoader的start()方法时,要完成以下的工作:
- 创建一个类载入器
- 设置仓库
- 设置类路径
- 设置访问权限
- 启动新进程来支持自动重载
public class WebappLoader
implements Lifecycle, Loader, PropertyChangeListener, Runnable {
......
public void start() throws LifecycleException {
//判断生命周期是否已经启动
if (started)
throw new LifecycleException
(sm.getString("webappLoader.alreadyStarted"));
if (debug >= 1)
log(sm.getString("webappLoader.starting"));
lifecycle.fireLifecycleEvent(START_EVENT, null);
started = true;
//主体部分
try {
//新建类载入器,下一小节要说
classLoader = createClassLoader();
//向类载入器中添加仓库
for (int i = 0; i < repositories.length; i++) {
classLoader.addRepository(repositories[i]);
}
//设置仓库,与上面的代码对比
setRepositories();
setClassPath();
setPermissions();
if (classLoader instanceof Lifecycle)
((Lifecycle) classLoader).start();
}
......
}
......
}
创建类载入器
Web应用程序实现对类的载入需要一个类载入器,在start()方法中使用了createClassLoader()方法来创造类载入器。该方法的实现如下所示:
public class WebappLoader
implements Lifecycle, Loader, PropertyChangeListener, Runnable {
......
private WebappClassLoader createClassLoader()
throws Exception {
Class clazz = Class.forName(loaderClass);
WebappClassLoader classLoader = null;
//可以使用默认的类载入器也可以自定义类载入器
if (parentClassLoader == null) {
classLoader = (WebappClassLoader) clazz.newInstance();
} else {
Class[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoader) constr.newInstance(args);
}
return classLoader;
}
}
设置仓库、类路径、访问权限
public class WebappLoader
implements Lifecycle, Loader, PropertyChangeListener, Runnable {
......
public void start() throws LifecycleException {
......
//设置仓库
for (int i = 0; i < repositories.length; i++) {
classLoader.addRepository(repositories[i]);
}
setRepositories();
//设置类路径
setClassPath();
//设置访问权限
setPermissions();
}
自动重载功能的实现
自动重载功能是指使用一个线程,每隔checkInterval时间间隔检查一遍WEB-INF/classes或WEB-INF/lib目录下是不是有类被重新编译了,如果有则该类需要被重新编译。
public class WebappLoader
implements Lifecycle, Loader, PropertyChangeListener, Runnable {
......
public void run() {
......
while (!threadDone) {
// 等待一段时间
threadSleep();
if (!started)
break;
try {
// 判断文件是否被修改,未被修改则重新while
if (!classLoader.modified())
continue;
}
//通知Context容器重新载入
notifyContext();
break;
}
......
}
......
}
WebappClassLoader类
优化:缓存之前已经载入的类来提升性能、缓存之前加载失败的类的名字来防止反复查找。
安全:WebappClassLoader类不允许载入某些类、某些包下的类。这些“禁忌”存在一些特定的数组中。
在载入类时需要执行的操作是:
- 检查缓存
- 若无缓存则使用系统的类载入器
- 检查是否允许载入
- 检查标志位delegate决定使用父载入器(自定义)还是系统载入器
- 若找不到,抛出ClassNotFoundException异常
类缓存
所有的已缓存的类都被视为资源,该资源的类是ResourceEntry类,存在一个名为resourceEntries的HashMap类型的变量中。
应用程序
监听器类:SimpleContextConfig
在第六章中我们具体说明了一下监听器运行的流程,不再赘述。
public class SimpleContextConfig implements LifecycleListener {
public void lifecycleEvent(LifecycleEvent event) {
if (Lifecycle.START_EVENT.equals(event.getType())) {
Context context = (Context) event.getLifecycle();
context.setConfigured(true);
}
}
}
与之前不同的地方是,本章使用的servlet容器是StandardContext类,需要设置一个系统属性来通知StandardContext在哪里查找应用程序目录,且在StandardContext中,servlet类只能放在应用程序目录下的WEB-INF/classes目录下。应用程序目录名为myApp。
public final class Bootstrap {
public static void main(String[] args) {
System.setProperty("catalina.base", System.getProperty("user.dir"));
......
Context context = new StandardContext();
// StandardContext's start method adds a default mapper
context.setPath("/myApp");
context.setDocBase("myApp");
......
}
}
???可以通过修改Context的path属性来指定url最终访问的是哪个应用,而docBase指明该应用的具体物理地址。
<Context path="/test" docBase="testapp" reloadable="true"/>
对于以下的内容还是存有疑惑,但是我觉得应该就是指myApp这个目录下的三个servlet。
- setPath("/myApp"):设置应用程序路径
- setDocBase(“myApp”):设置上下文的文档根路径(应用程序的路径或WAR文件存放的路径)
第九章:Session管理
session对象的管理是通过Session管理器来进行的,Session管理器是由实现了mannager接口的实例来实现的。
获取session的过程:
可见,接口HttpServletRequest的getSession()方法是获取session的关键方法,那么getSession()方法具体是如何实现的呢?
public class HttpRequestBase
extends RequestBase
implements HttpRequest, HttpServletRequest {
......
public HttpSession getSession(boolean create){
......
return doGetSession(create);
}
public HttpSession doGetSession(boolean create){
......
Manager manager = null;
if(context != null){
manager = context.getManager();
}
......
//创建一个session
session = manager.createSession();
......
}
}
Session对象
通过相关的UML图,可以看到StandardSession类实现了HttpSession接口和Session接口。但是Session管理器并不会直接将StandardSession实例直接交给servlet使用,而是会将StandardSession的外观类StandardSessionFacade传给servlet
Session接口
作为Catalina内部的外观类???
StandardSession类
StandardSession类的构造函数需要一个参数Manager(StandardSession类必须有一个Session管理器)
注意StandardSession类中的getSession()方法,通过传入一个自身实例来创建并返回StandardSessionFacade类的实例。
class StandardSession
implements HttpSession, Session, Serializable {
public HttpSession getSession() {
if (facade == null)
facade = new StandardSessionFacade(this);
return (facade);
}
}
如果某个Session管理器中的Session对象在maxInactiveInterval的时间内都没有被访问的话,将被设置为过期。设置的方法是expire()方法
class StandardSession
implements HttpSession, Session, Serializable {
public void expire(boolean notify) {
if (expiring)
return;
expiring = true;
setValid(false);
// 移除Session管理器的Session实例
if (manager != null)
manager.remove(this);
//触发一些事件
......
}
}
StandardSessionFacade类
该类是StandardSession类的外观类,为什么要使用此外观类呢?通过下面的UML类图可以看出来,StandardSessionFacade类只实现了HttpSession接口而没有实现Session接口,这样就导致servlet在获得StandardSessionFacade类后没法将其向下转换为StandardSession类型,更安全。
Manager(Session管理器组件)
Session管理器负责管理Session对象(删除、添加等),相关UML类图如下所示:
注意类图的结构,Session管理器相关的类都实现了Manager接口;ManagerBase是所有类的基类,其中PersistentManagerBase会将Session对象存储到辅助存储器中。
Manager接口
许多需要实现的方法:
- getContainer()、setContainer()方法
- createSession()方法
- add()、remove()方法:从Session池中添加或删除session对象
- load()、unload()
ManagerBase类
该类为抽象类,所有的Session管理器组件都会继承此类。其实现了的许多方法可以方便子类使用:
- createSession()方法
- generateSessionId()方法
Session管理器对应Context容器,Session管理器会管理该Context容器中的所有session对象,所有的Session对象都存在一个名为sessions的HashMap中。
其中add()、remove()方法都是往这个HashMap中添加或者删除session对象。
StandardManager类
Manager的标准实现,它实现了LifeCycle接口与其关联的Context容器一同启动或关闭。它将session存在内存中,当Catalina关闭时,将session对象存在一个名为SESSION.ser的文件中。该文件位于环境变量CATALINA_HOME指定的目录下的work目录中。
StandardManager还要销毁那些已经失效的session对象,在其run()方法中的processExpire()方法会遍历所有的session对象,如果session长时间未访问则调用Session接口的expire()方法使这个Session实例过期。
public class StandardManager
extends ManagerBase
implements Lifecycle, PropertyChangeListener, Runnable {
......
public void run() {
// Loop until the termination semaphore is set
while (!threadDone) {
threadSleep();
//遍历session查找是否有过期的,若有则调用expire()
processExpires();
}
}
}
PersistentManagerBase类
该类是持久化Session管理器的父类,也是个抽象类(所有持久化session管理器的父类);持久化Session管理器和StandardSession的区别在于持久化Session管理器存储Session对象的辅助存储器的形式(Store对象)。
Store对象的作用就是将Session对象存入某个文件中。
public abstract class PersistentManagerBase
extends ManagerBase
implements Lifecycle, PropertyChangeListener, Runnable {
......
public void run() {
// Loop until the termination semaphore is set
while (!threadDone) {
threadSleep();
processExpires();
processPersistenceChecks();
}
}
}
其中,processPersistenceChecks()方法负责调用processMaxIdleSwaps()、processMaxActiveSwaps()、processMaxIdleBackups()三个方法。
分别负责换出和备份。
由于可以换出和备份,findSession()方法会先在内存中查找,再在存储器中查找。
PersistentManager类
和PersistentManagerBase类差不多。
DistributedManager类
用于两个或多个节点的集群环境,一个节点表示部署的一台Tomcat服务器。每个节点都使用DistributedManager实例,来复制Session对象。为了实现这一主要功能,集群中的节点需要能够接受和发送数据给其他的节点。
public final class DistributedManager extends PersistentManagerBase {
......
private ClusterSender clusterSender = null;
......
public Session createSession() {
Session session = super.createSession();
ObjectOutputStream oos = null;
ByteArrayOutputStream bos = null;
ByteArrayInputStream bis = null;
try {
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(new BufferedOutputStream(bos));
((StandardSession)session).writeObjectData(oos);
oos.close();
byte[] obs = bos.toByteArray();
//与其他节点交互
clusterSender.send(obs);
if(debug > 0)
log("Replicating Session: "+session.getId());
} catch (IOException e) {
log("An error occurred when replicating Session: "+session.getId());
}
return (session);
}
......
public void run() {
// Loop until the termination semaphore is set
while (!threadDone) {
threadSleep();
//接收其他节点的消息
processClusterReceiver();
processExpires();
processPersistenceChecks();
}
}
}
存储器
存储器都是Store接口的实例,在Store接口中两个方法比较重要:
- save()方法:将指定的Session对象存到某种持久存储器中
- load()方法:从存储器中拿出某个Session对象放入内存中
StoreBase类
StoreBase类是一个抽象类,是FileStore和JDBCStore类的父类。但是StoreBase类没有实现save()、load()方法;因为这些方法需要根据具体的存储器来定。
public abstract class StoreBase
implements Lifecycle, Runnable, Store {
......
public void run() {
// Loop until the termination semaphore is set
while (!threadDone) {
threadSleep();
processExpires();
}
}
......
}
FileStore
JDBCStore
应用程序
本章的应用程序和上一章的差不多,不同的是本章的Context容器使用StandardManager实例来管理Session对象。回顾之前的内容,servlet对于session对象的获取是通过创建HttpRequestBase实例,调用其getSession()方法。
获取session的过程:
可见,接口HttpServletRequest的getSession()方法是获取session的关键方法,那么getSession()方法具体是如何实现的呢?
public class HttpRequestBase
extends RequestBase
implements HttpRequest, HttpServletRequest {
......
public HttpSession getSession(boolean create){
......
return doGetSession(create);
}
public HttpSession doGetSession(boolean create){
......
Manager manager = null;
if(context != null){
manager = context.getManager();
}
......
//创建一个session
session = manager.createSession();
......
}
}
所以传递给servlet的request必须与Context容器相关联。在HttpRequestBase实例中使用其相关联的context容器获取session管理器Manner。
那么如何将request和Context容器进行关联呢?在wrapper容器的阈中进行关联。
public class SimpleWrapperValve implements Valve, Contained {
......
public void invoke(Request request, Response response, ValveContext valveContext)
throws IOException, ServletException {
ServletRequest sreq = request.getRequest();
ServletResponse sres = response.getResponse();
HttpServletRequest hreq = null;
if (sreq instanceof HttpServletRequest)
hreq = (HttpServletRequest) sreq;
HttpServletResponse hres = null;
if (sres instanceof HttpServletResponse)
hres = (HttpServletResponse) sres;
......
//将request对象和COntext容器相关联
Context context = (Context) wrapper.getParent();
request.setContext(context);
//将request和response传入servlet
try {
servlet = wrapper.allocate();
if (hres!=null && hreq!=null) {
servlet.service(hreq, hres);
}
else {
servlet.service(sreq, sres);
}
}
catch (ServletException e) {
}
}
}
提一嘴,我将bootstrap中的addServletMapping方法中的“/myApp/Session”修改为“/my/Session”时,在浏览器中无法看到previous value,是因为程序路径path和上下文文档根路径被设置为了/myApp,该路径被用来发送Cookie,只有当访问的路径也是/myApp时,浏览器才会将Cookie发送回服务器。
另外,我将“myApp”修改为“webroot”后依旧可以正常执行
还有,对于书上没有提到的第九章其他内容也做一些了解
第十章:安全性
Web应用程序的一些内部内容是受限的,只有提供正确的用户名和密码才能访问。servlet容器通过验证器阈来支持安全限制。容器启动时,验证器阈被加入到Context容器的管道中。
在调用wrapper阈(Context的管道的基础阈)之前,会先调用验证器阈,只有输入正确才会继续调用后面的阈。
领域(Realm接口)
- 领域对象:用来对用户进行身份验证的组件
- 领域对象会与一个Context容器相关联,Context容器关联领域对象的方法是setRealm()
- 领域对象通过访问(默认)tomcat-user.xml文件来验证用户信息
- 领域对象是Realm接口的实例
- 领域对象对用户信息进行验证的方法为authenticate()
- 领域对象关联Context容器的方法是setContainer()
- 领域对象的基本实现类是RealmBase类(抽象类),默认使用的是MemoryRealm类的实例作为验证用的领域对象
GenericPrincipal类(主体)
- 主体对象是Principal接口的实例
- 在Catalina中的实现是GenericPrincipal类
- GenericPrincipal实例必须和一个领域对象相关联
- GenericPrincipal实例包含一个用户名和密码对,以及一个角色列表
- 可以通过hasRole()方法来检测该主体对象是否拥有该指定角色
LoginConfig类(登录配置)
-
包含一个领域对象的名字
-
包含一个所使用的身份验证方法:BASIC、DIGEST、FORM、CLIENT-CERT取其一。
-
Tomcat启动时会读取web.xml文件,如果文件中包含login-config元素,则Tomcat会创建一个LoginConfig对象并设置相应的属性。
-
验证器阈会调用LoginConfig实例的getRealmName()方法获取领域对象名并发送到浏览器。
Authenticator接口
验证器就是实现了Authenticator接口的实例:
- 该接口没有声明方法,只是起到了标记的作用
- 其他部件可以使用instanceof关键字来检查某个组件是不是一个验证器。
- Authenticator接口的一个基本实现:AuthenticatorBase类(它也是一个阈)
Authenticator包下的各个功能的UML类图:
我们发现AuthenticatorBase下有许多子类,
4. 验证器的主要工作就是对用户身份进行验证
5. 前面说过AuthenticatorBase类也是一个阈,当使用invoke()方法时会调用authenticate()方法时依赖于子类
安装验证器阈
- 一个Context实例只有一个LoginConfig实例
- AuthenticatorBase的哪个子类作为Context实例中的验证器阈依赖于LoginConfig的auth-method值
应用程序
SimpleContextConfig类(LifecycleListener)
public class SimpleContextConfig implements LifecycleListener {
private Context context;
//这里的方法是在Lifecycle的start()方法被调用后使用的
public void lifecycleEvent(LifecycleEvent event) {
if (Lifecycle.START_EVENT.equals(event.getType())) {
context = (Context) event.getLifecycle();
authenticatorConfig();
context.setConfigured(true);
}
}
private synchronized void authenticatorConfig() {
......
}
}
该类其实就是一个实现了LifecycleListener接口的类,其实现了LifecycleListener接口的方法lifecycleEvent(),在此方法的START_EVENT中调用了authenticatorConfig()方法,authenticatorConfig()方法主要做了四个工作:
- 检查关联的Context容器是否有安全限制,若有则继续
- 检查该Context实例是否有LoginConfig对象,若没有就创建个新的
- 检查当前StandardContext对象的pipeline中的阈中(基础阈和其他阈)是否有验证器,若有则直接返回(因为该方法的目的就是在pipeline中安装验证器阈)
- 发现pipeline中并未安装验证器阈,则在Context实例中检查是否有领域(realm)对象;若没有则返回(安装不了验证器阈了)
- 若找到了领域(realm)对象, 则开始动态的载入BasicAuthenticator
SimpleRealm类
该类的作用是说明领域对象是如何工作的,包含两个硬编码的用户名和密码对。
public class SimpleRealm implements Realm {
//构造函数
public SimpleRealm() {createUserDatabase();}
private void createUserDatabase() {
User user1 = new User("ken", "blackcomb");
user1.addRole("manager");
user1.addRole("programmer");
User user2 = new User("cindy", "bamboo");
user2.addRole("programmer");
users.add(user1);
users.add(user2);
}
......
//此处的Principal是代表该用户的对象
public Principal authenticate(String username, String credentials) {
System.out.println("SimpleRealm.authenticate()");
if (username==null || credentials==null)
return null;
User user = getUser(username, credentials);
if (user==null)
return null;
return new GenericPrincipal(this, user.username, user.password, user.getRoles());
}
//根据给出的密码对来得到对应的user对象
private User getUser(String username, String password) {
Iterator iterator = users.iterator();
while (iterator.hasNext()) {
User user = (User) iterator.next();
if (user.username.equals(username) && user.password.equals(password))
return user;
}
return null;
}
class User {
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String username;
public ArrayList roles = new ArrayList();
public String password;
public void addRole(String role) {
roles.add(role);
}
public ArrayList getRoles() {
return roles;
}
}
}
authenticate()方法是由验证器调用的。
SimpleUserDatabaseRealm
该类还是一个领域对象,但是稍微复杂一些。其并不将用户列表存储到对象自身中,而是读取conf文件下的tomcat-users.xml文件并将其内容存入内存。
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="tomcat"/>
<role rolename="role1"/>
<role rolename="manager"/>
<role rolename="admin"/>
<user username="tomcat" password="tomcat" roles="tomcat"/>
<user username="role1" password="tomcat" roles="role1"/>
<user username="both" password="tomcat" roles="tomcat,role1"/>
<user username="admin" password="password" roles="admin,manager"/>
</tomcat-users>
该类的authenticate()方法没有看懂,但是其通过createDatabase()方法来向xml文件传递路径,解析xml文件的内容。
Bootstrap1
只展示与之前章节不同的代码:
public final class Bootstrap1 {
public static void main(String[] args) {
......
// add constraint
SecurityCollection securityCollection = new SecurityCollection();
securityCollection.addPattern("/");
securityCollection.addMethod("GET");
SecurityConstraint constraint = new SecurityConstraint();
constraint.addCollection(securityCollection);
constraint.addAuthRole("manager");
LoginConfig loginConfig = new LoginConfig();
loginConfig.setRealmName("Simple Realm");
// add realm
Realm realm = new SimpleRealm();
context.setRealm(realm);
context.addConstraint(constraint);
context.setLoginConfig(loginConfig);
connector.setContainer(context);
}
......
}
其中进行了一些设置:
- 哪些URL需要遵循安全限制(原先为所有URL,我改为/Modern后只对/Modern有效)
- 哪些提交的方法需要遵循安全限制(get)
- 哪些角色可以访问受限资源(Manner)
- 将Context容器与realm、SecurityConstraint(已经与SecurityCollection联系起来了)、LoginConfig联系起来
Bootstrap2
Bootstrap2与Bootstrap1相似,区别就在于其使用的领域对象(Realm)是SimpleUserDatabaseRealm对象
public final class Bootstrap2 {
public static void main(String[] args) {
......
loginConfig.setRealmName("Simple User Database Realm");
// add realm
Realm realm = new SimpleUserDatabaseRealm();
((SimpleUserDatabaseRealm) realm).createDatabase("conf/tomcat-users.xml");
context.setRealm(realm);
......
}
......
}
第十一章:StandardWrapper
我们还记得Context容器包含一个或多个Wrapper实例,其中每一个Wrapper容器表示一个具体的servlet定义;
本章要对Catalina中Wrapper接口的标准实现(StandardWrapper)进行说明。