Scrapy安装

windows10  安装scrapy, windows 下的scrapy 目前暂时不支持python3,因为scrapy 依赖的 Twisted  暂不支持python3,所以只能用python2,并且是win32版本。

目前已支持Windows X64版本 20160928 更新

安装

安装scrapy 前需要如下环境内容:

python2-7-12(win32):

https://www.python.org/ftp/python/2.7.12/python-2.7.12.msi

lxml: https://pypi.python.org/packages/ce/23/e734f2f1a4e3efb40ec60a2cfa6daa08e5d46240c256f9fb146a5b64a9c0/lxml-3.3.5.win32-py2.7.exe#md5=2c10ce9cab81e0155a019fb6c0c3e2e9

libxml2:

http://xmlsoft.org/sources/win32/python/libxml2-python-2.7.7.win32-py2.7.exe

VCForPython27.msi

https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi

安装上诉之后,整个环境已经准备妥当,直接用如下pip 即可将Scrapy 安装好了

[bath]py -2 -m pip install Scrapy[/bath]

python3.5 Scrapy1.1.3安装 20160928

需要使用wheel格式的文件直接本地安装lxml ,Twisted. 这个几个包可恶意从http://www.lfd.uci.edu/~gohlke/pythonlibs/  找到。

lxml:http://www.lfd.uci.edu/~gohlke/pythonlibs/dp2ng7en/lxml-3.6.4-cp35-cp35m-win_amd64.whl

Twisted:http://www.lfd.uci.edu/~gohlke/pythonlibs/dp2ng7en/Twisted-16.4.1-cp35-cp35m-win_amd64.whl

scrapy:https://pypi.python.org/packages/1f/91/81b32afce9676a0542ee42e8755ff1d61a80acd0101035929d7355b8cc50/Scrapy-1.1.3-py2.py3-none-any.whl#md5=eb35996066a3802dd9d2b2070098bdbb

依次安装上述下载的wheel 文件:

[bath]py -3 -m pip install lxml-3.6.4-cp35-cp35m-win_amd64.whl

py -3 -m pip install Twisted-16.4.1-cp35-cp35m-win_amd64.whl

py -3 -m pip install Scrapy-1.1.3-py2.py3-none-any.whl[/bath]

 

运行调试

创建项目

scrapy startproject tutorial

编写spider

运行

scrapy crawl spider_name

scrapy crawl dmoz

在pycharm 环境中,则通过配置 run configuration实现,

%_H8K{A)339[P2A67%V7J}P

 

配置完成后,直接run 就行了,可断点调试

mysql5.6安装

RPM方式安装MySQL5.6

a. 检查MySQL及相关RPM包,是否安装,如果有安装,则移除(rpm –e 名称)

1
[root@localhost ~]# rpm -qa | grep -i mysql

2
mysql-libs-5.1.66-2.el6_3.x86_64

3
[root@localhost ~]# yum -y remove mysql-libs*

b. 下载Linux对应的RPM包,如:CentOS6.4_64对应的RPM包,如下:

1
[root@localhost rpm]# ll

2
total 74364

3
-rw-r--r--. 1 root root 18442536 Dec 11 20:19 MySQL-client-5.6.15-1.el6.x86_64.rpm

4
-rw-r--r--. 1 root root  3340660 Dec 11 20:06 MySQL-devel-5.6.15-1.el6.x86_64.rpm

5
-rw-r--r--. 1 root root 54360600 Dec 11 20:03 MySQL-server-5.6.15-1.el6.x86_64.rpm

c. 安装MySQL

1
[root@localhost rpm]# rpm -ivh MySQL-server-5.6.15-1.el6.x86_64.rpm

2
[root@localhost rpm]# rpm -ivh MySQL-devel-5.6.15-1.el6.x86_64.rpm

3
[root@localhost rpm]# rpm -ivh MySQL-client-5.6.15-1.el6.x86_64.rpm

4
#修改配置文件位置

5
[root@localhost rpm]# cp /usr/share/mysql/my-default.cnf /etc/my.cnf

d. 初始化MySQL及设置密码

1
[root@localhost rpm]# /usr/bin/mysql_install_db

2
[root@localhost rpm]# service mysql start

3
[root@localhost rpm]# cat /root/.mysql_secret  #查看root账号密码

4
# The random password set for the root user at Wed Dec 11 23:32:50 2013 (local time): qKTaFZnl

5
[root@localhost ~]# mysql -uroot –pqKTaFZnl

6
mysql> SET PASSWORD = PASSWORD('123456');    #设置密码为123456

7
mysql> exit

8
[root@localhost ~]# mysql -uroot -p123456

e. 允许远程登陆

01
mysql> use mysql;

02
mysql> select host,user,password from user;

03
+-----------------------+------+-------------------------------------------+

04
| host                  | user | password                                  |

05
+-----------------------+------+-------------------------------------------+

06
| localhost             | root | *6BB4837EB74329105EE4568DDA7DC67ED2CA2AD9 |

07
| localhost.localdomain | root | *1237E2CE819C427B0D8174456DD83C47480D37E8 |

08
| 127.0.0.1             | root | *1237E2CE819C427B0D8174456DD83C47480D37E8 |

09
| ::1                   | root | *1237E2CE819C427B0D8174456DD83C47480D37E8 |

10
+-----------------------+------+-------------------------------------------+

11

12
mysql> update user set password=password('123456') where user='root';

13
mysql> update user set host='%' where user='root' and host='localhost';

14
mysql> flush privileges;

15
mysql> exit

f. 设置开机自启动

1
[root@localhost ~]# chkconfig mysql on

2
[root@localhost ~]# chkconfig --list | grep mysql

3
mysql           0:off   1:off   2:on    3:on    4:on    5:on    6:off

g. MySQL的默认安装位置

1
/var/lib/mysql/               #数据库目录

2
/usr/share/mysql              #配置文件目录

3
/usr/bin                     #相关命令目录

4
/etc/init.d/mysql              #启动脚本

修改字符集和数据存储路径

配置/etc/my.cnf文件,修改数据存放路径、mysql.sock路径以及默认编码utf-8.

view plain copy

在CODE上查看代码片派生到我的代码片

  1. [client]  
  2. password        = 123456  
  3. port            = 3306  
  4. default-character-set=utf8
  5. [mysqld]  
  6. port            = 3306  
  7. character_set_server=utf8
  8. character_set_client=utf8
  9. collation-server=utf8_general_ci
  10. #(注意linux下mysql安装完后是默认:表名区分大小写,列名不区分大小写; 0:区分大小写,1:不区分大小写)  
  11. lower_case_table_names=1
  12. #(设置最大连接数,默认为 151,MySQL服务器允许的最大连接数16384; )  
  13. max_connections=1000
  14. [mysql]  
  15. default-character-set = utf8

查看字符集

python安装

python2 python3 windows 下共存

关于Windows平台上Python2与Python3共存,使用过程中可能会遇到一些问题。关于网上其他的修改python.exe名字的做法都是不合适的。
下面是我从网上搜集过来的正确使用方法,可以完美使用,具体也可以参考官方说明3.4小节有详细说明

关于调用不同版本Python

如果同时安装了Python2和Python3,在安装Python3的时候勾选Install launcher for all users选项,这个选项默认会生成一个Python引导程序。同时最好也加入系统环境变量。

那么在使用的时候可以这样:

  • 使用py -2 hello.py即可调用Python2执行hello.py。py -3 hello.py即可调用Python3来执行hello.py。

  • 在hello.py文件的开头进行标记(可能叫法不对)在文件第一行写上#!python2,或者#!python3这样标记以后,在执行的时候只需要输入py hello.py,会自动调用相应的解释器来执行文件。
    记得#!python2一定要放在第一行。

关于pip的使用
  • 若需要安装Python2的库,执行py -2 -m pip install xxxx即可,xxxx是库的名字。
    若需要安装Python3的库,执行py -3 -m pip install xxxx即可。

Nginx与Tomcat整合

因为nginx处理静态页面的速度很快,并且是免费的,它还可以配置负载均衡的服务器集群来搭建多个tomcat,所以nginx+tomcat是企业搭 建javaee项目很好的选择。nginx主要是通过反向代理的方法将jsp,jspx后缀或者是javaee框架设置的特定的页面 (.do,.action)请求来交给tomcat处理,自己处理.html,.css或者是一些图片和flash。

Nginx与Tomcat整合的好处如下所示:

◆静态分离,加快用户访问网站的速度。

◆整个负载均衡层和Web层的工作流程为LVS/DR+Keeaplived→Nginx反向代理(动静分离)→Tomcat集群,可以保证整个网站不会因为某一台LVS或Nginx+tomcat机器挂掉而影响网站的运营。

◆Nginx稳定,宕机的可能性微乎其乎。

配置很简单,自己本身监听80端口过来的所有请求,如果发现是.jsp或者是.do后缀的文件请求就交给监听8080端口的tomcat来处理,配置如下:

server
{
                listen       80;#定义访问的端口号
                server_name www.myhost.com;  #定义访问的域名
index index.html index.htm index.jsp default.html default.htm default.php;#默认的根目录访问文件
                root  /home/wwwroot/www.myhost.com;#定义服务器访问的默认根目录
                location ~ \.(jsp|jspx|do)?$ #tomcat的访问文件后缀
{
                                proxy_pass http://127.0.0.1:8080;#反向代理到tomcat监听的端口
include proxy.conf
}
                location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
                                expires      30d;
}
                location ~ .*\.(js|css)?$
{
                                expires      12h;
}
                access_log  /home/wwwlogs/www.myhost.com.log  www.myhost.com;
}

这个是nginx最简单的配置,如果有更多的需求需要参考别的地方。

这种默认的配置方法写java代码 request.getRemoteAddr()是获取不到用户访问的真实ip的。只能得到你自己服务器的ip 地址,因为nginx转发了请求。注意上面代码中我在 proxy_pass http://127.0.0.1:8080 下面配置了include proxy.conf,所以你需要在nginx目录里proxy.conf里面配置一些东西。

proxy_connect_timeout 300s;
proxy_send_timeout   900;
proxy_read_timeout   900;
proxy_buffer_size    32k;
proxy_buffers     4 32k;
proxy_busy_buffers_size 64k;
proxy_redirect     off;
proxy_hide_header  Vary;
proxy_set_header   Accept-Encoding ”;
proxy_set_header   Host   $host;
proxy_set_header   Referer $http_referer;
proxy_set_header   Cookie $http_cookie;
proxy_set_header   X-Real-IP  $remote_addr;
proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;#后端的Web服务器可以通过X-Forwarded-For获取用户真实IP

proxy.conf文件配置如上,当然还配置了一些response请求头里面的东西,你可以都加进去。然后你用java代码这个req.getHeader(“X-Forwarded-For”);方法就能获取用户的真实ip了。

通过以上的配置nginx这一块就弄好了,你还需要更改一下tomcat的server.xml文件,把tomcat解析javaee项目的目录地 址改成和nginx的目录一致。打开tomcat/conf/server.xml文件,把Host标签appBase属性改成nginx配置里面的 root默认路径。

<Host name=”localhost”  appBase=”/home/wwwroot/www.myhost.com”
unpackWARs=”true” autoDeploy=”true” />

以上nginx+tomcat就配置好了,重启tomcat刷新下nginx配置文件,然后把javaee项目放在/home/wwwroot/www.myhost.com目录就可以用80端口测试使用了。

详细操作:

一、安装Tomcat和JDK

1、上传apache-tomcat-6.0.18.tar.gz和jdk-6u12-linux-i586.bin至/usr/local
2、执行如下命令安装tomcat:

#cd /usr/local

#tar zxvf apache-tomcat-6.0.18.tar.gz

解压完成后将apache-tomcat-6.0.18重命名为tomcat
3、执行如下命令安装JDK:

#./jdk-6u12-linux-i586.bin

4、配置环境变量:
编辑/etc下的profile文件,加上如下内容:

JAVA_HOME="/usr/local/jdk1.6.0_12"

CLASS_PATH="$JAVA_HOME/lib:$JAVA_HOME/jre/lib"

PATH=".:$PATH:$JAVA_HOME/bin "

CATALINA_HOME="/usr/local/tomcat"

export JAVA_HOME CATALINA_HOME

5、启动tomcat并输入http://localhost:8080,如果看到猫的页面即tomcat和jdk安装成功
6、新建文件目录/home/www为网站存放目录,设置server.xml文件,在Host name=”localhost”处将appBase=的指向路径改为/home/www/web
7、创建index.jsp至/home/www/web/ROOT,内容为:“My web!”  

二、安装Nginx
1、上传nginx-0.7.63.tar.gz至/usr/local

2、执行如下命令解压nginx:

#cd /usr/local

#tar zxvf  nginx-0.7.63.tar.gz

3、编译安装nginx

#cd nginx-0.7.63

#./configure --with-http_stub_status_module --with-http_ssl_module  #启动server状态页和https模块

执行完后会提示一个错误,说缺少PCRE library 这个是HTTP Rewrite 模块,也即是url静态化的包
可上传pcre-7.9.tar.gz,输入如下命令安装:

#tar zxvf pcre-7.9.tar.gz

#cd pcre-7.9

#./configure

#make

#make install

安装pcre成功后,继续安装nginx

#cd nginx-0.7.63

#./configure

#make

#make install

4、nginx安装成功后的安装目录为/usr/local/nginx
在conf文件夹中新建proxy.conf,用于配置一些代理参数,内容如下:

#!nginx (-) 

# proxy.conf 

proxy_redirect          off;

proxy_set_header        Host $host;

proxy_set_header        X-Real-IP $remote_addr;  #获取真实ip

#proxy_set_header       X-Forwarded-For   $proxy_add_x_forwarded_for; #获取代理者的真实ip

client_max_body_size    10m;

client_body_buffer_size 128k;

proxy_connect_timeout   90;

proxy_send_timeout      90;

proxy_read_timeout      90;

proxy_buffer_size       4k;

proxy_buffers           4 32k;

proxy_busy_buffers_size 64k;

proxy_temp_file_write_size 64k;

编辑安装目录下conf文件夹中的nginx.conf,输入如下内容

#运行nginx所在的用户名和用户组

#user  www www; 

#启动进程数

worker_processes 8;

#全局错误日志及PID文件

error_log  /usr/local/nginx/logs/nginx_error.log  crit;

pid        /usr/local/nginx/nginx.pid;

#Specifies the value for maximum file descriptors that can be opened by thisprocess.

worker_rlimit_nofile 65535;

#工作模式及连接数上限

events

{

use epoll;

worker_connections 65535;

}

#设定http服务器,利用它的反向代理功能提供负载均衡支持

http

{

#设定mime类型

include       mime.types;

default_type  application/octet-stream;

include /usr/local/nginx/conf/proxy.conf;

#charset  gb2312;

#设定请求缓冲    

server_names_hash_bucket_size 128;

client_header_buffer_size 32k;

large_client_header_buffers 4 32k;

client_max_body_size 8m;

sendfile on;

tcp_nopush     on;

keepalive_timeout 60;

tcp_nodelay on;

#  fastcgi_connect_timeout 300;

#  fastcgi_send_timeout 300;

#  fastcgi_read_timeout 300;

#  fastcgi_buffer_size 64k;

#  fastcgi_buffers 4 64k;

#  fastcgi_busy_buffers_size 128k;

#  fastcgi_temp_file_write_size 128k;

#  gzip on;

#  gzip_min_length  1k;

#  gzip_buffers     4 16k;

#  gzip_http_version 1.0;

#  gzip_comp_level 2;

#  gzip_types       text/plain application/x-javascript text/css application/xml;

#  gzip_vary on;

#limit_zone  crawler  $binary_remote_addr  10m;

###禁止通过ip访问站点

server{

server_name _;

return 404;

}

server

{

listen       80;

server_name  localhost;

index index.html index.htm index.jsp;#设定访问的默认首页地址

root  /home/www/web/ROOT;#设定网站的资源存放路径

#limit_conn   crawler  20;    

location ~ .*.jsp$ #所有jsp的页面均交由tomcat处理

{

index index.jsp;

proxy_pass http://localhost:8080;#转向tomcat处理

}

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ #设定访问静态文件直接读取不经过tomcat

{

expires      30d;

}

location ~ .*\.(js|css)?$

{

expires      1h;

}    

#定义访问日志的写入格式

log_format  access  '$remote_addr - $remote_user [$time_local] "$request" '

'$status $body_bytes_sent "$http_referer" '

'"$http_user_agent" $http_x_forwarded_for';

access_log  /usr/local/nginx/logs/localhost.log access;#设定访问日志的存放路径

}

}

5、修改/usr/local/nginx/conf/nginx.conf配置文件后,请执行以下命令检查配置文件是否正确:

#/usr/local/nginx/sbin/nginx -t

如果屏幕显示以下两行信息,说明配置文件正确:

the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok

  the configuration file /usr/local/nginx/conf/nginx.conf was tested successfully

如果提示unknown host,则可在服务器上执行:ping www.baidu.com如果也是同样提示unknown host则有两种可能:
    a、服务器没有设置DNS服务器地址,查看/etc/resolv.conf下是否设置,若无则加上
    b、防火墙拦截

6、启动nginx的命令

#/usr/local/nginx/sbin/nginx

这时,输入以下命令查看Nginx主进程号:

ps -ef | grep "nginx: master process" | grep -v "grep" | awk -F ' ' '{print $2}'

7、停止nginx的命令

#/usr/local/nginx/sbin/nginx -s stop

8、在不停止Nginx服务的情况下平滑变更Nginx配置
a、修改/usr/local/nginx/conf/nginx.conf配置文件后,请执行以下命令检查配置文件是否正确:

/usr/local/nginx/sbin/nginx -t

  如果屏幕显示以下两行信息,说明配置文件正确:

  the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok

  the configuration file /usr/local/nginx/conf/nginx.conf was tested successfully

b、这时,输入以下命令查看Nginx主进程号:

ps -ef | grep "nginx: master process" | grep -v "grep" | awk -F ' ' '{print $2}'

屏幕显示的即为Nginx主进程号,例如:
  6302
  这时,执行以下命令即可使修改过的Nginx配置文件生效:

kill -HUP 6302

或者无需这么麻烦,找到Nginx的Pid文件:

kill -HUP `cat /usr/local/nginx/nginx.pid`

9、nginx启动好后启动tomcat,此时输入http://主机ip地址即可看到“My web!”

dubbo系列二—应用服务的编译安装

dubbo作为一个成熟RPC业务的框架,如果只是使用,那么我们只需要写一些服务,其他的一切诸如协议,通信等都交由dubbo处理即可。学习我们是从上往下学的,所以系列一 我们介绍其周边运维dubbo-admin,现在介绍一个简单的应用服务的编译安装。

编译

其实对于使用第三方的开源产品,有时候最头疼的就是安装编译,经常会缺点什么,环境这不对,那不对,出了问题因为不了解具体实现会比自己写的东西解决起来麻烦一点。那么在编译dubbo里面的示例,dubbo-demo 的时候碰到的最大问题就是各种依赖包找不到了,最终maven 设置文件中加入:

&lt;mirror&gt; &lt;id&gt;kafeitu&lt;/id&gt; &lt;mirrorOf&gt;central&lt;/mirrorOf&gt; &lt;name&gt;Human Readable Name for this Mirror.&lt;/name&gt; &lt;url&gt;http://maven.kafeitu.me/nexus/content/repositories/public&lt;/url&gt; &lt;/mirror&gt; &lt;mirror&gt; &lt;id&gt;ibiblio.org&lt;/id&gt; &lt;name&gt;ibiblio Mirror of http://repo1.maven.org/maven2/&lt;/name&gt; &lt;url&gt;http://mirrors.ibiblio.org/pub/mirrors/maven2&lt;/url&gt; &lt;mirrorOf&gt;*&lt;/mirrorOf&gt; &lt;/mirror&gt; &lt;mirror&gt; &lt;id&gt;lvu.cn&lt;/id&gt; &lt;name&gt;lvu.cn&lt;/name&gt; &lt;url&gt;http://lvu.cn/nexus/content/groups/public&lt;/url&gt; &lt;mirrorOf&gt;*&lt;/mirrorOf&gt; &lt;/mirror&gt;

才顺利编译好了。

安装

历史的provider 服务跟 costomer 服务消费者都是jar包,其运行原理都是由dubbo里面的一个容器进行统一加载启动起来,当然我们也可以直接如测试中一样自己整个main 函数就把服务启动起来。其运行环境目录如下:

\bin:启动脚本 \conf:配置信息 \lib:包,包括自身及依赖包

还有就是修改下配置,配置成你想要的方式,如可以更换下注册中心等。

后记

有点JAVA基础的,那么其简单的使用及业务服务开发应该就已经学会了(还个数据库模块没放进来,这个放后面吧)。会用了后,接下来我们就来看看dubbo到底如何架构的吧。

web框架基础实现介绍三—servlet

servlet,JAVA中动态服务器编程的核心内容,当然也可以把其他语言实现改响应式的服务都称为servlet,servlet不单独纯在,需要装载在servlet容器context中,这样的容器功能spring,tomcat,jetty都有,当然也可以按接口自己实现个容器吧自己实现的servlet包装起来然后启动。要实现servlet首先来详细深入的了解下servlet吧,这个我也再偷懒转一个吧。

https://www.ibm.com/developerworks/cn/java/j-lo-servlet/

 

从 Servlet 容器说起

要介绍 Servlet 必须要先把 Servlet 容器说清楚,Servlet 与 Servlet 容器的关系有点像枪和子弹的关系,枪是为子弹而生,而子弹又让枪有了杀伤力。虽然它们是彼此依存的,但是又相互独立发展,这一切都是为了适应工业化生产的结果。从技术角度来说是为了解耦,通过标准化接口来相互协作。既然接口是连接 Servlet 与 Servlet 容器的关键,那我们就从它们的接口说起。

前面说了 Servlet 容器作为一个独立发展的标准化产品,目前它的种类很多,但是它们都有自己的市场定位,很难说谁优谁劣,各有特点。例如现在比较流行的 Jetty,在定制化和移动领域有不错的发展,我们这里还是以大家最为熟悉 Tomcat 为例来介绍 Servlet 容器如何管理 Servlet。Tomcat 本身也很复杂,我们只从 Servlet 与 Servlet 容器的接口部分开始介绍,关于 Tomcat 的详细介绍可以参考我的另外一篇文章《 Tomcat 系统架构与模式设计分析》。

Tomcat 的容器等级中,Context 容器是直接管理 Servlet 在容器中的包装类 Wrapper,所以 Context 容器如何运行将直接影响 Servlet 的工作方式。

图 1 . Tomcat 容器模型

图 1 . Tomcat 容器模型

从上图可以看出 Tomcat 的容器分为四个等级,真正管理 Servlet 的容器是 Context 容器,一个 Context 对应一个 Web 工程,在 Tomcat 的配置文件中可以很容易发现这一点,如下:

清单 1 Context 配置参数
 <Context path="/projectOne " docBase="D:\projects\projectOne" 
 reloadable="true" />

下面详细介绍一下 Tomcat 解析 Context 容器的过程,包括如何构建 Servlet 的过程。

Servlet 容器的启动过程

Tomcat7 也开始支持嵌入式功能,增加了一个启动类 org.apache.catalina.startup.Tomcat。创建一个实例对象并调用 start 方法就可以很容易启动 Tomcat,我们还可以通过这个对象来增加和修改 Tomcat 的配置参数,如可以动态增加 Context、Servlet 等。下面我们就利用这个 Tomcat 类来管理新增的一个 Context 容器,我们就选择 Tomcat7 自带的 examples Web 工程,并看看它是如何加到这个 Context 容器中的。

清单 2 . 给 Tomcat 增加一个 Web 工程
 Tomcat tomcat = getTomcatInstance(); 
 File appDir = new File(getBuildDirectory(), "webapps/examples"); 
 tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); 
 tomcat.start(); 
 ByteChunk res = getUrl("http://localhost:" + getPort() + 
               "/examples/servlets/servlet/HelloWorldExample"); 
 assertTrue(res.toString().indexOf("<h1>Hello World!</h1>") > 0);

清单 1 的代码是创建一个 Tomcat 实例并新增一个 Web 应用,然后启动 Tomcat 并调用其中的一个 HelloWorldExample Servlet,看有没有正确返回预期的数据。

Tomcat 的 addWebapp 方法的代码如下:

清单 3 .Tomcat.addWebapp
 public Context addWebapp(Host host, String url, String path) { 
        silence(url); 
        Context ctx = new StandardContext(); 
        ctx.setPath( url ); 
        ctx.setDocBase(path); 
        if (defaultRealm == null) { 
            initSimpleAuth(); 
        } 
        ctx.setRealm(defaultRealm); 
        ctx.addLifecycleListener(new DefaultWebXmlListener()); 
        ContextConfig ctxCfg = new ContextConfig(); 
        ctx.addLifecycleListener(ctxCfg); 
        ctxCfg.setDefaultWebXml("org/apache/catalin/startup/NO_DEFAULT_XML"); 
        if (host == null) { 
            getHost().addChild(ctx); 
        } else { 
            host.addChild(ctx); 
        } 
        return ctx; 
 }

前面已经介绍了一个 Web 应用对应一个 Context 容器,也就是 Servlet 运行时的 Servlet 容器,添加一个 Web 应用时将会创建一个 StandardContext 容器,并且给这个 Context 容器设置必要的参数,url 和 path 分别代表这个应用在 Tomcat 中的访问路径和这个应用实际的物理路径,这个两个参数与清单 1 中的两个参数是一致的。其中最重要的一个配置是 ContextConfig,这个类将会负责整个 Web 应用配置的解析工作,后面将会详细介绍。最后将这个 Context 容器加到父容器 Host 中。

接下去将会调用 Tomcat 的 start 方法启动 Tomcat,如果你清楚 Tomcat 的系统架构,你会容易理解 Tomcat 的启动逻辑,Tomcat 的启动逻辑是基于观察者模式设计的,所有的容器都会继承 Lifecycle 接口,它管理者容器的整个生命周期,所有容器的的修改和状态的改变都会由它去通知已经注册的观察者(Listener),关于这个设计模式可以参考《 Tomcat 的系统架构与设计模式,第二部分:设计模式》。Tomcat 启动的时序图可以用图 2 表示。

图 2. Tomcat 主要类的启动时序图(查看大图

图 2. Tomcat 主要类的启动时序图

上图描述了 Tomcat 启动过程中,主要类之间的时序关系,下面我们将会重点关注添加 examples 应用所对应的 StandardContext 容器的启动过程。

当 Context 容器初始化状态设为 init 时,添加在 Contex 容器的 Listener 将会被调用。ContextConfig 继承了 LifecycleListener 接口,它是在调用清单 3 时被加入到 StandardContext 容器中。ContextConfig 类会负责整个 Web 应用的配置文件的解析工作。

ContextConfig 的 init 方法将会主要完成以下工作:

  1. 创建用于解析 xml 配置文件的 contextDigester 对象
  2. 读取默认 context.xml 配置文件,如果存在解析它
  3. 读取默认 Host 配置文件,如果存在解析它
  4. 读取默认 Context 自身的配置文件,如果存在解析它
  5. 设置 Context 的 DocBase

ContextConfig 的 init 方法完成后,Context 容器的会执行 startInternal 方法,这个方法启动逻辑比较复杂,主要包括如下几个部分:

  1. 创建读取资源文件的对象
  2. 创建 ClassLoader 对象
  3. 设置应用的工作目录
  4. 启动相关的辅助类如:logger、realm、resources 等
  5. 修改启动状态,通知感兴趣的观察者(Web 应用的配置)
  6. 子容器的初始化
  7. 获取 ServletContext 并设置必要的参数
  8. 初始化“load on startup”的 Servlet
Web 应用的初始化工作

Web 应用的初始化工作是在 ContextConfig 的 configureStart 方法中实现的,应用的初始化主要是要解析 web.xml 文件,这个文件描述了一个 Web 应用的关键信息,也是一个 Web 应用的入口。

Tomcat 首先会找 globalWebXml 这个文件的搜索路径是在 engine 的工作目录下寻找以下两个文件中的任一个 org/apache/catalin/startup/NO_DEFAULT_XML 或 conf/web.xml。接着会找 hostWebXml 这个文件可能会在 System.getProperty(“catalina.base”)/conf/${EngineName}/${HostName}/web.xml.default,接着寻找应用的配置文件 examples/WEB-INF/web.xml。web.xml 文件中的各个配置项将会被解析成相应的属性保存在 WebXml 对象中。如果当前应用支持 Servlet3.0,解析还将完成额外 9 项工作,这个额外的 9 项工作主要是为 Servlet3.0 新增的特性,包括 jar 包中的 META-INF/web-fragment.xml 的解析以及对 annotations 的支持。

接下去将会将 WebXml 对象中的属性设置到 Context 容器中,这里包括创建 Servlet 对象、filter、listener 等等。这段代码在 WebXml 的 configureContext 方法中。下面是解析 Servlet 的代码片段:

清单 4. 创建 Wrapper 实例
 for (ServletDef servlet : servlets.values()) { 
            Wrapper wrapper = context.createWrapper(); 
            String jspFile = servlet.getJspFile(); 
            if (jspFile != null) { 
                wrapper.setJspFile(jspFile); 
            } 
            if (servlet.getLoadOnStartup() != null) { 
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); 
            } 
            if (servlet.getEnabled() != null) { 
                wrapper.setEnabled(servlet.getEnabled().booleanValue()); 
            } 
            wrapper.setName(servlet.getServletName()); 
            Map<String,String> params = servlet.getParameterMap(); 
            for (Entry<String, String> entry : params.entrySet()) { 
                wrapper.addInitParameter(entry.getKey(), entry.getValue()); 
            } 
            wrapper.setRunAs(servlet.getRunAs()); 
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); 
            for (SecurityRoleRef roleRef : roleRefs) { 
                wrapper.addSecurityReference( 
                        roleRef.getName(), roleRef.getLink()); 
            } 
            wrapper.setServletClass(servlet.getServletClass()); 
            MultipartDef multipartdef = servlet.getMultipartDef(); 
            if (multipartdef != null) { 
                if (multipartdef.getMaxFileSize() != null && 
                        multipartdef.getMaxRequestSize()!= null && 
                        multipartdef.getFileSizeThreshold() != null) { 
                    wrapper.setMultipartConfigElement(new 
 MultipartConfigElement( 
                            multipartdef.getLocation(), 
                            Long.parseLong(multipartdef.getMaxFileSize()), 
                            Long.parseLong(multipartdef.getMaxRequestSize()), 
                            Integer.parseInt( 
                                    multipartdef.getFileSizeThreshold()))); 
                } else { 
                    wrapper.setMultipartConfigElement(new 
 MultipartConfigElement( 
                            multipartdef.getLocation())); 
                } 
            } 
            if (servlet.getAsyncSupported() != null) { 
                wrapper.setAsyncSupported( 
                        servlet.getAsyncSupported().booleanValue()); 
            } 
            context.addChild(wrapper); 
 }

这段代码清楚的描述了如何将 Servlet 包装成 Context 容器中的 StandardWrapper,这里有个疑问,为什么要将 Servlet 包装成 StandardWrapper 而不直接是 Servlet 对象。这里 StandardWrapper 是 Tomcat 容器中的一部分,它具有容器的特征,而 Servlet 为了一个独立的 web 开发标准,不应该强耦合在 Tomcat 中。

除了将 Servlet 包装成 StandardWrapper 并作为子容器添加到 Context 中,其它的所有 web.xml 属性都被解析到 Context 中,所以说 Context 容器才是真正运行 Servlet 的 Servlet 容器。一个 Web 应用对应一个 Context 容器,容器的配置属性由应用的 web.xml 指定,这样我们就能理解 web.xml 到底起到什么作用了。

回页首

创建 Servlet 实例

前面已经完成了 Servlet 的解析工作,并且被包装成 StandardWrapper 添加在 Context 容器中,但是它仍然不能为我们工作,它还没有被实例化。下面我们将介绍 Servlet 对象是如何创建的,以及如何被初始化的。

创建 Servlet 对象

如果 Servlet 的 load-on-startup 配置项大于 0,那么在 Context 容器启动的时候就会被实例化,前面提到在解析配置文件时会读取默认的 globalWebXml,在 conf 下的 web.xml 文件中定义了一些默认的配置项,其定义了两个 Servlet,分别是:org.apache.catalina.servlets.DefaultServlet 和 org.apache.jasper.servlet.JspServlet 它们的 load-on-startup 分别是 1 和 3,也就是当 Tomcat 启动时这两个 Servlet 就会被启动。

创建 Servlet 实例的方法是从 Wrapper. loadServlet 开始的。loadServlet 方法要完成的就是获取 servletClass 然后把它交给 InstanceManager 去创建一个基于 servletClass.class 的对象。如果这个 Servlet 配置了 jsp-file,那么这个 servletClass 就是 conf/web.xml 中定义的 org.apache.jasper.servlet.JspServlet 了。

创建 Servlet 对象的相关类结构图如下:

图 3. 创建 Servlet 对象的相关类结构

图 3. 创建 Servlet 对象的相关类结构

初始化 Servlet

初始化 Servlet 在 StandardWrapper 的 initServlet 方法中,这个方法很简单就是调用 Servlet 的 init 的方法,同时把包装了 StandardWrapper 对象的 StandardWrapperFacade 作为 ServletConfig 传给 Servlet。Tomcat 容器为何要传 StandardWrapperFacade 给 Servlet 对象将在后面做详细解析。

如果该 Servlet 关联的是一个 jsp 文件,那么前面初始化的就是 JspServlet,接下去会模拟一次简单请求,请求调用这个 jsp 文件,以便编译这个 jsp 文件为 class,并初始化这个 class。

这样 Servlet 对象就初始化完成了,事实上 Servlet 从被 web.xml 中解析到完成初始化,这个过程非常复杂,中间有很多过程,包括各种容器状态的转化引起的监听事件的触发、各种访问权限的控制和一些不可预料的错误发生的判断行为等等。我们这里只抓了一些关键环节进行阐述,试图让大家有个总体脉络。

下面是这个过程的一个完整的时序图,其中也省略了一些细节。

图 4. 初始化 Servlet 的时序图(查看大图

图 4. 初始化 Servlet 的时序图

回页首

Servlet 体系结构

我们知道 Java Web 应用是基于 Servlet 规范运转的,那么 Servlet 本身又是如何运转的呢?为何要设计这样的体系结构。

图 5.Servlet 顶层类关联图

图 5.Servlet 顶层类关联图

从上图可以看出 Servlet 规范就是基于这几个类运转的,与 Servlet 主动关联的是三个类,分别是 ServletConfig、ServletRequest 和 ServletResponse。这三个类都是通过容器传递给 Servlet 的,其中 ServletConfig 是在 Servlet 初始化时就传给 Servlet 了,而后两个是在请求达到时调用 Servlet 时传递过来的。我们很清楚 ServletRequest 和 ServletResponse 在 Servlet 运行的意义,但是 ServletConfig 和 ServletContext 对 Servlet 有何价值?仔细查看 ServletConfig 接口中声明的方法发现,这些方法都是为了获取这个 Servlet 的一些配置属性,而这些配置属性可能在 Servlet 运行时被用到。而 ServletContext 又是干什么的呢? Servlet 的运行模式是一个典型的“握手型的交互式”运行模式。所谓“握手型的交互式”就是两个模块为了交换数据通常都会准备一个交易场景,这个场景一直跟随个这个交易过程直到这个交易完成为止。这个交易场景的初始化是根据这次交易对象指定的参数来定制的,这些指定参数通常就会是一个配置类。所以对号入座,交易场景就由 ServletContext 来描述,而定制的参数集合就由 ServletConfig 来描述。而 ServletRequest 和 ServletResponse 就是要交互的具体对象了,它们通常都是作为运输工具来传递交互结果。

ServletConfig 是在 Servlet init 时由容器传过来的,那么 ServletConfig 到底是个什么对象呢?

下图是 ServletConfig 和 ServletContext 在 Tomcat 容器中的类关系图。

图 6. ServletConfig 在容器中的类关联图

图 6. ServletConfig 在容器中的类关联图

上图可以看出 StandardWrapper 和 StandardWrapperFacade 都实现了 ServletConfig 接口,而 StandardWrapperFacade 是 StandardWrapper 门面类。所以传给 Servlet 的是 StandardWrapperFacade 对象,这个类能够保证从 StandardWrapper 中拿到 ServletConfig 所规定的数据,而又不把 ServletConfig 不关心的数据暴露给 Servlet。

同样 ServletContext 也与 ServletConfig 有类似的结构,Servlet 中能拿到的 ServletContext 的实际对象也是 ApplicationContextFacade 对象。ApplicationContextFacade 同样保证 ServletContex 只能从容器中拿到它该拿的数据,它们都起到对数据的封装作用,它们使用的都是门面设计模式。

通过 ServletContext 可以拿到 Context 容器中一些必要信息,比如应用的工作路径,容器支持的 Servlet 最小版本等。

Servlet 中定义的两个 ServletRequest 和 ServletResponse 它们实际的对象又是什么呢?,我们在创建自己的 Servlet 类时通常使用的都是 HttpServletRequest 和 HttpServletResponse,它们继承了 ServletRequest 和 ServletResponse。为何 Context 容器传过来的 ServletRequest、ServletResponse 可以被转化为 HttpServletRequest 和 HttpServletResponse 呢?

图 7.Request 相关类结构图

图 7.Request 相关类结构图

上图是 Tomcat 创建的 Request 和 Response 的类结构图。Tomcat 一接受到请求首先将会创建 org.apache.coyote.Request 和 org.apache.coyote.Response,这两个类是 Tomcat 内部使用的描述一次请求和相应的信息类它们是一个轻量级的类,它们作用就是在服务器接收到请求后,经过简单解析将这个请求快速的分配给后续线程去处理,所以它们的对象很小,很容易被 JVM 回收。接下去当交给一个用户线程去处理这个请求时又创建 org.apache.catalina.connector. Request 和 org.apache.catalina.connector. Response 对象。这两个对象一直穿越整个 Servlet 容器直到要传给 Servlet,传给 Servlet 的是 Request 和 Response 的门面类 RequestFacade 和 RequestFacade,这里使用门面模式与前面一样都是基于同样的目的——封装容器中的数据。一次请求对应的 Request 和 Response 的类转化如下图所示:

图 8.Request 和 Response 的转变过程

图 8.Request 和 Response 的转变过程

回页首

Servlet 如何工作

我们已经清楚了 Servlet 是如何被加载的、Servlet 是如何被初始化的,以及 Servlet 的体系结构,现在的问题就是它是如何被调用的。

当用户从浏览器向服务器发起一个请求,通常会包含如下信息:http://hostname: port /contextpath/servletpath,hostname 和 port 是用来与服务器建立 TCP 连接,而后面的 URL 才是用来选择服务器中那个子容器服务用户的请求。那服务器是如何根据这个 URL 来达到正确的 Servlet 容器中的呢?

Tomcat7.0 中这件事很容易解决,因为这种映射工作有专门一个类来完成的,这个就是 org.apache.tomcat.util.http.mapper,这个类保存了 Tomcat 的 Container 容器中的所有子容器的信息,当 org.apache.catalina.connector. Request 类在进入 Container 容器之前,mapper 将会根据这次请求的 hostnane 和 contextpath 将 host 和 context 容器设置到 Request 的 mappingData 属性中。所以当 Request 进入 Container 容器之前,它要访问那个子容器这时就已经确定了。

图 9.Request 的 Mapper 类关系图

图 9.Request 的 Mapper 类关系图

可能你有疑问,mapper 中怎么会有容器的完整关系,这要回到图 2 中 19 步 MapperListener 类的初始化过程,下面是 MapperListener 的 init 方法代码 :

清单 5. MapperListener.init
 public void init() { 
        findDefaultHost(); 
        Engine engine = (Engine) connector.getService().getContainer(); 
        engine.addContainerListener(this); 
        Container[] conHosts = engine.findChildren(); 
        for (Container conHost : conHosts) { 
            Host host = (Host) conHost; 
            if (!LifecycleState.NEW.equals(host.getState())) { 
                host.addLifecycleListener(this); 
                registerHost(host); 
            } 
        } 
 }

这段代码的作用就是将 MapperListener 类作为一个监听者加到整个 Container 容器中的每个子容器中,这样只要任何一个容器发生变化,MapperListener 都将会被通知,相应的保存容器关系的 MapperListener 的 mapper 属性也会修改。for 循环中就是将 host 及下面的子容器注册到 mapper 中。

图 10.Request 在容器中的路由图

图 10.Request 在容器中的路由图

上图描述了一次 Request 请求是如何达到最终的 Wrapper 容器的,我们现正知道了请求是如何达到正确的 Wrapper 容器,但是请求到达最终的 Servlet 还要完成一些步骤,必须要执行 Filter 链,以及要通知你在 web.xml 中定义的 listener。

接下去就要执行 Servlet 的 service 方法了,通常情况下,我们自己定义的 servlet 并不是直接去实现 javax.servlet.servlet 接口,而是去继承更简单的 HttpServlet 类或者 GenericServlet 类,我们可以有选择的覆盖相应方法去实现我们要完成的工作。

Servlet 的确已经能够帮我们完成所有的工作了,但是现在的 web 应用很少有直接将交互全部页面都用 servlet 来实现,而是采用更加高效的 MVC 框架来实现。这些 MVC 框架基本的原理都是将所有的请求都映射到一个 Servlet,然后去实现 service 方法,这个方法也就是 MVC 框架的入口。

当 Servlet 从 Servlet 容器中移除时,也就表明该 Servlet 的生命周期结束了,这时 Servlet 的 destroy 方法将被调用,做一些扫尾工作。

回页首

Session 与 Cookie

前面我们已经说明了 Servlet 如何被调用,我们基于 Servlet 来构建应用程序,那么我们能从 Servlet 获得哪些数据信息呢?

Servlet 能够给我们提供两部分数据,一个是在 Servlet 初始化时调用 init 方法时设置的 ServletConfig,这个类基本上含有了 Servlet 本身和 Servlet 所运行的 Servlet 容器中的基本信息。根据前面的介绍 ServletConfig 的实际对象是 StandardWrapperFacade,到底能获得哪些容器信息可以看看这类提供了哪些接口。还有一部分数据是由 ServletRequest 类提供,它的实际对象是 RequestFacade,从提供的方法中发现主要是描述这次请求的 HTTP 协议的信息。所以要掌握 Servlet 的工作方式必须要很清楚 HTTP 协议,如果你还不清楚赶紧去找一些参考资料。关于这一块还有一个让很多人迷惑的 Session 与 Cookie。

Session 与 Cookie 不管是对 Java Web 的熟练使用者还是初学者来说都是一个令人头疼的东西。Session 与 Cookie 的作用都是为了保持访问用户与后端服务器的交互状态。它们有各自的优点也有各自的缺陷。然而具有讽刺意味的是它们优点和它们的使用场景又是矛盾的,例如使用 Cookie 来传递信息时,随着 Cookie 个数的增多和访问量的增加,它占用的网络带宽也很大,试想假如 Cookie 占用 200 个字节,如果一天的 PV 有几亿的时候,它要占用多少带宽。所以大访问量的时候希望用 Session,但是 Session 的致命弱点是不容易在多台服务器之间共享,所以这也限制了 Session 的使用。

不管 Session 和 Cookie 有什么不足,我们还是要用它们。下面详细讲一下,Session 如何基于 Cookie 来工作。实际上有三种方式能可以让 Session 正常工作:

  1. 基于 URL Path Parameter,默认就支持
  2. 基于 Cookie,如果你没有修改 Context 容器个 cookies 标识的话,默认也是支持的
  3. 基于 SSL,默认不支持,只有 connector.getAttribute(“SSLEnabled”) 为 TRUE 时才支持

第一种情况下,当浏览器不支持 Cookie 功能时,浏览器会将用户的 SessionCookieName 重写到用户请求的 URL 参数中,它的传递格式如 /path/Servlet;name=value;name2=value2? Name3=value3,其中“Servlet;”后面的 K-V 对就是要传递的 Path Parameters,服务器会从这个 Path Parameters 中拿到用户配置的 SessionCookieName。关于这个 SessionCookieName,如果你在 web.xml 中配置 session-config 配置项的话,其 cookie-config 下的 name 属性就是这个 SessionCookieName 值,如果你没有配置 session-config 配置项,默认的 SessionCookieName 就是大家熟悉的“JSESSIONID”。接着 Request 根据这个 SessionCookieName 到 Parameters 拿到 Session ID 并设置到 request.setRequestedSessionId 中。

请注意如果客户端也支持 Cookie 的话,Tomcat 仍然会解析 Cookie 中的 Session ID,并会覆盖 URL 中的 Session ID。

如果是第三种情况的话将会根据 javax.servlet.request.ssl_session 属性值设置 Session ID。

有了 Session ID 服务器端就可以创建 HttpSession 对象了,第一次触发是通过 request. getSession() 方法,如果当前的 Session ID 还没有对应的 HttpSession 对象那么就创建一个新的,并将这个对象加到 org.apache.catalina. Manager 的 sessions 容器中保存,Manager 类将管理所有 Session 的生命周期,Session 过期将被回收,服务器关闭,Session 将被序列化到磁盘等。只要这个 HttpSession 对象存在,用户就可以根据 Session ID 来获取到这个对象,也就达到了状态的保持。

图 11.Session 相关类图

图 11.Session 相关类图

上从图中可以看出从 request.getSession 中获取的 HttpSession 对象实际上是 StandardSession 对象的门面对象,这与前面的 Request 和 Servlet 是一样的原理。下图是 Session 工作的时序图:

图 12.Session 工作的时序图(查看大图

图 12.Session 工作的时序图

还有一点与 Session 关联的 Cookie 与其它 Cookie 没有什么不同,这个配置的配置可以通过 web.xml 中的 session-config 配置项来指定。

回页首

Servlet 中的 Listener

整个 Tomcat 服务器中 Listener 使用的非常广泛,它是基于观察者模式设计的,Listener 的设计对开发 Servlet 应用程序提供了一种快捷的手段,能够方便的从另一个纵向维度控制程序和数据。目前 Servlet 中提供了 5 种两类事件的观察者接口,它们分别是:4 个 EventListeners 类型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 个 LifecycleListeners 类型的,ServletContextListener、HttpSessionListener。如下图所示:

图 13.Servlet 中的 Listener(查看大图

图 13.Servlet 中的 Listener

它们基本上涵盖了整个 Servlet 生命周期中,你感兴趣的每种事件。这些 Listener 的实现类可以配置在 web.xml 中的 <listener> 标签中。当然也可以在应用程序中动态添加 Listener,需要注意的是 ServletContextListener 在容器启动之后就不能再添加新的,因为它所监听的事件已经不会再出现。掌握这些 Listener 的使用,能够让我们的程序设计的更加灵活。

web框架基础实现介绍三—Tomcat容器

介绍一吧容器类的作用位置说的比较清晰了,主要作为动态内容的服务器容器,现在就把其中的Tomcat 的具体实现组成说下,这个比较多,我就偷个懒,转帖一篇吧!

http://www.ibm.com/developerworks/cn/java/j-lo-tomcat1/

本文以 Tomcat 5 为基础,也兼顾最新的 Tomcat 6 和 Tomcat 4。Tomcat 的基本设计思路和架构是具有一定连续性的。

Tomcat 总体结构

Tomcat 的结构很复杂,但是 Tomcat 也非常的模块化,找到了 Tomcat 最核心的模块,您就抓住了 Tomcat 的“七寸”。下面是 Tomcat 的总体结构图:

图 1.Tomcat 的总体结构

图 1.Tomcat 的总体结构

从上图中可以看出 Tomcat 的心脏是两个组件:Connector 和 Container,关于这两个组件将在后面详细介绍。Connector 组件是可以被替换,这样可以提供给服务器设计者更多的选择,因为这个组件是如此重要,不仅跟服务器的设计的本身,而且和不同的应用场景也十分相关,所以一个 Container 可以选择对应多个 Connector。多个 Connector 和一个 Container 就形成了一个 Service,Service 的概念大家都很熟悉了,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了。所以整个 Tomcat 的生命周期由 Server 控制。

以 Service 作为“婚姻”

我们将 Tomcat 中 Connector、Container 作为一个整体比作一对情侣的话,Connector 主要负责对外交流,可以比作为 Boy,Container 主要处理 Connector 接受的请求,主要是处理内部事务,可以比作为 Girl。那么这个 Service 就是连接这对男女的结婚证了。是 Service 将它们连接在一起,共同组成一个家庭。当然要组成一个家庭还要很多其它的元素。

说白了,Service 只是在 Connector 和 Container 外面多包一层,把它们组装在一起,向外面提供服务,一个 Service 可以设置多个 Connector,但是只能有一个 Container 容器。这个 Service 接口的方法列表如下:

图 2. Service 接口

图 2. Service 接口

从 Service 接口中定义的方法中可以看出,它主要是为了关联 Connector 和 Container,同时会初始化它下面的其它组件,注意接口中它并没有规定一定要控制它下面的组件的生命周期。所有组件的生命周期在一个 Lifecycle 的接口中控制,这里用到了一个重要的设计模式,关于这个接口将在后面介绍。

Tomcat 中 Service 接口的标准实现类是 StandardService 它不仅实现了 Service 借口同时还实现了 Lifecycle 接口,这样它就可以控制它下面的组件的生命周期了。StandardService 类结构图如下:

图 3. StandardService 的类结构图

图 3. StandardService 的类结构图

从上图中可以看出除了 Service 接口的方法的实现以及控制组件生命周期的 Lifecycle 接口的实现,还有几个方法是用于在事件监听的方法的实现,不仅是这个 Service 组件,Tomcat 中其它组件也同样有这几个方法,这也是一个典型的设计模式,将在后面介绍。

下面看一下 StandardService 中主要的几个方法实现的代码,下面是 setContainer 和 addConnector 方法的源码:

清单 1. StandardService. SetContainer
public void setContainer(Container container) {
    Container oldContainer = this.container;
    if ((oldContainer != null) && (oldContainer instanceof Engine))
        ((Engine) oldContainer).setService(null);
    this.container = container;
    if ((this.container != null) && (this.container instanceof Engine))
        ((Engine) this.container).setService(this);
    if (started && (this.container != null) && (this.container instanceof Lifecycle)) {
        try {
            ((Lifecycle) this.container).start();
        } catch (LifecycleException e) {
            ;
        }
    }
    synchronized (connectors) {
        for (int i = 0; i < connectors.length; i++)
            connectors[i].setContainer(this.container);
    }
    if (started && (oldContainer != null) && (oldContainer instanceof Lifecycle)) {
        try {
            ((Lifecycle) oldContainer).stop();
        } catch (LifecycleException e) {
            ;
        }
    }
    support.firePropertyChange("container", oldContainer, this.container);
}

这段代码很简单,其实就是先判断当前的这个 Service 有没有已经关联了 Container,如果已经关联了,那么去掉这个关联关系—— oldContainer.setService(null)。如果这个 oldContainer 已经被启动了,结束它的生命周期。然后再替换新的关联、再初始化并开始这个新的 Container 的生命周期。最后将这个过程通知感兴趣的事件监听程序。这里值得注意的地方就是,修改 Container 时要将新的 Container 关联到每个 Connector,还好 Container 和 Connector 没有双向关联,不然这个关联关系将会很难维护。

清单 2. StandardService. addConnector
public void addConnector(Connector connector) {
    synchronized (connectors) {
        connector.setContainer(this.container);
        connector.setService(this);
        Connector results[] = new Connector[connectors.length + 1];
        System.arraycopy(connectors, 0, results, 0, connectors.length);
        results[connectors.length] = connector;
        connectors = results;
        if (initialized) {
            try {
                connector.initialize();
            } catch (LifecycleException e) {
                e.printStackTrace(System.err);
            }
        }
        if (started && (connector instanceof Lifecycle)) {
            try {
                ((Lifecycle) connector).start();
            } catch (LifecycleException e) {
                ;
            }
        }
        support.firePropertyChange("connector", null, connector);
    }
}

上面是 addConnector 方法,这个方法也很简单,首先是设置关联关系,然后是初始化工作,开始新的生命周期。这里值得一提的是,注意 Connector 用的是数组而不是 List 集合,这个从性能角度考虑可以理解,有趣的是这里用了数组但是并没有向我们平常那样,一开始就分配一个固定大小的数组,它这里的实现机制是:重新创建一个当前大小的数组对象,然后将原来的数组对象 copy 到新的数组中,这种方式实现了类似的动态数组的功能,这种实现方式,值得我们以后拿来借鉴。

最新的 Tomcat6 中 StandardService 也基本没有变化,但是从 Tomcat5 开始 Service、Server 和容器类都继承了 MBeanRegistration 接口,Mbeans 的管理更加合理。

以 Server 为“居”

前面说一对情侣因为 Service 而成为一对夫妻,有了能够组成一个家庭的基本条件,但是它们还要有个实体的家,这是它们在社会上生存之本,有了家它们就可以安心的为人民服务了,一起为社会创造财富。

Server 要完成的任务很简单,就是要能够提供一个接口让其它程序能够访问到这个 Service 集合、同时要维护它所包含的所有 Service 的生命周期,包括如何初始化、如何结束服务、如何找到别人要访问的 Service。还有其它的一些次要的任务,如您住在这个地方要向当地政府去登记啊、可能还有要配合当地公安机关日常的安全检查什么的。

Server 的类结构图如下:

图 4. Server 的类结构图

图 4. Server 的类结构图

它的标准实现类 StandardServer 实现了上面这些方法,同时也实现了 Lifecycle、MbeanRegistration 两个接口的所有方法,下面主要看一下 StandardServer 重要的一个方法 addService 的实现:

清单 3. StandardServer.addService
public void addService(Service service) {
    service.setServer(this);
    synchronized (services) {
        Service results[] = new Service[services.length + 1];
        System.arraycopy(services, 0, results, 0, services.length);
        results[services.length] = service;
        services = results;
        if (initialized) {
            try {
                service.initialize();
            } catch (LifecycleException e) {
                e.printStackTrace(System.err);
            }
        }
        if (started && (service instanceof Lifecycle)) {
            try {
                ((Lifecycle) service).start();
            } catch (LifecycleException e) {
                ;
            }
        }
        support.firePropertyChange("service", null, service);
    }
}

从上面第一句就知道了 Service 和 Server 是相互关联的,Server 也是和 Service 管理 Connector 一样管理它,也是将 Service 放在一个数组中,后面部分的代码也是管理这个新加进来的 Service 的生命周期。Tomcat6 中也是没有什么变化的。

组件的生命线“Lifecycle”

前面一直在说 Service 和 Server 管理它下面组件的生命周期,那它们是如何管理的呢?

Tomcat 中组件的生命周期是通过 Lifecycle 接口来控制的,组件只要继承这个接口并实现其中的方法就可以统一被拥有它的组件控制了,这样一层一层的直到一个最高级的组件就可以控制 Tomcat 中所有组件的生命周期,这个最高的组件就是 Server,而控制 Server 的是 Startup,也就是您启动和关闭 Tomcat。

下面是 Lifecycle 接口的类结构图:

图 5. Lifecycle 类结构图

图 5. Lifecycle 类结构图

除了控制生命周期的 Start 和 Stop 方法外还有一个监听机制,在生命周期开始和结束的时候做一些额外的操作。这个机制在其它的框架中也被使用,如在 Spring 中。关于这个设计模式会在后面介绍。

Lifecycle 接口的方法的实现都在其它组件中,就像前面中说的,组件的生命周期由包含它的父组件控制,所以它的 Start 方法自然就是调用它下面的组件的 Start 方法,Stop 方法也是一样。如在 Server 中 Start 方法就会调用 Service 组件的 Start 方法,Server 的 Start 方法代码如下:

清单 4. StandardServer.Start
public void start() throws LifecycleException {
    if (started) {
        log.debug(sm.getString("standardServer.start.started"));
        return;
    }
    lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
    lifecycle.fireLifecycleEvent(START_EVENT, null);
    started = true;
    synchronized (services) {
        for (int i = 0; i < services.length; i++) {
            if (services[i] instanceof Lifecycle)
                ((Lifecycle) services[i]).start();
        }
    }
    lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);
}

监听的代码会包围 Service 组件的启动过程,就是简单的循环启动所有 Service 组件的 Start 方法,但是所有 Service 必须要实现 Lifecycle 接口,这样做会更加灵活。

Server 的 Stop 方法代码如下:

清单 5. StandardServer.Stop
public void stop() throws LifecycleException {
    if (!started)
        return;
    lifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null);
    lifecycle.fireLifecycleEvent(STOP_EVENT, null);
    started = false;
    for (int i = 0; i < services.length; i++) {
        if (services[i] instanceof Lifecycle)
            ((Lifecycle) services[i]).stop();
    }
    lifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null);
}

它所要做的事情也和 Start 方法差不多。

回页首

Connector 组件

Connector 组件是 Tomcat 中两个核心组件之一,它的主要任务是负责接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和 Response 对象分别用于和请求端交换数据,然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程,处理这个请求的线程就是 Container 组件要做的事了。

由于这个过程比较复杂,大体的流程可以用下面的顺序图来解释:

图 6. Connector 处理一次请求顺序图

图 6. Connector 处理一次请求顺序图

查看清晰大图

Tomcat5 中默认的 Connector 是 Coyote,这个 Connector 是可以选择替换的。Connector 最重要的功能就是接收连接请求然后分配线程让 Container 来处理这个请求,所以这必然是多线程的,多线程的处理是 Connector 设计的核心。Tomcat5 将这个过程更加细化,它将 Connector 划分成 Connector、Processor、Protocol, 另外 Coyote 也定义自己的 Request 和 Response 对象。

下面主要看一下 Tomcat 中如何处理多线程的连接请求,先看一下 Connector 的主要类图:

图 7. Connector 的主要类图

图 7. Connector 的主要类图

查看清晰大图

看一下 HttpConnector 的 Start 方法:

清单 6. HttpConnector.Start
public void start() throws LifecycleException {
    if (started)
        throw new LifecycleException
            (sm.getString("httpConnector.alreadyStarted"));
    threadName = "HttpConnector[" + port + "]";
    lifecycle.fireLifecycleEvent(START_EVENT, null);
    started = true;
    threadStart();
    while (curProcessors < minProcessors) {
        if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
            break;
        HttpProcessor processor = newProcessor();
        recycle(processor);
    }
}

threadStart() 执行就会进入等待请求的状态,直到一个新的请求到来才会激活它继续执行,这个激活是在 HttpProcessor 的 assign 方法中,这个方法是代码如下

清单 7. HttpProcessor.assign
synchronized void assign(Socket socket) {
    while (available) {
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }
    this.socket = socket;
    available = true;
    notifyAll();
    if ((debug >= 1) && (socket != null))
        log(" An incoming request is being assigned");
}

创建 HttpProcessor 对象是会把 available 设为 false,所以当请求到来时不会进入 while 循环,将请求的 socket 赋给当期处理的 socket,并将 available 设为 true,当 available 设为 true 是 HttpProcessor 的 run 方法将被激活,接下去将会处理这次请求。

Run 方法代码如下:

清单 8. HttpProcessor.Run
public void run() { 
    while (!stopped) { 
        Socket socket = await(); 
        if (socket == null) 
            continue; 
        try { 
            process(socket); 
        } catch (Throwable t) { 
            log("process.invoke", t); 
        } 
        connector.recycle(this); 
    } 
    synchronized (threadSync) { 
        threadSync.notifyAll(); 
    } 
}

解析 socket 的过程在 process 方法中,process 方法的代码片段如下:

清单 9. HttpProcessor.process
 private void process(Socket socket) {
    boolean ok = true;
    boolean finishResponse = true;
    SocketInputStream input = null;
    OutputStream output = null;
    try {
        input = new SocketInputStream(socket.getInputStream(),connector.getBufferSize());
    } catch (Exception e) {
        log("process.create", e);
        ok = false;
    }
    keepAlive = true;
    while (!stopped && ok && keepAlive) {
        finishResponse = true;
        try {
            request.setStream(input);
            request.setResponse(response);
            output = socket.getOutputStream();
            response.setStream(output);
            response.setRequest(request);
            ((HttpServletResponse) response.getResponse())
				.setHeader("Server", SERVER_INFO);
        } catch (Exception e) {
            log("process.create", e);
            ok = false;
        }
        try {
            if (ok) {
                parseConnection(socket);
                parseRequest(input, output);
                if (!request.getRequest().getProtocol().startsWith("HTTP/0"))
                    parseHeaders(input);
                if (http11) {
                    ackRequest(output);
                    if (connector.isChunkingAllowed())
                        response.setAllowChunking(true);
                }
            }
        。。。。。。
        try {
            ((HttpServletResponse) response).setHeader
                ("Date", FastHttpDateFormat.getCurrentDate());
            if (ok) {
                connector.getContainer().invoke(request, response);
            }
            。。。。。。
        }
        try {
            shutdownInput(input);
            socket.close();
        } catch (IOException e) {
            ;
        } catch (Throwable e) {
            log("process.invoke", e);
        }
    socket = null;
}

当 Connector 将 socket 连接封装成 request 和 response 对象后接下来的事情就交给 Container 来处理了。

回页首

Servlet 容器“Container”

Container 是容器的父接口,所有子容器都必须实现这个接口,Container 容器的设计用的是典型的责任链的设计模式,它有四个子容器组件构成,分别是:Engine、Host、Context、Wrapper,这四个组件不是平行的,而是父子关系,Engine 包含 Host,Host 包含 Context,Context 包含 Wrapper。通常一个 Servlet class 对应一个 Wrapper,如果有多个 Servlet 就可以定义多个 Wrapper,如果有多个 Wrapper 就要定义一个更高的 Container 了,如 Context,Context 通常就是对应下面这个配置:

清单 10. Server.xml
<Context 
    path="/library"
    docBase="D:\projects\library\deploy\target\library.war" 
    reloadable="true"
/>
容器的总体设计

Context 还可以定义在父容器 Host 中,Host 不是必须的,但是要运行 war 程序,就必须要 Host,因为 war 中必有 web.xml 文件,这个文件的解析就需要 Host 了,如果要有多个 Host 就要定义一个 top 容器 Engine 了。而 Engine 没有父容器了,一个 Engine 代表一个完整的 Servlet 引擎。

那么这些容器是如何协同工作的呢?先看一下它们之间的关系图:

图 8. 四个容器的关系图

图 8. 四个容器的关系图

查看清晰大图

当 Connector 接受到一个连接请求时,将请求交给 Container,Container 是如何处理这个请求的?这四个组件是怎么分工的,怎么把请求传给特定的子容器的呢?又是如何将最终的请求交给 Servlet 处理。下面是这个过程的时序图:

图 9. Engine 和 Host 处理请求的时序图

图 9. Engine 和 Host 处理请求的时序图

查看清晰大图

这里看到了 Valve 是不是很熟悉,没错 Valve 的设计在其他框架中也有用的,同样 Pipeline 的原理也基本是相似的,它是一个管道,Engine 和 Host 都会执行这个 Pipeline,您可以在这个管道上增加任意的 Valve,Tomcat 会挨个执行这些 Valve,而且四个组件都会有自己的一套 Valve 集合。您怎么才能定义自己的 Valve 呢?在 server.xml 文件中可以添加,如给 Engine 和 Host 增加一个 Valve 如下:

清单 11. Server.xml
<Engine defaultHost="localhost" name="Catalina">

    <Valve className="org.apache.catalina.valves.RequestDumperValve"/>
    ………
    <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" 
	    xmlNamespaceAware="false" xmlValidation="false">

        <Valve className="org.apache.catalina.valves.FastCommonAccessLogValve"
            directory="logs"  prefix="localhost_access_log." suffix=".txt"
            pattern="common" resolveHosts="false"/>	   
    …………
    </Host>
</Engine>

StandardEngineValve 和 StandardHostValve 是 Engine 和 Host 的默认的 Valve,它们是最后一个 Valve 负责将请求传给它们的子容器,以继续往下执行。

前面是 Engine 和 Host 容器的请求过程,下面看 Context 和 Wrapper 容器时如何处理请求的。下面是处理请求的时序图:

图 10. Context 和 wrapper 的处理请求时序图

图 10. Context 和 wrapper 的处理请求时序图

查看清晰大图

从 Tomcat5 开始,子容器的路由放在了 request 中,request 中保存了当前请求正在处理的 Host、Context 和 wrapper。

Engine 容器

Engine 容器比较简单,它只定义了一些基本的关联关系,接口类图如下:

图 11. Engine 接口的类结构

图 11. Engine 接口的类结构

它的标准实现类是 StandardEngine,这个类注意一点就是 Engine 没有父容器了,如果调用 setParent 方法时将会报错。添加子容器也只能是 Host 类型的,代码如下:

清单 12. StandardEngine. addChild
public void addChild(Container child) {
    if (!(child instanceof Host))
        throw new IllegalArgumentException
            (sm.getString("standardEngine.notHost"));
    super.addChild(child);
}

public void setParent(Container container) {
    throw new IllegalArgumentException
        (sm.getString("standardEngine.notParent"));
}

它的初始化方法也就是初始化和它相关联的组件,以及一些事件的监听。

Host 容器

Host 是 Engine 的字容器,一个 Host 在 Engine 中代表一个虚拟主机,这个虚拟主机的作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context,它除了关联子容器外,还有就是保存一个主机应该有的信息。

下面是和 Host 相关的类关联图:

图 12. Host 相关的类图

图 12. Host 相关的类图

查看清晰大图

从上图中可以看出除了所有容器都继承的 ContainerBase 外,StandardHost 还实现了 Deployer 接口,上图清楚的列出了这个接口的主要方法,这些方法都是安装、展开、启动和结束每个 web application。

Deployer 接口的实现是 StandardHostDeployer,这个类实现了的最要的几个方法,Host 可以调用这些方法完成应用的部署等。

Context 容器

Context 代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat 可以没有 Engine 和 Host。

Context 最重要的功能就是管理它里面的 Servlet 实例,Servlet 实例在 Context 中是以 Wrapper 出现的,还有一点就是 Context 如何才能找到正确的 Servlet 来执行它呢? Tomcat5 以前是通过一个 Mapper 类来管理的,Tomcat5 以后这个功能被移到了 request 中,在前面的时序图中就可以发现获取子容器都是通过 request 来分配的。

Context 准备 Servlet 的运行环境是在 Start 方法开始的,这个方法的代码片段如下:

清单 13. StandardContext.start
public synchronized void start() throws LifecycleException {
    ………
    if( !initialized ) { 
        try {
            init();
        } catch( Exception ex ) {
            throw new LifecycleException("Error initializaing ", ex);
        }
    }
    
	………
    lifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);
    setAvailable(false);
    setConfigured(false);
    boolean ok = true;
    File configBase = getConfigBase();
    if (configBase != null) {
        if (getConfigFile() == null) {
            File file = new File(configBase, getDefaultConfigFile());
            setConfigFile(file.getPath());
            try {
                File appBaseFile = new File(getAppBase());
                if (!appBaseFile.isAbsolute()) {
                    appBaseFile = new File(engineBase(), getAppBase());
                }
                String appBase = appBaseFile.getCanonicalPath();
                String basePath = 
                    (new File(getBasePath())).getCanonicalPath();
                if (!basePath.startsWith(appBase)) {
                    Server server = ServerFactory.getServer();
                    ((StandardServer) server).storeContext(this);
                }
            } catch (Exception e) {
                log.warn("Error storing config file", e);
            }
        } else {
            try {
                String canConfigFile =  (new File(getConfigFile())).getCanonicalPath();
                if (!canConfigFile.startsWith (configBase.getCanonicalPath())) {
                    File file = new File(configBase, getDefaultConfigFile());
                    if (copy(new File(canConfigFile), file)) {
                        setConfigFile(file.getPath());
                    }
                }
            } catch (Exception e) {
                log.warn("Error setting config file", e);
            }
        }
    }

    ………
    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();
    ………

}

它主要是设置各种资源属性和管理组件,还有非常重要的就是启动子容器和 Pipeline。

我们知道 Context 的配置文件中有个 reloadable 属性,如下面配置:

清单 14. Server.xml
<Context 
    path="/library" 
    docBase="D:\projects\library\deploy\target\library.war" 
    reloadable="true" 
/>

当这个 reloadable 设为 true 时,war 被修改后 Tomcat 会自动的重新加载这个应用。如何做到这点的呢 ? 这个功能是在 StandardContext 的 backgroundProcess 方法中实现的,这个方法的代码如下:

清单 15. StandardContext. backgroundProcess
public void backgroundProcess() {
    if (!started) return;
    count = (count + 1) % managerChecksFrequency;
    if ((getManager() != null) && (count == 0)) {
        try {
            getManager().backgroundProcess();
        } catch ( Exception x ) {
            log.warn("Unable to perform background process on manager",x);
        }
    }
    if (getLoader() != null) {
        if (reloadable && (getLoader().modified())) {
            try {
                Thread.currentThread().setContextClassLoader
                    (StandardContext.class.getClassLoader());
                reload();
            } finally {
                if (getLoader() != null) {
                    Thread.currentThread().setContextClassLoader
                        (getLoader().getClassLoader());
                }
            }
        }
        if (getLoader() instanceof WebappLoader) {
            ((WebappLoader) getLoader()).closeJARs(false);
        }
    }
}

它会调用 reload 方法,而 reload 方法会先调用 stop 方法然后再调用 Start 方法,完成 Context 的一次重新加载。可以看出执行 reload 方法的条件是 reloadable 为 true 和应用被修改,那么这个 backgroundProcess 方法是怎么被调用的呢?

这个方法是在 ContainerBase 类中定义的内部类 ContainerBackgroundProcessor 被周期调用的,这个类是运行在一个后台线程中,它会周期的执行 run 方法,它的 run 方法会周期调用所有容器的 backgroundProcess 方法,因为所有容器都会继承 ContainerBase 类,所以所有容器都能够在 backgroundProcess 方法中定义周期执行的事件。

Wrapper 容器

Wrapper 代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

Wrapper 的实现类是 StandardWrapper,StandardWrapper 还实现了拥有一个 Servlet 初始化信息的 ServletConfig,由此看出 StandardWrapper 将直接和 Servlet 的各种信息打交道。

下面看一下非常重要的一个方法 loadServlet,代码片段如下:

清单 16. StandardWrapper.loadServlet
public synchronized Servlet loadServlet() throws ServletException {
    ………
    Servlet servlet;
    try {
        ………
        ClassLoader classLoader = loader.getClassLoader();
        ………
        Class classClass = null;
        ………
        servlet = (Servlet) classClass.newInstance();
        if ((servlet instanceof ContainerServlet) &&
            (isContainerProvidedServlet(actualClass) ||
            ((Context)getParent()).getPrivileged() )) {
                ((ContainerServlet) servlet).setWrapper(this);
        }
        classLoadTime=(int) (System.currentTimeMillis() -t1);
        try {
            instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT,servlet);
            if( System.getSecurityManager() != null) {
                Class[] classType = new Class[]{ServletConfig.class};
                Object[] args = new Object[]{((ServletConfig)facade)};
                SecurityUtil.doAsPrivilege("init",servlet,classType,args);
            } else {
                servlet.init(facade);
            }
            if ((loadOnStartup >= 0) && (jspFile != null)) {
                ………
                if( System.getSecurityManager() != null) {
                    Class[] classType = new Class[]{ServletRequest.class,
                        ServletResponse.class};
                    Object[] args = new Object[]{req, res};
                    SecurityUtil.doAsPrivilege("service",servlet,classType,args);
                } else {
                    servlet.service(req, res);
                }
            }
            instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,servlet);
            ………
        
	return servlet;
}

它基本上描述了对 Servlet 的操作,当装载了 Servlet 后就会调用 Servlet 的 init 方法,同时会传一个 StandardWrapperFacade 对象给 Servlet,这个对象包装了 StandardWrapper,ServletConfig 与它们的关系图如下:

图 13. ServletConfig 与 StandardWrapperFacade、StandardWrapper 的关系

图 13. ServletConfig 与 StandardWrapperFacade、StandardWrapper 的关系

Servlet 可以获得的信息都在 StandardWrapperFacade 封装,这些信息又是在 StandardWrapper 对象中拿到的。所以 Servlet 可以通过 ServletConfig 拿到有限的容器的信息。

当 Servlet 被初始化完成后,就等着 StandardWrapperValve 去调用它的 service 方法了,调用 service 方法之前要调用 Servlet 所有的 filter。

tuxedo(三)-tuxedo与oracle连接配置

tuxedo(二)中已经基本启动了tuxedo服务,只是与oracle的连接还未配置好,如果配置完成就可以完成并开发最基本的三层服务应用结构。本节将介绍与oracle12c  pdb 数据库的连接配置。我们使用XA接口,因为该模式可以与多个数据库连接。

一.oracle数据库配置准备

oracle12c 引入了CDB与PDB新特性,该特性详见 ORACLE 12C新特性——CDB与PDB。

我们在PDB上新建用户表空间及权限用于tuxedo 系统连接使用。

1.启动数据库监听

修改监听及TNS配置然后启动监听

添加list_listener sid:

同时当PDB启动后,会动态的默认启动pdborcl sid数据库的监听。当然也可以静态的配置上去

/oracle/11g/network/admin/listener.ora:

 

[bath]#ADD BY ZWR SID_LIST_LISTENER = (SID_LIST = (SID_DESC = (SID_NAME = PLSExtProc) (ORACLE_HOME =/oracle/11g) (PROGRAM = extproc) ) (SID_DESC = (GLOBAL_DBNAME = orcl) (ORACLE_HOME =/oracle/11g) (SID_NAME = orcl) ) ) [/bath]

启动监听

[bath]lsnrctl status lsnrctl stop lsnrctl start [/bath]

image

启动数据库

sqlplus /nolog 

conn sys/Mwb123456 as sysdba startup 

2.创建PDB表空间用户

使用sqlplus 登陆到 CDB表容器后,先登陆到PDB

切换PDB数据库

C## user zwruser identified by zwruser default tablespace zwr temporary tablespace zwr_temp;

alter session set container=pdborcl;

startup

创建表空间用户

create tablespace zwr logging datafile'/oracle/oradata/orcl/zwr1.dbf' size 100m autoextend on next 100m maxsize 10240m extent management local;

create temporary tablespace zwr_temp tempfile'/oracle/oradata/orcl/zwr_temp1.dbf ' size 100m autoextend on next 100m maxsize 10240m extent management local;

create user zwruser identified by zwruser default tablespace zwr temporary tablespace zwr_temp;

grant create user,drop user,alter user,create any view,connect,resource,dba,create session,create any sequence to zwruser ;

使用新建用户连接数据库

conn zwruser/zwruser@//localhost:1521/pdborcl as sysdba

二.tuxedo 连接配置

1.配置ubbsimple 内Oralce_XA连接参数:

OPENINFO="Oracle_XA:Oracle_XA+Acc=P/zwruser/zwruser+SqlNet=pdborcl+SesTm=600+MaxCur=5+LogDir=."

连接用户名:

Acc=P/zwruser/zwruser:用户名,密码Acc=P/zwruser as sysdba/zwruser

数据库TNS名称:

SqlNet=pdborcl:数据库名称

2.修改tnsnames,添加 pdborcl  :

/oracle/11g/network/admin/tnsnames.ora:

ORCL = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = orcl.localdomain) ) ) pdborcl = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = pdborcl.localdomain) ) ) 

三:启动调试

tmboot -y

启动texedo 服务:

image

以上表示启动成功,如果红框内显示Fail,则在配置的XA配置的log目录下查看日志信息:

ULOG.120813以及xa 开头的日志。

 

以上就已经完成 tuxedo服务与数据库的连接。

tuxedo(二)-tuxedo安装配置

前面一节准备好了系统数据库环境,然后我们就可以安装tuxedo了。

一、创建用户

root 用户下创建用户组及tuxedo用户

groupadd tux useradd tuxedo -g tux -G dba 

passwd tuxedo

二、tuxedo 安装

tuxedo下载

[bath]sh tuxedo111120_64_Linux_01_x86.bin -i console[/bath]

tuxedo111120_64_Linux_01_x86.bin:http://download.oracle.com/otn/bea/tuxedo/11g/111120/tuxedo111120_64_Linux_01_x86.bin?AuthParam=1477891570_b560b9a7eac57fd458be753ed0f84778

[bath]sh tuxedo111120_64_Linux_01_x86.bin -i console[/bath]

安装语言,选择English,默认回车

image

软件介绍,有警告信息,跳过,回车

image

选择安装模式,默认全部安装,回车

image

选择oracle home目录,输入准备安装的路径:/home/tuxedo/oracle

image

是否安装样例,选择是

image

确认安装信息后安装:tuxedo 安装到 /home/tuxedo/oracle/tuxedo11gR1

至此tuxedo 中间件已经安装到系统中,下面确认tuxedo 服务的运行配置

三、 tuxedo服务配置

我们使用 样例中的 samples/atmi/simpapp 来配置服务及验证系统。

重新设置服务目录,将样例拷贝到该服务目录:

/home/tuxedo/oracle/tuxedoapp/src/simpapp。

1.配置环境变量:更改使用

[bath]sh /home/tuxedo/oracle/tuxedo11gR1/tux.env[/bath]

[bath]TUXDIR=/home/tuxedo/oracle/tuxedo11gR1; export TUXDIR JAVA_HOME=$TUXDIR/jre; export JAVA_HOME JVMLIBS=$JAVA_HOME/lib/amd64/server:$JAVA_HOME/jre/bin ORACLE_HOME=/oracle/11g;export ORACLE_HOME ORACLE_BASE=/oracle;export ORACLE_BASE PATH=$ORACLE_HOME/bin:$TUXDIR/bin:$JAVA_HOME/bin:$PATH; export PATH COBCPY=:$TUXDIR/cobinclude; export COBCPY COBOPT=”-C ANS85 -C ALIGN=8 -C NOIBMCOMP -C TRUNC=ANSI -C OSEXT=cbl”; export COBOPT SHLIB_PATH=$TUXDIR/lib:$JVMLIBS:$SHLIB_PATH; export SHLIB_PATH LIBPATH=$TUXDIR/lib:$JVMLIBS:$LIBPATH; export LIBPATH LD_LIBRARY_PATH=$TUXDIR/lib:$JVMLIBS:$LD_LIBRARY_PATH; export LD_LIBRARY_PATH WEBJAVADIR=$TUXDIR/udataobj/webgui/java; export WEBJAVADIR TUXCONFIG=/home/tuxedo/oracle/tuxedoapp/src/simpapp/tuxconfig;export TUXCONFIG [/bath]

2.配置服务ubbsimple文件如下:

特别要注意的有:#add by zwr 的项及与oracle 数据连接的配置OPENINFO,TMSNAME。

#    (c) 2003 BEA Systems, Inc. All Rights Reserved.
#ident    “@(#) samples/atmi/simpapp/ubbsimple    $Revision: 1.7 $”

#Skeleton UBBCONFIG file for the TUXEDO Simple Application.
#Replace the <bracketed> items with the appropriate values.

*RESOURCES
#IPCKEY        <Replace with a valid IPC Key>

#Example:
IPCKEY        123456

DOMAINID    simpapp
MASTER        simple
MAXACCESSERS    10
MAXSERVERS    5
MAXSERVICES    10
MODEL        SHM
LDBAL        N

*MACHINES
DEFAULT:
        APPDIR=”/home/tuxedo/oracle/tuxedoapp/src/simpapp”
        TUXCONFIG=”/home/tuxedo/oracle/tuxedoapp/src/simpapp/tuxconfig”
        TUXDIR=”/home/tuxedo/oracle/tuxedo11gR1″
#Example:
#        APPDIR=”/home/me/simpapp”
#        TUXCONFIG=”/home/me/simpapp/tuxconfig”
#        TUXDIR=”/usr/tuxedo”
#add by zwr xa
TLOGDEVICE= “/home/tuxedo/oracle/tuxedoapp/TLOG”
TLOGNAME=TLOG
TLOGSIZE=200
“localhost.localdomain”    LMID=simple

#Example:
#beatux        LMID=simple

*GROUPS
GROUP1
    LMID=simple    GRPNO=1
OPENINFO=”Oracle_XA:Oracle_XA+Acc=P/zwruser/zwruser+SqlNet=pdborcl+SesTm=600+MaxCur=5+LogDir=.”
TMSNAME=”TMS_ORA11g” TMSCOUNT=2
*SERVERS
DEFAULT:
        CLOPT=”-A”

simpserv    SRVGRP=GROUP1 SRVID=1

*SERVICES
TOUPPER

 

3.生成服务配置

[bath]$tmloadcf -y ubbsimple[/bath]

4.生成与数据库连接的TMS_ORA11g

[bath]buildtms -o $ORACLE_HOME/bin/TMS_ORA11g -r Oracle_XA[/bath]

5.编译服务

[bath]buildserver -o simpserv -f simpserver.c -r Oracle_XA -s TOUPPER[/bath]

6.启动关闭服务

[bath]tmboot -y tmshutdown -y [/bath]

7.编译客户端并测试

[bath]buildclient -o simpcl -f simpcl.c[/bath]

$./simpcl hello 显示Returned string is: HELLO表示测试成功

tuxedo(一)-服务器准备vmware12,oracle12c

tuxedo 中间件一般与oracle 数据结合使用,构成一个3层的分布式服务框架,现在就安装一下tuxedo 所准备的环境,centos6.5、oracle12c,为了测试方便我们将其安装到VMware中。

软件版本

VMware 12,oracle12c,centos6.5

安装

一、安装VMWare12

没什么说的基本上就是选择文件夹和下一步,最后需要输入License,5A02H-AU243-TZJ49-GTC7K-3C61N。

二.安装CentOS 6.5

没什么好说的

三、安装Oracle

1.下载oracle11c,zip文件,一起解压即可

2.使用root用户登录,并通过yum安装必须软件

Shell代码 收藏代码

  1. # yum install binutils compat-libstdc++-33 elfutils elfutils-libelf-devel gcc gcc-c++ glibc glibc-common glibc-devel glibc-headers libaio libaio-devel libgcc libstdc++ libstdc++-devel make sysstat unixODBC unixODBC-devel 

3.新建oracle用户及oracle安装文件目录

Shell代码 收藏代码

  1. #groupadd oinstall  
  2. #groupadd dba  
  3. #useradd -g oinstall -G dba oracle  
  4. #passwd oracle  
  5. #mkdir /oracle  
  6. #mkdir /oracle/11g  
  7. #chown -R oracle:oinstall /oracle 

4.修改系统环境变量

这一步用vi其实比较麻烦,推荐用sFtp工具将文件拷贝出来修改

另,附件中有要修改的文件备份,和修改结果

1).#vi /etc/sysctl.conf

Shell代码 收藏代码

  1. kernel.shmall = 2097152
  2. kernel.shmmax = 2147483648
  3. kernel.shmmni = 4096
  4. kernel.sem= 250 32000 100 128
  5. fs.file-max=6815744
  6. net.ipv4.ip_local_port_range = 9000 65500
  7. net.core.rmem_default = 262144
  8. net.core.rmem_max = 4194304
  9. net.core.wmem_default = 262144
  10. net.core.wmem_max = 10488576
  11. fs.aio-max-nr = 10488576

2).#vi /etc/security/limits.conf

Shell代码 收藏代码

  1. oracle  soft    nproc   2047
  2. oracle  hard    nproc   16384
  3. oracle  soft    nofile  1024
  4. oracle  hard    nofile  65536

3).vi /etc/pam.d/login

Shell代码 收藏代码

  1. session required /lib/security/pam_limits.so  
  2. session required pam_limits.so 

4).vi /etc/profile

Java代码 收藏代码

  1. if [ $USER = “oracle” ]; then  
  2. if [ $SHELL = “/bin/ksh” ]; then  
  3.         ulimit -p 16384
  4.         ulimit -n 65536
  5. else
  6.         ulimit -u 16384 -n 65536
  7.     fi  
  8. fi 

5).cd /home/oracle

vi .bash_profile

Java代码 收藏代码

  1. ORACLE_BASE=/oracle  
  2. ORACLE_HOME=$ORACLE_BASE/11g  
  3. –下面的配置是安装oracle后新建的oracle实例的名字  
  4. ORACLE_SID=orcl  
  5. PATH=$ORACLE_HOME/bin:$PATH  
  6. export ORACLE_BASE ORACLE_HOME ORACLE_SID PATH 

6).关闭SELinux,安装完成后可以打开

#vim /etc/selinux/config 确保以下内容

Shell代码 收藏代码

  1. SELINUX=disabled 

5.安装oracle

用oracle用户进入linux,进入终端,并进入安装文件目录(这个目录放在哪里都可以,就是一开始两个压缩解压缩的目录)

Shell代码 收藏代码

  1. $cd oralce 安装文件目录 

–下一步骤是为了防止oracle安装过程中的中文乱码问题

Shell代码 收藏代码

  1. $LANG=en 

开始安装,如果提示Permission denied,用chmod设置一下权限

Shell代码 收藏代码

  1. $chmod 777 database -R  
  2. $cd database  
  3. $sh runInstaller 

安装过程是图形界面,按照提示一直安装就行。安装过程中会要求你用root用户执行命令root.sh和orainstRoot.sh,自己照着提示的完整路径做就是。还有会要求制订一个inventory目录,随便哪个位置都行。






Shell代码 收藏代码

  1. wget http://mirror.centos.org/centos/5/os/x86_64/CentOS/pdksh-5.2.14-37.el5_8.1.x86_64.rpm  
  2. rpm -ivh pdksh-5.2.14-37.el5_8.1.x86_64.rpm 

中间出现Password Management直接选择OK

然后等待提示用root用户执行sh,中间输入地址直接回车。

6.验证

(1)系统重启后启动oracle过程

在oracle用户下,进入终端输入

Shell代码 收藏代码

  1. $ sqlplus  
  2. SQL*Plus: Release 11.2.0.1.0 Production on Mon Feb 20 22:46:00 2012
  3. Copyright (c) 1982, 2009, Oracle.  All rights reserved.  
  4. Enter user-name: sys as sysdba  
  5. Enter password:    

输入正确的用户名和密码,注意sys用户登录的话必须加上 as sysdba

然后启动oracle服务:

Sqlplus代码 收藏代码

  1. sql> startup  
  2. sql> exit 

关闭Oracle服务

Sqlplus代码 收藏代码

  1. sql> shutdown immediate 

(2)再启动oracle监听程序

Shell代码 收藏代码

  1. $ cd $ORACLE_HOME/bin  
  2. $ lsnrctl start 

测试

重新打开一个终端,如下输入:

Shell代码 收藏代码

  1. $ sqlplus sys as sysdba @localhost :1521/orcl 

输入密码,进入到oracle的sqlplus命令行下,执行一个简单的查询(显示所有的用户):

Sqlplus代码 收藏代码

  1. SQL> select * from all_users; 

或执行:

Sqlplus代码 收藏代码

  1. SQL> select table_name from user_tables 

另:

1.创建一个具有DBA权限的用户

(1)以管理员身份进入数据库 SQLPLUS SYSTEM/密码

(2)创建用户 CREATE USER 用户名 IDENTIFIED BY 密码;

Sqlplus代码 收藏代码

  1. SQL> create user sgq0085 identified by 123456;  
  2. User created. 

(3)将用户上锁/解锁  ALTER USER 用户名 ACCOUNT UNLOCK/LOCK;

Sqlplus代码 收藏代码

  1. SQL> alter user sgq0085 account lock;  
  2. User altered.  
  3. SQL> alter user sgq0085 account unlock;  
  4. User altered. 

(4)授予新创建的用户登录权限 GRANT CREATE SESSION TO 用户名;

Sqlplus代码 收藏代码

  1. SQL> grant create session to sgq0085;  
  2. Grant succeeded. 

(5)授予新创建的用户数据库管理员权限 GRANT DBA TO 用户名;

Sqlplus代码 收藏代码

  1. SQL> grant dba to sgq0085;  
  2. Grant succeeded.  
  3. SQL> select * from dba_users; 

(6)切换到新创建的用户登录 CONNECT 用户名/密码;

Sqlplus代码 收藏代码

  1. SQL> connect sgq0085/123456;  
  2. Connected. 

(7)删除用户 DROP USER 用户名

Sqlplus代码 收藏代码

  1. SQL> drop user sgq0085;  
  2. User dropped. 

2.指定开放系统端口

Shell代码 收藏代码

  1. # su – root  
  2. # vi /etc/sysconfig/iptables 

希望开放的端口写为如下这种

-A INPUT -m state –state NEW -m tcp -p tcp –dport 80 -j ACCEPT

-A INPUT -m state –state NEW -m tcp -p tcp –dport 1158 -j ACCEPT

-A INPUT -m state –state NEW -m tcp -p tcp –dport 1521 -j ACCEPT

vi中复制一行的方法

Vi代码 收藏代码

  1. 1)把光标移动到要复制的行上 2)按yy 3)把光标移动到要复制的位置 4)按p 

重启iptables

Shell代码 收藏代码

  1. # service iptables restart 

检测

Shell代码 收藏代码

  1. # iptables -L 

3.设置系统时间和时区

Shell代码 收藏代码

  1. [root@localhost ~]# date -s 09/29/2013
  2. Sun Sep 29 00:00:00 CST 2013
  3. [root@localhost ~]# date -s 09/29/2013
  4. Sun Sep 29 00:00:00 CST 2013
  5. [root@localhost ~]# cp -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime  
  6. cp: overwrite `/etc/localtime’? y  
  7. [root@localhost ~]# clock -w  
  8. [root@localhost ~]# date -R  
  9. Sun, 29 Sep 2013 00:02:01 +0800
  10. [root@localhost ~]#  

4.NAT 固定IP地址

/etc/sysconfig/network-scripts/ifcfg-eth0

Txt代码 收藏代码

  1. DEVICE=”eth0″
  2. BOOTPROTO=”static” #还有一种是dhcp  
  3. BROADCAST=192.168.130.255 #广播地址  
  4. NETWORK=192.168.130.0 #网段  
  5. IPADDR=192.168.130.128 #静态IP  
  6. NETMASK=255.255.255.0 #子网掩码  
  7. HWADDR=”00:0C:29:08:5F:12″
  8. IPV6INIT=”yes”
  9. NM_CONTROLLED=”yes”
  10. ONBOOT=”yes”
  11. TYPE=”Ethernet”
  12. UUID=”ebfda46e-4e37-4126-8b30-96b7650843de”

/etc/sysconfig/network

Txt代码 收藏代码

  1. NETWORKING=yes  
  2. HOSTNAME=localhost.localdomain  
  3. GATEWAY=192.168.130.2 #默认网关 

重启

Shell代码 收藏代码

  1. service network restart  

route add default  gw 192.168.130.2

/etc/resolv.conf

Txt代码 收藏代码

  1. # Generated by NetworkManager  
  2. nameserver 192.168.130.2 #指定DNS  
  3. # No nameservers found; try putting DNS servers into your  
  4. # ifcfg files in /etc/sysconfig/network-scripts like so:  
  5. #  
  6. # DNS1=xxx.xxx.xxx.xxx  
  7. # DNS2=xxx.xxx.xxx.xxx  
  8. # DOMAIN=lab.foo.com bar.foo.com 

防止resolv.conf被修改

Shell代码 收藏代码

  1. chattr +i /etc/resolv.conf 

—–Linux下查看及更改oracle字符集编码
[root@OracleDB ~]# cd /usr/local/oracle/
[root@OracleDB oracle]# env|grep NLS_LANG
NLS_LANG=american_america.zhs16gbk
[root@OracleDB oracle]# vi .bash_profile
# 使 bash_profile 设置生效
source .bash_profile

# 常用unicode字符集
export NLS_LANG=american_america.AL32UTF8
# 常用中文字符集
export
可以编辑 bash_profile 文件进行永久设置
vi .bash_profile
export NLS_LANG=”SIMPLIFIED CHINESE_CHINA.ZHS16GBK”

or export NLS_LANG=”Simplified Chinese_china”.ZHS16GBK
# 使 bash_profile 设置生效

單一使用者可進行 root 所有指令(sudo)

Shell代码 收藏代码

  1. [root@~]# visudo  
  2. ….(前面省略)….  
  3. root    ALL=(ALL)       ALL  <==找到這一行,大約在 76 行左右  
  4. vbird1  ALL=(ALL)       ALL  <==這一行是你要新增的!  
  5. ….(前面省略)…. 

vbird1 是账号名