`
niejun0205
  • 浏览: 1433 次
  • 性别: Icon_minigender_1
  • 来自: 上海
最近访客 更多访客>>
社区版块
存档分类
最新评论
收藏列表
标题 标签 来源
MySQL错误1042-Can't get hostname for your address解决方法
2011-06-19 17:29:20
标签:mysql mysql5.1 数据库 休闲 职场
原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://qdjalone.blog.51cto.com/1222376/591495

公司业务需求,可能需要将mysql5.1更好值5.5。于是,在ftp://ftp.ntu.edu.tw/pub/MySQL/Downloads/网站download了mysql5.5绿色版。于windows server 2003 r2环境下测试。

发现远程连接的时候,报1042-Can't get hostname for your address错误,而授权工作已经于服务器上做好。

解决方法如下:

编辑my.ini
在[mysqld]节点下新增或修改如下两行行
skip-name-resolve #忽略主机名的方式访问
lower_case_table_names=1 #忽略数据库表名大小写

重启mysql服务,问题得到解决。
修改mysql密码
use mysql
update user set password=PASSWORD('fz123') where user='root'
重启
十款好用的UI工具 http://www.iteye.com/news/25137

        
关闭占用端口进程的方法 http://www.iteye.com/topic/1124046
在做开发的时候常常会遇到端口被占用的问题,下面是我在网上找的比较好用的一种关闭占用端口进程的方法

1.在运行中输入cmd打开dos命令窗口,比如我想找到端口8888对应的PID(通过PID找到相应的进程)
键入命令:netstat -ano|findstr 8888

这样就找到端口8888对应的PID 5100

2.根据PID找到对应的进程,打开任务管理器-点击查看选项-选择(选择列)

这样就会在任务管理器上看到PID的值

3.找到对应的PID关闭进程,关闭PID为5100的进程



tomcat https
SSL(Secure Sockets Layer 安全套接层)协议位于TCP/IP协议与各种应用层协议之间,为数据通讯提供安全支持。SSL协议可分为两层: SSL记录协议(SSL Record Protocol):它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。 SSL握手协议(SSL Handshake Protocol):它建立在SSL记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。SSL协议提供的服务主要有:

1)认证用户和服务器,确保数据发送到正确的客户机和服务器;

2)加密数据以防止数据中途被窃取;

3)维护数据的完整性,确保数据在传输过程中不被改变。

    HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议,HTTPS实际上应用了SSL作为HTTP应用层的子层。HTTPS使用端口443,而不是象HTTP那样使用端口80来和TCP/IP进行通信。SSL使用40 位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。HTTPS和SSL支持使用X.509数字认证,如果需要的话用户可以确认发送者是谁。

   HTTPS是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,https的安全基础是SSL,tomcat中配置ssl很方便,大致有几步:

环境:jdk1.6;tomcat6;Windows2003

一、准备工作

    1) 安装JDK 1.5 或更高版本, 并配置JAVA_HOME 环境变量(必须完成);
    2) 安装tomcat 6 ;
  
二、配置过程

1.生成 server key :

    以命令行方式切换到目录%tomcat_HOME%,在command命令行输入如下命令(jdk1.4以上带的工具):
 
keytool -genkey -alias tomcat -keyalg RSA -keypass changit -storepass changit -

keystore server.keystore -validity 3600 
      
    用户名输入域名,如localhost(开发或测试用)或hostname.domainname(用户拥有的域名),其它全部以 enter 跳过,最后确认,此时会在%JAVA_HOME%/bin下生成server.keystore 文件。
      
注:

参数 -keypass changit -storepass changit, changit是密码,根据自己要求更改。

参数 -validity 指证书的有效期(天),缺省有效期很短,只有90天。
 
 
keypass and storepass

    
2.将证书导入的JDK的证书信任库中:
    这步对于tomcat的SSL配置不是必须,但对于CAS SSO是必须的,否则会出现如下错误:

edu.yale.its.tp.cas.client.CASAuthenticationException: Unable to validate

ProxyTicketValidator。。。
      
    导入过程分2步,第一步是导出证书,第二步是导入到证书信任库,命令如下:
keytool -export -trustcacerts -alias tomcat -file server.cer -keystore server.keystore -storepass changit
      
keytool -import -trustcacerts -alias tomcat -file server.cer -keystore  %JAVA_HOME%/jre/lib/security/cacerts -storepass changeit
      
    如果有提示,输入Y就可以了。

   
   其他有用keytool命令(列出信任证书库中所有已有证书,删除库中某个证书):
   
keytool -list -v -keystore %JAVA_HOME%/jre/lib/security/cacerts
keytool -delete -trustcacerts -alias tomcat  -keystore  %JAVA_HOME%/jre/lib/security/cacerts -storepass changit

  3.配置tomcat (see attachment:server.xml):

   修改%TOMCAT_HOME%"conf"server.xml,以文字编辑器打开,查找这一行:
   xml 代码,将之后的那段的注释去掉,并加上 keystorePass及keystoreFile属性。
   注意,tomcat不同版本配置是不同的,文章仅给出: tomcat6 配置:xml 代码

   <Connector protocol="org.apache.coyote.http11.Http11NioProtocol"  
              port="8443" minSpareThreads="5" maxSpareThreads="75"  
              enableLookups="true" disableUploadTimeout="true"    
              acceptCount="100"  maxThreads="200"  
              scheme="https" secure="true" SSLEnabled="true"  
              clientAuth="false" sslProtocol="TLS"  
              keystoreFile="server.keystore"    
              keystorePass="changit"/>  

   tomcat6支持3种,请参考以下文档:
   http://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html
  
  4.验证配置
   访问 https://localhost:8443/
   <connector protocol="org.apache.coyote.http11.Http11NioProtocol"></connector>
  
  5. 如果默认想用HTTPS 方式进行网站, 可以作如下配置:

    一般Tomcat默认的SSL端口号是8443,但是对于SSL标准端口号是443,这样在访问网页的时候,直接使用https而不需要输入端口号就可以访问,如:https://localhost
 
   1)non-SSL HTTP/1.1 Connector定义的地方,一般如下:
        <Connector port="80" maxHttpHeaderSize="8192"  scheme="https"
                maxThreads="500" minSpareThreads="25" maxSpareThreads="75"
                enableLookups="false" redirectPort="443" acceptCount="100"
                connectionTimeout="20000" disableUploadTimeout="true" />  
 
      将其中的redirectPort端口号改为:443

   2)SSL HTTP/1.1 Connector定义的地方,修改端口号为:443,如下:
       <Connector    
       port="443" maxHttpHeaderSize="8192"
       maxThreads="150" minSpareThreads="25"
       maxSpareThreads="75"
       enableLookups="false"
       disableUploadTimeout="true"
       acceptCount="100" scheme="https"
       secure="true"
       clientAuth="false" sslProtocol="TLS"
        SSLEnabled="true"  
       keystoreFile="e:/server.keystore"
       keystorePass="changit" />
    3)AJP 1.3 Connector定义的地方,修改redirectPort为443,如下:  
    <Connector port="8009"
          enableLookups="false" redirectPort="443" protocol="AJP/1.3" />
                   
                   
   如上配置好后便可以用 Https://localhost 方式直接访问,无需输入端口号,启动日志和界面如下;

tomcat startup log

 

访问https://localhost/manager/status页面
 

 

 

三、参考资料

  1.Keytool使用指南:
    http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/keytool.html
 
  2.tomcat-ssl配置指南:
    http://tomcat.apache.org/tomcat-5.5-doc/ssl-howto.html
    http://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html

  3.http://www.blogjava.net/sealyu/archive/2010/01/13/309264.html
    http://www.wenhq.com/article/view_710.html

  4.https 443 默认端口配置文件server.xml server.xml

  5.Tomcat_SSL_config_txt
 
用keytool生成证书

详细请见:Tomcat的帮助文档,:https://localhost:8080/tomcat-docs/ssl-howto.html  。
1、用keytool生成证书:
        keytool -genkey -alias tomcat -keyalg RSA -keystore c:/tomcat/mykey
说明:
    这里-alias tomcat 是表示生成的这个证书的别名叫tomcat,-keyalg RSA  指的是采用的RSA算法,-keystore c:/tomcat/mykey 是指生成的证书存储的位置。回车后会提示你输入keystore password,这可以自己定,然后是一些个人信息及组织信息,可以轻松搞定。
2、server.xml中添加的配置信息:
< Connector port="8888"
               maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
               enableLookups="false" disableUploadTimeout="true"
               acceptCount="100" debug="0" scheme="https" secure="true"
               clientAuth="false" sslProtocol="TLS" keystoreFile="C:\tomcat、mykey" keystorePass="123456" />
说明:
   Tomcat的用的端口是8888,后面keystoreFile是说明你的证书的位置,keystorePass是指密码。
3、重启Tomcat后在地址栏输入:https://localhost:8888 就搞定了~~~~~~~~
浏览显示的时候提示安装证书。

====================================
JDK中keytool常用命令
-genkey      在用户主目录中创建一个默认文件".keystore",还会产生一个mykey的别名,mykey中包含用户的公钥、私钥和证书
-alias       产生别名
-keystore    指定密钥库的名称(产生的各类信息将不在.keystore文件中
-keyalg      指定密钥的算法  
-validity    指定创建的证书有效期多少天
-keysize     指定密钥长度
-storepass   指定密钥库的密码
-keypass     指定别名条目的密码
-dname       指定证书拥有者信息 例如:  "CN=sagely,OU=atr,O=szu,L=sz,ST=gd,C=cn"
-list        显示密钥库中的证书信息      keytool -list -v -keystore sage -storepass ....
-v           显示密钥库中的证书详细信息
-export      将别名指定的证书导出到文件  keytool -export -alias caroot -file caroot.crt
-file        参数指定导出到文件的文件名
-delete      删除密钥库中某条目          keytool -delete -alias sage -keystore sage
-keypasswd   修改密钥库中指定条目口令    keytool -keypasswd -alias sage -keypass .... -new .... -storepass ... -keystore sage
-import      将已签名数字证书导入密钥库  keytool -import -alias sage -keystore sagely -file sagely.crt
             导入已签名数字证书用keytool -list -v 以后可以明显发现多了认证链长度,并且把整个CA链全部打印出来。
 
1.证书的显示
-list
[-v | -rfc] [-alias <alias>]
[-keystore <keystore>] [-storepass <storepass>]
[-storetype <storetype>] [-provider <provider_class_name>]
例如:keytool -list -v -alias RapaServer -keystore cacerts -storepass 12345678
keytool -list -v -keystore d2aapplet.keystore -storepass 12345678 -storetype IAIKKeystore
2.将证书导出到证书文件
例如:keytool -export -keystore monitor.keystore -alias monitor -file monitor.cer
将把证书库 monitor.keystore 中的别名为 monitor 的证书导出到 monitor.cer 证书文件中,它包含证书主体的信息及证书的公钥,不包括私钥,可以公开。
keytool -export -keystore d2aApplet.keystore -alias RapaServer -file Rapa.cert -storetype IAIKKeystore
3.将keystore导入证书中
这里向Java默认的证书 cacerts导入Rapa.cert
keytool -import -alias RapaServer -keystore cacerts -file Rapa.cert -keystore cacerts
4.证书条目的删除
keytool的命令行参数 -delete 可以删除密钥库中的条目,如: keytool -delete -alias RapaServer -keystore d2aApplet.keystore ,这条命令将 d2aApplet.keystore 中的 RapaServer 这一条证书删除了。
5.证书条目口令的修改
使用 -keypasswd 参数,如:keytool -keypasswd -alias RapaServer -keystore d2aApplet.keystore,可以以交互的方式修改 d2aApplet.keystore证书库中的条目为 RapaServer 的证书。
Keytool -keypasswd -alias RapaServer -keypass 654321 -new 123456 -storepass 888888 -keystore d2aApplet.keystore这一行命令以非交互式的方式修改库中别名为 RapaServer 的证书的密码为新密码 654321,行中的 123456 是指该条证书的原密码, 888888 是指证书库的密码。
数字证书原理SSL http://www.cnblogs.com/JeffreySun/archive/2010/06/24/1627247.html
文中首先解释了加密解密的一些基础知识和概念,然后通过一个加密通信过程的例子说明了加密算法的作用,以及数字证书的出现所起的作用。接着对数字证书做一个详细的解释,并讨论一下windows中数字证书的管理,最后演示使用makecert生成数字证书。如果发现文中有错误的地方,或者有什么地方说得不够清楚,欢迎指出!

 
1、基础知识

      这部分内容主要解释一些概念和术语,最好是先理解这部分内容。
1.1、公钥密码体制(public-key cryptography)

公钥密码体制分为三个部分,公钥、私钥、加密解密算法,它的加密解密过程如下:

    加密:通过加密算法和公钥对内容(或者说明文)进行加密,得到密文。加密过程需要用到公钥。
    解密:通过解密算法和私钥对密文进行解密,得到明文。解密过程需要用到解密算法和私钥。注意,由公钥加密的内容,只能由私钥进行解密,也就是说,由公钥加密的内容,如果不知道私钥,是无法解密的。

公钥密码体制的公钥和算法都是公开的(这是为什么叫公钥密码体制的原因),私钥是保密的。大家都以使用公钥进行加密,但是只有私钥的持有者才能解密。在实际的使用中,有需要的人会生成一对公钥和私钥,把公钥发布出去给别人使用,自己保留私钥。

 
1.2、对称加密算法(symmetric key algorithms)

在对称加密算法中,加密使用的密钥和解密使用的密钥是相同的。也就是说,加密和解密都是使用的同一个密钥。因此对称加密算法要保证安全性的话,密钥要做好保密,只能让使用的人知道,不能对外公开。这个和上面的公钥密码体制有所不同,公钥密码体制中加密是用公钥,解密使用私钥,而对称加密算法中,加密和解密都是使用同一个密钥,不区分公钥和私钥。

 

        // 密钥,一般就是一个字符串或数字,在加密或者解密时传递给加密/解密算法。前面在公钥密码体制中说到的公钥、私钥就是密钥,公钥是加密使用的密钥,私钥是解密使用的密钥。
 
1.3、非对称加密算法(asymmetric key algorithms)

在非对称加密算法中,加密使用的密钥和解密使用的密钥是不相同的。前面所说的公钥密码体制就是一种非对称加密算法,他的公钥和是私钥是不能相同的,也就是说加密使用的密钥和解密使用的密钥不同,因此它是一个非对称加密算法。

 
1.4、RSA简介

RSA是一种公钥密码体制,现在使用得很广泛。如果对RSA本身有兴趣的,后面看我有没有时间写个RSA的具体介绍。

RSA密码体制是一种公钥密码体制,公钥公开,私钥保密,它的加密解密算法是公开的。 由公钥加密的内容可以并且只能由私钥进行解密,并且由私钥加密的内容可以并且只能由公钥进行解密。也就是说,RSA的这一对公钥、私钥都可以用来加密和解密,并且一方加密的内容可以由并且只能由对方进行解密。

 
1.5、签名和加密

我们说加密,是指对某个内容加密,加密后的内容还可以通过解密进行还原。 比如我们把一封邮件进行加密,加密后的内容在网络上进行传输,接收者在收到后,通过解密可以还原邮件的真实内容。

这里主要解释一下签名,签名就是在信息的后面再加上一段内容,可以证明信息没有被修改过,怎么样可以达到这个效果呢?一般是对信息做一个hash计算得到一个hash值,注意,这个过程是不可逆的,也就是说无法通过hash值得出原来的信息内容。在把信息发送出去时,把这个hash值加密后做为一个签名和信息一起发出去。 接收方在收到信息后,会重新计算信息的hash值,并和信息所附带的hash值(解密后)进行对比,如果一致,就说明信息的内容没有被修改过,因为这里hash计算可以保证不同的内容一定会得到不同的hash值,所以只要内容一被修改,根据信息内容计算的hash值就会变化。当然,不怀好意的人也可以修改信息内容的同时也修改hash值,从而让它们可以相匹配,为了防止这种情况,hash值一般都会加密后(也就是签名)再和信息一起发送,以保证这个hash值不被修改。至于如何让别人可以解密这个签名,这个过程涉及到数字证书等概念,我们后面在说到数字证书时再详细说明,这里您先只需先理解签名的这个概念。

 
2、一个加密通信过程的演化

      我们来看一个例子,现在假设“服务器”和“客户”要在网络上通信,并且他们打算使用RSA(参看前面的RSA简介)来对通信进行加密以保证谈话内容的安全。由于是使用RSA这种公钥密码体制,“服务器”需要对外发布公钥(算法不需要公布,RSA的算法大家都知道),自己留着私钥。“客户”通过某些途径拿到了“服务器”发布的公钥,客户并不知道私钥。“客户”具体是通过什么途径获取公钥的,我们后面再来说明,下面看一下双方如何进行保密的通信:

 
2.1 第一回合:

“客户”->“服务器”:你好

“服务器”->“客户”:你好,我是服务器

“客户”->“服务器”:????

因为消息是在网络上传输的,有人可以冒充自己是“服务器”来向客户发送信息。例如上面的消息可以被黑客截获如下:

“客户”->“服务器”:你好

“服务器”->“客户”:你好,我是服务器

“客户”->“黑客”:你好        // 黑客在“客户”和“服务器”之间的某个路由器上截获“客户”发给服务器的信息,然后自己冒充“服务器”

“黑客”->“客户”:你好,我是服务器

因此“客户”在接到消息后,并不能肯定这个消息就是由“服务器”发出的,某些“黑客”也可以冒充“服务器”发出这个消息。如何确定信息是由“服务器”发过来的呢?有一个解决方法,因为只有服务器有私钥,所以如果只要能够确认对方有私钥,那么对方就是“服务器”。因此通信过程可以改进为如下:

 
2.2 第二回合:

“客户”->“服务器”:你好

“服务器”->“客户”:你好,我是服务器

“客户”->“服务器”:向我证明你就是服务器

“服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA]

      // 注意这里约定一下,{} 表示RSA加密后的内容,[ | ]表示用什么密钥和算法进行加密,后面的示例中都用这种表示方式,例如上面的 {你好,我是服务器}[私钥|RSA]  就表示用私钥对“你好,我是服务器”进行加密后的结果。

为了向“客户”证明自己是“服务器”, “服务器”把一个字符串用自己的私钥加密,把明文和加密后的密文一起发给“客户”。对于这里的例子来说,就是把字符串 “你好,我是服务器”和这个字符串用私钥加密后的内容 {你好,我是服务器}[私钥|RSA] 发给客户。

“客户”收到信息后,她用自己持有的公钥解密密文,和明文进行对比,如果一致,说明信息的确是由服务器发过来的。也就是说“客户”把 {你好,我是服务器}[私钥|RSA] 这个内容用公钥进行解密,然后和“你好,我是服务器”对比。因为由“服务器”用私钥加密后的内容,由并且只能由公钥进行解密,私钥只有“服务器”持有,所以如果解密出来的内容是能够对得上的,那说明信息一定是从“服务器”发过来的。

假设“黑客”想冒充“服务器”:

“黑客”->“客户”:你好,我是服务器

“客户”->“黑客”:向我证明你就是服务器

“黑客”->“客户”:你好,我是服务器 {你好,我是服务器}[???|RSA]    //这里黑客无法冒充,因为他不知道私钥,无法用私钥加密某个字符串后发送给客户去验证。

“客户”->“黑客”:????

由于“黑客”没有“服务器”的私钥,因此它发送过去的内容,“客户”是无法通过服务器的公钥解密的,因此可以认定对方是个冒牌货!

到这里为止,“客户”就可以确认“服务器”的身份了,可以放心和“服务器”进行通信,但是这里有一个问题,通信的内容在网络上还是无法保密。为什么无法保密呢?通信过程不是可以用公钥、私钥加密吗?其实用RSA的私钥和公钥是不行的,我们来具体分析下过程,看下面的演示:

 
2.3 第三回合:

“客户”->“服务器”:你好

“服务器”->“客户”:你好,我是服务器

“客户”->“服务器”:向我证明你就是服务器

“服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA]

“客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[公钥|RSA]

“服务器”->“客户”:{你的余额是100元}[私钥|RSA]

注意上面的的信息 {你的余额是100元}[私钥],这个是“服务器”用私钥加密后的内容,但是我们之前说了,公钥是发布出去的,因此所有的人都知道公钥,所以除了“客户”,其它的人也可以用公钥对{你的余额是100元}[私钥]进行解密。所以如果“服务器”用私钥加密发给“客户”,这个信息是无法保密的,因为只要有公钥就可以解密这内容。然而“服务器”也不能用公钥对发送的内容进行加密,因为“客户”没有私钥,发送个“客户”也解密不了。

这样问题就又来了,那又如何解决呢?在实际的应用过程,一般是通过引入对称加密来解决这个问题,看下面的演示:

 
2.4 第四回合:

“客户”->“服务器”:你好

“服务器”->“客户”:你好,我是服务器

“客户”->“服务器”:向我证明你就是服务器

“服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA]

“客户”->“服务器”:{我们后面的通信过程,用对称加密来进行,这里是对称加密算法和密钥}[公钥|RSA]    //蓝色字体的部分是对称加密的算法和密钥的具体内容,客户把它们发送给服务器。

“服务器”->“客户”:{OK,收到!}[密钥|对称加密算法]

“客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[密钥|对称加密算法]

“服务器”->“客户”:{你的余额是100元}[密钥|对称加密算法]

在上面的通信过程中,“客户”在确认了“服务器”的身份后,“客户”自己选择一个对称加密算法和一个密钥,把这个对称加密算法和密钥一起用公钥加密后发送给“服务器”。注意,由于对称加密算法和密钥是用公钥加密的,就算这个加密后的内容被“黑客”截获了,由于没有私钥,“黑客”也无从知道对称加密算法和密钥的内容。

由于是用公钥加密的,只有私钥能够解密,这样就可以保证只有服务器可以知道对称加密算法和密钥,而其它人不可能知道(这个对称加密算法和密钥是“客户”自己选择的,所以“客户”自己当然知道如何解密加密)。这样“服务器”和“客户”就可以用对称加密算法和密钥来加密通信的内容了。

 

总结一下,RSA加密算法在这个通信过程中所起到的作用主要有两个:

    因为私钥只有“服务器”拥有,因此“客户”可以通过判断对方是否有私钥来判断对方是否是“服务器”。
    客户端通过RSA的掩护,安全的和服务器商量好一个对称加密算法和密钥来保证后面通信过程内容的安全。

如果这里您理解了为什么不用RSA去加密通信过程,而是要再确定一个对称加密算法来保证通信过程的安全,那么就说明前面的内容您已经理解了。(如果不清楚,再看下2.3和2.4,如果还是不清楚,那应该是我们说清楚,您可以留言提问。)

到这里,“客户”就可以确认“服务器”的身份,并且双方的通信内容可以进行加密,其他人就算截获了通信内容,也无法解密。的确,好像通信的过程是比较安全了。

 

但是这里还留有一个问题,在最开始我们就说过,“服务器”要对外发布公钥,那“服务器”如何把公钥发送给“客户”呢?我们第一反应可能会想到以下的两个方法:

a)把公钥放到互联网的某个地方的一个下载地址,事先给“客户”去下载。

b)每次和“客户”开始通信时,“服务器”把公钥发给“客户”。

但是这个两个方法都有一定的问题,

对于a)方法,“客户”无法确定这个下载地址是不是“服务器”发布的,你凭什么就相信这个地址下载的东西就是“服务器”发布的而不是别人伪造的呢,万一下载到一个假的怎么办?另外要所有的“客户”都在通信前事先去下载公钥也很不现实。

对于b)方法,也有问题,因为任何人都可以自己生成一对公钥和私钥,他只要向“客户”发送他自己的私钥就可以冒充“服务器”了。示意如下:

“客户”->“黑客”:你好           //黑客截获“客户”发给“服务器”的消息

“黑客”->“客户”:你好,我是服务器,这个是我的公钥    //黑客自己生成一对公钥和私钥,把公钥发给“客户”,自己保留私钥

“客户”->“黑客”:向我证明你就是服务器

“黑客”->“客户”:你好,我是服务器 {你好,我是服务器}[黑客自己的私钥|RSA]      //客户收到“黑客”用私钥加密的信息后,是可以用“黑客”发给自己的公钥解密的,从而会误认为“黑客”是“服务器”

因此“黑客”只需要自己生成一对公钥和私钥,然后把公钥发送给“客户”,自己保留私钥,这样由于“客户”可以用黑客的公钥解密黑客的私钥加密的内容,“客户”就会相信“黑客”是“服务器”,从而导致了安全问题。这里问题的根源就在于,大家都可以生成公钥、私钥对,无法确认公钥对到底是谁的。 如果能够确定公钥到底是谁的,就不会有这个问题了。例如,如果收到“黑客”冒充“服务器”发过来的公钥,经过某种检查,如果能够发现这个公钥不是“服务器”的就好了。

为了解决这个问题,数字证书出现了,它可以解决我们上面的问题。先大概看下什么是数字证书,一个证书包含下面的具体内容:

    证书的发布机构
    证书的有效期
    公钥
    证书所有者(Subject)
    签名所使用的算法
    指纹以及指纹算法

证书的内容的详细解释会在后面详细解释,这里先只需要搞清楚一点,数字证书可以保证数字证书里的公钥确实是这个证书的所有者(Subject)的,或者证书可以用来确认对方的身份。也就是说,我们拿到一个数字证书,我们可以判断出这个数字证书到底是谁的。至于是如何判断的,后面会在详细讨论数字证书时详细解释。现在把前面的通信过程使用数字证书修改为如下:

 
2.5 第五回合:

“客户”->“服务器”:你好

“服务器”->“客户”:你好,我是服务器,这里是我的数字证书        //这里用证书代替了公钥

“客户”->“服务器”:向我证明你就是服务器

“服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA]

注意,上面第二次通信,“服务器”把自己的证书发给了“客户”,而不是发送公钥。“客户”可以根据证书校验这个证书到底是不是“服务器”的,也就是能校验这个证书的所有者是不是“服务器”,从而确认这个证书中的公钥的确是“服务器”的。后面的过程和以前是一样,“客户”让“服务器”证明自己的身份,“服务器”用私钥加密一段内容连同明文一起发给“客户”,“客户”把加密内容用数字证书中的公钥解密后和明文对比,如果一致,那么对方就确实是“服务器”,然后双方协商一个对称加密来保证通信过程的安全。到这里,整个过程就完整了,我们回顾一下:

 
2.6 完整过程:

step1: “客户”向服务端发送一个通信请求

“客户”->“服务器”:你好

  

step2: “服务器”向客户发送自己的数字证书。证书中有一个公钥用来加密信息,私钥由“服务器”持有

“服务器”->“客户”:你好,我是服务器,这里是我的数字证书 

 

step3: “客户”收到“服务器”的证书后,它会去验证这个数字证书到底是不是“服务器”的,数字证书有没有什么问题,数字证书如果检查没有问题,就说明数字证书中的公钥确实是“服务器”的。检查数字证书后,“客户”会发送一个随机的字符串给“服务器”用私钥去加密,服务器把加密的结果返回给“客户”,“客户”用公钥解密这个返回结果,如果解密结果与之前生成的随机字符串一致,那说明对方确实是私钥的持有者,或者说对方确实是“服务器”。

“客户”->“服务器”:向我证明你就是服务器,这是一个随机字符串     //前面的例子中为了方便解释,用的是“你好”等内容,实际情况下一般是随机生成的一个字符串。

“服务器”->“客户”:{一个随机字符串}[私钥|RSA]

 

step4: 验证“服务器”的身份后,“客户”生成一个对称加密算法和密钥,用于后面的通信的加密和解密。这个对称加密算法和密钥,“客户”会用公钥加密后发送给“服务器”,别人截获了也没用,因为只有“服务器”手中有可以解密的私钥。这样,后面“服务器”和“客户”就都可以用对称加密算法来加密和解密通信内容了。

“服务器”->“客户”:{OK,已经收到你发来的对称加密算法和密钥!有什么可以帮到你的?}[密钥|对称加密算法]

“客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[密钥|对称加密算法]

“服务器”->“客户”:{你好,你的余额是100元}[密钥|对称加密算法]

…… //继续其它的通信

 
2.7 其它问题:

上面的过程已经十分接近HTTPS的真实通信过程了,完全可以按照这个过程去理解HTTPS的工作原理。但是我为了方便解释,上面有些细节没有说到,有兴趣的人可以看下这部分的内容。可以跳过不看,无关紧要。

 

【问题1】

上面的通信过程中说到,在检查完证书后,“客户”发送一个随机的字符串给“服务器”去用私钥加密,以便判断对方是否真的持有私钥。但是有一个问题,“黑客”也可以发送一个字符串给“服务器”去加密并且得到加密后的内容,这样对于“服务器”来说是不安全的,因为黑客可以发送一些简单的有规律的字符串给“服务器”加密,从而寻找加密的规律,有可能威胁到私钥的安全。所以说,“服务器”随随便便用私钥去加密一个来路不明的字符串并把结果发送给对方是不安全的。

〖解决方法〗

每次收到“客户”发来的要加密的的字符串时,“服务器”并不是真正的加密这个字符串本身,而是把这个字符串进行一个hash计算,加密这个字符串的hash值(不加密原来的字符串)后发送给“客户”,“客户”收到后解密这个hash值并自己计算字符串的hash值然后进行对比是否一致。也就是说,“服务器”不直接加密收到的字符串,而是加密这个字符串的一个hash值,这样就避免了加密那些有规律的字符串,从而降低被破解的机率。“客户”自己发送的字符串,因此它自己可以计算字符串的hash值,然后再把“服务器”发送过来的加密的hash值和自己计算的进行对比,同样也能确定对方是否是“服务器”。

 

【问题2】

在双方的通信过程中,“黑客”可以截获发送的加密了的内容,虽然他无法解密这个内容,但是他可以捣乱,例如把信息原封不动的发送多次,扰乱通信过程。

〖解决方法〗

可以给通信的内容加上一个序号或者一个随机的值,如果“客户”或者“服务器”接收到的信息中有之前出现过的序号或者随机值,那么说明有人在通信过程中重发信息内容进行捣乱,双方会立刻停止通信。有人可能会问,如果有人一直这么捣乱怎么办?那不是无法通信了? 答案是的确是这样的,例如有人控制了你连接互联网的路由器,他的确可以针对你。但是一些重要的应用,例如军队或者政府的内部网络,它们都不使用我们平时使用的公网,因此一般人不会破坏到他们的通信。 

 

【问题3】

在双方的通信过程中,“黑客”除了简单的重复发送截获的消息之外,还可以修改截获后的密文修改后再发送,因为修改的是密文,虽然不能完全控制消息解密后的内容,但是仍然会破坏解密后的密文。因此发送过程如果黑客对密文进行了修改,“客户”和“服务器”是无法判断密文是否被修改的。虽然不一定能达到目的,但是“黑客”可以一直这样碰碰运气。

〖解决方法〗

在每次发送信息时,先对信息的内容进行一个hash计算得出一个hash值,将信息的内容和这个hash值一起加密后发送。接收方在收到后进行解密得到明文的内容和hash值,然后接收方再自己对收到信息内容做一次hash计算,与收到的hash值进行对比看是否匹配,如果匹配就说明信息在传输过程中没有被修改过。如果不匹配说明中途有人故意对加密数据进行了修改,立刻中断通话过程后做其它处理。

 
3. 证书的构成和原理
3.1 证书的构成和原理

之前已经大概说了一个证书由什么构成,但是没有仔细进行介绍,这里对证书的内容做一个详细的介绍。先看下一个证书到底是个什么东西,在windows下查看一个证书时,界面是这样的,我们主要关注一下Details Tab页,其中的内容比较长,我滚动内容后后抓了三个图,把完整的信息显示出来:

certificateDetails

里面的内容比较多——Version、Serial number、Signature algorithm 等等,挑几个重要的解释一下。

 

◆Issuer (证书的发布机构)

指出是什么机构发布的这个证书,也就是指明这个证书是哪个公司创建的(只是创建证书,不是指证书的使用者)。对于上面的这个证书来说,就是指"SecureTrust CA"这个机构。

 

◆Valid from , Valid to (证书的有效期)

也就是证书的有效时间,或者说证书的使用期限。 过了有效期限,证书就会作废,不能使用了。

 

◆Public key (公钥)

这个我们在前面介绍公钥密码体制时介绍过,公钥是用来对消息进行加密的,第2章的例子中经常用到的。这个数字证书的公钥是2048位的,它的值可以在图的中间的那个对话框中看得到,是很长的一串数字。

 

◆Subject (主题)

这个证书是发布给谁的,或者说证书的所有者,一般是某个人或者某个公司名称、机构的名称、公司网站的网址等。 对于这里的证书来说,证书的所有者是Trustwave这个公司。

 

◆Signature algorithm (签名所使用的算法)

就是指的这个数字证书的数字签名所使用的加密算法,这样就可以使用证书发布机构的证书里面的公钥,根据这个算法对指纹进行解密。指纹的加密结果就是数字签名(第1.5节中解释过数字签名)。

 

◆Thumbprint, Thumbprint algorithm (指纹以及指纹算法)

这个是用来保证证书的完整性的,也就是说确保证书没有被修改过,这东西的作用和2.7中说到的第3个问题类似。 其原理就是在发布证书时,发布者根据指纹算法(一个hash算法)计算整个证书的hash值(指纹)并和证书放在一起,使用者在打开证书时,自己也根据指纹算法计算一下证书的hash值(指纹),如果和刚开始的值对得上,就说明证书没有被修改过,因为证书的内容被修改后,根据证书的内容计算的出的hash值(指纹)是会变化的。 注意,这个指纹会使用"SecureTrust CA"这个证书机构的私钥用签名算法(Signature algorithm)加密后和证书放在一起。

 

注意,为了保证安全,在证书的发布机构发布证书时,证书的指纹和指纹算法,都会加密后再和证书放到一起发布,以防有人修改指纹后伪造相应的数字证书。这里问题又来了,证书的指纹和指纹算法用什么加密呢?他们是用证书发布机构的私钥进行加密的。可以用证书发布机构的公钥对指纹和指纹算法解密,也就是说证书发布机构除了给别人发布证书外,他自己本身也有自己的证书。证书发布机构的证书是哪里来的呢???这个证书发布机构的数字证书(一般由他自己生成)在我们的操作系统刚安装好时(例如windows xp等操作系统),这些证书发布机构的数字证书就已经被微软(或者其它操作系统的开发机构)安装在操作系统中了,微软等公司会根据一些权威安全机构的评估选取一些信誉很好并且通过一定的安全认证的证书发布机构,把这些证书发布机构的证书默认就安装在操作系统里面了,并且设置为操作系统信任的数字证书。这些证书发布机构自己持有与他自己的数字证书对应的私钥,他会用这个私钥加密所有他发布的证书的指纹作为数字签名。

 
3.2 如何向证书的发布机构去申请证书

举个例子方便大家理解,假设我们公司"ABC Company"花了1000块钱,向一个证书发布机构"SecureTrust CA"为我们自己的公司"ABC Company"申请了一张证书,注意,这个证书发布机构"SecureTrust CA"是一个大家公认并被一些权威机构接受的证书发布机构,我们的操作系统里面已经安装了"SecureTrust CA"的证书。"SecureTrust CA"在给我们发布证书时,把Issuer,Public key,Subject,Valid from,Valid to等信息以明文的形式写到证书里面,然后用一个指纹算法计算出这些数字证书内容的一个指纹,并把指纹和指纹算法用自己的私钥进行加密,然后和证书的内容一起发布,同时"SecureTrust CA"还会给一个我们公司"ABC Company"的私钥给到我们。我们花了1000块钱买的这个证书的内容如下:

×××××××××××××××证书内容开始×××××××××××××××××

Issuer : SecureTrust CA

Subject : ABC Company

Valid from : 某个日期

Valid to: 某个日期

Public Key : 一串很长的数字

…… 其它的一些证书内容……

{证书的指纹和计算指纹所使用的指纹算法}[SecureTrust CA的私钥|RSA]      //这个就是"SecureTrust CA"对这个证书的一个数字签名,表示这个证书确实是他发布的,有什么问题他会负责(收了我们1000块,出了问题肯定要负责任的)

×××××××××××××××证书内容结束×××××××××××××××××

               // 记不记得前面的约定?{} 表示RSA加密后的内容,[ | ]表示用什么密钥和算法进行加密

 

我们"ABC Company"申请到这个证书后,我们把证书投入使用,我们在通信过程开始时会把证书发给对方,对方如何检查这个证书的确是合法的并且是我们"ABC Company"公司的证书呢?首先应用程序(对方通信用的程序,例如IE、OUTLook等)读取证书中的Issuer(发布机构)为"SecureTrust CA" ,然后会在操作系统中受信任的发布机构的证书中去找"SecureTrust CA"的证书,如果找不到,那说明证书的发布机构是个水货发布机构,证书可能有问题,程序会给出一个错误信息。 如果在系统中找到了"SecureTrust CA"的证书,那么应用程序就会从证书中取出"SecureTrust CA"的公钥,然后对我们"ABC Company"公司的证书里面的指纹和指纹算法用这个公钥进行解密,然后使用这个指纹算法计算"ABC Company"证书的指纹,将这个计算的指纹与放在证书中的指纹对比,如果一致,说明"ABC Company"的证书肯定没有被修改过并且证书是"SecureTrust CA" 发布的,证书中的公钥肯定是"ABC Company"的。对方然后就可以放心的使用这个公钥和我们"ABC Company"进行通信了。

★这个部分非常重要,一定要理解,您可以重新回顾一下之前的两章“1、基础知识”和“ 2、一个加密通信过程的演化”,然后再来理解这部分的内容。如果您把这节的内容看了几遍还没有搞懂证书的工作原理,您可以留言指出我没有说清楚的内容,我好方便进行修正。

 
3.3 证书的发布机构

前面已经初步介绍了一下证书发布机构,这里再深入讨论一下。

其实所有的公司都可以发布证书,我们自己也可以去注册一家公司来专门给别人发布证书。但是很明显,我们自己的专门发布证书的公司是不会被那些国际上的权威机构认可的,人家怎么知道你是不是个狗屁皮包公司?因此微软在它的操作系统中,并不会信任我们这个证书发布机构,当应用程序在检查证书的合法信的时候,一看证书的发布机构并不是操作系统所信任的发布机构,就会抛出错误信息。也就是说windows操作系统中不会预先安装好我们这个证书发布机构的证书,不信任我们这个发布机构。

  

不受信任的证书发布机构的危害

为什么一个证书发布机构受不受信任这么重要?我们举个例子。假设我们开了一个狗屁公司来为别人发布证书,并且我和微软有一腿,微软在他们的操作系统中把我设置为了受信任的证书发布机构。现在如果有个小公司叫Wicrosoft 花了10块钱让我为他们公司申请了一个证书,并且公司慢慢壮大,证书的应用范围也越来越广。然后有个奸商的公司JS Company想冒充Wicrosoft,于是给了我¥10000,让我为他们颁布一个证书,但是证书的名字(Subject)要写Wicrosoft,假如我为了这¥10000,真的把证书给了他们,那么他们以后就可以使用这个证书来冒充Wicrosoft了。

如果是一个优秀的证书发布机构,比如你要向他申请一个名字叫Wicrosoft的证书,它会让你提供很多资料证明你确实可以代表Wicrosoft这个公司,也就是说他回去核实你的身份。证书发布机构是要为他发布出的证书负法律责任的。

  

到这里,你可能会想,TMD,那我们自己就不能发布证书吗?就一定要花钱去申请?当然不是,我们自己也可以成立证书发布机构,但是需要通过一些安全认证等等,只是有点麻烦。另外,如果数字证书只是要在公司内部使用,公司可以自己给自己生成一个证书,在公司的所有机器上把这个证书设置为操作系统信任的证书发布机构的证书(这句话仔细看清楚,有点绕口),这样以后公司发布的证书在公司内部的所有机器上就可以通过验证了(在发布证书时,把这些证书的Issuer(发布机构)设置为我们自己的证书发布机构的证书的Subject(主题)就可以了)。但是这只限于内部应用,因为只有我们公司自己的机器上设置了信任我们自己这个所谓的证书发布机构,而其它机器上并没有事先信任我们这个证书发布机构,所以在其它机器上,我们发布的证书就无法通过安全验证。

 
4. 在windows中对数字证书进行管理
4.1 查看、删除、安装 数字证书

我们在上一章中说到了,我们的操作系统中会预先安装好一些证书发布机构的证书,我们看下在windows中如何找到这些证书,步骤如下:

1)开始菜单->运行,输入mmc,回车

2)在打开的窗口中选择 File-> Add/Remove Snap-in…

3)然后在弹出的对话框的 Standalone Tab页里面点击 Add… 按钮

4)在弹出的对对话框中选择 certificates 后点击 Add 按钮

具体的步骤如下图所示:

 

上面的步骤结束后,会又弹出一个对话框,里面有三个单选按钮如下:

    My user account 
    Service account
    Computer account

可以选择第一或者第三个选项,用来查看当前用户的证书或整个计算里面安装的证书。我们这里就默认选择第一个,平时一般安装证书的时候都会给所有用户安装,所以选择第一个和第三个选项看到的证书会差不多。我们在左边的导航树中选中受信任的证书发布机构(Trusted Root Certificate Authorities),然后点击下面的证书(Certificates),在右边的区域中就可以看到所有的受信任的证书发布机构的证书。

trustedcaAuth

注意上面的图片中,右边我们选中的这个证书发布机构"SecureTrust CA",我们前面在第3章3.2节中举例子的时候,就是去向这个证书发布机构申请的证书,由于我们申请的证书是这个机构发布的,所以应用程序在检查我们的证书的发布机构时(会检查我们证书的签名,确认是该机构发布的证书),就会发现是可以信任的证书发布机构,从而就会相信我们证书的真实性。

删除数字证书很简单,直接在右边的列表中右键然后删除就可以了。

数字证书的安装也比较简单,直接双击数字证书文件,会打开数字证书,对话框下面会有一个Install Certificate按钮,点击后就可以根据向导进行安装,如下图所示:

installCertificate

这个证书是我自己生成的测试证书,在证书的导入向导里面,它会让你选择导入到什么位置,如果是一个我们自己信任的证书发布机构自己的证书,只要导入到Certificate Authorities就可以了。Trusted Root Certificate Authorities, Intermediate Certification Authorities, Third-Party Root Certification Authorities 都是可以的,他们只是对证书的发布机构做了一个分类,还有一些其它的证书类型,例如Personal(个人证书)等等,具体就不介绍了。安装的时候一般来说可以用默认的选择项一直"下一步"到底。

 
4.2 如何自己创建证书

每个证书发布机构都有自己的用来创建证书的工具,当然,具体他们怎么去创建一个证书的我也不太清楚,不同类型的证书都有一定的格式和规范,我没有仔细去研究过这部分内容。 微软为我们提供了一个用来创建证书的工具makecert.exe,在安装Visual Studio的时候会安装上。如果没有安装也无所谓,可以上网去下一个,搜索makecert就可以了。可以直接从我的博客下载,这是链接。

向一些正规的证书发布机构申请证书一般是要收费的(因为别人要花时间检查你的身份,确认有没有同名的证书等等),这里我们看下如何自己创建一个证书,为后面在IIS中配置Https做准备。

我们用到的是makecert这个工具,微软有很详细的使用帮助,我这里只做一个简单的解释,详细的各种参数和使用方法请查看MSDN的makecert的帮助。但是里面有些参数说得不够清楚,而且还有遗漏的,可以参看我后面的解释作为一个补充。

 

先看下makecert最简单的使用方式:

makecert.exe test.cer

上面的命令会在makecert.exe所在的目录生成一个证书文件test.cer的数字证书文件。可以双击证书打开,看看证书的内容如下:

testCertificate1

证书的发布机构是"Root Agency",证书的主题(证书发布给谁)是"Joe’s-Software-Emporium",因为我们没有指定把证书发布给谁,makecert自己给我们随便生成了一个公司的名字。另外还指定了公钥、签名算法(用来解密签名)、指纹和指纹算法等。

注意,因为这个证书是由微软的工具生成的,严格来说它没什么发布机构,所以微软虚拟了一个叫做"Root Agency"的发布机构,默认情况下,windows里面安装了这个所谓的证书发布机构的证书,但是这证书默认情况下不是受信任的,原因很简单,这样做大家都可以用makecert来制作合法的数字证书了。如果我们自己硬是要,也可以把它设置为受信任的。

 

下面我们看下其它的参数,比如我们要给网站 www.jefferysun.com 生成一个证书MyCA.cer,假设我们把makecert.exe放在C:盘下,命令行如下:
makecert -r -pe -n "CN=10.30.146.206" -b 01/01/2000 -e 01/01/2036 -eku 1.3.6.1.5.5.7.3.1 -ss my -sr localMachine -sky exchange -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12

C:\> makecert.exe –pe -r  –n  "CN=www.jefferysun.com" -ss my -sr LocalMachine -a sha1 -len 2048  MyCA.cer

解释一下makecert的常用参数的意思:

    -n 指定主题的名字,这个是有固定的格式的, CN=主题名字 ,CN应该是Certificate Name的缩写。我这里的主题的名字就是我们的IIS所在机器的IP。这里可以指定一些主题的其它附加信息,例如 O= *** 表示组织信息等等。
    -r 创建自签署证书,意思就是说在生成证书时,将证书的发布机构设置为自己。
    -pe 将所生成的私钥标记为可导出。注意,服务器发送证书给客户端的时候,客户端只能从证书里面获取公钥,私钥是无法获取的。如果我们指定了这个参数,证书在安装在机器上后,我们还可以从证书中导出私钥,默认情况下是不能导出私钥的。正规的途径发布的证书,是不可能让你导出私钥的。
    -b –e 证书的有效期
    -ss 证书的存储名称,就是windows证书存储区的目录名,如果不存在在的话就创建一个。
    -sr 证书的存储位置,只有currentuser(默认值)或 localmachine两个值。
    -sv 指定保存私钥的文件,文件里面除了包含私钥外,其实也包含了证书。这个文件是需要保密的,这个文件在服务端配置时是需要用到的。
    这个CN=10.30.146.206要与自己的服务器相对应,要不然在配置HTTPS的时候会出现错误
    -a 指定签名算法,必须是md5或rsa1。(还记得签名算法的作用不?可以看一下3章的第1节中关于签名算法的介绍)
    -in 指定证书发布机构的名称
    -len 这个参数在中文的帮助文档中好像没有提到,但是这个其实很重要,用于指定公钥的位数,越大越安全,默认值是1024,推荐2048。我试了下,这个不为1024的倍数也是可以的。

生成证书后可以进行安装,安装过程可以参看4.1节。
海量数据处理
在实际的工作环境下,许多人会遇到海量数据这个复杂而艰巨的问题,它的主要难点有以下几个方面:

一、数据量过大,数据中什么情况都可能存在。

如果说有10条数据,那么大不了每条去逐一检查,人为处理,如果有上百条数据,也可以考虑,如果数据上到千万级别,甚至过亿,那不是手工能解决的了,必须通过工具或者程序进行处理,尤其海量的数据中,什么情况都可能存在,例如,数据中某处格式出了问题,尤其在程序处理时,前面还能正常处理,突然到了某个地方问题出现了,程序终止了。

二、软硬件要求高,系统资源占用率高。

对海量的数据进行处理,除了好的方法,最重要的就是合理使用工具,合理分配系统资源。一般情况,如果处理的数据过TB级,小型机是要考虑的,普通的机子如果有好的方法可以考虑,不过也必须加大CPU和内存,就象面对着千军万马,光有勇气没有一兵一卒是很难取胜的。

三、要求很高的处理方法和技巧。

这也是本文的写作目的所在,好的处理方法是一位工程师长期工作经验的积累,也是个人的经验的总结。没有通用的处理方法,但有通用的原理和规则。

下面我们来详细介绍一下处理海量数据的经验和技巧:

一、选用优秀的数据库工具

现在的数据库工具厂家比较多,对海量数据的处理对所使用的数据库工具要求比较高,一般使用Oracle或者DB2,微软公司最近发布的SQL Server 2005性能也不错。另外在BI领域:数据库,数据仓库,多维数据库,数据挖掘等相关工具也要进行选择,象好的ETL工具和好的OLAP工具都十分必要,例如Informatic,Eassbase等。笔者在实际数据分析项目中,对每天6000万条的日志数据进行处理,使用SQL Server 2000需要花费6小时,而使用SQL Server 2005则只需要花费3小时。

二、编写优良的程序代码

处理数据离不开优秀的程序代码,尤其在进行复杂数据处理时,必须使用程序。好的程序代码对数据的处理至关重要,这不仅仅是数据处理准确度的问题,更是数据处理效率的问题。良好的程序代码应该包含好的算法,包含好的处理流程,包含好的效率,包含好的异常处理机制等。

三、对海量数据进行分区操作

对海量数据进行分区操作十分必要,例如针对按年份存取的数据,我们可以按年进行分区,不同的数据库有不同的分区方式,不过处理机制大体相同。例如SQL Server的数据库分区是将不同的数据存于不同的文件组下,而不同的文件组存于不同的磁盘分区下,这样将数据分散开,减小磁盘I/O,减小了系统负荷,而且还可以将日志,索引等放于不同的分区下。

四、建立广泛的索引

对海量的数据处理,对大表建立索引是必行的,建立索引要考虑到具体情况,例如针对大表的分组、排序等字段,都要建立相应索引,一般还可以建立复合索引,对经常插入的表则建立索引时要小心,笔者在处理数据时,曾经在一个ETL流程中,当插入表时,首先删除索引,然后插入完毕,建立索引,并实施聚合操作,聚合完成后,再次插入前还是删除索引,所以索引要用到好的时机,索引的填充因子和聚集、非聚集索引都要考虑。

五、建立缓存机制

当数据量增加时,一般的处理工具都要考虑到缓存问题。缓存大小设置的好差也关系到数据处理的成败,例如,笔者在处理2亿条数据聚合操作时,缓存设置为100000条/Buffer,这对于这个级别的数据量是可行的。

六、加大虚拟内存

如果系统资源有限,内存提示不足,则可以靠增加虚拟内存来解决。笔者在实际项目中曾经遇到针对18亿条的数据进行处理,内存为1GB,1个P42.4G的CPU,对这么大的数据量进行聚合操作是有问题的,提示内存不足,那么采用了加大虚拟内存的方法来解决,在6块磁盘分区上分别建立了6个4096M的磁盘分区,用于虚拟内存,这样虚拟的内存则增加为 4096*6 + 1024 =25600 M,解决了数据处理中的内存不足问题。

七、分批处理

海量数据处理难因为数据量大,那么解决海量数据处理难的问题其中一个技巧是减少数据量。可以对海量数据分批处理,然后处理后的数据再进行合并操作,这样逐个击破,有利于小数据量的处理,不至于面对大数据量带来的问题,不过这种方法也要因时因势进行,如果不允许拆分数据,还需要另想办法。不过一般的数据按天、按月、按年等存储的,都可以采用先分后合的方法,对数据进行分开处理。

八、使用临时表和中间表

数据量增加时,处理中要考虑提前汇总。这样做的目的是化整为零,大表变小表,分块处理完成后,再利用一定的规则进行合并,处理过程中的临时表的使用和中间结果的保存都非常重要,如果对于超海量的数据,大表处理不了,只能拆分为多个小表。如果处理过程中需要多步汇总操作,可按汇总步骤一步步来,不要一条语句完成,一口气吃掉一个胖子。

九、优化查询SQL语句

在对海量数据进行查询处理过程中,查询的SQL语句的性能对查询效率的影响是非常大的,编写高效优良的SQL脚本和存储过程是数据库工作人员的职责,也是检验数据库工作人员水平的一个标准,在对SQL语句的编写过程中,例如减少关联,少用或不用游标,设计好高效的数据库表结构等都十分必要。笔者在工作中试着对1亿行的数据使用游标,运行3个小时没有出结果,这是一定要改用程序处理了。

十、使用文本格式进行处理

对一般的数据处理可以使用数据库,如果对复杂的数据处理,必须借助程序,那么在程序操作数据库和程序操作文本之间选择,是一定要选择程序操作文本的,原因为:程序操作文本速度快;对文本进行处理不容易出错;文本的存储不受限制等。例如一般的海量的网络日志都是文本格式或者csv格式(文本格式),对它进行处理牵扯到数据清洗,是要利用程序进行处理的,而不建议导入数据库再做清洗。

十一、定制强大的清洗规则和出错处理机制

海量数据中存在着不一致性,极有可能出现某处的瑕疵。例如,同样的数据中的时间字段,有的可能为非标准的时间,出现的原因可能为应用程序的错误,系统的错误等,这是在进行数据处理时,必须制定强大的数据清洗规则和出错处理机制。

十二、建立视图或者物化视图

视图中的数据来源于基表,对海量数据的处理,可以将数据按一定的规则分散到各个基表中,查询或处理过程中可以基于视图进行,这样分散了磁盘I/O,正如10根绳子吊着一根柱子和一根吊着一根柱子的区别。

十三、避免使用32位机子(极端情况)

目前的计算机很多都是32位的,那么编写的程序对内存的需要便受限制,而很多的海量数据处理是必须大量消耗内存的,这便要求更好性能的机子,其中对位数的限制也十分重要。

十四、考虑操作系统问题

海量数据处理过程中,除了对数据库,处理程序等要求比较高以外,对操作系统的要求也放到了重要的位置,一般是必须使用服务器的,而且对系统的安全性和稳定性等要求也比较高。尤其对操作系统自身的缓存机制,临时空间的处理等问题都需要综合考虑。

十五、使用数据仓库和多维数据库存储

数据量加大是一定要考虑OLAP的,传统的报表可能5、6个小时出来结果,而基于Cube的查询可能只需要几分钟,因此处理海量数据的利器是OLAP多维分析,即建立数据仓库,建立多维数据集,基于多维数据集进行报表展现和数据挖掘等。

十六、使用采样数据,进行数据挖掘

基于海量数据的数据挖掘正在逐步兴起,面对着超海量的数据,一般的挖掘软件或算法往往采用数据抽样的方式进行处理,这样的误差不会很高,大大提高了处理效率和处理的成功率。一般采样时要注意数据的完整性和,防止过大的偏差。笔者曾经对1亿2千万行的表数据进行采样,抽取出400万行,经测试软件测试处理的误差为千分之五,客户可以接受。

还有一些方法,需要在不同的情况和场合下运用,例如使用代理键等操作,这样的好处是加快了聚合时间,因为对数值型的聚合比对字符型的聚合快得多。类似的情况需要针对不同的需求进行处理。

海量数据是发展趋势,对数据分析和挖掘也越来越重要,从海量数据中提取有用信息重要而紧迫,这便要求处理要准确,精度要高,而且处理时间要短,得到有价值信息要快,所以,对海量数据的研究很有前途,也很值得进行广泛深入的研究。
海量数据处理专题(一)——开篇

  大数据量的问题是很多面试笔试中经常出现的问题,比如baidu google 腾讯 这样的一些涉及到海量数据的公司经常会问到。

  下面的方法是我对海量数据的处理方法进行了一个一般性的总结,当然这些方法可能并不能完全覆盖所有的问题,但是这样的一些方法也基本可以处理绝大多数遇到的问题。下面的一些问题基本直接来源于公司的面试笔试题目,方法不一定最优,如果你有更好的处理方法,欢迎与我讨论。

  本贴从解决这类问题的方法入手,开辟一系列专题来解决海量数据问题。拟包含 以下几个方面。

    Bloom Filter
    Hash
    Bit-Map
    堆(Heap)
    双层桶划分
    数据库索引
    倒排索引(Inverted Index)
    外排序
    Trie树
    MapReduce

  在这些解决方案之上,再借助一定的例子来剖析海量数据处理问题的解决方案。
海量数据处理专题(二)——Bloom Filter

【什么是Bloom Filter】 
Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。 这里有一篇关于Bloom Filter的详细介绍,不太懂的博友可以看看。 
【适用范围】 
可以用来实现数据字典,进行数据的判重,或者集合求交集 
【基本原理及要点】 
对于原理来说很简单,位数组+k个独立hash函数。将hash函数对应的值的位数组置1,查找时如果发现所有hash函数对应位都是1说明存在,很明显这 个过程并不保证查找的结果是100%正确的。同时也不支持删除一个已经插入的关键字,因为该关键字对应的位会牵动到其他的关键字。所以一个简单的改进就是 counting Bloom filter,用一个counter数组代替位数组,就可以支持删除了。 

还有一个比较重要的问题,如 何根据输入元素个数n,确定位数组m的大小及hash函数个数。当hash函数个数k=(ln2)*(m/n)时错误率最小。在错误率不大于E的情况 下,m至少要等于n*lg(1/E)才能表示任意n个元素的集合。但m还应该更大些,因为还要保证bit数组里至少一半为0,则m应 该>=nlg(1/E)*lge 大概就是nlg(1/E)1.44倍(lg表示以2为底的对数)。 

举个例子我们假设错误率为0.01,则此时m应大概是n的13倍。这样k大概是8个。 

注意这里m与n的单位不同,m是bit为单位,而n则是以元素个数为单位(准确的说是不同元素的个数)。通常单个元素的长度都是有很多bit的。所以使用bloom filter内存上通常都是节省的。 

【扩展】 
Bloom filter将集合中的元素映射到位数组中,用k(k为哈希函数个数)个映射位是否全1表示元素在不在这个集合中。Counting bloom filter(CBF)将位数组中的每一位扩展为一个counter,从而支持了元素的删除操作。Spectral Bloom Filter(SBF)将其与集合元素的出现次数关联。SBF采用counter中的最小值来近似表示元素的出现频率。 

【问题实例】 
给你A,B两个文件,各存放50亿条URL,每条URL占用64字节,内存限制是4G,让你找出A,B文件共同的URL。如果是三个乃至n个文件呢? 

根据这个问题我们来计算下内存的占用,4G=2^32大概是40亿*8大概是340亿bit,n=50亿,如果按出错率0.01算需要的大概是650亿个bit。 现在可用的是340亿,相差并不多,这样可能会使出错率上升些。另外如果这些urlip是一一对应的,就可以转换成ip,则大大简单了。

 
海量数据处理专题(三)——Hash


【什么是Hash】 
  Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。 
HASH主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做HASH值. 也可以说,hash就是找到一种数据内容和数据存放地址之间的映射关系。 
  数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”,如图: 


 
左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。 
元素特征转变为数组下标的方法就是散列法。散列法当然不止一种,下面列出三种比较常用的。 
1,除法散列法 
最直观的一种,上图使用的就是这种散列法,公式: 
index = value % 16 
学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。 
2,平方散列法 
求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式: 
index = (value * value) >> 28 
如果数值分配比较均匀的话这种方法能得到不错的结果,但我上面画的那个图的各个元素的值算出来的index都是0——非常失败。也许你还有个问题,value如果很大,value * value不会溢出吗?答案是会的,但我们这个乘法不关心溢出,因为我们根本不是为了获取相乘结果,而是为了获取index。 
3,斐波那契(Fibonacci)散列法 
平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?答案是肯定的。 
1,对于16位整数而言,这个乘数是40503 
2,对于32位整数而言,这个乘数是2654435769 
3,对于64位整数而言,这个乘数是11400714819323198485 
这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,如果你还有兴趣,就到网上查找一下“斐波那契数列”等关键字,我数学水平有限,不知道怎么描述清楚为什么,另外斐波那契数列的值居然和太阳系八大行星的轨道半径的比例出奇吻合,很神奇,对么?
对我们常见的32位整数而言,公式: 
i ndex = (value * 2654435769) >> 28 
如果用这种斐波那契散列法的话,那我上面的图就变成这样了: 

 


很明显,用斐波那契散列法调整之后要比原来的取摸散列法好很多。 
【适用范围】 
快速查找,删除的基本数据结构,通常需要总数据量可以放入内存。 
【基本原理及要点】 
hash函数选择,针对字符串,整数,排列,具体相应的hash方法。 
碰撞处理,一种是open hashing,也称为拉链法;另一种就是closed hashing,也称开地址法,opened addressing。 
【扩展】 
d-left hashing中的d是多个的意思,我们先简化这个问题,看一看2-left hashing。2-left hashing指的是将一个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备一个哈希函数,h1和h2。在存储一个新的key时,同 时用两个哈希函数进行计算,得出两个地址h1[key]和h2[key]。这时需要检查T1中的h1[key]位置和T2中的h2[key]位置,哪一个 位置已经存储的(有碰撞的)key比较多,然后将新key存储在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key 存储在左边的T1子表中,2-left也由此而来。在查找一个key时,必须进行两次hash,同时查找两个位置。 
【问题实例】 
1).海量日志数据,提取出某日访问百度次数最多的那个IP。 
IP的数目还是有限的,最多2^32个,所以可以考虑使用hash将ip直接存入内存,然后进行统计。

 
海量数据处理专题(四)——Bit-map

【什么是Bit-map】 
所谓的Bit-map就是用一个bit位来标记某个元素对应的Value, 而Key即是该元素。由于采用了Bit为单位来存储数据,因此在存储空间方面,可以大大节省。 
如果说了这么多还没明白什么是Bit-map,那么我们来看一个具体的例子,假设我们要对0-7内的5个元素(4,7,2,5,3)排序(这里假设这些元素没有重复)。那么我们就可以采用Bit-map的方法来达到排序的目的。要表示8个数,我们就只需要8个Bit(1Bytes),首先我们开辟1Byte的空间,将这些空间的所有Bit位都置为0(如下图:) 


 
然后遍历这5个元素,首先第一个元素是4,那么就把4对应的位置为1(可以这样操作 p+(i/8)|(0x01<<(i%8)) 当然了这里的操作涉及到Big-ending和Little-ending的情况,这里默认为Big-ending),因为是从零开始的,所以要把第五位置为一(如下图): 

 


然后再处理第二个元素7,将第八位置为1,,接着再处理第三个元素,一直到最后处理完所有的元素,将相应的位置为1,这时候的内存的Bit位的状态如下: 

 


然后我们现在遍历一遍Bit区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的。下面的代码给出了一个BitMap的用法:排序。 

C代码  
复制代码

 1     //定义每个Byte中有8个Bit位  
 2     #include <memory.h>  
 3     #define BYTESIZE 8  
 4     void SetBit(char *p, int posi)  
 5     {  
 6         for(int i=0; i < (posi/BYTESIZE); i++)  
 7         {  
 8             p++;  
 9         }  
10        
11         *p = *p|(0x01<<(posi%BYTESIZE));//将该Bit位赋值1  
12         return;  
13     }  
14        
15     void BitMapSortDemo()  
16     {  
17         //为了简单起见,我们不考虑负数  
18         int num[] = {3,5,2,10,6,12,8,14,9};  
19        
20         //BufferLen这个值是根据待排序的数据中最大值确定的  
21 //待排序中的最大值是14,因此只需要2个Bytes(16个Bit)  
22 //就可以了。  
23         const int BufferLen = 2;  
24         char *pBuffer = new char[BufferLen];  
25        
26         //要将所有的Bit位置为0,否则结果不可预知。  
27         memset(pBuffer,0,BufferLen);  
28         for(int i=0;i<9;i++)  
29         {  
30             //首先将相应Bit位上置为1  
31             SetBit(pBuffer,num[i]);  
32         }  
33        
34         //输出排序结果  
35         for(int i=0;i<BufferLen;i++)//每次处理一个字节(Byte)  
36         {  
37             for(int j=0;j<BYTESIZE;j++)//处理该字节中的每个Bit位  
38             {  
39                 //判断该位上是否是1,进行输出,这里的判断比较笨。  
40 //首先得到该第j位的掩码(0x01<<j),将内存区中的  
41 //位和此掩码作与操作。最后判断掩码是否和处理后的  
42 //结果相同  
43                 if((*pBuffer&(0x01<<j)) == (0x01<<j))  
44                 {  
45                     printf("%d ",i*BYTESIZE + j);  
46                 }  
47             }  
48             pBuffer++;  
49         }  
50     }  
51        
52     int _tmain(int argc, _TCHAR* argv[])  
53     {  
54         BitMapSortDemo();  
55         return 0;  
56     }  

复制代码

【适用范围】 

可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下 

【基本原理及要点】 

使用bit数组来表示某些元素是否存在,比如8位电话号码 

【扩展】 

Bloom filter可以看做是对bit-map的扩展 

【问题实例】 

1)已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。 

8位最多99 999 999,大概需要99m个bit,大概10几m字节的内存即可。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话) 

2)2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。 

将bit-map扩展一下,用2bit表示一个数即可,0表示未出现,1表示出现一次,2表示出现2次及以上,在遍历这些数的时候,如果对应位置的值是0,则将其置为1;如果是1,将其置为2;如果是2,则保持不变。或者我们不用2bit来进行表示,我们用两个bit-map即可模拟实现这个2bit-map,都是一样的道理。 

 
海量数据处理专题(五)——堆

【什么是堆】
概念:堆是一种特殊的二叉树,具备以下两种性质
1)每个节点的值都大于(或者都小于,称为最小堆)其子节点的值
2)树是完全平衡的,并且最后一层的树叶都在最左边
这样就定义了一个最大堆。如下图用一个数组来表示堆:

 

那么下面介绍二叉堆:二叉堆是一种完全二叉树,其任意子树的左右节点(如果有的话)的键值一定比根节点大,上图其实就是一个二叉堆。

你一定发觉了,最小的一个元素就是数组第一个元素,那么二叉堆这种有序队列如何入队呢?看图:

 

假设要在这个二叉堆里入队一个单元,键值为2,那只需在数组末尾加入这个元素,然后尽可能把这个元素往上挪,直到挪不动,经过了这种复杂度为Ο(logn)的操作,二叉堆还是二叉堆。

那如何出队呢?也不难,看图:


出队一定是出数组的第一个元素,这么来第一个元素以前的位置就成了空位,我们需要把这个空位挪至叶子节点,然后把数组最后一个元素插入这个空位,把这个“空位”尽量往上挪。这种操作的复杂度也是Ο(logn)。

【适用范围】
海量数据前n大,并且n比较小,堆可以放入内存

【基本原理及要点】
最大堆求前n小,最小堆求前n大。方法,比如求前n小,我们比较当前元素与最大堆里的最大元素,如果它小于最大元素,则应该替换那个最大元 素。这样最后得到的n个元素就是最小的n个。适合大数据量,求前n小,n的大小比较小的情况,这样可以扫描一遍即可得到所有的前n元素,效率很高。

【扩展】
双堆,一个最大堆与一个最小堆结合,可以用来维护中位数。

【问题实例】
1)100w个数中找最大的前100个数。
用一个100个元素大小的最小堆即可。

 
海量数据处理专题(六)

【什么是双层桶】  
事实上,与其说双层桶划分是一种数据结构,不如说它是一种算法设计思想。面对一堆大量的数据我们无法处理的时候,我们可以将其分成一个个小的单元,然后根据一定的策略来处理这些小单元,从而达到目的。

【适用范围】 
第k大,中位数,不重复或重复的数字

【基本原理及要点】 
因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。可以通过多次缩小,双层只是一个例子,分治才是其根本(只是“只分不治”)。

【扩展】 
当有时候需要用一个小范围的数据来构造一个大数据,也是可以利用这种思想,相比之下不同的,只是其中的逆过程。

【问题实例】 
1).2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。

有 点像鸽巢原理,整数个数为2^32,也就是,我们可以将这2^32个数,划分为2^8个区域(比如用单个文件代表一个区域),然后将数据分离到不同的区 域,然后不同的区域在利用bitmap就可以直接解决了。也就是说只要有足够的磁盘空间,就可以很方便的解决。 当然这个题也可以用我们前面讲过的BitMap方法解决,正所谓条条大道通罗马~~~

2).5亿个int找它们的中位数。

这个例子比上面那个更明显。首先我们将int划分为2^16个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。

实 际上,如果不是int是int64,我们可以经过3次这样的划分即可降低到可以接受的程度。即可以先将int64分成2^24个区域,然后确定区域的第几 大数,在将该区域分成2^20个子区域,然后确定是子区域的第几大数,然后子区域里的数的个数只有2^20,就可以直接利用direct addr table进行统计了。

3).现在有一个0-30000的随机数生成器。请根据这个随机数生成器,设计一个抽奖范围是0-350000彩票中奖号码列表,其中要包含20000个中奖号码。

这个题刚好和上面两个思想相反,一个0到3万的随机数生成器要生成一个0到35万的随机数。那么我们完全可以将0-35万的区间分成35/3=12个区 间,然后每个区间的长度都小于等于3万,这样我们就可以用题目给的随机数生成器来生成了,然后再加上该区间的基数。那么要每个区间生成多少个随机数呢?计 算公式就是:区间长度*随机数密度,在本题目中就是30000*(20000/350000)。最后要注意一点,该题目是有隐含条件的:彩票,这意味着你 生成的随机数里面不能有重复,这也是我为什么用双层桶划分思想的另外一个原因。
海量数据处理专题(七)——数据库索引及优化

索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
数据库索引
什么是索引

  数据库索引好比是一本书前面的目录,能加快数据库的查询速度。
  例如这样一个查询:select * from table1 where id=44。如果没有索引,必须遍历整个表,直到ID等于44的这一行被找到为止;有了索引之后(必须是在ID这一列上建立的索引),直接在索引里面找44(也就是在ID这一列找),就可以得知这一行的位置,也就是找到了这一行。可见,索引是用来定位的。
  索引分为聚簇索引和非聚簇索引两种,聚簇索引 是按照数据存放的物理位置为顺序的,而非聚簇索引就不一样了;聚簇索引能提高多行检索的速度,而非聚簇索引对于单行的检索很快。
概述

  建立索引的目的是加快对表中记录的查找或排序。
  为表设置索引要付出代价的:一是增加了数据库的存储空间,二是在插入和修改数据时要花费较多的时间(因为索引也要随之变动)。

 

 

B树索引-Sql Server索引方式
为什么要创建索引

  创建索引可以大大提高系统的性能。
    第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
    第二,可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
    第三,可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
    第四,在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
    第五,通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
  也许会有人要问:增加索引有如此多的优点,为什么不对表中的每一个列创建一个索引呢?因为,增加索引也有许多不利的方面。
    第一,创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
    第二,索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
    第三,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
在哪建索引

  索引是建立在数据库表中的某些列的上面。在创建索引的时候,应该考虑在哪些列上可以创建索引,在哪些列上不能创建索引。一般来说,应该在这些列上创建索引:
  在经常需要搜索的列上,可以加快搜索的速度;
  在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;
  在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
  在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
  在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
  同样,对于有些列不应该创建索引。一般来说,不应该创建索引的的这些列具有下列特点:
  第一,对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
  第二,对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。
  第三,对于那些定义为text, image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少,不利于使用索引。
  第四,当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改操作远远多于检索操作时,不应该创建索引。
数据库优化

  此外,除了数据库索引之外,在LAMP结果如此流行的今天,数据库(尤其是MySQL)性能优化也是海量数据处理的一个热点。下面就结合自己的经验,聊一聊MySQL数据库优化的几个方面。
  首先,在数据库设计的时候,要能够充分的利用索引带来的性能提升,至于如何建立索引,建立什么样的索引,在哪些字段上建立索引,上面已经讲的很清楚了,这里不在赘述。另外就是设计数据库的原则就是尽可能少的进行数据库写操作(插入,更新,删除等),查询越简单越好。如下:

 

数据库设计


  其次,配置缓存是必不可少的,配置缓存可以有效的降低数据库查询读取次数,从而缓解数据库服务器压力,达到优化的目的,一定程度上来讲,这算是一个“围魏救赵”的办法。可配置的缓存包括索引缓存(key_buffer),排序缓存(sort_buffer),查询缓存(query_buffer),表描述符缓存(table_cache),如下图:

 

配置缓存

  第三,切表,切表也是一种比较流行的数据库优化法。分表包括两种方式:横向分表和纵向分表,其中,横向分表比较有使用意义,故名思议,横向切表就是指把记录分到不同的表中,而每条记录仍旧是完整的(纵向切表后每条记录是不完整的),例如原始表中有100条记录,我要切成2个表,那么最简单也是最常用的方法就是ID取摸切表法,本例中,就把ID为1,3,5,7。。。的记录存在一个表中,ID为2,4,6,8,。。。的记录存在另一张表中。虽然横向切表可以减少查询强度,但是它也破坏了原始表的完整性,如果该表的统计操作比较多,那么就不适合横向切表。横向切表有个非常典型的用法,就是用户数据:每个用户的用户数据一般都比较庞大,但是每个用户数据之间的关系不大,因此这里很适合横向切表。最后,要记住一句话就是:分表会造成查询的负担,因此在数据库设计之初,要想好是否真的适合切表的优化:

 

分表

第四,日志分析,在数据库运行了较长一段时间以后,会积累大量的LOG日志,其实这里面的蕴涵的有用的信息量还是很大的。通过分析日志,可以找到系统性能的瓶颈,从而进一步寻找优化方案。

 

性能分析

以上讲的都是单机MySQL的性能优化的一些经验,但是随着信息大爆炸,单机的数据库服务器已经不能满足我们的需求,于是,多多节点,分布式数据库网络出现了,其一般的结构如下:

 

分布式数据库结构

这种分布式集群的技术关键就是“同步复制”。。。

 

 
海量数据处理专题(八)——倒排索引(搜索引擎之基石)
引言:

在信息大爆炸的今天,有了搜索引擎的帮助,使得我们能够快速,便捷的找到所求。提到搜索引擎,就不得不说VSM模型,说到VSM,就不得不聊倒排索引。可以毫不夸张的讲,倒排索引是搜索引擎的基石。
VSM检索模型

VSM全称是Vector Space Model(向量空间模型),是IR(Information Retrieval信息检索)模型中的一种,由于其简单,直观,高效,所以被广泛的应用到搜索引擎的架构中。98年的Google就是凭借这样的一个模型,开始了它的疯狂扩张之路。废话不多说,让我们来看看到底VSM是一个什么东东。

在开始之前,我默认大家对线性代数里面的向量(Vector)有一定了解的。向量是既有大小又有方向的量,通常用有向线段表示,向量有:加、减、倍数、内积、距离、模、夹角的运算。

文档(Document):一个完整的信息单元,对应的搜索引擎系统里,就是指一个个的网页。

标引项(Term):文档的基本构成单位,例如在英文中可以看做是一个单词,在中文中可以看作一个词语。

查询(Query):一个用户的输入,一般由多个Term构成。

那么用一句话概况搜索引擎所做的事情就是:对于用户输入的Query,找到最相似的Document返回给用户。而这正是IR模型所解决的问题:

信息检索模型是指如何对查询和文档进行表示,然后对它们进行相似度计算的框架和方法。

举个简单的例子:

现在有两篇文章(Document)分别是 “春风来了,春天的脚步近了” 和 “春风不度玉门关”。然后输入的Query是“春风”,从直观上感觉,前者和输入的查询更相关一些,因为它包含有2个春,但这只是我们的直观感觉,如何量化呢,要知道计算机是门严谨的学科^_^。这个时候,我们前面讲的Term和VSM模型就派上用场了。

首先我们要确定向量的维数,这时候就需要一个字典库,字典库的大小,即是向量的维数。在该例中,字典为{春风,来了,春天, 的,脚步,近了,不度,玉门关} ,文档向量,查询向量如下图:

 

VSM模型示例

PS:为了简单起见,这里分词的粒度很大。

将Query和Document都量化为向量以后,那么就可以计算用户的查询和哪个文档相似性更大了。简单的计算结果是D1和D2同Query的内积都是1,囧。当然了,如果分词粒度再细一些,查询的结果就是另外一个样子了,因此分词的粒度也是会对查询结果(主要是召回率和准确率)造成影响的。

上述的例子是用一个很简单的例子来说明VSM模型的,计算文档相似度的时候也是采用最原始的内积的方法,并且只考虑了词频(TF)影响因子,而没有考虑反词频(IDF),而现在比较常用的是cos夹角法,影响因子也非常多,据传Google的影响因子有100+之多。
大名鼎鼎的Lucene项目就是采用VSM模型构建的,VSM的核心公式如下(由cos夹角法演变,此处省去推导过程)

 

VSM模型公式

从上面的例子不难看出,如果向量的维度(对汉语来将,这个值一般在30w-45w)变大,而且文档数量(通常都是海量的)变多,那么计算一次相关性,开销是非常大的,如何解决这个问题呢?不要忘记了我们这节的主题就是 倒排索引,主角终于粉墨登场了!!!
倒排索引

倒排索引非常类似我们前面提到的Hash结构。以下内容来自维基百科:

倒排索引(英语:Inverted index),也常被称为反向索引、置入档案或反向档案,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。它是文档检索系统中最常用的数据结构。

有两种不同的反向索引形式:

    一条记录的水平反向索引(或者反向档案索引)包含每个引用单词的文档的列表。
    一个单词的水平反向索引(或者完全反向索引)又包含每个单词在一个文档中的位置。

后者的形式提供了更多的兼容性(比如短语搜索),但是需要更多的时间和空间来创建。

由上面的定义可以知道,一个倒排索引包含一个字典的索引和所有词的列表。其中字典索引中包含了所有的Term(通俗理解为文档中的词),索引后面跟的列表则保存该词的信息(出现的文档号,甚至包含在每个文档中的位置信息)。下面我们还采用上面的方法举一个简单的例子来说明倒排索引。

例如现在我们要对三篇文档建立索引(实际应用中,文档的数量是海量的):

文档1(D1):中国移动互联网发展迅速

文档2(D2):移动互联网未来的潜力巨大

文档3(D3):中华民族是个勤劳的民族

那么文档中的词典集合为:{中国,移动,互联网,发展,迅速,未来,的,潜力,巨大,中华,民族,是,个,勤劳}

建好的索引如下图:

 

倒排索引

在上面的索引中,存储了两个信息,文档号和出现的次数。建立好索引以后,我们就可以开始查询了。例如现在有一个Query是”中国移动”。首先分词得到Term集合{中国,移动},查倒排索引,分别计算query和d1,d2,d3的距离。有没有发现,倒排表建立好以后,就不需要在检索整个文档库,而是直接从字典集合中找到“中国”和“移动”,然后遍历后面的列表直接计算。

对倒排索引结构我们已经有了初步的了解,但在实际应用中还有些需要解决的问题(主要是由海量数据引起的)。笔者列举一些问题,并给出相应的解决方案,抛砖以引玉,希望大家可以展开讨论:

1.左侧的索引表如何建立?怎么做才能最高效?

可能有人不假思索回答:左侧的索引当然要采取hash结构啊,这样可以快速的定位到字典项。但是这样问题又来了,hash函数如何选取呢?而且hash是有碰撞的,但是倒排表似乎又是不允许碰撞的存在的。事实上,虽然倒排表和hash异常的相思,但是两者还是有很大区别的,其实在这里我们可以采用前面提到的Bitmap的思想,每个Term(单词)对应一个位置(当然了,这里不是一个比特位),而且是一一对应的。如何能够做到呢,一般在文字处理中,有很多的编码,汉字中的GBK编码基本上就可以包含所有用到的汉字,每个汉字的GBK编码是确定的,因此一个Term的”ID”也就确定了,从而可以做到快速定位。注:得到一个汉字的GBK号是非常快的过程,可以理解为O(1)的时间复杂度。

2.如何快速的添加删除更新索引?

有经验的码农都知道,一般在系统的“做加法”的代价比“做减法”的代价要低很多,在搜索引擎中中也不例外。因此,在倒排表中,遇到要删除一个文档,其实不是真正的删除,而是将其标记删除。这样一个减法操作的代价就比较小了。

3.那么多的海量文档,如果存储呢?有么有什么备份策略呢?

当然了,一台机器是存储不下的,分布式存储是采取的。一般的备份保存3份就足够了。

好了,倒排索引终于完工了,不足的地方请指正。谢谢

 

 
H2内存数据库
1.H2简介

H2是纯JAVA编写的轻量级数据库,类似于HSQLDB,但比HSQLDB好用多了。使用非常方便,无需安装,你就可以在程序中启动数据库服务器、连接、创建数据库,客户端连接也不需要任何安装,只要有IE即可。官网http://www.h2database.com/html/main.html。下载后bin目录下会有数据库驱动包,假设名叫h2-1.3.162.jar,如果你不需要看源代码,就这一个文件就够用了,启动数据库及连接数据库所用的资源都在里面了。为了方便,我们将它改名为h2.jar。

 

2.H2启动与连接

H2数据库启动选项可通过java -classpath h2.jar org.h2.tools.Server -?查询,如下:

 

1)文件方式启动

严格来说应该叫文件方式连接,因为文件方式启动不需要先启动数据库服务器,直接在连接URL中填写绝对或相对路径即可,由于创建数据库连接时,H2引擎发现指定的路径下如果不存在数据库,则会自动创建,如:

Class.forName("org.h2.Driver");

Connection conn = DriverManager.getConnection("jdbc:h2:file:~/test", "sa", "");

这段代码可以正常执行,其中file表示以文件方式启动数据库连接,但如果是第一次使用,可能不知道H2把数据库创建在了什么位置,熟悉linux的会知道~符号表示系统当前用户的主文件夹,所以(以win7为例),上面这段代码会在C:\Users\Administrator目录下创建名为test的数据库,用户名为sa,密码为空(用户名密码在首次创建时可以随意设置)。由于H2默认连接方式就是文件方式,所以file关键字可以省略,也就是说,在win7下,以下三种写法是等价的:

Connection conn = DriverManager.getConnection("jdbc:h2:~/test", "sa", "");

Connection conn = DriverManager.getConnection("jdbc:h2:file:~/test", "sa", "");

Connection conn = DriverManager.getConnection("jdbc:h2:file:C:/Users/Administrator/test", "sa", "");

以上的三种写法使用的是绝对路径,在实际应用中,你可能会希望程序在哪运行,就是哪创建数据库,所以会用到相对路径,如:

Connection conn = DriverManager.getConnection("jdbc:h2:file:h2db/test", "sa", "");

当执行以上代码时,H2会在JAVA程序运行的根目录下创建h2db文件夹,然后在h2db下创建test数据库。

如果数据库已经创建,下次再进行连接时,就必须提供与第一次创建时一致的用户名密码,否则将连接不成功。

另外需要强调的是,以文件方式连接数据库时,H2引擎会新建两个守护线程,一个用来锁文件,一个用来写文件:

所以这种方式是独占的,即当以文件方式连接数据库所创建的Connection如果没有关闭,不允许再有其他的进程以文件方式连接此数据库。如果尝试连接,将会报以下错误:

Exception in thread "main" org.h2.jdbc.JdbcSQLException: Database may be already in use: Locked by another process. Possible solutions: close all other connection(s); use the server mode [90020-72]
 at org.h2.message.Message.getSQLException(Message.java:92)
 at org.h2.message.Message.getSQLException(Message.java:96)
 at org.h2.message.Message.getSQLException(Message.java:74)

所以文件方式不适合做类似数据库连接池的应用。

 

2)使用web控制台

在讲完一种启动方式时需要先讲一下H2的web控制台,然后再讲更高级的数据库启动方式。因为无论以哪一种数据库方式启动数据库,你都可以使用web控制台来连接,并执行CRUD操作,非常方便,要启动web控制台,可以执行java -classpath h2.jar org.h2.tools.Console -web,登陆界面如下:

登陆成功后,可以在里面直接执行SQL语句:

 

3)tcp方式启动

tcp方式启动时,H2将在本地启动一个tcp服务器,你可以用客户端连接至此端口访问不同的数据库,这种方式支持多客户端。在启动数据库时,你不需要指定具体数据库(因为在连接时才需要指定),只需指定需要监听的端口即可,如:

java -classpath h2.jar org.h2.tools.Server -tcp

这样即可启动数据库服务器了,由于你没有指定端口,所以H2将监听默认端口9092,如果你想指定端口,需要使用tcpPort参数,如:

java -classpath h2.jar org.h2.tools.Server -tcp -tcpPort 9092

注意,此种方式启动后,你将可以在本地通过9092访问H2数据库,但不允许其他机器访问,如果你希望从其他机器也能通过此端口访问数据库,就需要用到tcpAllowOthers参数了,如:

java -classpath h2.jar org.h2.tools.Server -tcp -tcpPort 9092 -tcpAllowOthers

数据库启动后,通过web console或JDBC连接时,需要把URL也更改为tcp方式,如:

jdbc:h2:tcp://localhost:9092/~/test

可以看到,在端口号后面追加绝对或相对路径和前面讲的文件方式启动时规则是一样的。

 

4)tcp方式启动(匿名ssl)

当使用tcp方式启动数据库时,如果需要从外网访问,就必须考虑安全问题,H2官网对SSL方式启动数据库做了非常精要的说明,如下:

Remote SSL/TLS connections are supported using the Java Secure Socket Extension (SSLServerSocket, SSLSocket). By default, anonymous SSL is enabled. The default cipher suite is SSL_DH_anon_WITH_RC4_128_MD5.
To use your own keystore, set the system properties javax.net.ssl.keyStore and javax.net.ssl.keyStorePassword before starting the H2 server and client. See also Customizing the Default Key and Trust Stores, Store Types, and Store Passwords for more information.
To disable anonymous SSL, set the system property h2.enableAnonymousSSL to false.

要使用ssl方式启动数据库,需要使用h2.enableAnonymousSSL和tcpSSL参数,如:

java -Dh2.enableAnonymousSSL=true -classpath h2.jar org.h2.tools.Server -tcp -tcpPort 9092 -tcpAllowOthers -tcpSSL

注意,前面的h2.enableAnonymousSSL需要设置到JAVA系统属性里,所以要在前面追加-D。由于默认使用的就是匿名SSL,所以h2.enableAnonymousSSL参数可以省略,也就是说,下面两个命令是等价的:

java -Dh2.enableAnonymousSSL=true -classpath h2.jar org.h2.tools.Server -tcp -tcpPort 9092 -tcpAllowOthers -tcpSSL

java -classpath h2.jar org.h2.tools.Server -tcp -tcpPort 9092 -tcpAllowOthers -tcpSSL

启动完毕,客户端连接的方式还和原来一样,不过要把url更改为ssl方式,如:

jdbc:h2:ssl://localhost:9092/~/test

 

5)tcp方式启动(ssl双向认证)

尽管匿名ssl方式启动数据库保证了数据传输的可靠与安全性,但仍然没有解决客户端及服务器身份问题。也就是说,通过上面的方式启动数据库后,任何一个人都可以通过其他机器连接至9092端口,只需要破解数据库密码即可随意操作你的数据库了,这无疑非常危险,所以我们需要更安全的方式。SSL双向认证的原理及细节这里就不再阐述了,我们主要讲解如何使用SSL双向认证的方式启动H2数据库。步骤如下:

  a)制作证书

  特别说明,以下制作证书的步骤我是参考http://www.blogjava.net/stone2083/archive/2007/12/20/169015.html上的。

  我们将使用jdk的keytool制作安全证书,请确保你的JDK已正确安装并且已经设置好环境变量。我的JDK安装在D:\soft\Java\jdk\jdk1.6.0_10目录下,为了简单,我直接在D:\soft\Java\jdk\jdk1.6.0_10\bin目录下运行相关命令,请依次运行:

(1)keytool -genkey -alias serverkey -keystore kserver.keystore
(2)keytool -export -alias serverkey -keystore kserver.keystore -file server.crt
(3)keytool -import -alias serverkey -file server.crt -keystore tclient.keystore

(4)keytool -genkey -alias clientkey -keystore kclient.keystore
(5)keytool -export -alias clientkey -keystore kclient.keystore -file client.crt
(6)keytool -import -alias clientkey -file client.crt -keystore tserver.keystore

为了更清楚的看到证书的制作过程,这里贴上部分截图:

为简单其见,凡是需要输入密码的地方都输入123456,凡是需要输入y和n的地方都输入y,这样命令执行成功后会生成以下文件

kserver.keystore
tserver.keystore
server.crt

kclient.keystore
tclient.keystore
client.crt

我们只需要其中的四个就行了,服务器需要使用kserver.keystore、tserver.keystore,客户端使用kclient.keystore、tclient.keystore。

  b)启动数据库

如果成功制作了安全证书,那么启动数据库就很简单了,只需要把上一步制作的kserver.keystore、tserver.keystore放在h2.jar同目录下(当然放在别的目录也可以,只要在参数上指定对的路径即可),启动命令如下:

java -Dh2.enableAnonymousSSL=false -Djavax.net.ssl.keyStore=kserver.keystore -Djavax.net.ssl.keyStorePassword=123456 -Djavax.net.ssl.trustStore=tserver.keystore -Djavax.net.ssl.trustStorePassword=123456 -classpath h2.jar org.h2.tools.Server -tcp -tcpPort 9092 -tcpAllowOthers -tcpSSL

  c)客户端连接

客户端连接时需要对应的kclient.keystore、tclient.keystore,放在h2.jar同一目录下即可,连接命令如下:

java -Djavax.net.ssl.keyStore=kclient.keystore -Djavax.net.ssl.keyStorePassword=123456 -Djavax.net.ssl.trustStore=tclient.keystore -Djavax.net.ssl.trustStorePassword=123456 -classpath h2.jar org.h2.tools.Console -web

如果你连接时出现Connection is broken [90067-72] 90067/90067 (Help)的错误,可能的原因有:

(1)未在启动参数中指明安全证书路径或指明的证书路径错误,或者忘了把证书放在指定的路径下

(2)启动参数中指明的密码错误

(3)连接的URL有误,比如忘了加ssl或者端口号指定错误等等

(4)连接的命令中忘了加-web参数,如果没有加-web,在本机测试时客户端也会占用9092端口,但它发现此端口已经被服务器占用了,就会启动失败,结果程序退出,这样的话IE虽然能打开,但是也会连接不上
spring3注解 ModelAttribute requestParam

SpringMVC Controller 介绍
一、简介

         在SpringMVC 中,控制器Controller 负责处理由DispatcherServlet 分发的请求,它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Model 返回给对应的View 进行展示。在SpringMVC 中提供了一个非常简便的定义Controller 的方法,你无需继承特定的类或实现特定的接口,只需使用@Controller 标记一个类是Controller ,然后使用@RequestMapping 和@RequestParam 等一些注解用以定义URL 请求和Controller 方法之间的映射,这样的Controller 就能被外界访问到。此外Controller 不会直接依赖于HttpServletRequest 和HttpServletResponse 等HttpServlet 对象,它们可以通过Controller 的方法参数灵活的获取到。为了先对Controller 有一个初步的印象,以下先定义一个简单的Controller :
Java代码  收藏代码

    @Controller  
    public class MyController {  
      
        @RequestMapping ( "/showView" )  
        public ModelAndView showView() {  
           ModelAndView modelAndView = new ModelAndView();  
           modelAndView.setViewName( "viewName" );  
           modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );  
           return modelAndView;  
        }  
      
    }   

 

在上面的示例中,@Controller 是标记在类MyController 上面的,所以类MyController 就是一个SpringMVC Controller 对象了,然后使用@RequestMapping(“/showView”) 标记在Controller 方法上,表示当请求/showView.do 的时候访问的是MyController 的showView 方法,该方法返回了一个包括Model 和View 的ModelAndView 对象。这些在后续都将会详细介绍。
二、使用 @Controller 定义一个 Controller 控制器

         @Controller 用于标记在一个类上,使用它标记的类就是一个SpringMVC Controller 对象。分发处理器将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping 注解。@Controller 只是定义了一个控制器类,而使用@RequestMapping 注解的方法才是真正处理请求的处理器,这个接下来就会讲到。

   单单使用@Controller 标记在一个类上还不能真正意义上的说它就是SpringMVC 的一个控制器类,因为这个时候Spring 还不认识它。那么要如何做Spring 才能认识它呢?这个时候就需要我们把这个控制器类交给Spring 来管理。拿MyController 来举一个例子

 
Java代码  收藏代码

    @Controller  
    public class MyController {  
        @RequestMapping ( "/showView" )  
        public ModelAndView showView() {  
           ModelAndView modelAndView = new ModelAndView();  
           modelAndView.setViewName( "viewName" );  
           modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );  
           return modelAndView;  
        }  
      
    }   

 

这个时候有两种方式可以把MyController 交给Spring 管理,好让它能够识别我们标记的@Controller 。

   第一种方式是在SpringMVC 的配置文件中定义MyController 的bean 对象。

<bean class="com.host.app.web.controller.MyController"/>

   第二种方式是在SpringMVC 的配置文件中告诉Spring 该到哪里去找标记为@Controller 的Controller 控制器。
Xml代码  收藏代码

    < context:component-scan base-package = "com.host.app.web.controller" >  
       < context:exclude-filter type = "annotation"  
           expression = "org.springframework.stereotype.Service" />  
    </ context:component-scan >   

    注:

       上面 context:exclude-filter 标注的是不扫描 @Service 标注的类
三、使用 @RequestMapping 来映射 Request 请求与处理器

         可以使用@RequestMapping 来映射URL 到控制器类,或者是到Controller 控制器的处理方法上。当@RequestMapping 标记在Controller 类上的时候,里面使用@RequestMapping 标记的方法的请求地址都是相对于类上的@RequestMapping 而言的;当Controller 类上没有标记@RequestMapping 注解时,方法上的@RequestMapping 都是绝对路径。这种绝对路径和相对路径所组合成的最终路径都是相对于根路径“/ ”而言的。

 
Java代码  收藏代码

    @Controller  
    public class MyController {  
        @RequestMapping ( "/showView" )  
        public ModelAndView showView() {  
           ModelAndView modelAndView = new ModelAndView();  
           modelAndView.setViewName( "viewName" );  
           modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );  
           return modelAndView;  
        }  
      
    }   

 

在这个控制器中,因为MyController 没有被@RequestMapping 标记,所以当需要访问到里面使用了@RequestMapping 标记的showView 方法时,就是使用的绝对路径/showView.do 请求就可以了。

 
Java代码  收藏代码

    @Controller  
    @RequestMapping ( "/test" )  
    public class MyController {  
        @RequestMapping ( "/showView" )  
        public ModelAndView showView() {  
           ModelAndView modelAndView = new ModelAndView();  
           modelAndView.setViewName( "viewName" );  
           modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );  
           return modelAndView;  
        }  
      
    }   

 

   这种情况是在控制器上加了@RequestMapping 注解,所以当需要访问到里面使用了@RequestMapping 标记的方法showView() 的时候就需要使用showView 方法上@RequestMapping 相对于控制器MyController 上@RequestMapping 的地址,即/test/showView.do 。
(一)使用 URI 模板

   URI 模板就是在URI 中给定一个变量,然后在映射的时候动态的给该变量赋值。如URI 模板http://localhost/app/{variable1}/index.html ,这个模板里面包含一个变量variable1 ,那么当我们请求http://localhost/app/hello/index.html 的时候,该URL 就跟模板相匹配,只是把模板中的variable1 用hello 来取代。在SpringMVC 中,这种取代模板中定义的变量的值也可以给处理器方法使用,这样我们就可以非常方便的实现URL 的RestFul 风格。这个变量在SpringMVC 中是使用@PathVariable 来标记的。

   在SpringMVC 中,我们可以使用@PathVariable 来标记一个Controller 的处理方法参数,表示该参数的值将使用URI 模板中对应的变量的值来赋值。

 
Java代码  收藏代码

    @Controller  
    @RequestMapping ( "/test/{variable1}" )  
    public class MyController {  
      
        @RequestMapping ( "/showView/{variable2}" )  
        public ModelAndView showView( @PathVariable String variable1, @PathVariable ( "variable2" ) int variable2) {  
           ModelAndView modelAndView = new ModelAndView();  
           modelAndView.setViewName( "viewName" );  
           modelAndView.addObject( " 需要放到 model 中的属性名称 " , " 对应的属性值,它是一个对象 " );  
           return modelAndView;  
        }  
    }   

 

   在上面的代码中我们定义了两个URI 变量,一个是控制器类上的variable1 ,一个是showView 方法上的variable2 ,然后在showView 方法的参数里面使用@PathVariable 标记使用了这两个变量。所以当我们使用/test/hello/showView/2.do 来请求的时候就可以访问到MyController 的showView 方法,这个时候variable1 就被赋予值hello ,variable2 就被赋予值2 ,然后我们在showView 方法参数里面标注了参数variable1 和variable2 是来自访问路径的path 变量,这样方法参数variable1 和variable2 就被分别赋予hello 和2 。方法参数variable1 是定义为String 类型,variable2 是定义为int 类型,像这种简单类型在进行赋值的时候Spring 是会帮我们自动转换的,关于复杂类型该如何来转换在后续内容中将会讲到。

   在上面的代码中我们可以看到在标记variable1 为path 变量的时候我们使用的是@PathVariable ,而在标记variable2 的时候使用的是@PathVariable(“variable2”) 。这两者有什么区别呢?第一种情况就默认去URI 模板中找跟参数名相同的变量,但是这种情况只有在使用debug 模式进行编译的时候才可以,而第二种情况是明确规定使用的就是URI 模板中的variable2 变量。当不是使用debug 模式进行编译,或者是所需要使用的变量名跟参数名不相同的时候,就要使用第二种方式明确指出使用的是URI 模板中的哪个变量。

   除了在请求路径中使用URI 模板,定义变量之外,@RequestMapping 中还支持通配符“* ”。如下面的代码我就可以使用/myTest/whatever/wildcard.do 访问到Controller 的testWildcard 方法。

 
Java代码  收藏代码

    @Controller  
    @RequestMapping ( "/myTest" )  
    public class MyController {  
        @RequestMapping ( "*/wildcard" )  
        public String testWildcard() {  
           System. out .println( "wildcard------------" );  
           return "wildcard" ;  
        }    
    }   

 
(二)使用 @RequestParam 绑定 HttpServletRequest 请求参数到控制器方法参数

 
Java代码  收藏代码

    @RequestMapping ( "requestParam" )  
    ublic String testRequestParam( @RequestParam(required=false) String name, @RequestParam ( "age" ) int age) {  
       return "requestParam" ;  
    }   

 

在上面代码中利用@RequestParam 从HttpServletRequest 中绑定了参数name 到控制器方法参数name ,绑定了参数age 到控制器方法参数age 。值得注意的是和@PathVariable 一样,当你没有明确指定从request 中取哪个参数时,Spring 在代码是debug 编译的情况下会默认取更方法参数同名的参数,如果不是debug 编译的就会报错。此外,当需要从request 中绑定的参数和方法的参数名不相同的时候,也需要在@RequestParam 中明确指出是要绑定哪个参数。在上面的代码中如果我访问/requestParam.do?name=hello&age=1 则Spring 将会把request 请求参数name 的值hello 赋给对应的处理方法参数name ,把参数age 的值1 赋给对应的处理方法参数age 。

在@RequestParam 中除了指定绑定哪个参数的属性value 之外,还有一个属性required ,它表示所指定的参数是否必须在request 属性中存在,默认是true ,表示必须存在,当不存在时就会报错。在上面代码中我们指定了参数name 的required 的属性为false ,而没有指定age 的required 属性,这时候如果我们访问/requestParam.do 而没有传递参数的时候,系统就会抛出异常,因为age 参数是必须存在的,而我们没有指定。而如果我们访问/requestParam.do?age=1 的时候就可以正常访问,因为我们传递了必须的参数age ,而参数name 是非必须的,不传递也可以。
(三)使用 @CookieValue 绑定 cookie 的值到 Controller 方法参数

 
Java代码  收藏代码

    @RequestMapping ( "cookieValue" )  
    public String testCookieValue( @CookieValue ( "hello" ) String cookieValue, @CookieValue String hello) {  
       System. out .println(cookieValue + "-----------" + hello);  
       return "cookieValue" ;  
    }   

 

    在上面的代码中我们使用@CookieValue 绑定了cookie 的值到方法参数上。上面一共绑定了两个参数,一个是明确指定要绑定的是名称为hello 的cookie 的值,一个是没有指定。使用没有指定的形式的规则和@PathVariable 、@RequestParam 的规则是一样的,即在debug 编译模式下将自动获取跟方法参数名同名的cookie 值。
(四)使用 @RequestHeader 注解绑定 HttpServletRequest 头信息到 Controller 方法参数

 
Java代码  收藏代码

    @RequestMapping ( "testRequestHeader" )  
    public String testRequestHeader( @RequestHeader ( "Host" ) String hostAddr, @RequestHeader String Host, @RequestHeader String host ) {  
        System. out .println(hostAddr + "-----" + Host + "-----" + host );  
        return "requestHeader" ;  
    }   

 

         在上面的代码中我们使用了 @RequestHeader 绑定了 HttpServletRequest 请求头 host 到 Controller 的方法参数。上面方法的三个参数都将会赋予同一个值,由此我们可以知道在绑定请求头参数到方法参数的时候规则和 @PathVariable 、 @RequestParam 以及 @CookieValue 是一样的,即没有指定绑定哪个参数到方法参数的时候,在 debug 编译模式下将使用方法参数名作为需要绑定的参数。但是有一点 @RequestHeader 跟另外三种绑定方式是不一样的,那就是在使用 @RequestHeader 的时候是大小写不敏感的,即 @RequestHeader(“Host”) 和 @RequestHeader(“host”) 绑定的都是 Host 头信息。记住在 @PathVariable 、 @RequestParam 和 @CookieValue 中都是大小写敏感的。
(五) @RequestMapping 的一些高级应用

         在RequestMapping 中除了指定请求路径value 属性外,还有其他的属性可以指定,如params 、method 和headers 。这样属性都可以用于缩小请求的映射范围。

 
1.params属性

 

   params 属性用于指定请求参数的,先看以下代码。

 
Java代码  收藏代码

    @RequestMapping (value= "testParams" , params={ "param1=value1" , "param2" , "!param3" })  
    public String testParams() {  
       System. out .println( "test Params..........." );  
       return "testParams" ;  
    }   

 

   在上面的代码中我们用@RequestMapping 的params 属性指定了三个参数,这些参数都是针对请求参数而言的,它们分别表示参数param1 的值必须等于value1 ,参数param2 必须存在,值无所谓,参数param3 必须不存在,只有当请求/testParams.do 并且满足指定的三个参数条件的时候才能访问到该方法。所以当请求/testParams.do?param1=value1¶m2=value2 的时候能够正确访问到该testParams 方法,当请求/testParams.do?param1=value1¶m2=value2¶m3=value3 的时候就不能够正常的访问到该方法,因为在@RequestMapping 的params 参数里面指定了参数param3 是不能存在的。

 
2.method属性

 

   method 属性主要是用于限制能够访问的方法类型的。

 
Java代码  收藏代码

    @RequestMapping (value= "testMethod" , method={RequestMethod. GET , RequestMethod. DELETE })  
    public String testMethod() {  
       return "method" ;  
    }   

 

在上面的代码中就使用method 参数限制了以GET 或DELETE 方法请求/testMethod.do 的时候才能访问到该Controller 的testMethod 方法。

 
3.headers属性

 

         使用headers 属性可以通过请求头信息来缩小@RequestMapping 的映射范围。

 
Java代码  收藏代码

    @RequestMapping (value= "testHeaders" , headers={ "host=localhost" , "Accept" })  
    public String testHeaders() {  
       return "headers" ;  
    }   

 

   headers 属性的用法和功能与params 属性相似。在上面的代码中当请求/testHeaders.do 的时候只有当请求头包含Accept 信息,且请求的host 为localhost 的时候才能正确的访问到testHeaders 方法。
(六)以 @RequestMapping 标记的处理器方法支持的方法参数和返回类型
1. 支持的方法参数类型

         (1 )HttpServlet 对象,主要包括HttpServletRequest 、HttpServletResponse 和HttpSession 对象。 这些参数Spring 在调用处理器方法的时候会自动给它们赋值,所以当在处理器方法中需要使用到这些对象的时候,可以直接在方法上给定一个方法参数的申明,然后在方法体里面直接用就可以了。但是有一点需要注意的是在使用HttpSession 对象的时候,如果此时HttpSession 对象还没有建立起来的话就会有问题。

   (2 )Spring 自己的WebRequest 对象。 使用该对象可以访问到存放在HttpServletRequest 和HttpSession 中的属性值。

   (3 )InputStream 、OutputStream 、Reader 和Writer 。 InputStream 和Reader 是针对HttpServletRequest 而言的,可以从里面取数据;OutputStream 和Writer 是针对HttpServletResponse 而言的,可以往里面写数据。

   (4 )使用@PathVariable 、@RequestParam 、@CookieValue 和@RequestHeader 标记的参数。

   (5 )使用@ModelAttribute 标记的参数。

   (6 )java.util.Map 、Spring 封装的Model 和ModelMap 。 这些都可以用来封装模型数据,用来给视图做展示。

   (7 )实体类。 可以用来接收上传的参数。

   (8 )Spring 封装的MultipartFile 。 用来接收上传文件的。

   (9 )Spring 封装的Errors 和BindingResult 对象。 这两个对象参数必须紧接在需要验证的实体对象参数之后,它里面包含了实体对象的验证结果。
2. 支持的返回类型

         (1 )一个包含模型和视图的ModelAndView 对象。

   (2 )一个模型对象,这主要包括Spring 封装好的Model 和ModelMap ,以及java.util.Map ,当没有视图返回的时候视图名称将由RequestToViewNameTranslator 来决定。

   (3 )一个View 对象。这个时候如果在渲染视图的过程中模型的话就可以给处理器方法定义一个模型参数,然后在方法体里面往模型中添加值。

   (4 )一个String 字符串。这往往代表的是一个视图名称。这个时候如果需要在渲染视图的过程中需要模型的话就可以给处理器方法一个模型参数,然后在方法体里面往模型中添加值就可以了。

   (5 )返回值是void 。这种情况一般是我们直接把返回结果写到HttpServletResponse 中了,如果没有写的话,那么Spring 将会利用RequestToViewNameTranslator 来返回一个对应的视图名称。如果视图中需要模型的话,处理方法与返回字符串的情况相同。

   (6 )如果处理器方法被注解@ResponseBody 标记的话,那么处理器方法的任何返回类型都会通过HttpMessageConverters 转换之后写到HttpServletResponse 中,而不会像上面的那些情况一样当做视图或者模型来处理。

   (7 )除以上几种情况之外的其他任何返回类型都会被当做模型中的一个属性来处理,而返回的视图还是由RequestToViewNameTranslator 来决定,添加到模型中的属性名称可以在该方法上用@ModelAttribute(“attributeName”) 来定义,否则将使用返回类型的类名称的首字母小写形式来表示。使用@ModelAttribute 标记的方法会在@RequestMapping 标记的方法执行之前执行。
(七)使用 @ModelAttribute 和 @SessionAttributes 传递和保存数据

       SpringMVC 支持使用 @ModelAttribute 和 @SessionAttributes 在不同的模型和控制器之间共享数据。 @ModelAttribute 主要有两种使用方式,一种是标注在方法上,一种是标注在 Controller 方法参数上。

当 @ModelAttribute 标记在方法上的时候,该方法将在处理器方法执行之前执行,然后把返回的对象存放在 session 或模型属性中,属性名称可以使用 @ModelAttribute(“attributeName”) 在标记方法的时候指定,若未指定,则使用返回类型的类名称(首字母小写)作为属性名称。关于 @ModelAttribute 标记在方法上时对应的属性是存放在 session 中还是存放在模型中,我们来做一个实验,看下面一段代码。

 
Java代码  收藏代码

    @Controller  
    @RequestMapping ( "/myTest" )  
    public class MyController {  
      
        @ModelAttribute ( "hello" )  
        public String getModel() {  
           System. out .println( "-------------Hello---------" );  
           return "world" ;  
        }  
      
        @ModelAttribute ( "intValue" )  
        public int getInteger() {  
           System. out .println( "-------------intValue---------------" );  
           return 10;  
        }  
      
        @RequestMapping ( "sayHello" )  
        public void sayHello( @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpSession session) throws IOException {  
           writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num);  
           writer.write( "\r" );  
           Enumeration enume = session.getAttributeNames();  
           while (enume.hasMoreElements())  
               writer.write(enume.nextElement() + "\r" );  
        }  
      
        @ModelAttribute ( "user2" )  
        public User getUser() {  
           System. out .println( "---------getUser-------------" );  
           return new User(3, "user2" );  
        }  
    }   

 

当我们请求 /myTest/sayHello.do 的时候使用 @ModelAttribute 标记的方法会先执行,然后把它们返回的对象存放到模型中。最终访问到 sayHello 方法的时候,使用 @ModelAttribute 标记的方法参数都能被正确的注入值。执行结果如下图所示:


       由执行结果我们可以看出来,此时 session 中没有包含任何属性,也就是说上面的那些对象都是存放在模型属性中,而不是存放在 session 属性中。那要如何才能存放在 session 属性中呢?这个时候我们先引入一个新的概念 @SessionAttributes ,它的用法会在讲完 @ModelAttribute 之后介绍,这里我们就先拿来用一下。我们在 MyController 类上加上 @SessionAttributes 属性标记哪些是需要存放到 session 中的。看下面的代码:

 
Java代码  收藏代码

    @Controller  
    @RequestMapping ( "/myTest" )  
    @SessionAttributes (value={ "intValue" , "stringValue" }, types={User. class })  
    public class MyController {  
      
        @ModelAttribute ( "hello" )  
        public String getModel() {  
           System. out .println( "-------------Hello---------" );  
           return "world" ;  
        }  
      
        @ModelAttribute ( "intValue" )  
        public int getInteger() {  
           System. out .println( "-------------intValue---------------" );  
           return 10;  
        }  
         
        @RequestMapping ( "sayHello" )  
        public void sayHello(Map<String, Object> map, @ModelAttribute ( "hello" ) String hello, @ModelAttribute ( "intValue" ) int num, @ModelAttribute ( "user2" ) User user, Writer writer, HttpServletRequest request) throws IOException {  
           map.put( "stringValue" , "String" );  
           writer.write( "Hello " + hello + " , Hello " + user.getUsername() + num);  
           writer.write( "\r" );  
           HttpSession session = request.getSession();  
           Enumeration enume = session.getAttributeNames();  
           while (enume.hasMoreElements())  
               writer.write(enume.nextElement() + "\r" );  
           System. out .println(session);  
        }  
      
        @ModelAttribute ( "user2" )  
        public User getUser() {  
           System. out .println( "---------getUser-------------" );  
           return new User(3, "user2" );  
        }  
    }   

 

       在上面代码中我们指定了属性为 intValue 或 stringValue 或者类型为 User 的都会放到 Session 中,利用上面的代码当我们访问 /myTest/sayHello.do 的时候,结果如下:


       仍然没有打印出任何 session 属性,这是怎么回事呢?怎么定义了把模型中属性名为 intValue 的对象和类型为 User 的对象存到 session 中,而实际上没有加进去呢?难道我们错啦?我们当然没有错,只是在第一次访问 /myTest/sayHello.do 的时候 @SessionAttributes 定义了需要存放到 session 中的属性,而且这个模型中也有对应的属性,但是这个时候还没有加到 session 中,所以 session 中不会有任何属性,等处理器方法执行完成后 Spring 才会把模型中对应的属性添加到 session 中。所以当请求第二次的时候就会出现如下结果:


当 @ModelAttribute 标记在处理器方法参数上的时候,表示该参数的值将从模型或者 Session 中取对应名称的属性值,该名称可以通过 @ModelAttribute(“attributeName”) 来指定,若未指定,则使用参数类型的类名称(首字母小写)作为属性名称。

 
Java代码  收藏代码

    @Controller  
    @RequestMapping ( "/myTest" )  
    public class MyController {  
      
        @ModelAttribute ( "hello" )  
        public String getModel() {  
           return "world" ;  
        }  
      
        @RequestMapping ( "sayHello" )  
        public void sayHello( @ModelAttribute ( "hello" ) String hello, Writer writer) throws IOException {  
           writer.write( "Hello " + hello);  
        }     
    }   

 

在上面代码中,当我们请求/myTest/sayHello.do 的时候,由于MyController 中的方法getModel 使用了注解@ModelAttribute 进行标记,所以在执行请求方法sayHello 之前会先执行getModel 方法,这个时候getModel 方法返回一个字符串world 并把它以属性名hello 保存在模型中,接下来访问请求方法sayHello 的时候,该方法的hello 参数使用@ModelAttribute(“hello”) 进行标记,这意味着将从session 或者模型中取属性名称为hello 的属性值赋给hello 参数,所以这里hello 参数将被赋予值world ,所以请求完成后将会在页面上看到Hello world 字符串。

@SessionAttributes 用于标记需要在Session 中使用到的数据,包括从Session 中取数据和存数据。@SessionAttributes 一般是标记在Controller 类上的,可以通过名称、类型或者名称加类型的形式来指定哪些属性是需要存放在session 中的。

 
Java代码  收藏代码

    @Controller  
    @RequestMapping ( "/myTest" )  
    @SessionAttributes (value={ "user1" , "blog1" }, types={User. class , Blog. class })  
    public class MyController {  
      
        @RequestMapping ( "setSessionAttribute" )  
        public void setSessionAttribute(Map<String, Object> map, Writer writer) throws IOException {  
           User user = new User(1, "user" );  
           User user1 = new User(2, "user1" );  
           Blog blog = new Blog(1, "blog" );  
           Blog blog1 = new Blog(2, "blog1" );  
           map.put( "user" , user);  
           map.put( "user1" , user1);  
           map.put( "blog" , blog);  
           map.put( "blog1" , blog1);  
           writer.write( "over." );  
        }  
      
       
      
        @RequestMapping ( "useSessionAttribute" )  
        public void useSessionAttribute(Writer writer, @ModelAttribute ( "user1" ) User user1, @ModelAttribute ( "blog1" ) Blog blog1) throws IOException {  
           writer.write(user1.getId() + "--------" + user1.getUsername());  
           writer.write( "\r" );  
           writer.write(blog1.getId() + "--------" + blog1.getTitle());  
        }  
      
        @RequestMapping ( "useSessionAttribute2" )  
        public void useSessionAttribute(Writer writer, @ModelAttribute ( "user1" ) User user1, @ModelAttribute ( "blog1" ) Blog blog1, @ModelAttribute User user, HttpSession session) throws IOException {  
           writer.write(user1.getId() + "--------" + user1.getUsername());  
           writer.write( "\r" );  
           writer.write(blog1.getId() + "--------" + blog1.getTitle());  
           writer.write( "\r" );  
           writer.write(user.getId() + "---------" + user.getUsername());  
           writer.write( "\r" );  
           Enumeration enume = session.getAttributeNames();  
           while (enume.hasMoreElements())  
               writer.write(enume.nextElement() + " \r" );  
        }  
      
        @RequestMapping ( "useSessionAttribute3" )  
        public void useSessionAttribute( @ModelAttribute ( "user2" ) User user) {  
      
        }  
    }   

 

   在上面代码中我们可以看到在MyController 上面使用了@SessionAttributes 标记了需要使用到的Session 属性。可以通过名称和类型指定需要存放到Session 中的属性,对应@SessionAttributes 注解的value 和types 属性。当使用的是types 属性的时候,那么使用的Session 属性名称将会是对应类型的名称(首字母小写)。当value 和types 两个属性都使用到了的时候,这时候取的是它们的并集,而不是交集,所以上面代码中指定要存放在Session 中的属性有名称为user1 或blog1 的对象,或类型为User 或Blog 的对象。在上面代码中我们首先访问/myTest/setSessionAttribute.do ,该请求将会请求到MyController 的setSessionAttribute 方法,在该方法中,我们往模型里面添加了user 、user1 、blog 和blog1 四个属性,因为它们或跟类上的@SessionAttributes 定义的需要存到session 中的属性名称相同或类型相同,所以在请求完成后这四个属性都将添加到session 属性中。接下来访问/myTest/useSessionAttribute.do ,该请求将会请求MyController 的useSessionAttribute(Writer writer, @ModelAttribute(“user1”) User user1, @ModelAttribute(“blog1”) Blog blog) 方法,该方法参数中用@ModelAttribute 指定了参数user1 和参数blog1 是需要从session 或模型中绑定的,恰好这个时候session 中已经有了这两个属性,所以这个时候在方法执行之前会先绑定这两个参数。执行结果如下图所示:


   接下来访问/myTest/useSessionAttribute2.do ,这个时候请求的是上面代码中对应的第二个useSessionAttribute 方法,方法参数user 、user1 和blog1 用@ModelAttribute 声明了需要session 或模型属性注入,我们知道在请求/myTest/setSessionAttribute.do 的时候这些属性都已经添加到了session 中,所以该请求的结果会如下图所示:


   接下来访问/myTest/useSessionAttribute3.do ,这个时候请求的是上面代码中对应的第三个useSessionAttribute 方法,我们可以看到该方法的方法参数user 使用了@ModelAttribute(“user2”) 进行标记,表示user 需要session 中的user2 属性来注入,但是这个时候我们知道session 中是不存在user2 属性的,所以这个时候就会报错了。执行结果如图所示:


(八)定制自己的类型转换器

         在通过处理器方法参数接收 request 请求参数绑定数据的时候,对于一些简单的数据类型 Spring 会帮我们自动进行类型转换,而对于一些复杂的类型由于 Spring 没法识别,所以也就不能帮助我们进行自动转换了,这个时候如果我们需要 Spring 来帮我们自动转换的话就需要我们给 Spring 注册一个对特定类型的识别转换器。 Spring 允许我们提供两种类型的识别转换器,一种是注册在 Controller 中的,一种是注册在 SpringMVC 的配置文件中。聪明的读者看到这里应该可以想到它们的区别了,定义在 Controller 中的是局部的,只在当前 Controller 中有效,而放在 SpringMVC 配置文件中的是全局的,所有 Controller 都可以拿来使用。
1. 在 @InitBinder 标记的方法中定义局部的类型转换器

         我们可以使用 @InitBinder 注解标注在 Controller 方法上,然后在方法体里面注册数据绑定的转换器,这主要是通过 WebDataBinder 进行的。我们可以给需要注册数据绑定的转换器的方法一个 WebDataBinder 参数,然后给该方法加上 @InitBinder 注解,这样当该 Controller 中在处理请求方法时如果发现有不能解析的对象的时候,就会看该类中是否有使用 @InitBinder 标记的方法,如果有就会执行该方法,然后看里面定义的类型转换器是否与当前需要的类型匹配。

 
Java代码  收藏代码

    @Controller  
    @RequestMapping ( "/myTest" )  
    public class MyController {  
      
        @InitBinder  
        public void dataBinder(WebDataBinder binder) {  
           DateFormat dateFormat = new SimpleDateFormat( "yyyyMMdd" );  
           PropertyEditor propertyEditor = new CustomDateEditor(dateFormat, true ); // 第二个参数表示是否允许为空  
           binder.registerCustomEditor(Date. class , propertyEditor);  
        }  
      
        @RequestMapping ( "dataBinder/{date}" )  
        public void testDate( @PathVariable Date date, Writer writer) throws IOException {  
           writer.write(String.valueOf (date.getTime()));  
        }  
      
    }   

 

       在上面的代码中当我们请求 /myTest/dataBinder/20121212.do 的时候, Spring 就会利用 @InitBinder 标记的方法里面定义的类型转换器把字符串 20121212 转换为一个 Date 对象。这样定义的类型转换器是局部的类型转换器,一旦出了这个 Controller 就不会再起作用。类型转换器是通过 WebDataBinder 对象的 registerCustomEditor 方法来注册的,要实现自己的类型转换器就要实现自己的 PropertyEditor 对象。 Spring 已经给我们提供了一些常用的属性编辑器,如 CustomDateEditor 、 CustomBooleanEditor 等。

       PropertyEditor 是一个接口,要实现自己的 PropertyEditor 类我们可以实现这个接口,然后实现里面的方法。但是 PropertyEditor 里面定义的方法太多了,这样做比较麻烦。在 java 中有一个封装类是实现了 PropertyEditor 接口的,它是 PropertyEditorSupport 类。所以如果需要实现自己的 PropertyEditor 的时候只需要继承 PropertyEditorSupport 类,然后重写其中的一些方法。一般就是重写 setAsText 和 getAsText 方法就可以了, setAsText 方法是用于把字符串类型的值转换为对应的对象的,而 getAsText 方法是用于把对象当做字符串来返回的。在 setAsText 中我们一般先把字符串类型的对象转为特定的对象,然后利用 PropertyEditor 的 setValue 方法设定转换后的值。在 getAsText 方法中一般先使用 getValue 方法取代当前的对象,然后把它转换为字符串后再返回给 getAsText 方法。下面是一个示例:

 
Java代码  收藏代码

    @InitBinder  
    public void dataBinder(WebDataBinder binder) {  
       // 定义一个 User 属性编辑器  
       PropertyEditor userEditor = new PropertyEditorSupport() {  
      
           @Override  
           public String getAsText() {  
              // TODO Auto-generated method stub  
              User user = (User) getValue();  
              return user.getUsername();  
           }  
      
           @Override  
           public void setAsText(String userStr) throws IllegalArgumentException {  
              // TODO Auto-generated method stub  
              User user = new User(1, userStr);  
              setValue(user);  
           }  
       };  
       // 使用 WebDataBinder 注册 User 类型的属性编辑器  
       binder.registerCustomEditor(User. class , userEditor);  
    }   

 

   
2. 实现 WebBindingInitializer 接口定义全局的类型转换器

       如果需要定义全局的类型转换器就需要实现自己的 WebBindingInitializer 对象,然后把该对象注入到 AnnotationMethodHandlerAdapter 中,这样 Spring 在遇到自己不能解析的对象的时候就会到全局的 WebBindingInitializer 的 initBinder 方法中去找,每次遇到不认识的对象时, initBinder 方法都会被执行一遍。

 
Java代码  收藏代码

    public class MyWebBindingInitializer implements WebBindingInitializer {  
      
        @Override  
        public void initBinder(WebDataBinder binder, WebRequest request) {  
           // TODO Auto-generated method stub  
           DateFormat dateFormat = new SimpleDateFormat( "yyyyMMdd" );  
           PropertyEditor propertyEditor = new CustomDateEditor(dateFormat, true );  
           binder.registerCustomEditor(Date. class , propertyEditor);  
        }  
      
    }   

 

定义了这么一个 WebBindingInitializer 对象之后 Spring 还是不能识别其中指定的对象,这是因为我们只是定义了 WebBindingInitializer 对象,还没有把它交给 Spring , Spring 不知道该去哪里找解析器。要让 Spring 能够识别还需要我们在 SpringMVC 的配置文件中定义一个 AnnotationMethodHandlerAdapter 类型的 bean 对象,然后利用自己定义的 WebBindingInitializer 覆盖它的默认属性 webBindingInitializer 。

 
Xml代码  收藏代码

    < bean class = "org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" >  
       < property name = "webBindingInitializer" >  
           < bean class = "com.host.app.web.util.MyWebBindingInitializer" />  
       </ property >  
    </ bean >   

 
3.触发数据绑定方法的时间

当Controller处理器方法参数使用@RequestParam、@PathVariable、@RequestHeader、@CookieValue和@ModelAttribute标记的时候都会触发initBinder方法的执行,这包括使用WebBindingInitializer定义的全局方法和在Controller中使用@InitBinder标记的局部方法。而且每个使用了这几个注解标记的参数都会触发一次initBinder方法的执行,这也意味着有几个参数使用了上述注解就会触发几次initBinder方法的执行。

 
Servlet 3.0 新特性之文件上传
文件上传改进API

 

原本文件上传时通过 common-fileupload或者SmartUpload,上传比较麻烦,在Servlet 3.0 中不需要导入任何第三方jar包,并且提供了很方便进行文件上传的功能;

 

注意点:

1. html中 <input type="file">表示文件上传控件;

2. form的 enctype="multipart/form-data";

3.在Servlet类前加上 @MultipartConfig

4.request.getPart()获得;

 

下面是一个文件上传的例子:

upload.html

 
[html] view plaincopy

    <html>  
           <body>  
                  <form method="post" enctype="multipart/form-data" action="upload">  
                 <input type="file" id="file" name="file"/>  
                 <input type="text" id="name" name="name"/>  
                  <input type="submit" value="提交"/>  
                  </form>  
           </body>  
    </html>  


UploadServlet.java

 
[java] view plaincopy

    package org.servlet;  
    import java.io.*;  
    import javax.servlet.*;  
    import javax.servlet.http.*;  
    import javax.servlet.annotation.*;  
       
    @WebServlet(name="UploadServlet" ,urlPatterns={"/upload"})  
    @MultipartConfig  
    public class UploadServlet extends HttpServlet{  
           public void init(ServletConfig config)throws ServletException{  
                  super.init(config);  
           }  
           public void service(HttpServletRequest request,HttpServletResponse response)throws ServletException,IOException{  
                  Part part = request.getPart("file");  
                  PrintWriter out = response.getWriter();  
                  out.println("此文件的大小:"+part.getSize()+"<br />");  
                  out.println("此文件类型:"+part.getContentType()+"<br />");  
                  out.println("文本框内容:"+request.getParameter("name")+"<br />");  
                  out.println(UploadUtil.getFileName(part)+"<br />");  
                  part.write("F:\\1."+UploadUtil.getFileType(part));  
           }  
    }  

UploadUtil.java

由于在Servlet 3.0中很难获取上传文件的类型,因此我写了两个工具类,可以方便开发;

 
[java] view plaincopy

    /** 
     * 此工具类只适用于Servlet 3.0 
     * 为了弥补 Servlet 3.0 文件上传时获取文件类型的困难问题 
     *  
     * @author xiazdong 
     */  
    import javax.servlet.http.*;  
    public class UploadUtil{  
           public static String getFileType(Part p){  
                  String name = p.getHeader("content-disposition");  
                  String fileNameTmp = name.substring(name.indexOf("filename=")+10);  
                  String type = fileNameTmp.substring(fileNameTmp.indexOf(".")+1,fileNameTmp.indexOf("\""));  
                  return type;  
           }  
           public static String getFileName(Part p){  
                  String name = p.getHeader("content-disposition");  
                  String fileNameTmp = name.substring(name.indexOf("filename=")+10);  
                  String fileName = fileNameTmp.substring(0,fileNameTmp.indexOf("\""));  
                  return fileName;  
           }  
    }  
      
       
       
webApplicationContext 与servletContext
webApplicationContext 与servletContext
(2011-12-01 16:39:52)
转载▼
标签:
杂谈
	分类: java

1.WebApplicationContext的研究

      ApplicationContext是spring的核心,Context通常解释为上下文环境,用“容器”来表述更容易理解一些,ApplicationContext则是“应用的容器了”了。

     spring把bean放在这个容器中,在需要的时候,用getBean()方法取出,在web应用中,会用到webApplicationContext,继承自ApplicationContext

    在web.xml初始化WebApplicationContext:

   <context-param>

           <param-name>contextConfigLocation</param-name>

           <param-value>/WEB-INF/applicationContext.xml</param-value>

  </context-param>

<listener>

     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

</listener>

或者用ContextLoaderServlert亦可(加<load-on-startup>1</load-on-startup>)

 

2.ServletContext详解

    ServletContext 是Servlet与Servlet容器之间直接通信的接口,Servlet容器在启动一个web应用时,会为它创建一个ServletContext对 象,每个web应用有唯一的ServletContext对象,同一个web应用的所有Servlet对象共享一个 ServletContext,Servlet对象可以通过它来访问容器中的各种资源

存取共享数据方法:

 setAttribute(String name,Object obj)

getAttribute(String name)


     WebApplicationContext  ctx=WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext());
       
         DataSource ds=    ctx.getBean(DataSource.class);
         jRExportService.setDataSource(ds)

这里得到了spring 的webApplicationContext ,spring的bean都放在里面,然后直接getBean就可以得到了
从Spring的Bean中获取servletcontext 和 applicationContext[摘抄]
    import javax.servlet.ServletContext;  
      
    import org.springframework.context.ApplicationContext;  
    import org.springframework.context.ApplicationContextAware;  
    import org.springframework.web.context.ServletContextAware;  
      
    /** 
     * @作者 黄嘉寅 
     * @功能 客户端与服务端数据传输接口 
     * @日期 2008-8-28 
     */  
    public class SysInterface implements ApplicationContextAware,ServletContextAware{  
       
     /**  
      * 功能 : 实现 ApplicationContextAware接口,由Spring自动注入 Spring上下文对象 
      *  
      **/  
     public void setApplicationContext(ApplicationContext actx)   throws BeansException   
     {  
      }  
      
     /**  
      * 功能 : 实现 ServletContextAware接口,由Spring自动注入 系统上下文对象 
      *  
      **/  
     public void setServletContext(ServletContext sctx)   
     {   
    }  
    }  
Comet:基于 HTTP 长连接的“服务器推”技术 http://www.ibm.com/developerworks/cn/web/wa-lo-comet/index.html#resources
“服务器推”技术的应用

请访问 Ajax 技术资源中心,这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。

	RSS 	订阅 Ajax 相关文章和教程的 RSS 提要

传统模式的 Web 系统以客户端发出请求、服务器端响应的方式工作。这种方式并不能满足很多现实应用的需求,譬如:

    监控系统:后台硬件热插拔、LED、温度、电压发生变化;
    即时通信系统:其它用户登录、发送信息;
    即时报价系统:后台数据库内容发生变化;

这些应用都需要服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求。“服务器推”技术在现实应用中有一些解决方案,本文将这些解决方案分为两类:一类需要在浏览器端安装插件,基于套接口传送信息,或是使用 RMI、CORBA 进行远程调用;而另一类则无须浏览器安装任何插件、基于 HTTP 长连接。

将“服务器推”应用在 Web 程序中,首先考虑的是如何在功能有限的浏览器端接收、处理信息:

    客户端如何接收、处理信息,是否需要使用套接口或是使用远程调用。客户端呈现给用户的是 HTML 页面还是 Java applet 或 Flash 窗口。如果使用套接口和远程调用,怎么和 JavaScript 结合修改 HTML 的显示。
    客户与服务器端通信的信息格式,采取怎样的出错处理机制。
    客户端是否需要支持不同类型的浏览器如 IE、Firefox,是否需要同时支持 Windows 和 Linux 平台。

回页首

基于客户端套接口的“服务器推”技术

Flash XMLSocket

如果 Web 应用的用户接受应用只有在安装了 Flash 播放器才能正常运行, 那么使用 Flash 的 XMLSocket 也是一个可行的方案。

这种方案实现的基础是:

    Flash 提供了 XMLSocket 类。
    JavaScript 和 Flash 的紧密结合:在 JavaScript 可以直接调用 Flash 程序提供的接口。

具体实现方法:在 HTML 页面中内嵌入一个使用了 XMLSocket 类的 Flash 程序。JavaScript 通过调用此 Flash 程序提供的套接口接口与服务器端的套接口进行通信。JavaScript 在收到服务器端以 XML 格式传送的信息后可以很容易地控制 HTML 页面的内容显示。

关于如何去构建充当了 JavaScript 与 Flash XMLSocket 桥梁的 Flash 程序,以及如何在 JavaScript 里调用 Flash 提供的接口,我们可以参考 AFLAX(Asynchronous Flash and XML)项目提供的 Socket Demo 以及 SocketJS(请参见 参考资源)。

Javascript 与 Flash 的紧密结合,极大增强了客户端的处理能力。从 Flash 播放器 V7.0.19 开始,已经取消了 XMLSocket 的端口必须大于 1023 的限制。Linux 平台也支持 Flash XMLSocket 方案。但此方案的缺点在于:

    客户端必须安装 Flash 播放器;
    因为 XMLSocket 没有 HTTP 隧道功能,XMLSocket 类不能自动穿过防火墙;
    因为是使用套接口,需要设置一个通信端口,防火墙、代理服务器也可能对非 HTTP 通道端口进行限制;

不过这种方案在一些网络聊天室,网络互动游戏中已得到广泛使用。

Java Applet 套接口

在客户端使用 Java Applet,通过 java.net.Socket 或 java.net.DatagramSocket 或 java.net.MulticastSocket 建立与服务器端的套接口连接,从而实现“服务器推”。

这种方案最大的不足在于 Java applet 在收到服务器端返回的信息后,无法通过 JavaScript 去更新 HTML 页面的内容。

回页首

基于 HTTP 长连接的“服务器推”技术

Comet 简介

浏览器作为 Web 应用的前台,自身的处理功能比较有限。浏览器的发展需要客户端升级软件,同时由于客户端浏览器软件的多样性,在某种意义上,也影响了浏览器新技术的推广。在 Web 应用中,浏览器的主要工作是发送请求、解析服务器返回的信息以不同的风格显示。AJAX 是浏览器技术发展的成果,通过在浏览器端发送异步请求,提高了单用户操作的响应性。但 Web 本质上是一个多用户的系统,对任何用户来说,可以认为服务器是另外一个用户。现有 AJAX 技术的发展并不能解决在一个多用户的 Web 应用中,将更新的信息实时传送给客户端,从而用户可能在“过时”的信息下进行操作。而 AJAX 的应用又使后台数据更新更加频繁成为可能。

图 1. 传统的 Web 应用模型与基于 AJAX 的模型之比较
图 1. 传统的 Web 应用模型与基于 AJAX 的模型之比较

“服务器推”是一种很早就存在的技术,以前在实现上主要是通过客户端的套接口,或是服务器端的远程调用。因为浏览器技术的发展比较缓慢,没有为“服务器推”的实现提供很好的支持,在纯浏览器的应用中很难有一个完善的方案去实现“服务器推”并用于商业程序。最近几年,因为 AJAX 技术的普及,以及把 IFrame 嵌在“htmlfile“的 ActiveX 组件中可以解决 IE 的加载显示问题,一些受欢迎的应用如 meebo,gmail+gtalk 在实现中使用了这些新技术;同时“服务器推”在现实应用中确实存在很多需求。因为这些原因,基于纯浏览器的“服务器推”技术开始受到较多关注,Alex Russell(Dojo Toolkit 的项目 Lead)称这种基于 HTTP 长连接、无须在浏览器端安装插件的“服务器推”技术为“Comet”。目前已经出现了一些成熟的 Comet 应用以及各种开源框架;一些 Web 服务器如 Jetty 也在为支持大量并发的长连接进行了很多改进。关于 Comet 技术最新的发展状况请参考关于 Comet 的 wiki。

下面将介绍两种 Comet 应用的实现模型。

基于 AJAX 的长轮询(long-polling)方式

如 图 1 所示,AJAX 的出现使得 JavaScript 可以调用 XMLHttpRequest 对象发出 HTTP 请求,JavaScript 响应处理函数根据服务器返回的信息对 HTML 页面的显示进行更新。使用 AJAX 实现“服务器推”与传统的 AJAX 应用不同之处在于:

    服务器端会阻塞请求直到有数据传递或超时才返回。
    客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。
    当客户端处理接收的数据、重新建立连接时,服务器端可能有新的数据到达;这些信息会被服务器端保存直到客户端重新建立连接,客户端会一次把当前服务器端所有的信息取回。


图 2. 基于长轮询的服务器推模型
图 2. 基于长轮询的服务器推模型

一些应用及示例如 “Meebo”, “Pushlet Chat” 都采用了这种长轮询的方式。相对于“轮询”(poll),这种长轮询方式也可以称为“拉”(pull)。因为这种方案基于 AJAX,具有以下一些优点:请求异步发出;无须安装插件;IE、Mozilla FireFox 都支持 AJAX。

在这种长轮询方式下,客户端是在 XMLHttpRequest 的 readystate 为 4(即数据传输结束)时调用回调函数,进行信息处理。当 readystate 为 4 时,数据传输结束,连接已经关闭。Mozilla Firefox 提供了对 Streaming AJAX 的支持, 即 readystate 为 3 时(数据仍在传输中),客户端可以读取数据,从而无须关闭连接,就能读取处理服务器端返回的信息。IE 在 readystate 为 3 时,不能读取服务器返回的数据,目前 IE 不支持基于 Streaming AJAX。

基于 Iframe 及 htmlfile 的流(streaming)方式

iframe 是很早就存在的一种 HTML 标记, 通过在 HTML 页面里嵌入一个隐蔵帧,然后将这个隐蔵帧的 SRC 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。

图 3. 基于流方式的服务器推模型
图 3. 基于流方式的服务器推模型

上节提到的 AJAX 方案是在 JavaScript 里处理 XMLHttpRequest 从服务器取回的数据,然后 Javascript 可以很方便的去控制 HTML 页面的显示。同样的思路用在 iframe 方案的客户端,iframe 服务器端并不返回直接显示在页面的数据,而是返回对客户端 Javascript 函数的调用,如“<script type="text/javascript">js_func(“data from server ”)</script>”。服务器端将返回的数据作为客户端 JavaScript 函数的参数传递;客户端浏览器的 Javascript 引擎在收到服务器返回的 JavaScript 调用时就会去执行代码。

从 图 3 可以看到,每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。

使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。Google 的天才们使用一个称为“htmlfile”的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。Alex Russell 在 “What else is burried down in the depth's of Google's amazing JavaScript?”文章中介绍了这种方法。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。(请参见 参考资源)

回页首

使用 Comet 模型开发自己的应用

上面介绍了两种基于 HTTP 长连接的“服务器推”架构,更多描述了客户端处理长连接的技术。对于一个实际的应用而言,系统的稳定性和性能是非常重要的。将 HTTP 长连接用于实际应用,很多细节需要考虑。

不要在同一客户端同时使用超过两个的 HTTP 长连接

我们使用 IE 下载文件时会有这样的体验,从同一个 Web 服务器下载文件,最多只能有两个文件同时被下载。第三个文件的下载会被阻塞,直到前面下载的文件下载完毕。这是因为 HTTP 1.1 规范中规定,客户端不应该与服务器端建立超过两个的 HTTP 连接, 新的连接会被阻塞。而 IE 在实现中严格遵守了这种规定。

HTTP 1.1 对两个长连接的限制,会对使用了长连接的 Web 应用带来如下现象:在客户端如果打开超过两个的 IE 窗口去访问同一个使用了长连接的 Web 服务器,第三个 IE 窗口的 HTTP 请求被前两个窗口的长连接阻塞。

所以在开发长连接的应用时, 必须注意在使用了多个 frame 的页面中,不要为每个 frame 的页面都建立一个 HTTP 长连接,这样会阻塞其它的 HTTP 请求,在设计上考虑让多个 frame 的更新共用一个长连接。

服务器端的性能和可扩展性

一般 Web 服务器会为每个连接创建一个线程,如果在大型的商业应用中使用 Comet,服务器端需要维护大量并发的长连接。在这种应用背景下,服务器端需要考虑负载均衡和集群技术;或是在服务器端为长连接作一些改进。

应用和技术的发展总是带来新的需求,从而推动新技术的发展。HTTP 1.1 与 1.0 规范有一个很大的不同:1.0 规范下服务器在处理完每个 Get/Post 请求后会关闭套接口连接; 而 1.1 规范下服务器会保持这个连接,在处理两个请求的间隔时间里,这个连接处于空闲状态。 Java 1.4 引入了支持异步 IO 的 java.nio 包。当连接处于空闲时,为这个连接分配的线程资源会返还到线程池,可以供新的连接使用;当原来处于空闲的连接的客户发出新的请求,会从线程池里分配一个线程资源处理这个请求。 这种技术在连接处于空闲的机率较高、并发连接数目很多的场景下对于降低服务器的资源负载非常有效。

但是 AJAX 的应用使请求的出现变得频繁,而 Comet 则会长时间占用一个连接,上述的服务器模型在新的应用背景下会变得非常低效,线程池里有限的线程数甚至可能会阻塞新的连接。Jetty 6 Web 服务器针对 AJAX、Comet 应用的特点进行了很多创新的改进,请参考文章“AJAX,Comet and Jetty”(请参见 参考资源)。

控制信息与数据信息使用不同的 HTTP 连接

使用长连接时,存在一个很常见的场景:客户端网页需要关闭,而服务器端还处在读取数据的堵塞状态,客户端需要及时通知服务器端关闭数据连接。服务器在收到关闭请求后首先要从读取数据的阻塞状态唤醒,然后释放为这个客户端分配的资源,再关闭连接。

所以在设计上,我们需要使客户端的控制请求和数据请求使用不同的 HTTP 连接,才能使控制请求不会被阻塞。

在实现上,如果是基于 iframe 流方式的长连接,客户端页面需要使用两个 iframe,一个是控制帧,用于往服务器端发送控制请求,控制请求能很快收到响应,不会被堵塞;一个是显示帧,用于往服务器端发送长连接请求。如果是基于 AJAX 的长轮询方式,客户端可以异步地发出一个 XMLHttpRequest 请求,通知服务器端关闭数据连接。

在客户和服务器之间保持“心跳”信息

在浏览器与服务器之间维持一个长连接会为通信带来一些不确定性:因为数据传输是随机的,客户端不知道何时服务器才有数据传送。服务器端需要确保当客户端不再工作时,释放为这个客户端分配的资源,防止内存泄漏。因此需要一种机制使双方知道大家都在正常运行。在实现上:

    服务器端在阻塞读时会设置一个时限,超时后阻塞读调用会返回,同时发给客户端没有新数据到达的心跳信息。此时如果客户端已经关闭,服务器往通道写数据会出现异常,服务器端就会及时释放为这个客户端分配的资源。
    如果客户端使用的是基于 AJAX 的长轮询方式;服务器端返回数据、关闭连接后,经过某个时限没有收到客户端的再次请求,会认为客户端不能正常工作,会释放为这个客户端分配、维护的资源。
    当服务器处理信息出现异常情况,需要发送错误信息通知客户端,同时释放资源、关闭连接。

Pushlet - 开源 Comet 框架

Pushlet 是一个开源的 Comet 框架,在设计上有很多值得借鉴的地方,对于开发轻量级的 Comet 应用很有参考价值。

观察者模型

Pushlet 使用了观察者模型:客户端发送请求,订阅感兴趣的事件;服务器端为每个客户端分配一个会话 ID 作为标记,事件源会把新产生的事件以多播的方式发送到订阅者的事件队列里。

客户端 JavaScript 库

pushlet 提供了基于 AJAX 的 JavaScript 库文件用于实现长轮询方式的“服务器推”;还提供了基于 iframe 的 JavaScript 库文件用于实现流方式的“服务器推”。

JavaScript 库做了很多封装工作:

    定义客户端的通信状态:STATE_ERROR、STATE_ABORT、STATE_NULL、STATE_READY、STATE_JOINED、STATE_LISTENING;
    保存服务器分配的会话 ID,在建立连接之后的每次请求中会附上会话 ID 表明身份;
    提供了 join()、leave()、subscribe()、 unsubsribe()、listen() 等 API 供页面调用;
    提供了处理响应的 JavaScript 函数接口 onData()、onEvent()…

网页可以很方便地使用这两个 JavaScript 库文件封装的 API 与服务器进行通信。

客户端与服务器端通信信息格式

pushlet 定义了一套客户与服务器通信的信息格式,使用 XML 格式。定义了客户端发送请求的类型:join、leave、subscribe、unsubscribe、listen、refresh;以及响应的事件类型:data、join_ack、listen_ack、refresh、heartbeat、error、abort、subscribe_ack、unsubscribe_ack。

服务器端事件队列管理

pushlet 在服务器端使用 Java Servlet 实现,其数据结构的设计框架仍可适用于 PHP、C 编写的后台客户端。

Pushlet 支持客户端自己选择使用流、拉(长轮询)、轮询方式。服务器端根据客户选择的方式在读取事件队列(fetchEvents)时进行不同的处理。“轮询”模式下 fetchEvents() 会马上返回。”流“和”拉“模式使用阻塞的方式读事件,如果超时,会发给客户端发送一个没有新信息收到的“heartbeat“事件,如果是“拉”模式,会把“heartbeat”与“refresh”事件一起传给客户端,通知客户端重新发出请求、建立连接。

客户服务器之间的会话管理

服务端在客户端发送 join 请求时,会为客户端分配一个会话 ID, 并传给客户端,然后客户端就通过此会话 ID 标明身份发出 subscribe 和 listen 请求。服务器端会为每个会话维护一个订阅的主题集合、事件队列。

服务器端的事件源会把新产生的事件以多播的方式发送到每个会话(即订阅者)的事件队列里。

回页首

小结

本文介绍了如何在现有的技术基础上选择合适的方案开发一个“服务器推”的应用,最优的方案还是取决于应用需求的本身。相对于传统的 Web 应用, 目前开发 Comet 应用还是具有一定的挑战性。

“服务器推”存在广泛的应用需求,为了使 Comet 模型适用于大规模的商业应用,以及方便用户构建 Comet 应用,最近几年,无论是服务器还是浏览器都出现了很多新技术,同时也出现了很多开源的 Comet 框架、协议。需求推动技术的发展,相信 Comet 的应用会变得和 AJAX 一样普及。

参考资料

学习

    developerWorks 文章“ 面向 Java 开发人员的 Ajax: 使用 Jetty 和 Direct Web Remoting 编写可扩展的 Comet 应用程序”:受异步服务器端事件驱动的 Ajax 应用程序实现较为困难,本文介绍了一种结合使用 Comet 模式和 Jetty 6 Continuations API 的解决方法。

    “Comet: Low Latency Data for the Browser”:Alex Russell 是 Dojo Toolkit 的项目主管和 Dojo Foundation 的主席,他在这篇博客文章中提出了 Comet 这个术语。

    “What else is burried down in the depth’s of Google’s amazing JavaScript?”(Alex Russel,2006 年 2 月):Alex 在这篇文章里介绍了如何使用“htmlfile”ActiveX 控件解决 iframe 请求长连接时 IE 的加载显示问题。

    Comet wiki:提供了很多开源 Comet 框架的链接。

    Jetty:Jetty 是一种开源的基于标准的 Web 服务器,完全使用 Java 语言实现。

    “Ajax, Comet and Jetty”(Greg Wilkins,Webtide,2006 年 1 月):Wilkins 的这份白皮书讨论了扩展 Ajax 连接的 Jetty 架构方法。

    Continuations:了解更多关于 Jetty 的 Continuations 特性的信息。

    “pushlet”:开源 comet 框架,使用了观察者模型。浏览器端提供了基于 AJAX 和 iframe 的 JavaScript 库,服务器端使用 Java Servlet。

    “How to implement COMET with PHP”:提供的 comet-iframe.tar.gz 使用 iframe/htmlfile 封装了一个 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器。

    “AFLAX”:Asynchronous Flash and XML,提供了强大的 Flash、Javascript 库和很多范例。

    developerWorks Ajax 技术资源中心:能找到更多关于 Ajax 技术的文章和教程。

    developerWorks Web 开发技术专区:提供了关于 Web 开发和架构方面的大量文章。

    developerWorks Java 技术专区:提供了关于 Java 编程各个方面的数百篇文章。

    浏览 技术书店,查阅有关本文所述主题以及其他技术主题的书籍。 
短信服务SMS,短信网关ISMG,CMPP协议
摘要:笔者主要对短信服务SMS、因特网短信网关ISMG等基本概念进行了简述,并对中国移动点对点CMPP协议所涉及的技术进行了分析。
  关键词:TCP/IP;CMPP协议;中国移动;SMS
  (一)短信业务及短信平台的应用
  1.1短信SMS
  短信(简称SMS)是用户通过手机或其他电信终端直接发送或接收的文字或数字信息,用户每次能接收和发送短信的字符数,是160个英文或数字字符,或者70个中文字符。短信的发起方式总体上分为3种,即由移动通信终端(手机)始发,也可由移动通信运营商(如中国移动、中国联通及中国电信)的短信平台服务器始发,还可由与移动运营商短信平台互联的服务提供商(Service/ContentProvider,以下简称SP/CP)发起。
  1.2短信平台及及短信业务
  当服务提供商(SP/CP)与移动通信运营商搭成合作后,将由SP/CP遵循相关短信通信协议(电信的SMGP,移动的CMPP、联通的SGIP)开发短信平台,短信平台通过专线或国际互联网连接到移动通信运营商短信网关ISMG,通过TCP/IP协议与ISMG之间进行收发、解析数据包来实现接收和发送SMS。SP/CP在能实现SMS的收发后,将整合自身在内容、营销、服务等方面的资源,从业务和技术的角度设计出短信业务。当移动终端(手机)用户有业务需求时,将通过发送相应信息到短信平台接入号,短信经短信中心(SMSC)后到达ISMG(因特网短信网关),然后在由ISMG推送到SP/CP的短信平台,短信平台经过一系列逻辑处理后,将短信提交到ISMG,由ISMG根据短信协议解析后传线SMSC,最终送到移动终端(手机)用户手机上。
  1.3短信网关ISMG
  短信网关(简称ISMG)主要是为了解决各网络、各运营商之间的短信互通和SP/CP的接入问题;是SP与运营冷商网内短信中心之间的中介实体,互联网短信网关一方面负责接收SP发送给移动用户的信息和提交给短信中心(SMSC)。另一方面,移动用户点播SP业务的信息将由短信中心通过ISMG发给SP另外,为了减轻短信中心的信令负荷,ISMG还应根据路由原则将SP提交的信息转发到相应的ISMG。ISMG通过向汇接网关(GNS)查询的方式获得网关间的转发路由信息。
  (二)移动CMPP协议及其与TCP/IP协议的关系
  2.1通信协议及CMPP协议
  SMS短信平台与ISMG间的通信是机器之间的通信,而大剖分是利用数据通信网将若干台计算机达成汁算机网络来实现的。所以数据通信也叫计算机通信。正由于数据通信是机器间的通信,所以和其它通信方式一样,应该在通信系统中规定一个统一标准,即通信的内存是什么、如何通信、何时通信,都必须在通信的实体之间达成大家都能接受的协定,这些协定就被称为通信协议。也可将协议定义为监督和管理两个实体之间的数据交换的一整套规则,概括地说,通信协议是对数据传送方式的规定,包括数据格式定义利数据位定义等。
  2.1.1CMPP中国移动点对点协议是SP/CP短信平台与中国移动通信集团ISMG之间的通信协议,当前版本为3.0。它是个基于数据包的交互式协议,TCP/IP作为底层通信承载。规定了以下三方面的内容:
  1)业务提供商与互联网短信网关之间的接口协议;
  2)互联网短信网关之间的接口协议;
  3)互联网短信网关与汇接网关之间的接口协议。
  在此协议中的业务提供商与互联网短信网关之间的接口协议是本论文中所要阐述的内容。
  2.1.2CMPP协议的协议栈
  CMPP协议以TCP/IP作为底层通信承载,由协议栈可以看出,SP/CP短信平台要与ISMG进行通信,首先要用TCP/IP协议作为通信承载,也就是说它是面向连结的,可靠的连接,在后续的编码实现中,就表现为能过Socket读取byte字节来通讯。
  2.2长连接、短连接及滑动窗口机制
  长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发链路检测包以维持此连接。通信双方以客户-服务器方式建立TCP连接,用于双方信息的相互提交。当信道上没有数据传输时,通信双方应每隔时间C发送链路检测包以维持此连接,当链路检测包发出超过时间T后未收到响应,应立即再发送链路检测包,再连续发送N-1次后仍未得到响应则断开此连接。
  2.3CMPP协议解析
  2.3.1消息数据包的构成由上面对CMPP的协议栈及通信方式的理解可以看出,SP/CP短信平台及中国移动ISMG之间将以客户-服务器方式建立TCP连接,在建立连接后通过收发数据包来进行通信,对数据包字段的语法、语义进行定义的就协议就是CMPP协议。在CMPP的定义中,消息数据包由包头和包体两部分组成。不同的CMPP包长度、功用及数据类型不一致,具体包体的大小由包头的Total_Length定义,功能由command_Id定义。
  2.3.2理解两种重要的操作
  1)Submit操作
  功能:是SP短信平台在与ISMG建立应用层连接后向ISMG提交短信。ISMG收到后会对Submit操作做出响应。方向:SP/CP短信平台→ISMG短信网关。
  2)Deliver操作
  功能:是ISMG把从短信中心或其它ISMG转发来的短信送交SP/CP短信平台。方向:ISMG短信网关→SP/CP短信平台。
  2.4数据包解
  从上面的论述得到在CP/SP短信平析及实现台通过TCP/IP连接到ISMG后,通过发CMPP数据包来实现SMS的收发,CMPP数据包由包头和包体构成。但只有ISMG和SP/CP短信平台在收到相应的数据流后,根据CMPP协议对数据转换解析后才能确定包的功能。对于从ISMG发来的数据流,先取12Byte的包头取第1-4Byte为Total_Length,将数据流转换成UnsignedInteger得到数据包的总体长度,随后的包体解析按此总长度进行取数据流并做相应转换。第5-8Byte为Command_Id,数据流转换成UnsignedInteger得到数据包的类型,标明是什么数据包,例如:command_id值为0x00000001表示请求连接CMPPCONNECT,值为0x00000005表示为CMPPDELIVER包。对于SP/CP短信平台发出的数据包,需根据CMPP协议规定包取出相应长度的存储空间,按协议规定进行包体组包。
  (三)结束语
  SMS短信是最早的短消息业务,而且也是现在普及率最高的短消息业务。SMS短信以简单方便的使用功能受到大众的欢迎。日前,一些服务于人们生活的方方面面的短信业务被陆续推出。新开通的短信业务,信息传递的双方变为商业机构与其用户,并利用GSM通信网络与企业内部的计算机网络相结合,通过向用户的手机上发送和接收纯文本信息的形式,达到双方沟通的目的。行业短信业务已涉及教育、金融、运输、检疫、电力、人才等多种领域。所以对短信平台开发的研究是必须也是迫切的。 
windows下修改mysql密码
Windows: 
1.用系统管理员登陆系统。 
3.进入命令窗口,然后进入MySQL的安装目录,比如我的安装目录是D:\xampp\mysql,进入D:\xampp\mysql\bin
4.启动MySQL, 
5.进入D:\xampp\mysql\bin目录,设置root的新密码 
D:\xampp\mysql\bin>mysqladmin -u root flush-privileges password "hitaii"
D:\xampp\mysql\bin>mysqladmin -u root -p shutdown
Enter password: ******
将newpassword替换为你要用的root的密码,第二个命令会提示你输入新密码,重复第一个命令输入的密码。 
6.停止MySQL Server,用正常模式启动Mysql 
7.你可以用新的密码链接到Mysql了。

php 操作sqllite 面向接口
SQLite PHP开发

目标读者

本文是为那些对学习SQLite 扩展类库感兴趣的PHP程序员而写的.本文将向读者介绍PHP SQLite扩展类库的功能,并且对SQLite与其它数据库系统的优势关系做了概要介绍.

假设读者对PHP和SQL有个基本的了解.如果有过MySQL或者PostgreSQL的经验那就是最好不过了.


介绍

在近几个月以来,你也许听过一个新的PHP数据库扩展类库SQLite.好多人认为SQLite是自有面包片以来最好的东东,其提供了一个快速的访问平面文件数据库的接口.并且提供了访问大容量数据库的简洁的手段,但是并没有所意想的功能或者速度上的损失.在本文中,我们将探讨这个新的激动人心的扩展库,并且希望以此来验证其传说中的优势和好处.


啥是SQLite?

SQLite 是实现了SQL 92标准的一个大子集的嵌入式数据库.其以在一个库中组合了数据库引擎和接口,能将所有数据存储于单个文件中而著名.我觉得SQLite的功能一定程度上居于MySQL 和PostgreSQL之间.尽管如此,在性能上面,SQLite常常快2-3倍 (甚至更多).这利益于其高度调整了的内部架构,因为它除去了服务器端到客户端和客户端到服务器端的通信.

所有这些都集在一个包中,也仅仅比MySQL的客户端的库稍微大一点.而令人印象深刻的特点是你可将你的整个数据库系统放在其中.利用非常高效的内存组织,SQLite只需在很小的内存中维护其很小的尺寸,远远比其它任何数据库系统都小.这些特点使得其成为在需要高效地应用数据库的任务中一个非常方便的工具.


它对我有啥用?

除了速度和效率,SQLite还有其它好多的优势使得其能成为许多任务中一个理想的解决方案.因为SQLite的数据库都是简单文件,因此无须一个管理队伍花时间来构造复杂的权限结构来保护用户的数据库.因为权限通过文件系统自动进行.这也同时意味着(数据库空间的大小只与环境有关,与本身无关)无段特殊的规则来了解用户磁盘空间.用户可以从创建他们想要的任意多的数据库和对其对这些数据库的绝对控制权而得到好处.

数据库就是一个文件的事实使用SQLite可以轻易地在服务器间移动.SQLite也除去了需要大量内存和其它系统资源的伺候进程.即使当数据库在大量地使用时也是如此.


SQLite 扩展库

作为最新的数据库扩展库,SQLite很幸运地没有遗留代码.不象其它的数据库比如MySQL,它必须为了保持向下兼容而维护一大堆陈旧过时的行为特性.它也使用扩展库可以利用最新的PHP特性来获得最高级的性能和功能.扩展库的开发人员努力地使用户可以很方便地将其它数据库迁移到SQLite.并且同时保持已经用PHP实现的界面.

SQLite也支持面向过程接口中数据库资源传递的灵活机制.使得其可以一样容易地从MySQL和PostgreSQL中迁移而来,MySQL中数据库资源是向后传递的( passed last),而PostgreSQL中是向前传递的(passed first).

SQLite 也具有强大的面向对象接口来从数据库中高效地存取数据.减少了你实现你对于过程接口的面向对象外包的时间.正如如下示例所示,面向对象接口可以避免你一次传递所有资源.( passing resources altogether.)



// 构造新数据库(面向对象接口)
$db = new SQLiteDatabase("db.sqlite");

// 创建foo表并插入示例数据
$db->query("BEGIN;
         CREATE TABLE foo(id INTEGER PRIMARY KEY, name CHAR(255));
         INSERT INTO foo (name) VALUES('Ilia');
         INSERT INTO foo (name) VALUES('Ilia2');
         INSERT INTO foo (name) VALUES('Ilia3');
         COMMIT;");

// 执行一个查询    
$result = $db->query("SELECT * FROM foo");
// 迭代地读取行
while ($result->valid()) {
    // 获得当前行数据
    $row = $result->current();     
    print_r($row);
// 前进到下一行
    $result->next();
}

// 不一定需要此句PHP会自动关闭链接
unset($db);

?>


安装 SQLite

在 PHP 5.0安装SQLite 是很轻易的事,因为其已经捆绑了SQLite扩展和库.因此所有你需要做的是在人的配置命令行中加入–with-sqlite 就可以了. 但我仍然推荐你安装一个SQLite库.这仅仅是因为这个扩展库是二进制的,你可以在没有PHP的情况下也可以打开和操纵SQLite数据库.这对于你在各种时间趋势下来调试和执行以及测试你的查询是很方便的.经常地,你会发展捆绑的SQLite库有点过时了,因此,用外部的库来编译你的PHP程序,可以使得你能获益于扩展库的最新修正和SQLite的最新特性.这也允许你在无需重新编译PHP的情况下来更新你的SQLite库.

用一个外部的扩展库来编译SQLite扩展用如下命令就可以了            –with-sqlite=/path/to/lib/.

我同样应当提到的是SQLite扩展同时提供了一系列可以理解的测试SQLite所支持的单个函数和方法面向对象和面向过程的测试.这不仅仅是理解如何运用每个函数和方法工作的绝佳的示例资源.也提供了预期的的输出,使你可以看见每个操作的最终结果.
使用 SQLiteSQL面向过程的接口跟MySQL和其它数据库的接口几乎是同样的.因而,对于大部分的将其它数据库迁移到SQLite的工作仅是将函数前缀mysql/pq/等等...改为sqlite.// 创建一个新的数据库 (面向过程接口)
$db = sqlite_open("db.sqlite");

// 如果你还需创建表foo,请将下面一句的注释标志清除.
// sqlite_query($db , "CREATE TABLE foo (id INTEGER PRIMARY KEY, name CHAR(255))");

//插入示例数据
sqlite_query($db, "INSERT INTO foo (name) VALUES ('Ilia')");
sqlite_query($db, "INSERT INTO foo (name) VALUES ('Ilia2')");
sqlite_query($db, "INSERT INTO foo (name) VALUES ('Ilia3')");

// 执行查询
$result = sqlite_query($db, "SELECT * FROM foo");
// 迭代访问数据行
while ($row = sqlite_fetch_array($result)) {
    print_r($row);
    /*查询结果的每一行如下所示
     Array
     (
         [0] => 1
         [id] => 1
         [1] => Ilia
         [name] => Ilia
     )
*/
}

// 手动关闭数据连接
sqlite_close($db);

?> SQLite和其它数据库一个大的不同在于其数据库引擎本身.不象其它数据库,SQLite是松散型的. SQLite中所有数据库都以空字符串结尾而不是对特定的列类型用特定的二进制表现.为了兼容性的原因,SQLite仍然在表创建时支持一些类型规范, 比如INT, CHAR, FLOAT, TEXT如此等等,但是实际上并没有用到这些数据类型.在内部,SQLite仅仅对字符串和整形在排序期间作了区分.因此,如果你不是有意地排序数据,你可以无需在CREATE TABLE语句 中指定数据类型.

SQLite的类型无关特性也在一定程度上降低了其数据排序和数据比较的速度.因为,每一次比较SQLite都需要判断数据,然后来决定是用字符串还是用数字型比较的机制.SQL表格也提供了自动增长的键值以对数据行快速访问. 同时也意味着对最后插入的一行的引用的访问.在SQLite里,这有一个不太常见的语法规则. 要创建这样一个自动增长的键值的字段,你必须将字段声明为INTEGER PRIMARY KEY, 而不是将其指定为一个特殊类型,并赋值以附加属性来指出这是一个自增长的字段.链式查询正如你所想象的.SQLite拥有许多提高其效能和扩展其功能的新的特性.这些特性的其中这五就是执行链式查询的能力.这就意味着你可以通过一个查询函数来执行多条查询.这大大地减少了你需要运行的PHP函数的量.也提高了脚本运行的速度.这也允许你在事特处理中包含查询语句块.大大提高性能.这是当执行多条写查询时提高性能的重要因素.当然,运用这个功能时也需要考虑到一些带来混乱的问题.

如果SQLite中的任何查询用到了用户指定的输入,你必须仔细地验证用户的输入以防止SQL注射攻击.不像MySQL,SQL注射仅仅导致一个令人难堪的查询错误.在SQLite中,允许攻击者在你的服务器上执行查询,可能导致潜在的灾难性的后果.如果查询展示会中包含了插入语句,并且你希望获得id, sqlite_last_insert_rowid() 函数可以达到这个目的,仅仅取得最后一次插入的id.另外一方面,当试图知道多少行受到查询影响时,可以用sqlite_changes(),这个函数的结果 就是所有执行的查询所影响的总行数.如果你的查询块中包含了SELECT,请确保它是第一个查询.否则你的结果集将不包含查询中所存取的行.
// 创建一个仅在内存中的数据库(不存为文件的)
$db = new SQLiteDatabase(":memory:");
// 创建一个两列的表 bar ,并且在其中插入两行.
/* 为了提高性能,整个查询块封装在 一个事物中. */
$db->query("BEGIN;
         CREATE TABLE bar ( id INTEGER PRIMARY KEY, id2 );
         INSERT INTO bar (id2) VALUES(1);
         INSERT INTO bar (id2) VALUES(2);
         COMMIT;");
// 将输出"2 insert queries"
echo $db->changes()." insert queries\n";
// 将输出 "last inserted row id: 2"
echo "last inserted row id: ".$db->lastInsertRowid();

?> 新函数除了向后前进的特性,(back end features), SQLite 也提供了一系统简化和提供数据存取速度的函数.
$db = new SQLiteDatabase("db.sqlite");
/* 执行查询,并将查询结果存入数组中 */
$result_array = $db->arrayQuery("SELECT * FROM foo", SQLITE_ASSOC);
print_r($result_array);

?> 这个函数使得查询执行和数据存取都由一个函数调用来实现.大大地减少了整个PHP执行的过程.PHP脚本也大大地简化,现在你只要一单个函数就可以了,而否则的话你将要在一个循环中调用一系列数据存取函数.例如,如果仅仅是需获得一列,可以用函数sqlite_single_query(),这将立刻返回 一个字符串或者一个字符串数组,这将取决于有多少行数据被查询出来.
$db正如运用其它许多特性一样,你应该合理地使用它而不是滥用它.当在一个查询中一次存取所有数据时,你要记得查询结果的所有数据是保存在内存中的.如果结果集中包含大量的数据,内存的分配就会将减少函数调用次数所带来的性能优势化为子虚乌有.因此,请在存取的数据量比较小的情况下运用这些函数.

SQLite 迭代器

在 PHP 5.0 中,有另外一种方式来从查询中获得行数据,那就是运用迭代器.

$db = new SQLiteDatabase("db.sqlite");
// 执行非缓冲的查询可以减少内在的使用.
$res = $db->unbufferedQuery("SELECT * FROM foo");
foreach ($res as $row) { //迭代结果集对象
        // 输出代码
        print_r($row);
}

?>

迭代对象的工作过程除了不需要要键值(’keys’)和用一个值来表示数组中所包含的特定的数据行之外,其它跟通过foreach()访问数组对象很相似.因为迭代器是内部引擎句柄而不是函数. 迭代器跟sqlite_fetch_*()函数比较用到的PHP代码就少得很多了.并且不需要把结果集缓冲到内存中.总之,迭代器是一种非常快速,但是简单易用的获取数据的方法.运用SQLite的对象迭代器,不会存在什么不足之处,当你需要遍历一个多行数据集时,你一定应该考虑使用它们.

工具函数

SQLite扩展也提供了一些在操作数据库时使用起来很方便的工具函数.其中一个就是sqlite_num_fields(), 可以用于获得一个特定的结果集中的字段数(列数).
当然,也有其它选择,如果你想取得数据的同时得到列数,你可以对数据集使用count()方法,就可以获得上面函数相同的数目.如果同时取回了字符串型和数字型的主键,你必须让结果一分为二,因为结果数组中的字段入口数会是字段数的两倍.如果你的脚本需要获得特定表的列名时,这个数目是很有用的.如果是这样的话,你就可以在一个循环中用sqlite_field_name()来获得字段名称信息.正如下例所示.[译者注,本例是PHP5的情况,用的是OO接口,在PHP4中,需要变换相对应的函数,具体请参见PHP Manual SQLite的参考一节]

$db = new SQLiteDatabase("db.sqlite");
$res = $db->unbufferedQuery("SELECT * FROM foo LIMIT 1");
//取得字段数目
$n_fields = $res->numFields();
$i = 0;
while ($i < $n_fields) {
    //获得单个字段名
    $field_name = $res->fieldName($i++);
    echo $field_name."\n";
}

?>

很显然,这是获得表中列名的一种很理想的方式.很明显示地,当源表中没有任何行时,这个方式就会失败.并且这个方法取了一些你可能并不想用的数据,因此,一个更好的解决方法是用sqlite_fetch_column_types()函数.这具函数 会返回特定表的列和类型,而不管表中的数据量如何.

缓存优势

在很多情况下,由于性能和内存的原因,你可能想执行非缓冲查询.尽管如此,这存在在某种情况下所必须的轻微的功能损失.这也就说明为什么,非缓冲查询不总是最优的选择.

例如,假设你想知道你的查询到底取回了多少行数据.如果用非缓存查询的话,你必须一行行地查询,才能知道这个数目.而用缓存查询,只要不费吹灰之力地执行sqlite_num_rows()函数就可以了. 这就可以取回查询结果的行数.非缓存查询还仅限于线性数据获取,这就意味着你必须取得一系列中所有行的信息,一次一行.而对于缓存查询则没有这个限制.你可用sqlite_seek() 函数来到达任意一行,并且取回数据.只要需要,逆向地获取行数据也是可以的.

$db = new SQLiteDatabase("db.sqlite");
$res = $db->query("SELECT * FROM foo");
$n_rows = $res->numRows(); // 获得行数
$res->seek($n_rows - 1); // 到最后一行
// 返回获取数据
do {
    $data = $res->current(SQLITE_ASSOC); // 获得行数据
    print_r($data);
}
while ($res->hasPrev()&& $res->prev()); // 逆向前进,直到第一行

?>

定制函数

SQLite扩展所带来的对表格操作的最有意思的特性之一是用SQL创建你自己的函数的能力.这可能得益于SQLite在同一个库中组合了数据库引擎和接口,而这个库又同PHP很好地耦合。通过使用sqlite_create_function(),你可以 构造能够运用于结果集或在WEHRE子句中使用的函数。

/* 此函数用于分别用户指定的字符串和数据库中字符串在包含的字符上的差异*/
function char_compare($db_str, $user_str) {
    return similar_text($db_str, $user_str);
}

$db = new SQLiteDatabase("db.sqlite");

/* 利用已有的PHP函数在SQLite内部创建char_compare()函数。 此调用的第3个参数指明创建的函数所需的参数数目*/
$db->createFunction('char_compare', 'char_compare', 2);
       
/* 执行包含char_compare()函数,并用其比较name和指定字符串的查询*/
$res = $db->arrayQuery("SELECT name, char_compare(name, 'Il2') AS sim_index FROM foo", SQLITE_ASSOC);

print_r($res);

?>

用PHP内嵌SQL的能力,允许你简化实际的脚本,从而使更多的开发者来运用它。这个特性也允许PHP作为一个模块引擎,简化用数据库中数据来生成HTML结构的过程。在很多情况下,这能大大简化代码,以达到无须在PHP顶部放置一个模块系统。除了代码简化之外,这也提高了程序性能和减少脚本内存占用。因为无须在用户空间中进行数据处理。

注意,如果你运用的数据有可能包含二进制数据。在处理这些数据之前,你必须用sqlite_udf_decode_binary() 函数来对SQLite内部二进制编码进行解码。当你完成了之后,你必须用函数sqlite_udf_encode_binary()来对二进制数据进行编码,以在后面某个时候能在没有损坏的情况下正常使用它。

总结

至此,你已经明白SQLite如何工作,并且了解了它能提供什么.你可能在你当前或将来的应用程序中运用它.希望,这个简短的介绍能使你熟悉SQLite提供的功能,并不会对你以往对SQLite的好印象带来任何不良影响.

正好所有的工具一样,SQLite也有其强项和弱项.对于小型的,并且大部分操作为读取操作的应用程序,SQLite 提供了理想的解决方案.而对大型的频繁写入的应用,SQLite是不太合适的.这种限制是由于SQLite的单文件架构导致的.这种架构不允许你在服务器间多路访问,也不允许在写时对数据库加锁.

关于作者

Ilia Alshanetsky 已经从事Web应用程序开发超过7年了,其中大部分都是基于PHP的开发.在最近的几年里,他热忱地加入PHP的开发中来,并合作开发大量的扩展,包括SQLite.现在Ilia正在经营他自己的公司. Advanced Internet Designs Inc.,公司的业务主要是开发和支持一个开源论坛FUDforum的开发.

欲联系Ilia,可以通过电子邮件ilia@prohost.org
= sqlite_open("db.sqlite");
// 获得列的id (作为字符串)
$id = sqlite_single_query($db, "SELECT id FROM foo WHERE name='Ilia'");
var_dump($id); //string(1)

// 如果结果多于一行,返回的将是数组
$ids = sqlite_single_query($db, "SELECT id FROM foo WHERE name LIKE 'I%'");
var_dump($ids); // array(3)

?>
js 正则表达式各种验证

/判断输入内容是否为空    
function IsNull(){    
    var str = document.getElementById('str').value.trim();    
    if(str.length==0){    
        alert('对不起,文本框不能为空或者为空格!');//请将“文本框”改成你需要验证的属性名称!    
    }    
}    
   
//判断日期类型是否为YYYY-MM-DD格式的类型    
function IsDate(){     
    var str = document.getElementById('str').value.trim();    
    if(str.length!=0){    
        var reg = /^(\d{1,4})(-|\/)(\d{1,2})\2(\d{1,2})$/;     
        var r = str.match(reg);     
        if(r==null)    
            alert('对不起,您输入的日期格式不正确!'); //请将“日期”改成你需要验证的属性名称!    
        }    
}     
   
//判断日期类型是否为YYYY-MM-DD hh:mm:ss格式的类型    
function IsDateTime(){     
    var str = document.getElementById('str').value.trim();    
    if(str.length!=0){    
        var reg = /^(\d{1,4})(-|\/)(\d{1,2})\2(\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/;     
        var r = str.match(reg);     
        if(r==null)    
        alert('对不起,您输入的日期格式不正确!'); //请将“日期”改成你需要验证的属性名称!    
    }    
}     
   
//判断日期类型是否为hh:mm:ss格式的类型    
function IsTime()     
{     
    var str = document.getElementById('str').value.trim();    
    if(str.length!=0){    
    reg=/^((20|21|22|23|[0-1]\d)\:[0-5][0-9])(\:[0-5][0-9])?$/     
        if(!reg.test(str)){    
            alert("对不起,您输入的日期格式不正确!");//请将“日期”改成你需要验证的属性名称!    
        }    
    }    
}     
   
//判断输入的字符是否为英文字母    
function IsLetter()     
{     
        var str = document.getElementById('str').value.trim();    
        if(str.length!=0){    
        reg=/^[a-zA-Z]+$/;     
        if(!reg.test(str)){    
            alert("对不起,您输入的英文字母类型格式不正确!");//请将“英文字母类型”改成你需要验证的属性名称!    
        }    
        }    
}     
   
//判断输入的字符是否为整数    
function IsInteger()     
{       
        var str = document.getElementById('str').value.trim();    
        if(str.length!=0){    
        reg=/^[-+]?\d*$/;     
        if(!reg.test(str)){    
            alert("对不起,您输入的整数类型格式不正确!");//请将“整数类型”要换成你要验证的那个属性名称!    
        }    
        }    
}     
   
//判断输入的字符是否为双精度    
function IsDouble(val)     
{     
        var str = document.getElementById('str').value.trim();    
        if(str.length!=0){    
        reg=/^[-\+]?\d+(\.\d+)?$/;    
        if(!reg.test(str)){    
            alert("对不起,您输入的双精度类型格式不正确!");//请将“双精度类型”要换成你要验证的那个属性名称!    
        }    
        }    
}     
   
   
//判断输入的字符是否为:a-z,A-Z,0-9    
function IsString()     
{     
        var str = document.getElementById('str').value.trim();    
        if(str.length!=0){    
        reg=/^[a-zA-Z0-9_]+$/;     
        if(!reg.test(str)){    
            alert("对不起,您输入的字符串类型格式不正确!");//请将“字符串类型”要换成你要验证的那个属性名称!    
        }    
        }    
}     
   
//判断输入的字符是否为中文    
function IsChinese()     
{     
        var str = document.getElementById('str').value.trim();    
        if(str.length!=0){    
        reg=/^[\u0391-\uFFE5]+$/;    
        if(!reg.test(str)){    
            alert("对不起,您输入的字符串类型格式不正确!");//请将“字符串类型”要换成你要验证的那个属性名称!    
        }    
        }    
}     
   
//判断输入的EMAIL格式是否正确    
function IsEmail()     
{     
        var str = document.getElementById('str').value.trim();    
        if(str.length!=0){    
        reg=/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;    
        if(!reg.test(str)){    
            alert("对不起,您输入的字符串类型格式不正确!");//请将“字符串类型”要换成你要验证的那个属性名称!    
        }    
        }    
}     
   
//判断输入的邮编(只能为六位)是否正确    
function IsZIP()     
{     
        var str = document.getElementById('str').value.trim();    
        if(str.length!=0){    
        reg=/^\d{6}$/;    
        if(!reg.test(str)){    
            alert("对不起,您输入的字符串类型格式不正确!");//请将“字符串类型”要换成你要验证的那个属性名称!    
        }    
        }    
}     
   
//判断输入的数字不大于某个特定的数字    
function MaxValue()     
{     
    var val = document.getElementById('str').value.trim();    
        if(str.length!=0){    
        reg=/^[-+]?\d*$/;     
        if(!reg.test(str)){//判断是否为数字类型    
            if(val>parseInt('123')) //“123”为自己设定的最大值    
            {     
                alert('对不起,您输入的数字超出范围');//请将“数字”改成你要验证的那个属性名称!    
            }     
        }    
    }    
}     
   
   
 Phone : /^((\(\d{2,3}\))|(\d{3}\-))?(\(0\d{2,3}\)|0\d{2,3}-)?[1-9]\d{6,7}(\-\d{1,4})?$/    
 Mobile : /^((\(\d{2,3}\))|(\d{3}\-))?13\d{9}$/    
 Url : /^http:\/\/[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/   
 IdCard : /^\d{15}(\d{2}[A-Za-z0-9])?$/   
 QQ : /^[1-9]\d{4,8}$/   
 某种特殊金额:/^((\d{1,3}(,\d{3})*)|(\d+))(\.\d{2})?$/               //说明:除“XXX    XX,XXX    XX,XXX.00”格式外

//为上面提供各个JS验证方法提供.trim()属性   
String.prototype.trim=function(){   
        return this.replace(/(^\s*)|(\s*$)/g, "");    
    }

调用:
<input type="text" name="str" >
<input type="button" value=" 确定 " onClick="">    //onClick中写自己要调用的JS验证函数
js 操作时间
var myDate = new Date();    

myDate.getYear();       //获取当前年份(2位)    

myDate.getFullYear();   //获取完整的年份(4位,1970-????)  

myDate.getMonth();      //获取当前月份(0-11,0代表1月)(要加1) 

myDate.getDate();       //获取当前日(1-31)    

myDate.getDay();        // 获取当前星期X(0-6,0代表星期天)    

myDate.getTime();       //获取当前时间(从1970.1.1开始的毫秒数)    

myDate.getHours();      //获取当前小时数(0-23)    

myDate.getMinutes();    // 获取当前分钟数(0-59)    

myDate.getSeconds();    //获取当前秒数(0-59)    

myDate.getMilliseconds();   //获取当前毫秒数(0-999)    

myDate.toLocaleDateString();    //获取当前日期    

var mytime=myDate.toLocaleTimeString();    //获取当前时间    

myDate.toLocaleString( );       //获取日期与时间 

 
设置时间的方法:

setDate()       //改变Date对象的日期

setHours()      //改变小时数 

setMinutes()    //改变分钟数 

setMonth()      //改变月份

 setSeconds()    //改变秒数

 setTime()       //改变完整的时间 

setYear()       //改变年份

转换时间的方法:

toGMTString() 把Date对象的日期(一个数值)转变成一个GMT时间字符串,返回类似下面的值:Weds,14 May 2007 10:02:02 GMT(精确的格式依赖于计算机上所运行的操作系统而变) toLocaleString() 把Date对象的日期(一个数值)转变成一个字符串,使用所在计算机上配置使用的特定日期格式 UTC() 使用Date UTC(年、月、日、时、分、秒),以自从1997年1月1日00:00:00(其中时、分、秒是可选的)以来的毫秒数的形式返回日期

1、当前系统区域设置格式(toLocaleDateString和toLocaleTimeString)

例子:(new Date()).toLocaleDateString() + " " + (new Date()).toLocaleTimeString()
结果: 2008年1月29日 16:13:11

2.普通字符串(toDateString和toTimeString)

例子: (new Date()).toDateString() + " " + (new Date()).toTimeString()
结果:Tue Jan 29 2008 16:13:11 UTC+0800

3.格林威治标准时间(toGMTString)

例子: (new Date()).toGMTString()
结果:Tue, 29 Jan 2008 08:13:11 UTC

4.全球标准时间(toUTCString)

例子: (new Date()).toUTCString()
结果:Tue, 29 Jan 2008 08:13:11 UTC

5.Date对象字符串(toString)

例子: (new Date()).toString()
结果:Tue Jan 29 16:13:11 UTC+0800 2008

 

 
几个需要注意的地方:

1、得到日期和年和设置日期和年时间,其中很怪的问题就是不能对月份进行设置(比较的怪):

<script type="text/javascript" language="javascript">//d = new Date();alert(d.toLocaleString());

d.setDate(14);

alert(d.toLocaleString());

d.setYear(2007);

alert(d.toLocaleString());

</script>

2、获得年的时候最好用getFullYear()方法来做 3、由于针对月份,JS是从0开始的,因此需要对月份进
行操作时要加1 下面是几个关于时间的经典而且经常会用到的例子,对大家会有帮助的: 1、将2005-8
-5转换成2005-08-05格式 

<script type="text/javascript" language="javascript">

//

var strDate = '2005-8-5'; 

 window.alert(strDate.replace(/\b(\w)\b/g, '0$1'));  

</script>

2、得到间隔天数 <script type="text/javascript">//&amp;lt;!--  alert("间隔天数为:"+(new Date('2005/8/15')-new Date('2003/9/18'))/1000/60/60/24+"天")  //</script>


3、得到间隔时间 <script type="text/javascript">//var d1=new Date("2004/09/16 20:08:00");  var d2=new Date("2004/09/16 10:18:03");  var d3=d1-d2;  var h=Math.floor(d3/3600000);  var m=Math.floor((d3-h*3600000)/60000);  var s=(d3-h*3600000-m*60000)/1000;  alert("相差"+h+"小时"+m+"分"+s+"秒");  //</script>

4、得到今天的日期 <script type="text/javascript" language="javascript">//d = new Date();  alert(d.getFullYear()+"年"+(d.getMonth()+1)+"月"+d.getDate()+"日");  //</script>

6、得到前N天或后N天的日期 方法1: <script type="text/javascript">//function showdate(n)  {  var uom = new Date(new Date()-0+n*86400000);  uom = uom.getFullYear() + "-" + (uom.getMonth()+1) + "-" + uom.getDate();  return uom;  }  

window.alert("今天是:"+showdate(0));  window.alert("昨天是:"+showdate(-1));  window.alert("明天是:"+showdate(1));  window.alert("10天前是:"+showdate(-10));  window.alert("5天后是:"+showdate(5));  //</script>

方法2: <script type="text/javascript">//function showdate(n)  {  var uom = new Date();  uom.setDate(uom.getDate()+n);  uom = uom.getFullYear() + "-" + (uom.getMonth()+1) + "-" + uom.getDate();  return uom;  }   

window.alert("今天是:"+showdate(0));  window.alert("昨天是:"+showdate(-1));  window.alert("明天是:"+showdate(1));  window.alert("10天前是:"+showdate(-10));  window.alert("5天后是:"+showdate(5));  //</script>

方法3: <script type="text/javascript" language="Javascript">//Date.prototype.getDays=function(){  var _newDate=new Date();  _newDate.setMonth(_newDate.getMonth()+1);  _newDate.setDate(0);  $_days=_newDate.getDate();  delete _newDate;  return $_days;  }  function showdate(n)  {  var uom = new Date();  uom.setDate(uom.getDate()+n);  uom = uom.getFullYear() + "-" + (uom.getMonth()+1) + "-" + uom.getDate()+"\n星期"+('天一二
三四五六'.charAt(uom.getDay()))+"\n本月有"+ uom.getDays()+"天";  return uom;  }   window.alert("今天是:"+showdate(0));  window.alert("昨天是:"+showdate(-1));  window.alert("明天是:"+showdate(1));  window.alert("10天前是:"+showdate(-10));  window.alert("5天后是:"+showdate(5));  //</script>


php 邮箱发送
    <?php  
    /*邮件发送类 
    *功能:使用smtp服务器发送邮件 
    */  
    class smtp {  
        /* 全局变量 */  
        var $smtp_port;  
        var $time_out;  
        var $host_name;  
        var $log_file;  
        var $relay_host;  
        var $debug;  
        var $auth;  
        var $user;  
        var $pass;    
        var $sock;  
          
        /* 构造函数 */  
        function smtp($relay_host = "", $smtp_port = 25, $auth = false, $log_file="", $user="", $pass="") {  
            $this->debug = FALSE;  
            $this->smtp_port = $smtp_port;  
            $this->relay_host = $relay_host;  
            $this->time_out = 30; //is used in fsockopen()   
            $this->auth = $auth; //auth  
            $this->user = $user;  
            $this->pass = $pass;  
            $this->host_name = "localhost"; //is used in HELO command   
            $this->log_file = $log_file; //邮件发送成功失败的日志记录文件     
            $this->sock = FALSE;  
        }  
          
        function mail_encode($str)  
        {  
            //return '=?utf-8?B?'.base64_encode(mb_convert_encoding($str, "GBK", "UTF-8")).'?=';  
            return "=?UTF-8?B?".base64_encode($str)."?=";  
        }  
          
          
        /* 主函数,发送邮件 */  
        function sendmail($flag, $boundary, $to, $from, $subject = "", $body = "", $mailtype, $cc = "", $bcc = "", $additional_headers = "") {  
            $mail_from = $this->get_address ( $this->strip_comment ( $from ) );  
            $body = ereg_replace ( '(^|(\r\n))(\.)', "\1.\3", $body );  
            $header = "MIME-Version:1.0\r\n";  
            if ($mailtype == "HTML") {  
                if ($flag == 2) {  
                    $header .= "Content-Type:multipart/mixed; boundary= $boundary\r\n";  
                } else {  
                    $header .= "Content-Type:text/html;  charset=\"UTF-8\" \r\n";  
                }  
            }  
            $header .= "To: " . $to . "\r\n";         
            if ($cc != "") {  
                $header .= "Cc: " . $cc . "\r\n";  
            }     
            $header .= "From: $from<" . $from . ">\r\n";  
            $subject = self::mail_encode($subject);  
            $header .= "Subject: " . $subject . "\r\n";  
            $header .= $additional_headers;  
            $header .= "Date: " . date ( "r" ) . "\r\n";  
            $header .= "X-Mailer:By redhat (PHP/" . phpversion () . ")\r\n";  
      
            list ( $msec, $sec ) = explode ( " ", microtime () );     
            $header .= "Message-ID: <" . date ( "YmdHis", $sec ) . "." . ($msec * 1000000) . "." . $mail_from . ">\r\n";        
            $TO = explode ( ",", $this->strip_comment ( $to ) );       
            if ($cc != "") {  
                $TO = array_merge ( $TO, explode ( ",", $this->strip_comment ( $cc ) ) );  
            }     
            if ($bcc != "") {  
                $TO = array_merge ( $TO, explode ( ",", $this->strip_comment ( $bcc ) ) );  
            }         
            $sent = TRUE;         
            foreach ( $TO as $rcpt_to ) {  
                $rcpt_to = $this->get_address ( $rcpt_to );            
                if (! $this->smtp_sockopen ( $rcpt_to )) {  
                    $this->log_write ( "Error: Cannot send email to " . $rcpt_to . "\n" );  
                    $sent = FALSE;  
                    continue;  
                }         
                if ($this->smtp_send ( $this->host_name, $mail_from, $rcpt_to, $header, $body )) {  
                    $this->log_write ( "E-mail has been sent to <" . $rcpt_to . ">\n" );  
                } else {  
                    $this->log_write ( "Error: Cannot send email to <" . $rcpt_to . ">\n" );  
                    $sent = FALSE;  
                }  
                fclose ( $this->sock );  
                $this->log_write ( "Disconnected from remote host\n" );  
            }  
            return $sent;  
        }  
          
        /* 私有函数 */  
        function smtp_send($helo, $from, $to, $header, $body = "") {  
            if (! $this->smtp_putcmd ( "HELO", $helo )) {  
                  
                return $this->smtp_error ( "sending HELO command" );  
            }  
      
            if ($this->auth) {  
                if (! $this->smtp_putcmd ( "AUTH LOGIN", base64_encode ( $this->user ) )) {  
                    return $this->smtp_error ( "sending HELO command" );  
                }  
                  
                if (! $this->smtp_putcmd ( "", base64_encode ( $this->pass ) )) {  
                    return $this->smtp_error ( "sending HELO command" );  
                }  
            }  
      
            if (! $this->smtp_putcmd ( "MAIL", "FROM:<" . $from . ">" )) {  
                return $this->smtp_error ( "sending MAIL FROM command" );  
            }  
              
            if (! $this->smtp_putcmd ( "RCPT", "TO:<" . $to . ">" )) {  
                return $this->smtp_error ( "sending RCPT TO command" );  
            }  
              
            if (! $this->smtp_putcmd ( "DATA" )) {  
                return $this->smtp_error ( "sending DATA command" );  
            }  
              
            if (! $this->smtp_message ( $header, $body )) {  
                return $this->smtp_error ( "sending message" );  
            }  
              
            if (! $this->smtp_eom ()) {  
                return $this->smtp_error ( "sending <CR><LF>.<CR><LF> [EOM]" );  
            }  
              
            if (! $this->smtp_putcmd ( "QUIT" )) {  
                return $this->smtp_error ( "sending QUIT command" );  
            }  
            return TRUE;  
        }  
          
        function smtp_sockopen($address) {  
            if ($this->relay_host == "") {  
                return $this->smtp_sockopen_mx ( $address );  
            } else {  
                return $this->smtp_sockopen_relay ();  
            }  
        }  
          
        function smtp_sockopen_relay() {  
            $this->log_write ( "Trying to " . $this->relay_host . ":" . $this->smtp_port . "\n" );  
            $this->sock = @fsockopen ( $this->relay_host, $this->smtp_port, $errno, $errstr, $this->time_out );  
            if (! ($this->sock && $this->smtp_ok ())) {  
                $this->log_write ( "Error: Cannot connenct to relay host " . $this->relay_host . "\n" );  
                $this->log_write ( "Error: " . $errstr . " (" . $errno . ")\n" );  
                return FALSE;  
            }  
            $this->log_write ( "Connected to relay host " . $this->relay_host . "\n" );  
            return TRUE;  
        }  
          
        function smtp_sockopen_mx($address) {  
            $domain = ereg_replace ( "^.+@([^@]+){1}quot;, "\1", $address );  
            if (! @getmxrr ( $domain, $MXHOSTS )) {  
                $this->log_write ( "Error: Cannot resolve MX \"" . $domain . "\"\n" );  
                return FALSE;  
            }  
              
            foreach ( $MXHOSTS as $host ) {  
                $this->log_write ( "Trying to " . $host . ":" . $this->smtp_port . "\n" );  
                  
                $this->sock = @fsockopen ( $host, $this->smtp_port, $errno, $errstr, $this->time_out );  
                  
                if (! ($this->sock && $this->smtp_ok ())) {  
                    $this->log_write ( "Warning: Cannot connect to mx host " . $host . "\n" );  
                    $this->log_write ( "Error: " . $errstr . " (" . $errno . ")\n" );  
                    continue;  
                }  
                  
                $this->log_write ( "Connected to mx host " . $host . "\n" );  
                return TRUE;  
            }  
              
            $this->log_write ( "Error: Cannot connect to any mx hosts (" . implode ( ", ", $MXHOSTS ) . ")\n" );  
            return FALSE;  
        }  
          
        function smtp_message($header, $body) {  
            fputs ( $this->sock, $header . "\r\n" . $body );  
            $this->smtp_debug ( "> " . str_replace ( "\r\n", "\n" . "> ", $header . "\n> " . $body . "\n> " ) );  
            return TRUE;  
        }  
          
        function smtp_eom() {  
            fputs ( $this->sock, "\r\n.\r\n" );  
              
            $this->smtp_debug ( ". [EOM]\n" );  
              
            return $this->smtp_ok ();  
        }  
          
        function smtp_ok() {  
            $response = str_replace ( "\r\n", "", fgets ( $this->sock, 512 ) );  
            $this->smtp_debug ( $response . "\n" );  
              
            if (! ereg ( "^[23]", $response )) {  
                fputs ( $this->sock, "QUIT\r\n" );  
                fgets ( $this->sock, 512 );  
                $this->log_write ( "Error: Remote host returned \"" . $response . "\"\n" );  
                return FALSE;  
              
            }  
            return TRUE;  
        }  
          
        function smtp_putcmd($cmd, $arg = "") {  
            if ($arg != "") {  
                if ($cmd == "")  
                    $cmd = $arg;  
                else  
                    $cmd = $cmd . " " . $arg;  
            }  
              
            fputs ( $this->sock, $cmd . "\r\n" );  
            $this->smtp_debug ( "> " . $cmd . "\n" );  
            return $this->smtp_ok ();  
        }  
          
        function smtp_error($string) {  
            $this->log_write ( "Error: Error occurred while " . $string . ".\n" );  
            return FALSE;  
        }  
          
        function log_write($message) {  
            $this->smtp_debug ( $message );  
            if ($this->log_file == "") {  
                return TRUE;  
            }  
              
            $message = date ( "M d H:i:s " ) . get_current_user () . "[" . getmypid () . "]: " . $message;  
            if (! @file_exists ( $this->log_file ) || ! ($fp = @fopen ( $this->log_file, "a" ))) {  
                $this->smtp_debug ( "Warning: Cannot open log file \"" . $this->log_file . "\"\n" );  
                return FALSE;  
            }  
              
            flock ( $fp, LOCK_EX );  
            fputs ( $fp, $message );  
            fclose ( $fp );  
            return TRUE;  
        }  
          
        function strip_comment($address) {  
            $comment = '\([^()]*\)';  
            while ( ereg ( $comment, $address ) ) {  
                $address = ereg_replace ( $comment, "", $address );  
            }  
            return $address;  
        }  
          
        function get_address($address) {  
            $address = ereg_replace ( "([ \t\r\n])+", "", $address );  
            $address = ereg_replace ( "^.*<(.+)>.*{1}quot;, "\1", $address );  
            return $address;  
        }  
          
        function smtp_debug($message) {  
            if ($this->debug) {  
                echo $message;  
            }  
        }  
    }  
      
    ?>  


调用
    <?php  
      
    //文件全路径名称,文件名称  
    function send_smtp_mail($file, $fileName) {  
        require ("smtp_mail.php");  
        date_default_timezone_set ( 'Asia/Shanghai' );  
        $subject = date ( "Y-m-d" ) . "邮件标题"; //邮件标题  
        $content = "邮件内容!"; //邮件内容  
        //$file = "/a/b/c.txt"; //附件  
        //$fileName = "email_log.log"; //附件名称  
          
        $smtpserver = "服务器ip"; //SMTP服务器  
        $smtpserverport = 25; //SMTP服务器端口  
        $bcc = ""; //副收件人  
        //$smtpuser = ""; //SMTP服务器的用户帐号  
        //$smtppass = ""; //SMTP服务器的用户密码  
        $smtpmailfrom = "aaa@bbb.com"; //SMTP服务器的用户邮箱,邮件发送者  
        $smtpemailto = "邮箱1,邮箱2,邮箱3"; //邮件接受者  
        $cc = ""; //抄送  
        $mailsubject = $subject; //邮件主题  
        $mailtype = "HTML"; //邮件格式(HTML/TXT),TXT为文本邮件  
        $additional_headers = "";  
        $smtplogfile = "";  //发送邮件的日志文件,如果没有就不记录  
          
        // 定义分界线   
        $boundary = uniqid ( "" );  
        $headers = "Content-type: multipart/mixed; boundary= $boundary\r\n";  
        //附件类型  
        $mimeType = "application/unknown";  
          
        // 打开文件   
        $fp = fopen ( $file, "r" );  
        // 把整个文件读入一个变量   
        $read = fread ( $fp, filesize ( $file ) );  
        //我们用base64方法把它编码   
        $read = base64_encode ( $read );  
        //把这个长字符串切成由每行76个字符组成的小块   
        $read = chunk_split ( $read );  
        fclose ( $fp ); //关闭文件  
          
      
        /* 邮件发送代码 */  
        $flag = 1; //判断使用什么样的文件头1或2  
        if ($fileName == "") {  
            //没有附件  
            $body = $content;  
        } else {  
            //有附件  
            $flag = 2;  
            if ($mailtype == "HTML") {  
                $body = "--$boundary\r\n"; //此必须\r\n  
                $body .= "Content-Type:text/html\r\n\r\n"; //此必须\r\n\r\n  
                $body .= "$content\r\n";  
                $body .= "--$boundary\r\n";  
                $body .= "Content-type: $mimeType; name=$fileName\r\n";  
                $body .= "Content-disposition: attachment; filename=$fileName\r\n";  
                $body .= "Content-Transfer-Encoding: BASE64\r\n\r\n";  
                $body .= "$read\r\n\r\n";  
                $body .= "--$boundary--\r\n";  
            } else {  
                $body = "--$boundary\r\n";  
                $body .= "Content-type: Content-type: text/plain; charset=iso-8859-1\r\n\r\n";  
                $body .= "Content-transfer-encoding: 8bit\r\n\r\n";  
                $body .= "$content\r\n\r\n";  
                $body .= "--$boundary\r\n";  
                $body .= "Content-type: $mimeType; name=$fileName\r\n\r\n";  
                $body .= "Content-disposition: attachment; filename=$fileName\r\n\r\n";  
                $body .= "Content-transfer-encoding: base64\r\n\r\n";  
                $body .= "$read\r\n\r\n";  
                $body .= "--$boundary--\r\n";  
            }  
        }  
          
        $mailbody = $body; //邮件内容  
        //这里面的一个true是表示使用身份验证,否则不使用身份验证.  
        $smtp = new smtp ( $smtpserver, $smtpserverport, false, $smtplogfile );  
        //$smtp->debug = TRUE;//是否显示发送的调试信息  
        if ($smtp->sendmail ( $flag, $boundary, $smtpemailto, $smtpmailfrom, $mailsubject, $mailbody, $mailtype, $cc, $bcc, $additional_headers )) {  
            echo ("发送成功");  
        } else {  
            echo ("发送失败");  
        }  
    }  
      
    ?>  
putty用法
下载页面 http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html
下载 putty.zip
解压文件 得到PSCP.EXE
把其放入到window下的C:\WINDWOS\system32下
同过dos命令
上传文件命令

pscp E:\udf.tar root@192.168.1.200:/root

pscp source  [user@]host:target
spring 定时器 quartz
 Spring中Quartz的配置
Quartz是一个强大的企业级任务调度框架,Spring中继承并简化了Quartz,下面就看看在Spring中怎样配置Quartz:
首先我们来写一个被调度的类:
package com.kay.quartz;
public class QuartzJob
{

    public void work()
    {
    System.out.println("Quartz的任务调度!!!");
    }
}
Spring的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>    
        <!-- 要调用的工作类 -->
        <bean id="quartzJob" class="com.kay.quartz.QuartzJob"></bean>
        <!-- 定义调用对象和调用对象的方法 -->
        <bean id="jobtask" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
            <!-- 调用的类 -->
            <property name="targetObject">
                <ref bean="quartzJob"/>
            </property>
            <!-- 调用类中的方法 -->
            <property name="targetMethod">
                <value>work</value>
            </property>
        </bean>
        <!-- 定义触发时间 -->
        <bean id="doTime" class="org.springframework.scheduling.quartz.CronTriggerBean">
            <property name="jobDetail">
                <ref bean="jobtask"/>
            </property>
            <!-- cron表达式 -->
            <property name="cronExpression">
                <value>10,15,20,25,30,35,40,45,50,55 * * * * ?</value>
            </property>
        </bean>
        <!-- 总管理类 如果将lazy-init='false'那么容器启动就会执行调度程序  -->
        <bean id="startQuertz" lazy-init="false" autowire="no" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
            <property name="triggers">
                <list>
                    <ref bean="doTime"/>
                </list>
            </property>
        </bean>
    
</beans>
测试程序:
package com.kay.quartz;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MainTest
{

    /**
     * @param args
     */
    public static void main(String[] args)
    {
        System.out.println("Test start.");
        ApplicationContext context = new ClassPathXmlApplicationContext("quartz-config.xml");
        //如果配置文件中将startQuertz bean的lazy-init设置为false 则不用实例化
        //context.getBean("startQuertz");
        System.out.print("Test end..");
        

    }

}
我们需要把log4j的配置文件放入src目录下,启动main类就可以了。

关于cron表达式(来自网络):

Cron 表达式包括以下 7 个字段:

    秒
    分
    小时
    月内日期
    月
    周内日期
    年(可选字段)

特殊字符

Cron 触发器利用一系列特殊字符,如下所示:

    反斜线(/)字符表示增量值。例如,在秒字段中“5/15”代表从第 5 秒开始,每 15 秒一次。

    问号(?)字符和字母 L 字符只有在月内日期和周内日期字段中可用。问号表示这个字段不包含具体值。所以,如果指定月内日期,可以在周内日期字段中插入“?”,表示周内日期值无关紧要。字母 L 字符是 last 的缩写。放在月内日期字段中,表示安排在当月最后一天执行。在周内日期字段中,如果“L”单独存在,就等于“7”,否则代表当月内周内日期的最后一个实例。所以“0L”表示安排在当月的最后一个星期日执行。

    在月内日期字段中的字母(W)字符把执行安排在最靠近指定值的工作日。把“1W”放在月内日期字段中,表示把执行安排在当月的第一个工作日内。

    井号(#)字符为给定月份指定具体的工作日实例。把“MON#2”放在周内日期字段中,表示把任务安排在当月的第二个星期一。

    星号(*)字符是通配字符,表示该字段可以接受任何可能的值。

字段 允许值 允许的特殊字符
秒 0-59 , - * /
分 0-59 , - * /
小时 0-23 , - * /
日期 1-31 , - * ? / L W C
月份 1-12 或者 JAN-DEC , - * /
星期 1-7 或者 SUN-SAT , - * ? / L C #
年(可选) 留空, 1970-2099 , - * /

表达式意义
"0 0 12 * * ?" 每天中午12点触发
"0 15 10 ? * *" 每天上午10:15触发
"0 15 10 * * ?" 每天上午10:15触发
"0 15 10 * * ? *" 每天上午10:15触发
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发
"0 15 10 15 * ?" 每月15日上午10:15触发
"0 15 10 L * ?" 每月最后一日的上午10:15触发
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
每天早上6点

0 6 * * *

每两个小时

0 */2 * * *
晚上11点到早上8点之间每两个小时,早上八点

0 23-7/2,8 * * *

每个月的4号和每个礼拜的礼拜一到礼拜三的早上11点

0 11 4 * 1-3
1月1日早上4点

0 4 1 1 *
更多知识:
http://www.ibm.com/developerworks/cn/java/j-quartz/index.html
http://www.javaeye.com/topic/117244
















在Spring中,使用JDK的Timer类库来做任务调度功能不是很方便,关键它不可以象cron服务那样可以指定具体年、月、日、时和分的时间。你只能将时间通过换算成微秒后传给它。如任务是每天执行一次,则需要在spring中如下配置:
­
<bean id="scheduledTask" class= "org.springframework.scheduling.timer.ScheduledTimerTask">
<!--程序启动后开始执行任务的延迟时间 -->
<property name="delay" value="0" />
<!--每隔一天【一天=24×60×60×1000微秒】执行一次-->
<property name="period" value="86400000" />
<!--业务统计报表bean -->
<property name="timerTask" ref="businessReport" />
</bean>
­
其中period就是一天的微秒数。如果每月1日运行一次,那就复杂了,不知如何配置。因为月份有大、小月之分,每月的微秒数都不一样。
­
而Quartz类库不但有着上述JDK的Timer类库类似的配置,更重要的,它还有着类似于unix的cron服务的配置。因此,在迁移中我们采用了Quartz类库的接口。

具体的步骤如下:
1 编写业务类,该类继承了org.quartz.Job,主要的逻辑在execute方法中编写

2 配置spring的applicationContext.xml文件
    2.1 配置任务JobDetailBean
    2.2配置触发器 CronTriggerBean
    2.3配置调度器  SchedulerFactoryBean

3 所需要的jar包:
         spring.jar,quartz.jar,commons-logging-1.0.4.jar,commons-dbcp-1.2.2.jar,commons-pool-1.3.jar

4 把quartz.properties放到类路径下

以下为一个demo

业务类:

Java代码  收藏代码

    package task;  
      
    import java.util.Date;  
      
    import org.quartz.JobExecutionContext;  
    import org.quartz.JobExecutionException;  
      
    public class BusinessReport implements org.quartz.Job{  
          public void perform(){ //执行报表统计入口函数  
                //业务逻辑  
              System.out.println("开始执行报表的业务逻辑了----现在的时间是--"+new Date());  
                
            }  
      
        public void execute(JobExecutionContext arg0) throws JobExecutionException {  
            perform();  
              
        }  
      
    }  
      
    applicationContext.xml文件  
      
    <?xml version="1.0" encoding="UTF-8"?>  
    <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">  
    <!--    
        <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" ":/spring-beans.dtd">  
    -->  
    <beans>  
        <bean id="businessReport" class="task.BusinessReport" />  
      
      
      
      
        <bean name="reportTask"  
            class="org.springframework.scheduling.quartz.JobDetailBean">  
            <property name="jobClass" value="task.BusinessReport" />  
        </bean>  
      
        <!-- 触发器 -->  
        <bean id="cronTrigger"  
            class="org.springframework.scheduling.quartz.CronTriggerBean">  
      
            <!-- 指向我们的任务 -->  
            <property name="jobDetail" ref="reportTask" />  
      
            <!--  每天下午16点50分到55分,每分钟运行一次 -->  
            <property name="cronExpression" value="0 50-55 16 * * ?" />  
        </bean>  
      
      
        <!-- 调度器  -->  
        <bean  
            class="org.springframework.scheduling.quartz.SchedulerFactoryBean">  
            <property name="triggers">  
                <list>  
                    <!--  触发器列表 -->  
                    <ref bean="cronTrigger" />  
                </list>  
            </property>  
            <property name="configLocation" value="classpath:quartz.properties" />   
        </bean>  
      
      
          
    </beans>  
       

 三 quartz.properties文件的内容(默认放在类路径下)
#============================================================================
# Configure Main Scheduler Properties 
#============================================================================
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false

#============================================================================
# Configure ThreadPool 
#============================================================================
#org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
#org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

#============================================================================
# Configure JobStore 
#============================================================================
#org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.misfireThreshold = 60000
#org.quartz.jobStore.useProperties = false
#org.quartz.jobStore.tablePrefix = QRTZ_
#org.quartz.jobStore.dataSource = myDS

#org.quartz.jobStore.isClustered = true
#org.quartz.jobStore.clusterCheckinInterval = 15000

#============================================================================
# Configure DataSource
#============================================================================
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost/test
org.quartz.dataSource.myDS.user = root
org.quartz.dataSource.myDS.password = root
org.quartz.dataSource.myDS.maxConnections = 10


附:cronExpression表达式解释:
0 0 12 * * ?---------------在每天中午12:00触发
0 15 10 ? * *---------------每天上午10:15 触发
0 15 10 * * ?---------------每天上午10:15 触发
0 15 10 * * ? *---------------每天上午10:15 触发
0 15 10 * * ? 2005---------------在2005年中的每天上午10:15 触发
0 * 14 * * ?---------------每天在下午2:00至2:59之间每分钟触发一次
0 0/5 14 * * ?---------------每天在下午2:00至2:59之间每5分钟触发一次
0 0/5 14,18 * * ?---------------每天在下午2:00至2:59和6:00至6:59之间的每5分钟触发一次
0 0-5 14 * * ?---------------每天在下午2:00至2:05之间每分钟触发一次
0 10,44 14 ? 3 WED---------------每三月份的星期三在下午2:00和2:44时触发
0 15 10 ? * MON-FRI---------------从星期一至星期五的每天上午10:15触发
0 15 10 15 * ?---------------在每个月的每15天的上午10:15触发
0 15 10 L * ?---------------在每个月的最后一天的上午10:15触发
0 15 10 ? * 6L---------------在每个月的最后一个星期五的上午10:15触发
0 15 10 ? * 6L 2002-2005---------------在2002, 2003, 2004 and 2005年的每个月的最后一个星期五的上午10:15触发
0 15 10 ? * 6#3---------------在每个月的第三个星期五的上午10:15触发
0 0 12 1/5 * ?---------------从每月的第一天起每过5天的中午12:00时触发
0 11 11 11 11 ?---------------在每个11月11日的上午11:11时触发.­
apache 访问目录受限及支持其他IP访问
目录权限问题:所在目录入root等目录,apache没有权限访问,解决方式;给apahce用户添加访问该目录的权限,或者就换到另一个有权限的目录下

对让其Ip访问的话就就在
<VirtualHost *:80>  
   
</VirtualHost>
加入*:80就可以了
操作dom4j
http://www.blogjava.net/i369/articles/154264.html
glassfish jndi配置 mysql
资源->连接池->新建

数据源类名称:com.mysql.jdbc.jdbc2.optional.MysqlXADataSource

资源类型:javax.sql.XADataSource;

其他属性标签中添加连接必要属性:

portNumber          3306

databaseName    test

datasourceName  test

serverName          localhost

password             root

user                     root

如提示找不到jdbc类,将mysql的jdbc驱动拷入glassfish对应domain中的lib/ext中即可
Xtrabackup增量备份
http://xikder.blog.51cto.com/1423200/309339
Java实现sqlsever数据库备份与还原功能
Java实现sqlsever数据库备份与还原功能
1.连接数据库代码:
[java] view plaincopy

    package com.once.xfd.dbutil;  
      
    import java.sql.Connection;  
    import java.sql.DriverManager;  
    import java.sql.SQLException;  
      
    public class DataBaseUtil {  
        /** 
         * 获取数据库连接 
         * @return Connection 对象 
         */  
        public static Connection getConnection() {  
            Connection conn = null;  
            try {  
                Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");  
                String url = "jdbc:sqlserver://127.0.0.1:1433;databaseName=master";  
                String username = "sa";  
                String password = "123456";   
                conn = DriverManager.getConnection(url, username, password);  
                  
            } catch (ClassNotFoundException e) {  
                e.printStackTrace();  
            } catch (SQLException e) {  
                e.printStackTrace();  
            }  
            return conn;  
        }  
          
        public static void closeConn(Connection conn) {  
            if (conn != null) {  
                try {  
                    conn.close();  
                } catch (SQLException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }  

2.备份功能代码:

[java] view plaincopy

    /** 
         * 备份数据库 
         * @return backup 
         * @throws Exception 
         */  
    public String backup() {  
            ActionContext context = ActionContext.getContext();  
            HttpServletRequest request = (HttpServletRequest) context  
                    .get(ServletActionContext.HTTP_REQUEST);  
            String webtruepath = request.getParameter("path");  
            String name = "dbname"; //数据库名  
            try {  
                File file = new File(webtruepath);  
                String path = file.getPath() + "\\" + name + ".bak";// name文件名  
                String bakSQL = "backup database 数据库名 to disk=? with init";// SQL语句  
                PreparedStatement bak = DataBaseUtil.getConnection()  
                        .prepareStatement(bakSQL);  
                bak.setString(1, path);// path必须是绝对路径  
                bak.execute(); // 备份数据库  
                bak.close();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            return "backup";  
        }  


3.还原功能代码(调用存储过程killrestore):

[java] view plaincopy

        /** 
         * 数据库还原 
         * @return recovery 
         */  
        public String recovery() {  
            ActionContext context = ActionContext.getContext();  
            HttpServletRequest request = (HttpServletRequest) context  
                    .get(ServletActionContext.HTTP_REQUEST);  
            String webtruepath = request.getParameter("path");  
            String name = "******";  
            String dbname = "******";  
            try {  
                File file = new File(webtruepath);  
                String path = file.getPath() + "\\" + name + ".bak";// name文件名  
                String recoverySql = "ALTER   DATABASE   数据库名   SET   ONLINE   WITH   ROLLBACK   IMMEDIATE";// 恢复所有连接  
                  
                PreparedStatement ps = DataBaseUtil.getConnection()  
                        .prepareStatement(recoverySql);  
                CallableStatement cs = DataBaseUtil.getConnection().prepareCall("{call killrestore(?,?)}");  
                    cs.setString(1, dbname); // 数据库名  
                    cs.setString(2, path); // 已备份数据库所在路径  
                    cs.execute(); // 还原数据库  
                    ps.execute(); // 恢复数据库连接          
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            return "recovery";  
        }  

4.存储过程代码:

[sql] view plaincopy

    create proc killrestore (@dbname varchar(20),@dbpath varchar(40))         
    as         
    begin         
    declare @sql   nvarchar(500)         
    declare @spid  int         
    set @sql='declare getspid cursor for select spid from sysprocesses where dbid=db_id('''+@dbname+''')'         
    exec (@sql)         
    open getspid         
    fetch next from getspid into @spid         
    while @@fetch_status <> -1         
    begin         
    exec('kill '+@spid)         
    fetch next from getspid into @spid         
    end         
    close getspid         
    deallocate getspid         
    restore database @dbname from disk= @dbpath with replace  
    end      
java代码控制Mysql的备份与恢复
import java.util.Properties;  
 
public class JavaMysql {  
 
 /** 
  * 
  * mysql数据备份 接收脚本名,并返回此路径 
  * 
  * sql为备份的脚本名比如xxx.sql 
  * 
  */ 
 
 public static void backup(String sql) {  
 
  Properties pros = getPprVue("prop.properties");  
 
  // 这里是读取的属性文件,也可以直接使用  
 
  String username = pros.getProperty("username");  
 
  String password = pros.getProperty("password");  
 
  // 得到MYSQL的用户名密码后调用 mysql 的 cmd:  
 
  String mysqlpaths = pros.getProperty("mysqlpath");  
  String databaseName = pros.getProperty("databaseName");  
  String address = pros.getProperty("address");  
  String sqlpath = pros.getProperty("sql");  
  File backupath = new File(sqlpath);  
  if (!backupath.exists()) {  
   backupath.mkdir();  
  }  
 
  StringBuffer sb = new StringBuffer();  
 
  sb.append(mysqlpaths);  
        sb.append("mysqldump ");  
  sb.append("--opt ");  
  sb.append("-h ");  
  sb.append(address);  
  sb.append(" ");  
  sb.append("--user=");  
  sb.append(username);  
  sb.append(" ");  
  sb.append("--password=");  
  sb.append(password);  
  sb.append(" ");  
  sb.append("--lock-all-tables=true ");  
  sb.append("--result-file=");  
  sb.append(sqlpath);  
  sb.append(sql);  
  sb.append(" ");  
  sb.append("--default-character-set=utf8 ");  
  sb.append(databaseName);  
  Runtime cmd = Runtime.getRuntime();  
  try {  
   Process p = cmd.exec(sb.toString());  
  } catch (IOException e) {  
   e.printStackTrace();  
  }  
 
 }  
 
 // 读取属性值  
 
 public static Properties getPprVue(String properName) {  
 
  InputStream inputStream = mysql_util.class.getClassLoader()  
 
  .getResourceAsStream(properName);  
 
  Properties p = new Properties();  
 
  try {  
   p.load(inputStream);  
   inputStream.close();  
  } catch (IOException e) {  
   e.printStackTrace();  
  }  
 
  return p;  
 
 }  
 
 public static void load(String filename) {  
  Properties pros = getPprVue("prop.properties");  
 
  // 这里是读取的属性文件,也可以直接使用  
 
  String root = pros.getProperty("jdbc.username");  
 
  String pass = pros.getProperty("jdbc.password");  
 
  // 得到MYSQL的用户名密码后调用 mysql 的 cmd:  
 
  String mysqlpaths = pros.getProperty("mysqlpath");  
  String sqlpath = pros.getProperty("sql");  
  String filepath = sqlpath + filename; // 备份的路径地址  
 
  // 新建数据库finacing  
  String stmt1 = "mysqladmin -u " + root + " -p" + pass  
    + " create finacing";  
  // -p后面加的是你的密码  
  String stmt2 = "mysql -u " + root + " -p" + pass + " finacing < " 
    + filepath;  
  String[] cmd = { "cmd", "/c", stmt2 };  
 
  try {  
   Runtime.getRuntime().exec(stmt1);  
   Runtime.getRuntime().exec(cmd);  
   System.out.println("数据已从 " + filepath + " 导入到数据库中");  
  } catch (IOException e) {  
   e.printStackTrace();  
  }  
 
 }  
 
 public static void main(String[] args) throws IOException {  
 
   backup("xx.sql");  
 
  //load("xx.sql");  
 }  
} 

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class JavaMysql {

 /**
  *
  * mysql数据备份 接收脚本名,并返回此路径
  *
  * sql为备份的脚本名比如xxx.sql
  *
  */

 public static void backup(String sql) {

  Properties pros = getPprVue("prop.properties");

  // 这里是读取的属性文件,也可以直接使用

  String username = pros.getProperty("username");

  String password = pros.getProperty("password");

  // 得到MYSQL的用户名密码后调用 mysql 的 cmd:

  String mysqlpaths = pros.getProperty("mysqlpath");
  String databaseName = pros.getProperty("databaseName");
  String address = pros.getProperty("address");
  String sqlpath = pros.getProperty("sql");
  File backupath = new File(sqlpath);
  if (!backupath.exists()) {
   backupath.mkdir();
  }

  StringBuffer sb = new StringBuffer();

  sb.append(mysqlpaths);
        sb.append("mysqldump ");
  sb.append("--opt ");
  sb.append("-h ");
  sb.append(address);
  sb.append(" ");
  sb.append("--user=");
  sb.append(username);
  sb.append(" ");
  sb.append("--password=");
  sb.append(password);
  sb.append(" ");
  sb.append("--lock-all-tables=true ");
  sb.append("--result-file=");
  sb.append(sqlpath);
  sb.append(sql);
  sb.append(" ");
  sb.append("--default-character-set=utf8 ");
  sb.append(databaseName);
  Runtime cmd = Runtime.getRuntime();
  try {
   Process p = cmd.exec(sb.toString());
  } catch (IOException e) {
   e.printStackTrace();
  }

 }

 // 读取属性值

 public static Properties getPprVue(String properName) {

  InputStream inputStream = mysql_util.class.getClassLoader()

  .getResourceAsStream(properName);

  Properties p = new Properties();

  try {
   p.load(inputStream);
   inputStream.close();
  } catch (IOException e) {
   e.printStackTrace();
  }

  return p;

 }

 public static void load(String filename) {
  Properties pros = getPprVue("prop.properties");

  // 这里是读取的属性文件,也可以直接使用

  String root = pros.getProperty("jdbc.username");

  String pass = pros.getProperty("jdbc.password");

  // 得到MYSQL的用户名密码后调用 mysql 的 cmd:

  String mysqlpaths = pros.getProperty("mysqlpath");
  String sqlpath = pros.getProperty("sql");
  String filepath = sqlpath + filename; // 备份的路径地址

  // 新建数据库finacing
  String stmt1 = "mysqladmin -u " + root + " -p" + pass
    + " create finacing";
  // -p后面加的是你的密码
  String stmt2 = "mysql -u " + root + " -p" + pass + " finacing < "
    + filepath;
  String[] cmd = { "cmd", "/c", stmt2 };

  try {
   Runtime.getRuntime().exec(stmt1);
   Runtime.getRuntime().exec(cmd);
   System.out.println("数据已从 " + filepath + " 导入到数据库中");
  } catch (IOException e) {
   e.printStackTrace();
  }

 }

 public static void main(String[] args) throws IOException {

   backup("xx.sql");

  //load("xx.sql");
 }
}


---------------------------------------prop.properties
username = root
password = root
mysqlpath = C:\\Program Files\\MySQL\\MySQL Server 5.0\\bin\\
sql = E:\\MySQl\\
address=localhost
databaseName=test

MYSQL备份和恢复
作/译者:叶金荣(Email: ),来源:http://imysql.cn,转载请注明作/译者和出处,并且不能用于商业用途,违者必究。

日期:2006/10/01

本文讨论 MySQL 的备份和恢复机制,以及如何维护数据表,包括最主要的两种表类型:MyISAM 和 Innodb,文中设计的 MySQL 版本为 5.0.22。

目前 MySQL 支持的免费备份工具有:mysqldump、mysqlhotcopy,还可以用 SQL 语法进行备份:BACKUP TABLE 或者 SELECT INTO OUTFILE,又或者备份二进制日志(binlog),还可以是直接拷贝数据文件和相关的配置文件。MyISAM 表是保存成文件的形式,因此相对比较容易备份,上面提到的几种方法都可以使用。Innodb 所有的表都保存在同一个数据文件 ibdata1 中(也可能是多个文件,或者是独立的表空间文件),相对来说比较不好备份,免费的方案可以是拷贝数据文件、备份 binlog,或者用 mysqldump。

1、mysqldump
1.1 备份
mysqldump 是采用SQL级别的备份机制,它将数据表导成 SQL 脚本文件,在不同的 MySQL 版本之间升级时相对比较合适,这也是最常用的备份方法。
现在来讲一下 mysqldump 的一些主要参数:

    * --compatible=name

      它告诉 mysqldump,导出的数据将和哪种数据库或哪个旧版本的 MySQL 服务器相兼容。值可以为 ansi、mysql323、mysql40、postgresql、Oracle、mssql、db2、maxdb、no_key_options、no_tables_options、no_field_options 等,要使用几个值,用逗号将它们隔开。当然了,它并不保证能完全兼容,而是尽量兼容。
    * --complete-insert,-c

      导出的数据采用包含字段名的完整 INSERT 方式,也就是把所有的值都写在一行。这么做能提高插入效率,但是可能会受到 max_allowed_packet 参数的影响而导致插入失败。因此,需要谨慎使用该参数,至少我不推荐。
    * --default-character-set=charset

      指定导出数据时采用何种字符集,如果数据表不是采用默认的 latin1 字符集的话,那么导出时必须指定该选项,否则再次导入数据后将产生乱码问题。

    * --disable-keys
      告诉 mysqldump 在 INSERT 语句的开头和结尾增加 /*!40000 ALTER TABLE table DISABLE KEYS */; 和 /*!40000 ALTER TABLE table ENABLE KEYS */; 语句,这能大大提高插入语句的速度,因为它是在插入完所有数据后才重建索引的。该选项只适合 MyISAM 表。

    * --extended-insert = true|false
      默认情况下,mysqldump 开启 --complete-insert 模式,因此不想用它的的话,就使用本选项,设定它的值为 false 即可。

    * --hex-blob
      使用十六进制格式导出二进制字符串字段。如果有二进制数据就必须使用本选项。影响到的字段类型有 BINARY、VARBINARY、BLOB。

    * --lock-all-tables,-x
      在开始导出之前,提交请求锁定所有数据库中的所有表,以保证数据的一致性。这是一个全局读锁,并且自动关闭 --single-transaction 和 --lock-tables 选项。

    * --lock-tables
      它和 --lock-all-tables 类似,不过是锁定当前导出的数据表,而不是一下子锁定全部库下的表。本选项只适用于 MyISAM 表,如果是 Innodb 表可以用 --single-transaction 选项。

    * --no-create-info,-t
      只导出数据,而不添加 CREATE TABLE 语句。

    * --no-data,-d
      不导出任何数据,只导出数据库表结构。

    * --opt
      这只是一个快捷选项,等同于同时添加 --add-drop-tables --add-locking --create-option --disable-keys --extended-insert --lock-tables --quick --set-charset 选项。本选项能让 mysqldump 很快的导出数据,并且导出的数据能很快导回。该选项默认开启,但可以用 --skip-opt 禁用。注意,如果运行 mysqldump 没有指定 --quick 或 --opt 选项,则会将整个结果集放在内存中。如果导出大数据库的话可能会出现问题。

    * --quick,-q
      该选项在导出大表时很有用,它强制 mysqldump 从服务器查询取得记录直接输出而不是取得所有记录后将它们缓存到内存中。

    * --routines,-R
      导出存储过程以及自定义函数。

    * --single-transaction
      该选项在导出数据之前提交一个 BEGIN SQL语句,BEGIN 不会阻塞任何应用程序且能保证导出时数据库的一致性状态。它只适用于事务表,例如 InnoDB 和 BDB。
      本选项和 --lock-tables 选项是互斥的,因为 LOCK TABLES 会使任何挂起的事务隐含提交。
      要想导出大表的话,应结合使用 --quick 选项。

    * --triggers
      同时导出触发器。该选项默认启用,用 --skip-triggers 禁用它。

其他参数详情请参考手册,我通常使用以下 SQL 来备份 MyISAM 表:

/usr/local/mysql/bin/mysqldump -uyejr -pyejr \
--default-character-set=utf8 --opt --extended-insert=false \
--triggers -R --hex-blob -x db_name > db_name.sql

使用以下 SQL 来备份 Innodb 表:

/usr/local/mysql/bin/mysqldump -uyejr -pyejr \
--default-character-set=utf8 --opt --extended-insert=false \
--triggers -R --hex-blob --single-transaction db_name > db_name.sql

另外,如果想要实现在线备份,还可以使用 --master-data 参数来实现,如下:

/usr/local/mysql/bin/mysqldump -uyejr -pyejr \
--default-character-set=utf8 --opt --master-data=1 \
--single-transaction --flush-logs db_name > db_name.sql

它只是在一开始的瞬间请求锁表,然后就刷新binlog了,而后在导出的文件中加入CHANGE MASTER 语句来指定当前备份的binlog位置,如果要把这个文件恢复到slave里去,就可以采用这种方法来做。
1.2 还原

用 mysqldump 备份出来的文件是一个可以直接倒入的 SQL 脚本,有两种方法可以将数据导入。

    * 直接用 mysql 客户端

      例如:

      /usr/local/mysql/bin/mysql -uyejr -pyejr db_name < db_name.sql

    * 用 SOURCE 语法

      其实这不是标准的 SQL 语法,而是 mysql 客户端提供的功能,例如:

      SOURCE /tmp/db_name.sql;

      这里需要指定文件的绝对路径,并且必须是 mysqld 运行用户(例如 nobody)有权限读取的文件。

2、 mysqlhotcopy
2.1 备份
mysqlhotcopy 是一个 PERL 程序,最初由Tim Bunce编写。它使用 LOCK TABLES、FLUSH TABLES 和 cp 或 scp 来快速备份数据库。它是备份数据库或单个表的最快的途径,但它只能运行在数据库文件(包括数据表定义文件、数据文件、索引文件)所在的机器上。mysqlhotcopy 只能用于备份 MyISAM,并且只能运行在 类Unix 和 NetWare 系统上。

mysqlhotcopy 支持一次性拷贝多个数据库,同时还支持正则表达。以下是几个例子:

root#/usr/local/mysql/bin/mysqlhotcopy -h=localhost -u=yejr -p=yejr \
db_name /tmp (把数据库目录 db_name 拷贝到 /tmp 下)
root#/usr/local/mysql/bin/mysqlhotcopy -h=localhost -u=yejr -p=yejr \
db_name_1 ... db_name_n /tmp
root#/usr/local/mysql/bin/mysqlhotcopy -h=localhost -u=yejr -p=yejr \
db_name./regex/ /tmp

更详细的使用方法请查看手册,或者调用下面的命令来查看 mysqlhotcopy 的帮助:

perldoc /usr/local/mysql/bin/mysqlhotcopy

注意,想要使用 mysqlhotcopy,必须要有 SELECT、RELOAD(要执行 FLUSH TABLES) 权限,并且还必须要能够有读取 datadir/db_name 目录的权限。

2.2 还原
mysqlhotcopy 备份出来的是整个数据库目录,使用时可以直接拷贝到 mysqld 指定的 datadir (在这里是 /usr/local/mysql/data/)目录下即可,同时要注意权限的问题,如下例:

root#cp -rf db_name /usr/local/mysql/data/
root#chown -R nobody:nobody /usr/local/mysql/data/ (将 db_name 目录的属主改成 mysqld 运行用户)

3、 SQL 语法备份
3.1 备份
BACKUP TABLE 语法其实和 mysqlhotcopy 的工作原理差不多,都是锁表,然后拷贝数据文件。它能实现在线备份,但是效果不理想,因此不推荐使用。它只拷贝表结构文件和数据文件,不同时拷贝索引文件,因此恢复时比较慢。
例子:

BACK TABLE tbl_name TO '/tmp/db_name/';

注意,必须要有 FILE 权限才能执行本SQL,并且目录 /tmp/db_name/ 必须能被 mysqld 用户可写,导出的文件不能覆盖已经存在的文件,以避免安全问题。

SELECT INTO OUTFILE 则是把数据导出来成为普通的文本文件,可以自定义字段间隔的方式,方便处理这些数据。
例子:

SELECT * INTO OUTFILE '/tmp/db_name/tbl_name.txt' FROM tbl_name;

注意,必须要有 FILE 权限才能执行本SQL,并且文件 /tmp/db_name/tbl_name.txt 必须能被 mysqld 用户可写,导出的文件不能覆盖已经存在的文件,以避免安全问题。

3.2 恢复
用 BACKUP TABLE 方法备份出来的文件,可以运行 RESTORE TABLE 语句来恢复数据表。
例子:

RESTORE TABLE FROM '/tmp/db_name/';

权限要求类似上面所述。

用 SELECT INTO OUTFILE 方法备份出来的文件,可以运行 LOAD DATA INFILE 语句来恢复数据表。
例子:

LOAD DATA INFILE '/tmp/db_name/tbl_name.txt' INTO TABLE tbl_name;

权限要求类似上面所述。倒入数据之前,数据表要已经存在才行。如果担心数据会发生重复,可以增加 REPLACE 关键字来替换已有记录或者用 IGNORE 关键字来忽略他们。
4、 启用二进制日志(binlog)

采用 binlog 的方法相对来说更灵活,省心省力,而且还可以支持增量备份。

启用 binlog 时必须要重启 mysqld。首先,关闭 mysqld,打开 my.cnf,加入以下几行:

server-id = 1
log-bin = binlog
log-bin-index = binlog.index

然后启动 mysqld 就可以了。运行过程中会产生 binlog.000001 以及 binlog.index,前面的文件是 mysqld 记录所有对数据的更新操作,后面的文件则是所有 binlog 的索引,都不能轻易删除。关于 binlog 的信息请查看手册。

需要备份时,可以先执行一下 SQL 语句,让 mysqld 终止对当前 binlog 的写入,就可以把文件直接备份,这样的话就能达到增量备份的目的了:

FLUSH LOGS;

如果是备份复制系统中的从服务器,还应该备份 master.info 和 relay-log.info 文件。

备份出来的 binlog 文件可以用 MySQL 提供的工具 mysqlbinlog 来查看,如:

/usr/local/mysql/bin/mysqlbinlog /tmp/binlog.000001

该工具允许你显示指定的数据库下的所有 SQL 语句,并且还可以限定时间范围,相当的方便,详细的请查看手册。

恢复时,可以采用类似以下语句来做到:

/usr/local/mysql/bin/mysqlbinlog /tmp/binlog.000001 | mysql -uyejr -pyejr db_name

把 mysqlbinlog 输出的 SQL 语句直接作为输入来执行它。

如果你有空闲的机器,不妨采用这种方式来备份。由于作为 slave 的机器性能要求相对不是那么高,因此成本低,用低成本就能实现增量备份而且还能分担一部分数据查询压力,何乐而不为呢?

5、 直接备份数据文件
相较前几种方法,备份数据文件最为直接、快速、方便,缺点是基本上不能实现增量备份。为了保证数据的一致性,需要在拷贝文件前,执行以下 SQL 语句:

FLUSH TABLES WITH READ LOCK;

也就是把内存中的数据都刷新到磁盘中,同时锁定数据表,以保证拷贝过程中不会有新的数据写入。这种方法备份出来的数据恢复也很简单,直接拷贝回原来的数据库目录下即可。

注意,对于 Innodb 类型表来说,还需要备份其日志文件,即 ib_logfile* 文件。因为当 Innodb 表损坏时,就可以依靠这些日志文件来恢复。

6、 备份策略

对于中等级别业务量的系统来说,备份策略可以这么定:第一次全量备份,每天一次增量备份,每周再做一次全量备份,如此一直重复。而对于重要的且繁忙的系统来说,则可能需要每天一次全量备份,每小时一次增量备份,甚至更频繁。为了不影响线上业务,实现在线备份,并且能增量备份,最好的办法就是采用主从复制机制(replication),在 slave 机器上做备份。

7、 数据维护和灾难恢复
作为一名DBA(我目前还不是,呵呵),最重要的工作内容之一是保证数据表能安全、稳定、高速使用。因此,需要定期维护你的数据表。以下 SQL 语句就很有用:

CHECK TABLE table_name 或 REPAIR TABLE table_name,检查或维护 MyISAM 表
OPTIMIZE TABLE table_name,优化 MyISAM 表
ANALYZE TABLE table_name,分析 MyISAM 表

当然了,上面这些命令起始都可以通过工具 myisamchk 来完成,在这里不作详述。

Innodb 表则可以通过执行以下语句来整理碎片,提高索引速度:

ALTER TABLE tbl_name ENGINE = Innodb;

这其实是一个 NULL 操作,表面上看什么也不做,实际上重新整理碎片了。

通常使用的 MyISAM 表可以用上面提到的恢复方法来完成。如果是索引坏了,可以用 myisamchk 工具来重建索引。而对于 Innodb 表来说,就没这么直接了,因为它把所有的表都保存在一个表空间了。不过 Innodb 有一个检查机制叫 模糊检查点,只要保存了日志文件,就能根据日志文件来修复错误。可以在 my.cnf 文件中,增加以下参数,让 mysqld 在启动时自动检查日志文件:

innodb_force_recovery = 4

关于该参数的信息请查看手册。

8、 总结
做好数据备份,定制作好合适的备份策略,这是一个DBA所做事情的一小部分,万事开头难,就从现在开始吧! 
mysql的增量备份
在数据库表丢失或损坏的情况下,备份你的数据库是很重要的。如果发生系统崩溃,你肯定想能够将你的表尽可能丢失最少的数据恢复到崩溃发生时的状态。本文主要对MyISAM表做备份恢复。

http://blog.sina.com.cn/s/blog_4e424e2101000c1x.html


备份策略一:直接拷贝数据库文件(不推荐)

备份策略二:使用mysqlhotcopy备份数据库(完全备份,适合小型数据库备份)

备份策略三:使用mysqldump备份数据库(完全+增量备份,适合中型数据库备份)

备份策略四:使用主从复制机制(replication)(实现数据库实时备份)




备份策略一、直接拷贝数据库文件
直接拷贝数据文件最为直接、快速、方便,但缺点是基本上不能实现增量备份。为了保证数据的一致性,需要在备份文件前,执行以下 SQL 语句:
FLUSH TABLES WITH READ LOCK;
也就是把内存中的数据都刷新到磁盘中,同时锁定数据表,以保证拷贝过程中不会有新的数据写入。这种方法备份出来的数据恢复也很简单,直接拷贝回原来的数据库目录下即可。


备份策略二、使用mysqlhotcopy备份数据库
mysqlhotcopy 是一个 PERL 程序,最初由Tim Bunce编写。它使用 LOCK TABLES、FLUSH TABLES 和 cp 或 scp 来快速备份数据库。它是备份数据库或单个表的最快的途径,但它只能运行在数据库文件(包括数据表定义文件、数据文件、索引文件)所在的机器上,并且mysqlhotcopy 只能用于备份 MyISAM表。

本备份策略适合于小型数据库的备份,数据量不大,可以采用mysqlhotcopy程序每天进行一次完全备份。

备份策略布置:

(1)、安装DBD-mysql perl模块,支持mysqlhotcopy脚本连接到MySQL数据库。

shell> tar -xzvf  DBD-mysql-4.005.tar.gz

shell> cd DBD-mysql-4.005

shell> unset LANG

shell> perl Makefile.PL -mysql_config=/usr/local/mysql/bin/mysql_config -testuser=root -testpassword=UserPWD

shell> make

shell> make test

shell> make install

(2)、设置crontab任务,每天执行备份脚本

shell> crontab -e

0 3 * * * /root/MySQLBackup/mysqlbackup.sh >/dev/null 2>&1

每天凌晨3:00执行备份脚本。


mysqlbackup.sh注释:

#!/bin/sh

# Name:mysqlbackup.sh

# PS:MySQL DataBase Backup,Use mysqlhotcopy script.

# Write by:i.Stone

# Last Modify:2007-11-15

#

# 定义变量,请根据具体情况修改

# 定义脚本所在目录

scriptsDir=`pwd`

# 数据库的数据目录

dataDir=/usr/local/mysql/data/

# 数据备份目录

tmpBackupDir=/tmp/tmpbackup/

backupDir=/tmp/mysqlbackup/

# 用来备份数据库的用户名和密码

mysqlUser=root

mysqlPWD=111111

# 定义eMail地址

eMail=alter@somode.com


# 如果临时备份目录存在,清空它,如果不存在则创建它

if [[ -e $tmpBackupDir ]]; then

  rm -rf $tmpBackupDir/*

else

  mkdir $tmpBackupDir

fi

# 如果备份目录不存在则创建它

if [[ ! -e $backupDir ]];then


mkdir $backupDir

fi


# 清空MySQLBackup.log

if [[ -s MySQLBackup.log ]]; then

  cat /dev/null >MySQLBackup.log

fi


# 得到数据库备份列表,在此可以过滤不想备份的数据库

for databases in `find $dataDir -type d | \

  sed -e "s/\/usr\/local\/mysql\/data\///" | \

  sed -e "s/test//"`; do


  if [[ $databases == "" ]]; then

    continue

  else

# 备份数据库

    /usr/local/mysql/bin/mysqlhotcopy --user=$mysqlUser --password=$mysqlPWD -q "$databases" $tmpBackupDir

    dateTime=`date "+%Y.%m.%d %H:%M:%S"`

    echo "$dateTime Databasedatabases backup success!" >>MySQLBackup.log

  fi

done


# 压缩备份文件

date=`date -I`

cd $tmpBackupDir

tar czf $backupDir/mysql-$date.tar.gz ./


# 发送邮件通知

if [[ -s MySQLBackup.log ]]; then

  cat MySQLBackup.log | mail -s "MySQL Backup" $eMail

fi


# 使用smbclientmv.sh脚本上传数据库备份到备份服务器

# $scriptsDir/smbclientmv.sh




smbclientmv.sh注释:

#!/bin/sh

# Name:smbclientmv.sh

# PS:Move the data to Backup Server.

# Write by:i.Stone

# Last Modify:2007-11-15

#

# 定义变量

# 备份服务器名

BackupServer="BackupServerName"

# 共享文件夹名

BackupShare="ShareName"

# 备份服务器的访问用户名和密码

BackupUser="SMBUser"

BackupPW="SMBPassword"

# 定义备份目录

BackupDir=/tmp/mysqlbackup

date=`date -I`


# Move the data to BackupServer

smbclient //$BackupServer/$BackupShare \

$BackupPW -d0 -W WORKGROUP -U $BackupUser \

-c "put $BackupDir/mysql-$date.tar.gz \

mysql-$date.tar.gz"


# Delete temp files

rm -f $BackupDir/mysql-$date.tar.gz



(3)、恢复数据库到备份时的状态

mysqlhotcopy 备份出来的是整个数据库目录,使用时可以直接拷贝到 mysqld 指定的 datadir (在这里是 /usr/local/mysql/data/)目录下即可,同时要注意权限的问题,如下例:
shell> cp -rf db_name /usr/local/mysql/data/

shell> chown -R mysql:mysql /usr/local/mysql/data/ (将 db_name 目录的属主改成 mysqld 运行用户)

本套备份策略只能恢复数据库到最后一次备份时的状态,要想在崩溃时丢失的数据尽量少应该更频繁的进行备份,要想恢复数据到崩溃时的状态请使用主从复制机制(replication)。

备份策略三、使用mysqldump备份数据库



mysqldump 是采用SQL级别的备份机制,它将数据表导成 SQL 脚本文件,在不同的 MySQL 版本之间升级时相对比较合适,这也是最常用的备份方法。mysqldump 比直接拷贝要慢些。关于mysqldump的更详细解释见最后的附录。

对于中等级别业务量的系统来说,备份策略可以这么定:第一次完全备份,每天一次增量备份,每周再做一次完全备份,如此一直重复。而对于重要的且繁忙的系统来说,则可能需要每天一次全量备份,每小时一次增量备份,甚至更频繁。为了不影响线上业务,实现在线备份,并且能增量备份,最好的办法就是采用主从复制机制(replication),在 slave 机器上做备份。

备份策略布置:

(1)、创建备份目录

Shell> mkdir /tmp/mysqlbackup

Shell> mkdir /tmp/mysqlbackup/daily

(2)、启用二进制日志

采用 binlog 的方法相对来说更灵活,省心省力,而且还可以支持增量备份。
启用 binlog 时必须要重启 mysqld。首先,关闭 mysqld,打开 /etc/my.cnf,加入以下几行:
[mysqld]
log-bin

然后启动 mysqld 就可以了。运行过程中会产生 HOSTNAME-bin.000001 以及 HOSTNAME-bin.index,前面的文件是 mysqld 记录所有对数据的更新操作,后面的文件则是所有 binlog 的索引,都不能轻易删除。关于 binlog 的更详细信息请查看手册。

(3)、配置SSH密钥登录,用于将MySQL备份传送到备份服务器(如果备份服务器为Windows,请跳过此部)。

1)、在MySQL所在服务器(192.168.0.20)生成SSH密钥

[root@lab ~]# ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
//直接回车
Enter passphrase (empty for no passphrase):
//直接回车,不使用密码
Enter same passphrase again:
//直接回车,不使用密码
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
c2:96:9f:2d:5a:8e:08:42:43:35:2f:85:5e:72:f8:1c root@lab

2)、在备份服务器(192.168.0.200)上创建目录,修改权限,并传送公钥。
[root@lab ~]# ssh 192.168.0.200 "mkdir .ssh;chmod 0700 .ssh"
The authenticity of host '192.168.0.200 (192.168.0.200)' can't be established.
RSA key fingerprint is 37:57:55:c1:32:f1:dd:bb:1b:8a:13:6f:89:fb:b8:9d.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.0.200' (RSA) to the list of known hosts.
root@192.168.0.200's password:
//输入备份服务器的root密码
[root@lab ~]# scp .ssh/id_rsa.pub 192.168.0.200:.ssh/authorized_keys2
root@192.168.0.200's password:
id_rsa.pub                                             100%  218     0.2KB/s   00:00   
3)、测试SSH登录
[root@lab ~]# ssh 192.168.0.200       //测试SSH登录
Last login: Fri Nov 16 10:34:02 2007 from 192.168.0.20
[root@lib ~]#


(4)、设置crontab任务,每天执行备份脚本

shell> crontab -e

#每个星期日凌晨3:00执行完全备份脚本

0 3 * * 0 /root/MySQLBackup/mysqlFullBackup.sh >/dev/null 2>&1

#周一到周六凌晨3:00做增量备份

0 3 * * 1-6 /root/MySQLBackup/mysqlDailyBackup.sh >/dev/null 2>&1


mysqlFullBackup.sh注释:

#!/bin/sh

# Name:mysqlFullBackup.sh

# PS:MySQL DataBase Full Backup.

# Write by:i.Stone

# Last Modify:2007-11-17

#

# Use mysqldump --help get more detail.

#

# 定义变量,请根据具体情况修改

# 定义脚本目录

scriptsDir=`pwd`

# 定义数据库目录

mysqlDir=/usr/local/mysql

# 定义用于备份数据库的用户名和密码

user=root

userPWD=111111

# 定义备份目录

dataBackupDir=/tmp/mysqlbackup

# 定义邮件正文文件

eMailFile=$dataBackupDir/email.txt

# 定义邮件地址

eMail=alter@somode.com

# 定义备份日志文件

logFile=$dataBackupDir/mysqlbackup.log

DATE=`date -I`


echo "" > $eMailFile

echo $(date +"%y-%m-%d %H:%M:%S" >> $eMailFile

cd $dataBackupDir

# 定义备份文件名

dumpFile=mysql_$DATE.sql

GZDumpFile=mysql_$DATE.sql.tar.gz


# 使用mysqldump备份数据库,请根据具体情况设置参数

$mysqlDir/bin/mysqldump -u$user -p$userPWD \

--opt --default-character-set=utf8 --extended-insert=false \

--triggers -R --hex-blob --all-databases \

--flush-logs --delete-master-logs \

--delete-master-logs \

-x > $dumpFile


# 压缩备份文件

if [[ $? == 0 ]]; then


tar czf $GZDumpFile $dumpFile >> $eMailFile 2>&1


echo "BackupFileNameGZDumpFile" >> $eMailFile


echo "DataBase Backup Success!" >> $eMailFile


rm -f $dumpFile


# Delete daily backup files.


cd $dataBackupDir/daily


rm -f *


# Delete old backup files(mtime>2).


$scriptsDir/rmBackup.sh


# 如果不需要将备份传送到备份服务器或备份服务器为Windows,请将标绿的行注释掉

# Move Backup Files To Backup Server.

#适合Linux(MySQL服务器)到Linux(备份服务器)


$scriptsDir/rsyncBackup.sh


if (( !$? )); then

    echo "Move Backup Files To Backup Server Success!" >> $eMailFile

    else

    echo "Move Backup Files To Backup Server Fail!" >> $eMailFile

  fi


else


echo "DataBase Backup Fail!" >> $emailFile

fi

# 写日志文件

echo "--------------------------------------------------------" >> $logFile

cat $eMailFile >> $logFile

# 发送邮件通知

cat $eMailFile | mail -s "MySQL Backup" $eMail





(5) 、恢复数据库到备份时的状态

用 mysqldump 备份出来的文件是一个可以直接倒入的 SQL 脚本,直接用 mysql 客户端导入就可以了。

/usr/local/mysql/bin/mysql -uroot -pUserPWD db_name < db_name.sql
对于任何可适用的更新日志,将它们作为 mysql 的输入:
  % ls -t -r -1 HOSTNAME-bin* | xargs mysqlbinlog | mysql -uUser -pUserPWD

ls 命令生成更新日志文件的一个单列列表,根据服务器产生它们的次序排序(注意:如果你修改任何一个文件,你将改变排序次序,这将导致更新日志以错误的次序被运用。)

本套备份策略只能恢复数据库到最后一次备份时的状态,要想在崩溃时丢失的数据尽量少应该更频繁的进行备份,要想恢复数据到崩溃时的状态请使用主从复制机制(replication)。如果使用本套备份脚本,将日志文件和数据文件放到不同的磁盘上是一个不错的主义,这样不仅可以提高数据写入速度,还能使数据更安全。

配置MySQL主从复制(Replication)
(2007-11-17 14:39:30)
转载▼
标签:
知识/探索
mysql复制
replication
mysql备份
	分类: 数据库

MySQL支持单向、异步复制,复制过程中一个服务器充当主服务器,而一个或多个其它服务器充当从服务器。主服务器将更新写入二进制日志文件,并维护日志文件的一个索引以跟踪日志循环。当一个从服务器连接到主服务器时,它通知主服务器从服务器在日志中读取的最后一次成功更新的位置。从服务器接收从那时起发生的任何更新,然后封锁并等待主服务器通知下一次更新。

 

为什么使用主从复制?

1、主服务器/从服务器设置增加了健壮性。主服务器出现问题时,你可以切换到从服务器作为备份。

2、通过在主服务器和从服务器之间切分处理客户查询的负荷,可以得到更好的客户响应时间。但是不要同时在主从服务器上进行更新,这样可能引起冲突。

3、使用复制的另一个好处是可以使用一个从服务器执行备份,而不会干扰主服务器。在备份过程中主服务器可以继续处理更新。

 

MySQL使用3个线程来执行复制功能(其中1个在主服务器上,另两个在从服务器上。当发出START SLAVE时,从服务器创建一个I/O线程,以连接主服务器并让主服务器发送二进制日志。主服务器创建一个线程将二进制日志中的内容发送到从服务器。从服务器I/O线程读取主服务器Binlog Dump线程发送的内容并将该数据拷贝到从服务器数据目录中的本地文件中,即中继日志。第3个线程是SQL线程,从服务器使用此线程读取中继日志并执行日志中包含的更新。SHOW PROCESSLIST语句可以查询在主服务器上和从服务器上发生的关于复制的信息。

 

默认中继日志使用host_name-relay-bin.nnnnnn形式的文件名,其中host_name是从服务器主机名,nnnnnn是序列号。用连续序列号来创建连续中继日志文件,从000001开始。从服务器跟踪中继日志索引文件来识别目前正使用的中继日志。默认中继日志索引文件名为host_name-relay-bin.index。在默认情况,这些文件在从服务器的数据目录中被创建。中继日志与二进制日志的格式相同,并且可以用mysqlbinlog读取。当SQL线程执行完中继日志中的所有事件后,中继日志将会被自动删除。

 

从服务器在数据目录中另外创建两个状态文件--master.info和relay-log.info。状态文件保存在硬盘上,从服务器关闭时不会丢失。下次从服务器启动时,读取这些文件以确定它已经从主服务器读取了多少二进制日志,以及处理自己的中继日志的程度。

 

设置主从复制:

 

1、确保在主服务器和从服务器上安装的MySQL版本相同,并且最好是MySQL的最新稳定版本。

2、在主服务器上为复制设置一个连接账户。该账户必须授予REPLICATION SLAVE权限。如果账户仅用于复制(推荐这样做),则不需要再授予任何其它权限。

mysql> GRANT REPLICATION SLAVE ON *.*

    -> TO 'replication'@'%.yourdomain.com' IDENTIFIED BY 'slavepass';

3、执行FLUSH TABLES WITH READ LOCK语句清空所有表和块写入语句:

mysql> FLUSH TABLES WITH READ LOCK;

保持mysql客户端程序不要退出。开启另一个终端对主服务器数据目录做快照。

shell> cd /usr/local/mysql/

shell> tar -cvf /tmp/mysql-snapshot.tar ./data

如果从服务器的用户账户与主服务器的不同,你可能不想复制mysql数据库。在这种情况下,应从归档中排除该数据库。你也不需要在归档中包括任何日志文件或者master.info或relay-log.info文件。

当FLUSH TABLES WITH READ LOCK所置读锁定有效时(即mysql客户端程序不退出),读取主服务器上当前的二进制日志名和偏移量值:

mysql > SHOW MASTER STATUS;

+---------------+----------+--------------+------------------+

| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB |

+---------------+----------+--------------+------------------+

| mysql-bin.003 | 73       | test         | manual,mysql     |

+---------------+----------+--------------+------------------+

File列显示日志名,而Position显示偏移量。在该例子中,二进制日志值为mysql-bin.003,偏移量为73。记录该值。以后设置从服务器时需要使用这些值。它们表示复制坐标,从服务器应从该点开始从主服务器上进行新的更新。

如果主服务器运行时没有启用--logs-bin,SHOW MASTER STATUS显示的日志名和位置值为空。在这种情况下,当以后指定从服务器的日志文件和位置时需要使用的值为空字符串('')和4.

取得快照并记录日志名和偏移量后,回到前一中端重新启用写活动:

mysql> UNLOCK TABLES;

4、确保主服务器主机上my.cnf文件的[mysqld]部分包括一个log-bin选项。该部分还应有一个server-id=Master_id选项,其中master_id必须为1到232–1之间的一个正整数值。例如:

[mysqld]

log-bin

server-id=1

如果没有提供那些选项,应添加它们并重启服务器。

5、停止从服务器上的mysqld服务并在其my.cnf文件中添加下面的行:

[mysqld]

server-id=2

slave_id值同Master_id值一样,必须为1到232–1之间的一个正整数值。并且,从服务器的ID必须与主服务器的ID不相同。

6、将数据备据目录中。确保对这些文件和目录的权限正确。服务器 MySQL运行的用户必须能够读写文件,如同在主服务器上一样。

Shell> chown -R mysql:mysql /usr/local/mysql/data

7、启动从服务器。在从服务器上执行下面的语句,用你的系统的实际值替换选项值:

 

        mysql> CHANGE MASTER TO

            -> MASTER_HOST='master_host_name',

            -> MASTER_USER='replication_user_name',

            -> MASTER_PASSWORD='replication_password',

            -> MASTER_LOG_FILE='recorded_log_file_name',

            -> MASTER_LOG_POS=recorded_log_position;

8、启动从服务器线程:

        mysql> START SLAVE;

执行这些程序后,从服务器应连接主服务器,并补充自从快照以来发生的任何更新。

9、如果出现复制错误,从服务器的错误日志(HOSTNAME.err)中也会出现错误消息。

10、从服务器复制时,会在其数据目录中发现文件master.info和HOSTNAME-relay-log.info。从服务器使用这两个文件跟踪已经处理了多少主服务器的二进制日志。不要移除或编辑这些文件,除非你确切知你正在做什么并完全理解其意义。即使这样,最好是使用CHANGE MASTER TO语句。
Gson与Json互相转换
package com.lupeng.javase.json.bean;

import java.io.Serializable;

/**
 * 普通JavaBean类
 * @author 翔林小刚
 * @date   2011-12-18
 */
public class JavaBean implements Serializable{
 private static final long serialVersionUID = 8720431164344424704L;
 
 private String id;
 private String name;
 private int age;
 private String addr;
 
 public JavaBean() {
  super();
 }
 
 public JavaBean(String id, String name, int age, String addr) {
  super();
  this.id = id;
  this.name = name;
  this.age = age;
  this.addr = addr;
 }
 public String getId() {
  return id;
 }

 public void setId(String id) {
  this.id = id;
 }
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
 public int getAge() {
  return age;
 }
 public void setAge(int age) {
  this.age = age;
 }
 public String getAddr() {
  return addr;
 }
 public void setAddr(String addr) {
  this.addr = addr;
 }

 @Override
 public String toString() {
  return "User [id=" + id + ", name=" + name + ", age=" + age + ", addr=" + addr + "]";
 }
 
}


 

-------------------------------------------------无情分割线--------------------------------------------

 

package com.lupeng.javase.json.bean;

import java.io.Serializable;
import java.util.Date;

/**
 * 带日期属性的JavaBean类
 * @author 翔林小刚
 * @date   2011-12-18
 */
public class DateBean implements Serializable {
 private static final long serialVersionUID = -2071405788146467301L;
 
 private String id;
 private String name;
 private int age;
 private Date birth;
 
 public DateBean() {
  super();
 }
 
 public DateBean(String id, String name, int age, Date birth) {
  super();
  this.id = id;
  this.name = name;
  this.age = age;
  this.birth = birth;
 }

 public String getId() {
  return id;
 }

 public void setId(String id) {
  this.id = id;
 }

 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
 public int getAge() {
  return age;
 }
 public void setAge(int age) {
  this.age = age;
 }
 public Date getBirth() {
  return birth;
 }
 public void setBirth(Date birth) {
  this.birth = birth;
 }

 @Override
 public String toString() {
  return "DateBean [age=" + age + ", birth=" + birth + ", id=" + id
    + ", name=" + name + "]";
 }
 
}

 

-------------------------------------------------无情分割线--------------------------------------------

 

package com.lupeng.javase.json.util;

import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
 * 日期序列化实用工具类
 * @author 翔林小刚
 * @date   2011-12-18
 */
public class DateDeserializerUtils implements JsonDeserializer<java.util.Date>{
 @Override
 public java.util.Date deserialize(JsonElement json, Type type,
   JsonDeserializationContext context) throws JsonParseException {
  return new java.util.Date(json.getAsJsonPrimitive().getAsLong());
 }

}

 

 

 

-------------------------------------------------无情分割线--------------------------------------------

package com.lupeng.javase.json.util;

import java.lang.reflect.Type;
import java.util.Date;

import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

/**
 * 日期解序列实用工具类
 * @author 翔林小刚
 * @date   2011-12-18
 */
public class DateSerializerUtils implements JsonSerializer<java.util.Date>{
 @Override
 public JsonElement serialize(Date date, Type type, JsonSerializationContext content) {
  return new JsonPrimitive(date.getTime());
 }

}


-------------------------------------------------无情分割线--------------------------------------------

 

 

 

 

 

package com.lupeng.javase.apps.json;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Before;
import org.junit.Test;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.lupeng.javase.json.bean.DateBean;
import com.lupeng.javase.json.bean.JavaBean;
import com.lupeng.javase.json.util.DateDeserializerUtils;
import com.lupeng.javase.json.util.DateSerializerUtils;

/**
 * Google Gson解析Json数据实例
 *
 * 1.Bean、Json转换 testBeanJson() 2.List -> Json转换 testList2Json()
 * 3.泛型List、Json相互转换 testGenericList2Json() 4.Map -> Json转换 testMap2Json()
 * 5.泛型Map、Json相互转换 testGenericMap2Json() 6.带日期属性Bean、Json转换 testDateBeanJson()
 * 7.带日期属性泛型List、Json转换 testDateGenericListJson()
 *
 * @author 翔林小刚
 * @create 2011-08-05
 * @modify 2011-12-18
 */
@SuppressWarnings("unchecked")
public class GsonTester {
 private Gson gson = null;
 private GsonBuilder gsonBuilder = null;

 @Before
 public void setUp() {
  gson = new Gson();
  gsonBuilder = new GsonBuilder();
 }

 /**
  * JavaBean、Json相互转换
  */
 @Test
 public void testBeanJson() {
  JavaBean bean = new JavaBean("1001", "scott", 20, "TL");

  // Bean -> Json
  String json = gson.toJson(bean);
  System.out.println(json);

  // Json -> Bean
  bean = gson.fromJson(json, JavaBean.class);
  System.out.println(bean);
 }

 /**
  * List转换成Json字符串
  */
 @Test
 public void testList2Json() {
  // List
  List list = new ArrayList();
  for (int i = 0; i < 5; i++) {
   list.add("element" + i);
  }
  System.out.println(list);

  // List -> Json
  String json = gson.toJson(list);
  System.out.println(json);
 }

 /**
  * 泛型List、Json相互转换
  */
 @Test
 public void testGenericListJson() {
  // 泛型List
  List<JavaBean> list = new ArrayList<JavaBean>();
  for (int i = 0; i < 3; i++) {
   JavaBean user = new JavaBean("100" + i, "name" + i, 20 + i, "BJ"
     + i);
   list.add(user);
  }
  System.out.println(list);

  // 泛型List -> Json
  java.lang.reflect.Type type = new com.google.gson.reflect.TypeToken<List<JavaBean>>() {
  }.getType();
  String json = gson.toJson(list, type);
  System.out.println(json);

  // Json -> 泛型List
  List<JavaBean> users = gson.fromJson(json.toString(), type);
  System.out.println(users);
 }

 /**
  * Map转换成Json字符串
  */
 @Test
 public void testMap2Json() {
  // Map数据
  Map map = new HashMap();
  map.put("id", "1001");
  map.put("name", "scott");
  map.put("age", 20);
  map.put("addr", "BJ");
  System.out.println(map);

  // Map -> Json
  String json = gson.toJson(map);
  System.out.println(json);
 }

 /**
  * 泛型Map、Json相互转换
  */
 @Test
 public void testGenericMapJson() {
  // 泛型Map数据
  Map<String, JavaBean> map = new HashMap<String, JavaBean>();
  for (int i = 0; i < 5; i++) {
   JavaBean user = new JavaBean("100" + i, "name" + i, 20 + i, "LN"
     + i);
   map.put("100" + i, user);
  }
  System.out.println(map);

  // 泛型Map -> Json
  java.lang.reflect.Type type = new com.google.gson.reflect.TypeToken<Map<String, JavaBean>>() {
  }.getType();
  String json = gson.toJson(map, type);
  System.out.println(json);

  // Json -> Map
  Map<String, JavaBean> users = gson.fromJson(json.toString(), type);
  System.out.println(users);

 }

 /**
  * 带日期类型Bean、Json相互转换
  */
 @Test
 public void testDateBeanJson() {
  // 日期Bean数据
  DateBean bean = new DateBean("1001", "scott", 20, new Date());

  // Bean(带日期属性) -> Json
  gson = gsonBuilder.registerTypeAdapter(java.util.Date.class,
    new DateSerializerUtils()).setDateFormat(DateFormat.LONG)
    .create();

  // gson = new
  // GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
  String json = gson.toJson(bean);
  System.out.println("_________________________" + json);

  // Json -> Bean(带日期类型属性)
  gson = gsonBuilder.registerTypeAdapter(java.util.Date.class,new DateDeserializerUtils()).setDateFormat("yyyy/MM/dd")
    .create();
  java.lang.reflect.Type type = new com.google.gson.reflect.TypeToken<DateBean>() {
  }.getType();
  DateBean b = gson.fromJson(json, type);
  System.out.println(b);
 }

 /**
  * 泛型日期List、Json相互转换
  */
 @Test
 public void testDateGenericListJson() {
  // 泛型日期List
  List<DateBean> list = new ArrayList<DateBean>();
  for (int i = 0; i < 3; i++) {
   DateBean user = new DateBean("100" + i, "name" + i, 20 + i,
     new Date());
   list.add(user);
  }
  System.out.println(list);

  // 泛型日期List -> Json
  gson = gsonBuilder.registerTypeAdapter(java.util.Date.class,
    new DateSerializerUtils()).setDateFormat(DateFormat.LONG)
    .create();
  java.lang.reflect.Type type = new com.google.gson.reflect.TypeToken<List<DateBean>>() {
  }.getType();
  String json = gson.toJson(list, type);
  System.out.println(json);

  // Json -> 泛型日期List
  gson = gsonBuilder.registerTypeAdapter(java.util.Date.class,
    new DateDeserializerUtils()).setDateFormat(DateFormat.LONG)
    .create();
  List<DateBean> users = gson.fromJson(json.toString(), type);
  System.out.println(users);
 }
}
 
Global site tag (gtag.js) - Google Analytics