truelover`s WebLog http://blog.donews.com/afxtruelover Weblog Sat, 29 Apr 2006 10:52:00 +0000 http://wordpress.org/?v=2.9.2 en hourly 1 [转载]一篇关于web.xml配置的详细说明 http://blog.donews.com/afxtruelover/archive/2006/04/29/851013.aspx http://blog.donews.com/afxtruelover/archive/2006/04/29/851013.aspx#comments Sat, 29 Apr 2006 10:52:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/04/29/851013.aspx 一篇关于web.xml配置的详细说明

(转载)[TOMCAT]用web.xml控制Web应用的行为



1 定义头和根元素

部署描述符文件就像所有XML文件一样,必须以一个XML头开始。这个头声明可以使用的XML版本并给出文件的字符编码。
DOCYTPE声明必须立即出现在此头之后。这个声明告诉服务器适用的servlet规范的版本(如2.2或2.3)并指定管理此文件其余部分内容的语法的DTD(Document Type Definition,文档类型定义)。
所有部署描述符文件的顶层(根)元素为web-app。请注意,XML元素不像HTML,他们是大小写敏感的。因此,web-App和WEB-APP都是不合法的,web-app必须用小写。

2 部署描述符文件内的元素次序

XML 元素不仅是大小写敏感的,而且它们还对出现在其他元素中的次序敏感。例如,XML头必须是文件中的第一项,]]>
一篇关于web.xml配置的详细说明

(转载)[TOMCAT]用web.xml控制Web应用的行为



1 定义头和根元素

部署描述符文件就像所有XML文件一样,必须以一个XML头开始。这个头声明可以使用的XML版本并给出文件的字符编码。
DOCYTPE声明必须立即出现在此头之后。这个声明告诉服务器适用的servlet规范的版本(如2.2或2.3)并指定管理此文件其余部分内容的语法的DTD(Document Type Definition,文档类型定义)。
所有部署描述符文件的顶层(根)元素为web-app。请注意,XML元素不像HTML,他们是大小写敏感的。因此,web-App和WEB-APP都是不合法的,web-app必须用小写。

2 部署描述符文件内的元素次序

XML 元素不仅是大小写敏感的,而且它们还对出现在其他元素中的次序敏感。例如,XML头必须是文件中的第一项,DOCTYPE声明必须是第二项,而web- app元素必须是第三项。在web-app元素内,元素的次序也很重要。服务器不一定强制要求这种次序,但它们允许(实际上有些服务器就是这样做的)完全拒绝执行含有次序不正确的元素的Web应用。这表示使用非标准元素次序的web.xml文件是不可移植的。
下面的列表给出了所有可直接出现在web-app元素内的合法元素所必需的次序。例如,此列表说明servlet元素必须出现在所有servlet-mapping元素之前。请注意,所有这些元素都是可选的。因此,可以省略掉某一元素,但不能把它放于不正确的位置。
l icon icon元素指出IDE和GUI工具用来表示Web应用的一个和两个图像文件的位置。
l display-name display-name元素提供GUI工具可能会用来标记这个特定的Web应用的一个名称。
l description description元素给出与此有关的说明性文本。
l context-param context-param元素声明应用范围内的初始化参数。
l filter 过滤器元素将一个名字与一个实现javax.servlet.Filter接口的类相关联。
l filter-mapping 一旦命名了一个过滤器,就要利用filter-mapping元素把它与一个或多个servlet或JSP页面相关联。
l listener servlet API的版本2.3增加了对事件监听程序的支持,事件监听程序在建立、修改和删除会话或servlet环境时得到通知。Listener元素指出事件监听程序类。
l servlet 在向servlet或JSP页面制定初始化参数或定制URL时,必须首先命名servlet或JSP页面。Servlet元素就是用来完成此项任务的。
l servlet-mapping 服务器一般为servlet提供一个缺省的URL:http://host/webAppPrefix/servlet/ServletName。但是,常常会更改这个URL,以便servlet可以访问初始化参数或更容易地处理相对URL。在更改缺省URL时,使用servlet-mapping元素。
l session-config 如果某个会话在一定时间内未被访问,服务器可以抛弃它以节省内存。可通过使用HttpSession的setMaxInactiveInterval方法明确设置单个会话对象的超时值,或者可利用session-config元素制定缺省超时值。
l mime-mapping 如果Web应用具有想到特殊的文件,希望能保证给他们分配特定的MIME类型,则mime-mapping元素提供这种保证。
l welcom-file-list welcome-file-list元素指示服务器在收到引用一个目录名而不是文件名的URL时,使用哪个文件。
l error-page error-page元素使得在返回特定HTTP状态代码时,或者特定类型的异常被抛出时,能够制定将要显示的页面。
l taglib taglib元素对标记库描述符文件(Tag Libraryu Descriptor file)指定别名。此功能使你能够更改TLD文件的位置,而不用编辑使用这些文件的JSP页面。
l resource-env-ref resource-env-ref元素声明与资源相关的一个管理对象。
l resource-ref resource-ref元素声明一个资源工厂使用的外部资源。
l security-constraint security-constraint元素制定应该保护的URL。它与login-config元素联合使用
l login-config 用login-config元素来指定服务器应该怎样给试图访问受保护页面的用户授权。它与sercurity-constraint元素联合使用。
l security-role security-role元素给出安全角色的一个列表,这些角色将出现在servlet元素内的security-role-ref元素的role-name子元素中。分别地声明角色可使高级IDE处理安全信息更为容易。
l env-entry env-entry元素声明Web应用的环境项。
l ejb-ref ejb-ref元素声明一个EJB的主目录的引用。
l ejb-local-ref ejb-local-ref元素声明一个EJB的本地主目录的应用。

3 分配名称和定制的UL

在web.xml中完成的一个最常见的任务是对servlet或JSP页面给出名称和定制的URL。用servlet元素分配名称,使用servlet-mapping元素将定制的URL与刚分配的名称相关联。
3.1 分配名称
为了提供初始化参数,对servlet或JSP页面定义一个定制URL或分配一个安全角色,必须首先给servlet或JSP页面一个名称。可通过 servlet元素分配一个名称。最常见的格式包括servlet-name和servlet-class子元素(在web-app元素内),如下所示:
<servlet>
<servlet-name>Test</servlet-name>
<servlet-class>moreservlets.TestServlet</servlet-class>
</servlet>
这表示位于WEB-INF/classes/moreservlets/TestServlet的servlet已经得到了注册名Test。给 servlet一个名称具有两个主要的含义。首先,初始化参数、定制的URL模式以及其他定制通过此注册名而不是类名引用此servlet。其次,可在 URL而不是类名中使用此名称。因此,利用刚才给出的定义,URL http://host/webAppPrefix/servlet/Test 可用于 http://host/webAppPrefix/servlet/moreservlets.TestServlet 的场所。
请记住:XML元素不仅是大小写敏感的,而且定义它们的次序也很重要。例如,web-app元素内所有servlet元素必须位于所有servlet- mapping元素(下一小节介绍)之前,而且还要位于5.6节和5.11节讨论的与过滤器或文档相关的元素(如果有的话)之前。类似地,servlet 的servlet-name子元素也必须出现在servlet-class之前。5.2节"部署描述符文件内的元素次序"将详细介绍这种必需的次序。
例如,程序清单5-1给出了一个名为TestServlet的简单servlet,它驻留在moreservlets程序包中。因为此servlet是扎根在一个名为deployDemo的目录中的Web应用的组成部分,所以TestServlet.class放在deployDemo/WEB- INF/classes/moreservlets中。程序清单5-2给出将放置在deployDemo/WEB-INF/内的web.xml文件的一部分。此web.xml文件使用servlet-name和servlet-class元素将名称Test与TestServlet.class相关联。图 5-1和图5-2分别显示利用缺省URL和注册名调用TestServlet时的结果。

程序清单5-1 TestServlet.java
package moreservlets;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/** Simple servlet used to illustrate servlet naming
* and custom URLs.
* <P>
* Taken from More Servlets and JavaServer Pages
* from Prentice Hall and Sun Microsystems Press,
* http://www.moreservlets.com/.
* & 2002 Marty Hall; may be freely used or adapted.
*/

public class TestServlet extends HttpServlet {
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
String uri = request.getRequestURI();
out.println(ServletUtilities.headWithTitle("Test Servlet") +
"<BODY BGCOLOR=\"#FDF5E6\">\n" +
"<H2>URI: " + uri + "</H2>\n" +
"</BODY></HTML>");
}
}


程序清单5-2 web.xml(说明servlet名称的摘录)
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
<!– … –>
<servlet>
<servlet-name>Test</servlet-name>
<servlet-class>moreservlets.TestServlet</servlet-class>
</servlet>
<!– … –>
</web-app>

3.2 定义定制的URL
大多数服务器具有一个缺省的serlvet URL:
http://host/webAppPrefix/servlet/packageName.ServletName。虽然在开发中使用这个URL很方便,但是我们常常会希望另一个URL用于部署。例如,可能会需要一个出现在Web应用顶层的URL(如,http: //host/webAppPrefix/Anyname),并且在此URL中没有servlet项。位于顶层的URL简化了相对URL的使用。此外,对许多开发人员来说,顶层URL看上去比更长更麻烦的缺省URL更简短。
事实上,有时需要使用定制的URL。比如,你可能想关闭缺省URL映射,以便更好地强制实施安全限制或防止用户意外地访问无初始化参数的servlet。如果你禁止了缺省的URL,那么你怎样访问servlet呢?这时只有使用定制的URL了。
为了分配一个定制的URL,可使用servlet-mapping元素及其servlet-name和url-pattern子元素。Servlet- name元素提供了一个任意名称,可利用此名称引用相应的servlet;url-pattern描述了相对于Web应用的根目录的URL。url- pattern元素的值必须以斜杠(/)起始。
下面给出一个简单的web.xml摘录,它允许使用URL http://host/webAppPrefix/UrlTest而不是http://host/webAppPrefix/servlet/Test或
http: //host/webAppPrefix/servlet/moreservlets.TestServlet。请注意,仍然需要XML头、 DOCTYPE声明以及web-app封闭元素。此外,可回忆一下,XML元素出现地次序不是随意的。特别是,需要把所有servlet元素放在所有 servlet-mapping元素之前。
<servlet>
<servlet-name>Test</servlet-name>
<servlet-class>moreservlets.TestServlet</servlet-class>
</servlet>
<!– … –>
<servlet-mapping>
<servlet-name>Test</servlet-name>
<url-pattern>/UrlTest</url-pattern>
</servlet-mapping>
URL模式还可以包含通配符。例如,下面的小程序指示服务器发送所有以Web应用的URL前缀开始,以..asp结束的请求到名为BashMS的servlet。
<servlet>
<servlet-name>BashMS</servlet-name>
<servlet-class>msUtils.ASPTranslator</servlet-class>
</servlet>
<!– … –>
<servlet-mapping>
<servlet-name>BashMS</servlet-name>
<url-pattern>/*.asp</url-pattern>
</servlet-mapping>
3.3 命名JSP页面
因为JSP页面要转换成sevlet,自然希望就像命名servlet一样命名JSP页面。毕竟,JSP页面可能会从初始化参数、安全设置或定制的URL中受益,正如普通的serlvet那样。虽然JSP页面的后台实际上是servlet这句话是正确的,但存在一个关键的猜疑:即,你不知道JSP页面的实际类名(因为系统自己挑选这个名字)。因此,为了命名JSP页面,可将jsp-file元素替换为servlet-calss元素,如下所示:
<servlet>
<servlet-name>Test</servlet-name>
<jsp-file>/TestPage.jsp</jsp-file>
</servlet>
命名JSP页面的原因与命名servlet的原因完全相同:即为了提供一个与定制设置(如,初始化参数和安全设置)一起使用的名称,并且,以便能更改激活 JSP页面的URL(比方说,以便多个URL通过相同页面得以处理,或者从URL中去掉.jsp扩展名)。但是,在设置初始化参数时,应该注意,JSP页面是利用jspInit方法,而不是init方法读取初始化参数的。
例如,程序清单5-3给出一个名为TestPage.jsp的简单JSP页面,它的工作只是打印出用来激活它的URL的本地部分。TestPage.jsp放置在deployDemo应用的顶层。程序清单5-4给出了用来分配一个注册名PageName,然后将此注册名与http://host/webAppPrefix/UrlTest2/anything 形式的URL相关联的web.xml文件(即,deployDemo/WEB-INF/web.xml)的一部分。

程序清单5-3 TestPage.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<TITLE>
JSP Test Page
</TITLE>
</HEAD>
<BODY BGCOLOR="#FDF5E6">
<H2>URI: <%= request.getRequestURI() %></H2>
</BODY>
</HTML>


程序清单5-4 web.xml(说明JSP页命名的摘录)
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
<!– … –>
<servlet>
<servlet-name>PageName</servlet-name>
<jsp-file>/TestPage.jsp</jsp-file>
</servlet>
<!– … –>
<servlet-mapping>
<servlet-name> PageName </servlet-name>
<url-pattern>/UrlTest2/*</url-pattern>
</servlet-mapping>
<!– … –>
</web-app>


4 禁止激活器servlet

对servlet或JSP页面建立定制URL的一个原因是,这样做可以注册从 init(servlet)或jspInit(JSP页面)方法中读取得初始化参数。但是,初始化参数只在是利用定制URL模式或注册名访问 servlet或JSP页面时可以使用,用缺省URL http://host/webAppPrefix/servlet/ServletName 访问时不能使用。因此,你可能会希望关闭缺省URL,这样就不会有人意外地调用初始化servlet了。这个过程有时称为禁止激活器servlet,因为多数服务器具有一个用缺省的servlet URL注册的标准servlet,并激活缺省的URL应用的实际servlet。
有两种禁止此缺省URL的主要方法:
l 在每个Web应用中重新映射/servlet/模式。
l 全局关闭激活器servlet。
重要的是应该注意到,虽然重新映射每个Web应用中的/servlet/模式比彻底禁止激活servlet所做的工作更多,但重新映射可以用一种完全可移植的方式来完成。相反,全局禁止激活器servlet完全是针对具体机器的,事实上有的服务器(如ServletExec)没有这样的选择。下面的讨论对每个Web应用重新映射/servlet/ URL模式的策略。后面提供在Tomcat中全局禁止激活器servlet的详细内容。
4.1 重新映射/servlet/URL模式
在一个特定的Web应用中禁止以http://host/webAppPrefix/servlet/ 开始的URL的处理非常简单。所需做的事情就是建立一个错误消息servlet,并使用前一节讨论的url-pattern元素将所有匹配请求转向该 servlet。只要简单地使用:
<url-pattern>/servlet/*</url-pattern>
作为servlet-mapping元素中的模式即可。
例如,程序清单5-5给出了将SorryServlet servlet(程序清单5-6)与所有以http://host/webAppPrefix/servlet/ 开头的URL相关联的部署描述符文件的一部分。

程序清单5-5 web.xml(说明JSP页命名的摘录)
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
<!– … –>
<servlet>
<servlet-name>Sorry</servlet-name>
<servlet-class>moreservlets.SorryServlet</servlet-class>
</servlet>
<!– … –>
<servlet-mapping>
<servlet-name> Sorry </servlet-name>
<url-pattern>/servlet/*</url-pattern>
</servlet-mapping>
<!– … –>
</web-app>


程序清单5-6 SorryServlet.java
package moreservlets;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

/** Simple servlet used to give error messages to
* users who try to access default servlet URLs
* (i.e., http://host/webAppPrefix/servlet/ServletName)
* in Web applications that have disabled this
* behavior.
* <P>
* Taken from More Servlets and JavaServer Pages
* from Prentice Hall and Sun Microsystems Press,
* http://www.moreservlets.com/.
* & 2002 Marty Hall; may be freely used or adapted.
*/

public class SorryServlet extends HttpServlet {
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
String title = "Invoker Servlet Disabled.";
out.println(ServletUtilities.headWithTitle(title) +
"<BODY BGCOLOR=\"#FDF5E6\">\n" +
"<H2>" + title + "</H2>\n" +
"Sorry, access to servlets by means of\n" +
"URLs that begin with\n" +
"http://host/webAppPrefix/servlet/\n" +
"has been disabled.\n" +
"</BODY></HTML>");
}

public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}


4.2 全局禁止激活器:Tomcat
Tomcat 4中用来关闭缺省URL的方法与Tomcat 3中所用的很不相同。下面介绍这两种方法:
1.禁止激活器: Tomcat 4
Tomcat 4用与前面相同的方法关闭激活器servlet,即利用web.xml中的url-mapping元素进行关闭。不同之处在于Tomcat使用了放在 install_dir/conf中的一个服务器专用的全局web.xml文件,而前面使用的是存放在每个Web应用的WEB-INF目录中的标准 web.xml文件。
因此,为了在Tomcat 4中关闭激活器servlet,只需在install_dir/conf/web.xml中简单地注释出/servlet/* URL映射项即可,如下所示:
<!–
<servlet-mapping>
<servlet-name>invoker</servlet-name>
<url-pattern>/servlet/*</url-pattern>
</servlet-mapping>
–>
再次提醒,应该注意这个项是位于存放在install_dir/conf的Tomcat专用的web.xml文件中的,此文件不是存放在每个Web应用的WEB-INF目录中的标准web.xml。
2.禁止激活器:Tomcat3
在Apache Tomcat的版本3中,通过在install_dir/conf/server.xml中注释出InvokerInterceptor项全局禁止缺省 servlet URL。例如,下面是禁止使用缺省servlet URL的server.xml文件的一部分。
<!–
<RequsetInterceptor
className="org.apache.tomcat.request.InvokerInterceptor"
debug="0" prefix="/servlet/" />
–>

5 初始化和预装载servlet与JSP页面

这里讨论控制servlet和JSP页面的启动行为的方法。特别是,说明了怎样分配初始化参数以及怎样更改服务器生存期中装载servlet和JSP页面的时刻。
5.1 分配servlet初始化参数
利用init-param元素向servlet提供初始化参数,init-param元素具有param-name和param-value子元素。例如,在下面的例子中,如果initServlet servlet是利用它的注册名(InitTest)访问的,它将能够从其方法中调用getServletConfig(). getInitParameter("param1")获得"Value 1",调用getServletConfig().getInitParameter("param2")获得"2"。
<servlet>
<servlet-name>InitTest</servlet-name>
<servlet-class>moreservlets.InitServlet</servlet-class>
<init-param>
<param-name>param1</param-name>
<param-value>value1</param-value>
</init-param>
<init-param>
<param-name>param2</param-name>
<param-value>2</param-value>
</init-param>
</servlet>
在涉及初始化参数时,有几点需要注意:
l 返回值。GetInitParameter的返回值总是一个String。因此,在前一个例子中,可对param2使用Integer.parseInt获得一个int。
l JSP中的初始化。JSP页面使用jspInit而不是init。JSP页面还需要使用jsp-file元素代替servlet-class。
l 缺省URL。初始化参数只在通过它们的注册名或与它们注册名相关的定制URL模式访问Servlet时可以使用。因此,在这个例子中,param1和 param2初始化参数将能够在使用URL http://host/webAppPrefix/servlet/InitTest 时可用,但在使用URL http://host/webAppPrefix/servlet/myPackage.InitServlet 时不能使用。
例如,程序清单5-7给出一个名为InitServlet的简单servlet

]]>
http://blog.donews.com/afxtruelover/archive/2006/04/29/851013.aspx/feed 0
[转载]敏捷设计原则 http://blog.donews.com/afxtruelover/archive/2006/04/21/840179.aspx http://blog.donews.com/afxtruelover/archive/2006/04/21/840179.aspx#comments Fri, 21 Apr 2006 11:04:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/04/21/840179.aspx “在按照我的理解方式审查了软件开发的生命周期后,我得出一个结论:实际上满足工程设计标准的唯一软件文档,就是源代码清单。”

                                                              &n]]> “在按照我的理解方式审查了软件开发的生命周期后,我得出一个结论:实际上满足工程设计标准的唯一软件文档,就是源代码清单。”

                                                                            ——Jack Reeves
       Reeves认为软件系统的源代码是它的主要设计文档。用来描绘源代码的图示只是设计的附属物而不是设计本身。
       软件设计是一个抽象的概念。它和程序的概括形状(Shape)、结构以及每一个模块、类和方法的详细形状和结构有关。可以使用许多不同的媒介去描绘它,但是它最终体现为源代码。最后,源代码就是设计。

      拙劣设计的症状

  • 僵化性(Rigidity):设计难以改变。很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分的其他改动。
  • 脆弱性(Fragility):设计易于遭到破坏。对系统的改动会导致系统中和改动的地方在概念上无关的许多地方发现问题。
  • 牢固性(Immobility):设计难以重用。很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。
  • 粘滞性(Viscosity):难以做正确的事情。做正确的事情比做错误的事情要困难。
  • 不必要的复杂性(Needless Complexity):过分设计。设计中包含有不具有任何直接好处的基础结构。
  • 不必要的重复性(Needlsee Repetition):滥用复制/粘贴。设计中包含有重复的结构,而该重复的对象本可以使用单一的抽象进行统一。
  • 晦涩性(Opacity):很难阅读、理解。没有很好的表现出意图。

      面向对象设计原则

  • 单一职责原则(The Single Responsibility Principle,简称SRP)
  • 开放-封闭原则(The Open-Close Principle,简称OCP)
  • Liskov替换原则(The Liskov Subsititution Principle,简称LSP)
  • 依赖倒置原则(The Dependency Inversion Principle,简称DIP)
  • 接口隔离原则(The Interface Segregation Interface,简称ISP)

 单一职责原则(SRP)
     就一个类而言,应该仅有一个引起它变化的原因。
     在SRP中,我们把职责定义为“变化的原因”。如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的原则。
     应用FACADE或PROXY模式对设计进行重构可以帮助分离类的职责。
开放—封闭原则(OCP)
    软件实体(类、模块、函数等等)应该是可以扩展的,但是不可修改的。
    遵循开放—封闭原则设计的模块具有两个主要的特征,他们是:
   1、“对于扩展是开放的”(Open for extension)。
        这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。换句话说,我们可以改变模块的功能。
   2、“对于更改是封闭的”(Closed for modification)。
    对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或者Java的“.jar”文件,都无需改动。
    这两个特征好像是互相矛盾的。扩展模块的行为的通常方式就是修改该模块的源代码。不允许修改的模块常常被认为是具有固定的行为。
   
    在面向对象的语言中,可以创建出固定却能够描述一组任意个可能行为的抽象体。这个抽象体就是抽象基类。而这一组任意个可能的行为则可以表现为可能的派生类。
    模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,所以它对于更改是可以关闭的。同时,通过这个抽象体派生,也可以扩展此模块的行为。
  
   实际上使用抽象接口比使用抽象类通常具有更好的效果。
  
   在许多方面,OCP是面向对象设计的核心所在。遵循这个原则可带来面向对象技术所声称的巨大好处(灵活性、可重用性以及可维护性)。然而,对于应用程序的每个部分都肆意地进行抽象并不是一个好主意。应该仅仅对程序中呈现出频繁变化的那部分作出抽象。拒绝不成熟的抽象和抽象本身一样重要。
 
Liskov简单替换原则(LSP)
       OCP背后的主要机制是抽象(abstraction)和多态(polymorphisim),支持抽象和多态的关键机制之一是继承。采用Liskov简单替换原则将保证系统使用了最佳的继承层次,正确使用继承不违反OCP。
      子类型(subtype)必须能够替换掉他们的基类型(base type)
    “这里需要如下替换性质:若对每个类型S的对象s1,都存在一个类型T的对象s2,使得在所有针对T编写的程序P中,用s1替换s2后,程序P行为功能不变,则S是T的子类型。”


——Barbara Liskov

       LSP让我们得出一个非常重要的结论:一个模型,如果孤立的看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来表现。
      在考虑一个特定设计是否恰当时,不能完全孤立地来看这个解决方案。必须要根据该设计的使用者作出的合理假设来审视它。
       1、基于契约设计
       为了使“合理假设”明确化,更好支持LSP,可以采用基于契约设计(Design Contract,简称DBC)方法。
        基于DBC方法,派生类的前置条件和后置条件规则是:
       在重新声明的派生类中的例程(routine)时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。
       2、不易察觉的违反LSP的设计
       a、 派生类的退化函数
       派生类的某些函数退化(变得没有用处),Base的使用者不知道不能调用f,会导致替换违规。在派生类中存在退化函数并不总是表示违反了LSP,但是当存在这种情况时,应该引起注意。
      b、从派生类抛出异常
      如果在派生类的方法中添加了其基类不会抛出的异常。如果基类的使用者不期望这些异常,那么把他们添加到派生类的方法中就可以能会导致不可替换性。
      LSP是保证OCP的重要原则。
 
依赖倒置原则(DIP)
     高层模块不应该依赖于底层模块。二者都应该依赖于抽象。
     抽象不应该依赖于细节,细节应该依赖于抽象。
 
      1、本质上倒置的应该是接口所有权。
      低层模块实现了在高层模块中声明并被高层模块调用的接口。
      2、依赖于抽象
      程序中所有的依赖关系都应该终止于抽象类或接口。
      任何变量都不应该持有一个具体类的指针和引用。
      任何类都不应该从具体类派生。
      任何方法都不应该覆写它的任何基类中已经实现的方法。
 
接口隔离原则(ISP)
      不应该强迫客户依赖于它们不使用的方法。
       一个对象的客户不是必须通过该对象的接口去访问它,也可以通过委托或者该对象的基类去访问它。
      如何使用ISP
      1、对客户进行分组
      常常可以根据客户调用的服务方法来对客户进行分组。这种分组方法使得可以为每组而不是每个客户创建分离的接口。
      2、改变接口
       在维护面相对象的程序时,常常会改变现有的类和组件的接口。为了减少这些改变的影响。可以通过为现有的对象增加新接口的方法来缓解,而不是去改变现有的接口。
胖类(fat class)会导致它们的客户程序之间产生不正常的并且有害的耦合关系。当一个客户程序要求该类进行一个改动时,会影响到其他所有的客户程序。因此,客户程序应该依赖于它们实际调用的方法。通过把胖类的接口分解为多个特定客户程序的接口,可以实现这个目标。
]]>
http://blog.donews.com/afxtruelover/archive/2006/04/21/840179.aspx/feed 0 [J2ME]跟我学制作Pak文件 http://blog.donews.com/afxtruelover/archive/2006/04/20/837467.aspx http://blog.donews.com/afxtruelover/archive/2006/04/20/837467.aspx#comments Thu, 20 Apr 2006 01:49:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/04/20/837467.aspx [J2ME]跟我学制作Pak文件


作者:cleverpig



版权声明:本文可以自由转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
作者:cleverpig(http://blog.matrix.org.cn/page/cleverpig)
原文:http://www.matrix.]]>

[J2ME]跟我学制作Pak文件


作者:cleverpig



版权声明:本文可以自由转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
作者:cleverpig(
http://blog.matrix.org.cn/page/cleverpig)
原文:http://www.matrix.org.cn/resource/article/43/43966_J2ME_Pak.html
关键字:pak,j2me,减肥


序言:

    由于前些时间,一些matrixer常问关于j2me中使用Pak文件的问题。本人虽学艺不深,但满怀热心的做了一番探索,现将制作Pak文件的看法和方法公布出来,大家多多提意见。

一、什么是Pak文件:

    Pak文件就是将多个文件打包为一个单独文件,在这个文件中保存着多个文件的数据,当然还有一些描述文件结构的数据。所以将“Pak”作为文件的后缀是一种常规的用法,大家可以自定义其它的文件后缀。

二、为什么使用Pak文件:

    由于MIDP对发布安装的j2me程序大小进行了限制,所以缩小发布程序就意味着能够提供更多的程序或者内容(如图片、音乐)给用户。而通过研究发现zip/jar算法对大文件的压缩率高于对等量的多个小文件的压缩率。

    当然还有其它方法,这里简单做一下讨论比如使用混淆器ProGuard的“-overloadaggressively”选项使jar文件缩小,但也会导致一些错误,因为这种方法生成jar中的class符合java byte code标准,但是与java语法相悖,严重的可能造成一些jre对Object的序列化错误。

    所以使用Pak方法将程序中要用到的资源(图片、音乐、文本)组合为单一文件是一个安全有效的方法。而且对于一些商用程序,完全可以在pak文件中对文件数据进行加密,很好的保护了作者和公司的权益。本人的sample中使用了简单的“加减法”加密,对于手机这类设备来讲是一个效率较高的选择。

三、Pak文件的结构:

    大家可以自己设计Pak文件结构,本人这里只是抛砖引玉的作个sample。下面就是本人设计的Pak文件结构:

PAK File Header:Pak文件的头部


  * 签名:6字节char数组
* 版本号:32位float
* 文件table数量:32位整数
* 密码行为:8位字节
* 密码:8位字节
* 文件唯一ID:10字节char数组
* 保留位:32位整数(4字节)



File Table:Pak文件中包含文件的列表,在一个Pak文件中一个被包含的文件对应一个File Table。


* 文件名:30字节char数组
* 文件大小:32位整型
* 文件在pak文件中的位移:32位整数



Concatenated File Data:按File Table的顺序连接在一起的文件数据。


* 文件数据



四、程序框架:
    
    说明:由于Pak文件的制作和使用分别要使用两个java应用领域:j2se和j2me,所以本人将PakUtil类制作了2个版本(j2se和j2me)。

    程序框架如下:
    1。PakHeader类,定义了Pak文件头。
    2。PakFileTable类,定义Pak文件table。
    3。PakUtil类(j2se版),具备两个功能:将多个png图片合成一个Pak文件,并使用简单的加减加密法对其进行加密;从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)或者写为文件。
       PakUtil类(j2me版),具备的功能:从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)。

五、PakHeader和PakFileTable类:
    
PakHeader.java:


package cn.org.matrix.gmatrix.gameLab.util.pak;

/**
* Pak文件头:
* 结构:
*   签名:6字节char数组
*   版本号:32位float
*   文件table数量:32位整数
*   密码行为:8位字节
*   密码:8位字节
*   文件唯一ID:10字节char数组
*   保留位:32位整数(4字节)
* @author cleverpig
*
*/
class PakHeader {
        //定义文件唯一ID长度
        public static final int UNIQUEID_LENGTH=10;
        //定义文件签名长度
        public static final int SIGNATURE_LENGTH=6;
        //定义加法运算
        public static final int ADDITION_CIPHERACTION=0;
        //定义减法运算
        public static final int SUBTRACT_CIHOERACTION=1;
        //文件签名
        private char[] signature=new char[SIGNATURE_LENGTH];
        //版本号
        private float version=0f;
        //文件table数量
        private long numFileTableEntries=0;
        //密码使用方法:在原数据上进行加法还是减法
        private byte cipherAction=ADDITION_CIPHERACTION;
        //密码值
        private byte cipherValue=0x00;
        //唯一ID
        private char[] uniqueID=new char[UNIQUEID_LENGTH];
        //保留的4字节
        private long reserved=0;
        
        public PakHeader(){        
        }
        
        /**
         * 构造方法
         * @param signature 签名
         * @param version 版本
         * @param numFileTableEntries 文件table数量
         * @param cipherAction 密码使用方法
         * @param cipherValue 密码值
         * @param uniqueID 唯一ID
         * @param reserved 保留的2字节
         */
        public PakHeader(char[] signature,float version,
                        long numFileTableEntries,byte cipherAction,
                        byte cipherValue,char[] uniqueID,long reserved){
                for(int i=0;i<SIGNATURE_LENGTH;this.signature[i]=signature[i],i++)
                        ;
                this.version=version;
                this.cipherAction=cipherAction;
                this.numFileTableEntries=numFileTableEntries;
                this.cipherValue=cipherValue;
                for(int i=0;i<UNIQUEID_LENGTH;this.uniqueID[i]=uniqueID[i],i++)
                        ;
                
                this.reserved=reserved;
        }
        
        public byte getCipherValue() {
                return cipherValue;
        }
        public void setCipherValue(byte cipherValue) {
                this.cipherValue = cipherValue;
        }
        public long getNumFileTableEntries() {
                return numFileTableEntries;
        }
        public void setNumFileTableEntries(long numFileTableEntries) {
                this.numFileTableEntries = numFileTableEntries;
        }
        public long getReserved() {
                return reserved;
        }
        public void setReserved(long reserved) {
                this.reserved = reserved;
        }
        public char[] getUniqueID() {
                return uniqueID;
        }
        public void setUniqueID(char[] uniqueID) {
                for(int i=0;i<UNIQUEID_LENGTH;this.uniqueID[i]=uniqueID[i],i++)
                        ;
        }
        public float getVersion() {
                return version;
        }
        public void setVersion(float version) {
                this.version = version;
        }
        public byte getCipherAction() {
                return cipherAction;
        }

        public void setCipherAction(byte cipherAction) {
                this.cipherAction = cipherAction;
        }

        public char[] getSignature() {
                return signature;
        }

        public void setSignature(char[] signature) {
                for(int i=0;i<SIGNATURE_LENGTH;this.signature[i] = signature[i],i++)
                        ;
        }
        
        /**
         * 返回PakHeader的大小
         * @return 返回PakHeader的大小
         */
        public static int size(){
                return SIGNATURE_LENGTH+4+4+1+1+UNIQUEID_LENGTH+4;
        }
        
        public String toString(){
                String result="";
                result+="\t签名:"+new String(this.signature).trim()
                        +"\t版本号:"+this.version
                        +"\t文件table数量:"+this.numFileTableEntries
                        +"\t密码行为:" +this.cipherAction
                        +"\t密码:"+this.cipherValue
                        +"\t文件唯一ID:"+new String(this.uniqueID).trim()
                        +"\t保留位:"+this.reserved;
                return result;
        }

}



PakFileTable.java


package cn.org.matrix.gmatrix.gameLab.util.pak;

/**
* Pak文件table类
* 文件table结构:
*         文件名:30字节char数组
*         文件大小:32位整型
*         文件在pak文件中的位移:32位整数
* @author cleverpig
*
*/
class PakFileTable {
        public static final int FILENAME_LENGTH=30;
        //文件名
        private char[] fileName=new char[FILENAME_LENGTH];
        //文件大小
        private long fileSize=0L;
        //文件在pak文件中的位移
        private long offSet=0L;
        
        public PakFileTable(){
        }
        
        /**
         * 构造方法
         * @param fileName 文件名
         * @param fileSize 文件大小
         * @param offSet 文件在Pak文件中的位移
         */
        public PakFileTable(char[] fileName,
                        long fileSize,long offSet){
                for(int i=0;i<FILENAME_LENGTH;this.fileName[i]=fileName[i],i++)
                        ;
                this.fileSize=fileSize;
                this.offSet=offSet;
        }
        
        public char[] getFileName() {
                return fileName;
        }
        public void setFileName(char[] fileName) {
                for(int i=0;i<fileName.length;this.fileName[i]=fileName[i],i++)
                        ;
        }
        public long getFileSize() {
                return fileSize;
        }
        public void setFileSize(long fileSize) {
                this.fileSize = fileSize;
        }
        public long getOffSet() {
                return offSet;
        }
        public void setOffSet(long offSet) {
                this.offSet = offSet;
        }
        /**
         * 返回文件Table的大小
         * @return 返回文件Table的大小
         */
        public static int size(){
                return FILENAME_LENGTH+4+4;
        }
        
        public String toString(){
                return "\t文件名:"+new String(this.fileName).trim()
                        +"\t文件大小:"+this.fileSize
                        +"\t文件位移:"+this.offSet;
        }
}



六、PakUtil类(j2se版):

PakUtil.java


package cn.org.matrix.gmatrix.gameLab.util.pak;

import java.io.*;
import java.util.Vector;
/**
* Pak工具类
* 功能:
* 1.将多个png图片合成一个Pak文件,并使用简单的加减加密法对其进行加密;
* 2.从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)或者写为文件
* @author cleverpig
*
*/
public class PakUtil {

        public PakUtil(){
        }
        
        /**
         * 返回文件长度
         * @param filePath 文件路径
         * @return 文件长度
         */
        private long getFileSize(String filePath){
                File file=new File(filePath);
                return file.length();
        }
        
        /**
         * 返回文件名
         * @param filePath 文件路径
         * @return 文件名
         */
        private String getFileName(String filePath){
                File file=new File(filePath);
                return file.getName();
        }
        
        /**
         * 计算文件位移的起始点
         * @return 文件位移的起始点
         */
        private long workOutOffsetStart(PakHeader header){
                //计算出文件头+文件table的长度
                return PakHeader.size()+header.getNumFileTableEntries()*PakFileTable.size();
        }
        
        /**
         * 计算文件位移
         * @param fileIndex 文件序号
         * @param lastFileOffset 上一个文件位移
         * @return  文件在pak文件中的位移
         */
        private long workOutNextOffset(long sourceFileSize,long lastFileOffset){
                return lastFileOffset+sourceFileSize;
        }
        
        /**
         * 生成文件table
         * @param sourceFileName 源文件名
         * @param sourceFileSize 源文件长度
         * @param currentFileOffset 当前文件位移
         * @return 生成的PakFileTable对象
         */
        private PakFileTable generateFileTable(String sourceFileName,
                        long sourceFileSize,long currentFileOffset){
                PakFileTable ft=new PakFileTable();
                ft.setFileName(sourceFileName.toCharArray());
                ft.setFileSize(sourceFileSize);
                ft.setOffSet(currentFileOffset);
                return ft;
        }
        
        /**
         * 将char字符数组写入到DataOutputStream中
         * @param toWriteCharArray 被写入的char数组
         * @param dos DataOutputStream
         * @throws Exception
         */
        private void writeCharArray(char[] toWriteCharArray,DataOutputStream dos) throws Exception{
                for(int i=0;i<toWriteCharArray.length;dos.writeChar(toWriteCharArray[i]),i++);
        }
        
        /**
         * 使用文件头中的密码对数据进行加密
         * @param buff 被加密的数据
         * @param buffLength 数据的长度
         * @param header 文件头
         */
        private void encryptBuff(byte[] buff,int buffLength,PakHeader header){
                for(int i=0;i<buffLength;i++){
                        switch(header.getCipherAction()){
                        case PakHeader.ADDITION_CIPHERACTION:
                                buff[i]+=header.getCipherValue();
                                break;
                        case PakHeader.SUBTRACT_CIHOERACTION:
                                buff[i]-=header.getCipherValue();
                                break;
                        }
                }
        }
        
        /**
         * 使用文件头中的密码对数据进行解密
         * @param buff 被解密的数据
         * @param buffLength 数据的长度
         * @param header 文件头
         */
        private void decryptBuff(byte[] buff,int buffLength,PakHeader header){
                for(int i=0;i<buffLength;i++){
                        switch(header.getCipherAction()){
                        case PakHeader.ADDITION_CIPHERACTION:
                                buff[i]-=header.getCipherValue();
                                break;
                        case PakHeader.SUBTRACT_CIHOERACTION:
                                buff[i]+=header.getCipherValue();
                                break;
                        }
                }
        }
        
        /**
         * 制作Pak文件
         * @param sourceFilePath 源文件路径数组
         * @param destinateFilePath 目的文件路径(Pak文件)
         * @param cipherAction 密码行为
         * @param cipherValue 密码
         * @throws Exception
         */
        public void makePakFile(String[] sourceFilePath,
                        String destinateFilePath,PakHeader header) throws Exception{
                
                PakFileTable[] fileTable=new PakFileTable[sourceFilePath.length];
                //计算文件位移起始点
                long fileOffset=workOutOffsetStart(header);
                //逐个建立文件table
                for(int i=0;i<sourceFilePath.length;i++){
                        String sourceFileName=getFileName(sourceFilePath[i]);
                        long sourceFileSize=getFileSize(sourceFilePath[i]);
                        PakFileTable ft=generateFileTable(sourceFileName,sourceFileSize,fileOffset);
                        //计算下一个文件位移
                        fileOffset=workOutNextOffset(sourceFileSize,fileOffset);
                        fileTable[i]=ft;
                }
                //写入文件头
                File wFile=new File(destinateFilePath);
                FileOutputStream fos=new FileOutputStream(wFile);
                DataOutputStream dos=new DataOutputStream(fos);
                writeCharArray(header.getSignature(),dos);
                dos.writeFloat(header.getVersion());
                dos.writeLong(header.getNumFileTableEntries());
                dos.writeByte(header.getCipherAction());
                dos.writeByte(header.getCipherValue());
                writeCharArray(header.getUniqueID(),dos);
                dos.writeLong(header.getReserved());
                //写入文件table
                for(int i=0;i<fileTable.length;i++){
                        writeCharArray(fileTable[i].getFileName(),dos);
                        dos.writeLong(fileTable[i].getFileSize());

                        dos.writeLong(fileTable[i].getOffSet());
                }
                //写入文件数据
                for(int i=0;i<fileTable.length;i++){
                        File ftFile=new File(sourceFilePath[i]);
                        FileInputStream ftFis=new FileInputStream(ftFile);
                        DataInputStream ftDis=new DataInputStream(ftFis);
                        byte[] buff=new byte[256];
                        int readLength=0;
                        while((readLength=ftDis.read(buff))!=-1){
                                encryptBuff(buff,readLength,header);
                                dos.write(buff,0,readLength);
                        }
                        ftDis.close();
                        ftFis.close();
                }
                dos.close();        
        }
        
        /**
         * 从DataInputStream读取char数组
         * @param dis DataInputStream
         * @param readLength 读取长度
         * @return char数组
         * @throws Exception
         */
        private char[] readCharArray(DataInputStream dis,int readLength) throws Exception{
                char[] readCharArray=new char[readLength];
                
                for(int i=0;i<readLength;i++){
                        readCharArray[i]=dis.readChar();
                }
                return readCharArray;
        }
        
        /**
         * 从PAK文件中读取文件头
         * @param dis DataInputStream
         * @return PakHeader
         * @throws Exception
         */
        private PakHeader readHeader(DataInputStream dis) throws Exception{
                PakHeader header=new PakHeader();
                char[] signature=readCharArray(dis,PakHeader.SIGNATURE_LENGTH);
                header.setSignature(signature);
                header.setVersion(dis.readFloat());
                header.setNumFileTableEntries(dis.readLong());
                header.setCipherAction(dis.readByte());
                header.setCipherValue(dis.readByte());
                char[] uniqueID=readCharArray(dis,PakHeader.UNIQUEID_LENGTH);
                header.setUniqueID(uniqueID);
                header.setReserved(dis.readLong());
                return header;
        }
        
        /**
         * 读取所有的文件table
         * @param dis DataInputStream
         * @param fileTableNumber 文件表总数
         * @return 文件table数组
         * @throws Exception
         */
        private PakFileTable[] readFileTable(DataInputStream dis,int fileTableNumber) throws Exception{
                PakFileTable[] fileTable=new PakFileTable[fileTableNumber];
                for(int i=0;i<fileTableNumber;i++){
                        PakFileTable ft=new PakFileTable();
                        ft.setFileName(readCharArray(dis,PakFileTable.FILENAME_LENGTH));
                        ft.setFileSize(dis.readLong());
                        ft.setOffSet(dis.readLong());
                        fileTable[i]=ft;
                }
                return fileTable;
        }
        
        /**
         * 从pak文件读取文件到byte数组
         * @param dis DataInputStream
         * @param fileTable PakFileTable
         * @return byte数组
         * @throws Exception
         */
        private byte[] readFileFromPak(DataInputStream dis,PakHeader header,PakFileTable fileTable) throws Exception{
                dis.skip(fileTable.getOffSet()-workOutOffsetStart(header));
                //
                int fileLength=(int)fileTable.getFileSize();
                byte[] fileBuff=new byte[fileLength];
                int readLength=dis.read(fileBuff,0,fileLength);
                if (readLength<fileLength){
                        System.out.println("读取数据长度不正确");
                        return null;
                }
                else{
                        decryptBuff(fileBuff,readLength,header);
                        return fileBuff;
                }
        }
        
        /**
         * 将buffer中的内容写入到文件
         * @param fileBuff 保存文件内容的buffer
         * @param fileName 文件名
         * @param extractDir 文件导出目录
         * @throws Exception
         */
        private void writeFileFromByteBuffer(byte[] fileBuff,String fileName,String extractDir) throws Exception{
                String extractFilePath=extractDir+fileName;
                File wFile=new File(extractFilePath);
                FileOutputStream fos=new FileOutputStream(wFile);
                DataOutputStream dos=new DataOutputStream(fos);
                dos.write(fileBuff);
                dos.close();
                fos.close();
        }
        
        /**
         * 从pak文件中取出指定的文件到byte数组,如果需要的话可以将byte数组写为文件
         * @param pakFilePath  pak文件路径
         * @param extractFileName pak文件中将要被取出的文件名
         * @param writeFile 是否需要将byte数组写为文件
         * @param extractDir 如果需要的话可以将byte数组写为文件,extractDir为取出数据被写的目录文件
         * @return byte数组
         * @throws Exception
         */
        public byte[] extractFileFromPak(String pakFilePath,
                        String extractFileName,boolean writeFile,String extractDir) throws Exception{
                File rFile=new File(pakFilePath);
                FileInputStream fis=new FileInputStream(rFile);
                DataInputStream dis=new DataInputStream(fis);
                PakHeader header=readHeader(dis);
                PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries());

                boolean find=false;
                int fileIndex=0;
                for(int i=0;i<fileTable.length;i++){
                        String fileName=new String(fileTable[i].getFileName()).trim();
                        if (fileName.equals(extractFileName)){
                                find=true;
                                fileIndex=i;
                                break;
                        }
                }
                if (find==false){
                        System.out.println("没有找到指定的文件");
                        return null;
                }
                else{
                        byte[] buff=readFileFromPak(dis,header,fileTable[fileIndex]);
                        if (writeFile){
                                writeFileFromByteBuffer(buff,extractFileName,extractDir);
                        }
                        else{
                                dis.close();
                                fis.close();
                        }
                        return buff;
                }
        }
        
        
        /**
         * 从pak文件中取出指定的Pak文件的信息
         * @param pakFilePath  pak文件路径
         * @return 装载文件头和文件table数组的Vector
         * @throws Exception
         */
        public Vector showPakFileInfo(String pakFilePath) throws Exception{
                File rFile=new File(pakFilePath);
                
                FileInputStream fis=new FileInputStream(rFile);
                DataInputStream dis=new DataInputStream(fis);
                
                PakHeader header=readHeader(dis);
                PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries());

                Vector result=new Vector();
                result.add(header);
                result.add(fileTable);
                return result;
        }
        
        public static void main(String[] argv) throws Exception{
                PakUtil pu=new PakUtil();
                
                //构造文件头
                char[] signature=new char[PakHeader.SIGNATURE_LENGTH];
                signature=new String("012345").toCharArray();
                char[] uniqueID=new char[PakHeader.UNIQUEID_LENGTH];
                uniqueID=new String("0123456789").toCharArray();
                PakHeader header=new PakHeader();
                header.setSignature(signature);
                header.setNumFileTableEntries(3);
                header.setCipherAction((byte)PakHeader.ADDITION_CIPHERACTION);
                header.setCipherValue((byte)0x0f);
                header.setUniqueID(uniqueID);
                header.setVersion(1.0f);
                header.setReserved(0L);
                
                String[] filePathArray={"F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\apple.png",
                                "F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\cushaw.png",
                                "F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\flash.png"};
                String extractFilePath="F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\test.pak";
                //制作Pak文件
                System.out.println("制作Pak文件...");
                pu.makePakFile(filePathArray,extractFilePath,header);
                System.out.println("制作Pak文件完成");
                
                //从Pak文件中取出所有的图片文件
                Vector pakInfo=pu.showPakFileInfo(extractFilePath);
                header=(PakHeader)pakInfo.elementAt(0);
                System.out.println("Pak文件信息:");
                System.out.println("文件头:");
                System.out.println(header);
                
                PakFileTable[] fileTable=(PakFileTable[])pakInfo.elementAt(1);
                for(int i=0;i<fileTable.length;i++){
                        System.out.println("文件table["+i+"]:");
                        System.out.println(fileTable[i]);
                }
                
                String restoreDir="F:\\eclipse3.1RC3\\workspace\\gmatrixProject_j2se\\testFiles\\extract\\";
                String restoreFileName=null;
                byte[] fileBuff=null;
                for(int i=0;i<fileTable.length;i++){
                        restoreFileName=new String(fileTable[i].getFileName()).trim();
                        System.out.println("从Pak文件中取出"+restoreFileName+"文件...");
                        fileBuff=pu.extractFileFromPak(extractFilePath,restoreFileName,true,restoreDir);
                        System.out.println("从Pak文件中取出"+restoreFileName+"文件保存在"+restoreDir+"目录");
                }
        }
}



七、PakUtil类(j2me版):

PakUtil.java


package cn.org.matrix.gmatrix.gameLab.util.pak;

import java.io.*;
import java.util.Vector;
/**
* Pak工具类
* 功能:
* 从Pak文件中取出png图片,构造byte数组(可以用来构造Image对象)
* @author cleverpig
*
*/
public class PakUtil {
        
        public PakUtil(){
        }
        
        /**
         * 计算文件位移的起始点
         * @return 文件位移的起始点
         */
        private long workOutOffsetStart(PakHeader header){
                //计算出文件头+文件table的长度
                return PakHeader.size()+header.getNumFileTableEntries()*PakFileTable.size();
        }
        
        /**
         * 从DataInputStream读取char数组
         * @param dis DataInputStream
         * @param readLength 读取长度
         * @return char数组
         * @throws Exception
         */
        private char[] readCharArray(DataInputStream dis,int readLength) throws Exception{
                char[] readCharArray=new char[readLength];
                
                for(int i=0;i<readLength;i++){
                        readCharArray[i]=dis.readChar();
                }
                return readCharArray;
        }
        
        /**
         * 从PAK文件中读取文件头
         * @param dis DataInputStream
         * @return PakHeader
         * @throws Exception
         */
        private PakHeader readHeader(DataInputStream dis) throws Exception{
                PakHeader header=new PakHeader();
                char[] signature=readCharArray(dis,PakHeader.SIGNATURE_LENGTH);
                header.setSignature(signature);
                header.setVersion(dis.readFloat());
                header.setNumFileTableEntries(dis.readLong());
                header.setCipherAction(dis.readByte());
                header.setCipherValue(dis.readByte());
                char[] uniqueID=readCharArray(dis,PakHeader.UNIQUEID_LENGTH);
                header.setUniqueID(uniqueID);
                header.setReserved(dis.readLong());
                return header;
        }
        
        /**
         * 读取所有的文件table
         * @param dis DataInputStream
         * @param fileTableNumber 文件表总数
         * @return 文件table数组
         * @throws Exception
         */
        private PakFileTable[] readFileTable(DataInputStream dis,int fileTableNumber) throws Exception{
                PakFileTable[] fileTable=new PakFileTable[fileTableNumber];
                for(int i=0;i<fileTableNumber;i++){
                        PakFileTable ft=new PakFileTable();
                        ft.setFileName(readCharArray(dis,PakFileTable.FILENAME_LENGTH));
                        ft.setFileSize(dis.readLong());
                        ft.setOffSet(dis.readLong());
                        fileTable[i]=ft;
                }
                return fileTable;
        }
        
        /**
         * 从pak文件读取文件到byte数组
         * @param dis DataInputStream
         * @param fileTable PakFileTable
         * @return byte数组
         * @throws Exception
         */
        private byte[] readFileFromPak(DataInputStream dis,PakHeader header,PakFileTable fileTable) throws Exception{
                dis.skip(fileTable.getOffSet()-workOutOffsetStart(header));
                //
                int fileLength=(int)fileTable.getFileSize();
                byte[] fileBuff=new byte[fileLength];
                int readLength=dis.read(fileBuff,0,fileLength);
                if (readLength<fileLength){
                        System.out.println("读取数据长度不正确");
                        return null;
                }
                else{
                        decryptBuff(fileBuff,readLength,header);
                }
                return fileBuff;
        }
        
        /**
         * 使用文件头中的密码对数据进行解密
         * @param buff 被解密的数据
         * @param buffLength 数据的长度
         * @param header 文件头
         */
        private void decryptBuff(byte[] buff,int buffLength,PakHeader header){
                for(int i=0;i<buffLength;i++){
                        switch(header.getCipherAction()){
                        case PakHeader.ADDITION_CIPHERACTION:
                                buff[i]-=header.getCipherValue();
                                break;
                        case PakHeader.SUBTRACT_CIHOERACTION:
                                buff[i]+=header.getCipherValue();
                                break;
                        }
                }
        }
        
        /**
         * 从pak文件中取出指定的文件到byte数组
         * @param pakResourceURL  pak文件的资源路径
         * @param extractResourceName pak文件中将要被取出的文件名
         * @return byte数组
         * @throws Exception
         */
        public byte[] extractResourceFromPak(String pakResourceURL
                        ,String extractResourceName) throws Exception{
                InputStream is=this.getClass().getResourceAsStream(pakResourceURL);
                DataInputStream dis=new DataInputStream(is);
                PakHeader header=readHeader(dis);
//                System.out.println("文件头:");
//                System.out.println(header);
                PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries());
//                for(int i=0;i<fileTable.length;i++){
//                        System.out.println("文件table["+i+"]:");
//                        System.out.println(fileTable[i]);
//                }
                boolean find=false;
                int fileIndex=0;
                for(int i=0;i<fileTable.length;i++){
                        String fileName=new String(fileTable[i].getFileName()).trim();
                        if (fileName.equals(extractResourceName)){
                                find=true;
                                fileIndex=i;
                                break;
                        }
                }
                if (find==false){
                        System.out.println("没有找到指定的文件");
                        return null;
                }
                else{
                        byte[] buff=readFileFromPak(dis,header,fileTable[fileIndex]);
                        return buff;
                }
        }
        
        
        /**
         * 从pak文件中取出指定的Pak文件的信息
         * @param pakResourcePath  pak文件资源路径
         * @return 装载文件头和文件table数组的Vector
         * @throws Exception
         */
        public Vector showPakFileInfo(String pakResourcePath) throws Exception{
                InputStream is=this.getClass().getResourceAsStream(pakResourcePath);
                DataInputStream dis=new DataInputStream(is);
                
                PakHeader header=readHeader(dis);
                PakFileTable[] fileTable=readFileTable(dis,(int)header.getNumFileTableEntries());

                Vector result=new Vector();
                result.addElement(header);
                result.addElement(fileTable);
                return result;
        }
        
        public static void main(String[] argv) throws Exception{
                PakUtil pu=new PakUtil();
                String extractResourcePath="/test.pak";
                //从Pak文件中取出所有的图片文件
                Vector pakInfo=pu.showPakFileInfo(extractResourcePath);
                PakHeader header=(PakHeader)pakInfo.elementAt(0);
                System.out.println("Pak文件信息:");
                System.out.println("文件头:");
                System.out.println(header);
                
                PakFileTable[] fileTable=(PakFileTable[])pakInfo.elementAt(1);
                for(int i=0;i<fileTable.length;i++){
                        System.out.println("文件table["+i+"]:");
                        System.out.println(fileTable[i]);
                }
                
                String restoreFileName=null;
                byte[] fileBuff=null;
                for(int i=0;i<fileTable.length;i++){
                        restoreFileName=new String(fileTable[i].getFileName()).trim();
                        System.out.println("从Pak文件中取出"+restoreFileName+"文件数据...");
                        fileBuff=pu.extractResourceFromPak(extractResourcePath,restoreFileName);
                        System.out.println("从Pak文件中取出"+restoreFileName+"文件数据完成");
                }
        }
}



八、源代码使用简介:

    Pak过程:j2se版的PakUtil将testFiles目录中的三个png文件Pak成为test.pak文件。
    UnPak过程:j2se版的PakUtil将testFiles目录中test.pak文件释放到testFiles\extract目录下;j2me版的PakUtil从res目录中的test.pak文件读取出其中所包含的3个png文件数据并装入到byte数据,用来构造Image对象,大家请运行PakUtilTestMIDlet.java便可看到输出的信息。

九、源代码下载:

j2se版源代码[下载文件]
j2sme版源代码[下载文件]

资源
·CleverPig的blog:http://blog.matrix.org.cn/page/cleverpig
·Matrix-Java开发者社区:http://www.matrix.org.cn/
·ProGuard
·j2me.org上的Topic: Give me all your tricks for minimizing jar file size

]]> http://blog.donews.com/afxtruelover/archive/2006/04/20/837467.aspx/feed 0
WAP开发FAQ[转载] http://blog.donews.com/afxtruelover/archive/2006/03/25/791273.aspx http://blog.donews.com/afxtruelover/archive/2006/03/25/791273.aspx#comments Sat, 25 Mar 2006 00:15:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/03/25/791273.aspx         1. 开发WAP软件需要哪些工具?
   为了开发WAP应用程序,需要一个WAP网关(注意:这里的网关可能是指支持WML的服务器。可以通过配置WWW服务器达到这个目的)和WAP工具包。工具包应当包括模拟器和能让开发者浏览WML网页。WML页面的开发和HTML页面的开发一样,可以使用Notepad或者其他文本编辑器来进行编辑。
  2. 有哪些公司现在提供这样的开发环境?
  Nokia、Ericsson、UpPhone和Motorola都提供免费的WAP网关和工具包。
  Nokia:Nokia Toolkit和Nokia WAP Server。
  Ericsson:Ericsson R320和WapIDE。
  UpPhone:UpPhone SDK。
  Motorola:Motorola ADK。
  3. 开发WAP应用一定要有WAP手机吗?
   不是,当开发WAP应用的时候,不一定]]>
        1. 开发WAP软件需要哪些工具?
   为了开发WAP应用程序,需要一个WAP网关(注意:这里的网关可能是指支持WML的服务器。可以通过配置WWW服务器达到这个目的)和WAP工具包。工具包应当包括模拟器和能让开发者浏览WML网页。WML页面的开发和HTML页面的开发一样,可以使用Notepad或者其他文本编辑器来进行编辑。
  2. 有哪些公司现在提供这样的开发环境?
  Nokia、Ericsson、UpPhone和Motorola都提供免费的WAP网关和工具包。
  Nokia:Nokia Toolkit和Nokia WAP Server。
  Ericsson:Ericsson R320和WapIDE。
  UpPhone:UpPhone SDK。
  Motorola:Motorola ADK。
  3. 开发WAP应用一定要有WAP手机吗?
   不是,当开发WAP应用的时候,不一定需要WAP手机。模拟器可以帮助开发者解决大部分的问题。但是如果是开发商业网站,特别是想知道各种移动电话在显示WML页面上的差别的时候,最好是配备一个。目前各种手机对WML标记的支持和中文的支持状况大不相同,因此WAP手机还是必要的。
  4. 开发者需要一个WAP网关吗?
   不是很必要。如果只想进行简单的WAP内容服务,可以使用现有的Web服务器(只需要修改MIME类型)。移动电话会通过坐落在本地的网关连接到你的服务器上。
   但是在网关上驻留开发者的程序有很多好处。既然开发者的程序是网关的一个部分,开发者就可以知道呼叫号码、身份、位置等等。
  5. 可以看到WML的源代码么?
   如果开发者使用SDK浏览的时候将能够看到WML的代码。如果只有一个HTML浏览器,可以访问“Fetch Page”服务(
http://www.webcab.de)来取得代码。这个可以显示在Internet上的任何WML页面中。
  6. 可能在WML中加入applets吗?
   不能。
  7. 可以使用HTML开发工具来开发WAP应用吗?
   在大多数情况下开发工具是使用基于PC的浏览器。HTML、JavaScript和Java对于WAP开发来说都没有用处。但是,越来越多的开发工具在加入对WML的支持。
   Allair的Cold Fusion 4.5 和 HomeSite已经有WML支持,虽然Allair也许需要清除一些BUG。另外PHP和ASP在Coldfusion/HomeSite也能支持。
   可以到 Marjolei Katsma的 HomeSite Help site 上得到更多的信息。
  8. 可以通过WML页面来操作数据库吗?
   可以,与创建HTML页面相同。任何相关的服务器端的技术都可以用来生成WML页面。
  9. 可以使用CGI生成WML页面吗?
   当然。可以用创建HTML同样的方法来创建WML。如果想书写一个CGI来创建WML,只要记住在页面的开头正确设置MIME类型。具体的形式根据所使用的语言不同而不同。例如在Perl中:
  print ("Content-type:application/vnd.wap.wml \n\n\n"); 
   注意至少要使用2个换行。
  10. 如何使用Cold Fusion来生成页面?
   使用Cold Fusion只需要加上:
   <CFCONTENT type="text/vnd.wap.wml">
  11. 如何使用PHP来书写动态的WML页面?
   PHP(和大多数其他服务端脚本语言一样)可以被用来书写动态的WML内容。只需要将输出的标记限制在WML微型浏览器可接受的范围内。
   注意PHP有很多内建的HTML功能,特别是错误功能,这些功能WML微型浏览器可能无法识别。
   PHP同样可以在一个HTML文件中编写出既适合于HTML,也适合于WML的内容。PHP的源代码对于客户端来说是不可见的。因此可以针对HTML浏览器输出HTML页面,针对WML浏览器输出WML页面。
   可以在开发PHP编写的WML页面的时候把以下代码加在开头:
  <?
  // header("Content-type: text/vnd.wap.wml");
   echo("<?xml version=\"1.0\"?>\n");
  echo("<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\"
  //"
http://www.wapforum.org/DTD/wml_1.1.xml\">\n\n");
  ?>
   基于PC的浏览器将忽略这些无法理解的WML标记。但是如果想在WAP设备或者模拟器上测试的时候,只需要将"//"去掉,页面自动变成WML页面。
  12. 使用PHP动态输出WML
   这些例子生成一个非常有用的应用叫做:PizzaCalc。它将输入所有的pizza的帐单和人的数目,可以算出每个人的花费。
   应用生成一个动态的页面叫做“calc”或者“input”。注意到所有的转义字符例如双引号。该页显示了一个简单的变量处理,和如何传递参数到另外的卡片:
   使用WML浏览器就可以测试应用程序:
  
http://wap.colorline.no/wap-faq/apps/pizzacalc.html
   或者输入:
  
http://wap.colorline.no/demos.html选择应用。
  <?
  header("Content-type: text/vnd.wap.wml");
  echo("<?xml version=\"1.0\"?>\n");
  echo("<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\"
  \"
http://www.wapforum.org/DTD/wml_1.1.xml\">\n\n");
  echo("<!–The application PizzaCalc was originally made by The Crusaders
  
www.crusaders.no on the Commodore Amiga –>\n");
  echo("<!– It was unfortunately not possible to emulate the crap interger handling of the
  original program –>\n");
  ?>
  <wml>
  <?
   if($action == "calc") {
   echo("<card id=\"result\" title=\"PizzaCalc\">\n");
   echo("<do type=\"prev\" label=\"Back\">\n");
   echo("<go href=\"pizzacalc.html#input\"/>\n");
   echo("</do>\n");
   echo("<p>\n");
   echo("The cost per eater will be ".$total / $eaters."<br/>\n");
   }
   else {
   echo("<card id=\"input\" title=\"PizzaCalc\">\n");
   echo("<p>\n");
  echo("<anchor>Split Pizza bill
  <go href=\"pizzacalc.html?total=\$(total)&eaters=\$(eaters)&action=calc\"/>
  </anchor>\n");
   echo("<br/>\n");
   echo("Total cost: <input type=\"text\" name=\"total\" format=\"*N\"/>\n");
   echo("Eaters: <input type=\"text\" name=\"eaters\" format=\"*N\"/>\n");
   }
  ?>
  </p>
  </card>
  </wml>
  13. 可以使用Java Servlet来生成WML页面吗?
   当然。可以使用创建HTML同样的方法来创建WML。如果想书写一个CGI来创建WML,只要记住在页面的开头正确设置MIME类型:
   response.setContentType("text/vnd.wap.wml");
  14. 可以使用ASP、Perl等生成动态的应用吗?
   是的。可以使用任何服务器端的脚本语言来生成WAP应用。
  15. 如何使用ASP书写WML内容?
   ASP(Active Server Pages)可以做到和PHP一样,也可以用来书写动态的WML。如果需要一些好的例子请参考Luca Passani’s WAP and ASP articles。或者查看Jean-Luc Praz’s (
jeanluc@corobori.com)。更多的ASP例子在:http://www.corobori.com/wap/
  16. 在使用ASP动态输出WML页面的时候,已经设置了Content-type,但是浏览器返回的仍然是text/html,有什么问题吗?
   如果在ASP脚本中有一个错误,那么诊断程序会发还一个HTML页面,请检查脚本。
  17. 在使用ASP生成WML页面的时候出现了错误: <MIME type "text/html" is not supported>,会是什么问题?
   这个问题是Web浏览器不知道WML的正确类型,修改ASP的第一行,加入:
   <Response.ContentType = "text/vnd.wap.wml"> 
   后就可以工作了。
  18. 下面的代码有什么问题吗?
  <%Response.ContentType = "text/vnd.WAP.WML"%>
  <?xml version="1.0"?>
   去掉<?xml version="1.0"?>之前的空格。XML解释器希望在这行中没有其他字符,甚至是空行。
  19. ASP代码可以在模拟器上工作,在真正的浏览器上怎么不行?
   在很多模拟器上没有像真正的WML浏览器那么严格。这些对于那些没有使用网关的模拟器(Nokia SDK/Toolkit)来说更是这样,有些就根本没有使用网关(WinWAP、WapMAN)。
   一个真正的WML浏览器应该只读取二进制的数据(从WML编码得来的)WMLC,对于网关应该将文本WML转换/编译成WMLC。语法是非常严格的。ASP是为HTML浏览器设置的,但是HTML没有WML那么严格。
   这里在ASP生成动态页面的时候有一个微小的“bug”。它在WML浏览器上不允许有任何地方输出白行(例如:空格,回车,换行)。注意到有些网关可能会修正这些问题,但有的则不管(例如:CMG网关)。
   下面是一个常见的ASP代码用来输出WML页面开头的MIME类型:
  <%Response.ContentType = "text/vnd.wap.wml"%>
  <?xml version="1.0"?>
   问题就在ASP将会在 .wml"%> 和 <?xml vers 之间输出换行和回车。这两行就被分割了。这将打乱WML代码的内容。WML必须以“<”开头,而且第一行是<?xml version="1.0"?>。就上面的WML页面回车/换行将会出现问题。
   最简单的解决办法是:
   <%Response.ContentType = "text/vnd.wap.wml"%><?xml version="1.0"?>
   在XML定义正确的格式化以后,后面的部分WML对空格就没有那么严格的要求。
   要注意的是有些网关在输出ASP的时候会有问题,因此在WML代码中最好使用 Response.Write 而不是<%=MyVar%>。
  20. 如何使用Perl来生成WML内容?
   和其他Server端程序一样。Perl也可以用来书写漂亮的WAP应用程序。
   最常见的就是如何使用Perl输出正确的MIME类型,下面的例子说明了这一点:
  print "Content-type: text/vnd.wap.wml\n\n";
  print "<?xml version=\"1.0\"?>\n";
  print "<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\"
  \"
http://www.wapforum.org/DTD/wml_1.1.xml\">\n";
  print "<wml>\n";
  ……
  21. 应当如何下手书写WAP应用程序?
   其实需要的只是Text编辑器。但是使用一个开发工具可以节约很多时间。
   在这之前应该浏览一下WAP的权威站点:
www.wapforum.com
   在Nokia WAP 开发论坛中进行注册,并且下载Nokia WAP Developer Toolkit 。Toolkit中的PDF文件可以给出一定的WML和WMLScript指导。Nokia Toolkit需要JRE (Java Runtime Environment) v.1.2.2 或者更高版本。
   虽然工具可以用来为WAP设备设计应用,但是不是为专门的移动电话。在WAP开发工具上所看到的并不代表用户在手机上所看到的。为了确定想看到的事情,最好需要一个WAP设备,例如移动电话,或者模拟器。
   Nokia WAP SDK 2有一个7110的模拟器。模拟器是一个有效的检测方式,能检测程序中的bug。 Nokia SDK 同样还包括一个小的WAP server让开发者可以从本地或者HTTP服务器上下载WML页面。
   到 Phone.com 开发站点注册后,Phone.com 提供UP.browser。这是最流行的浏览器,特别是在美国,Phone.com 提供UP.SDK。 在注册之后就可以下载。
   对于Ericsson R320 和 R380是最近的事情。应该注册并查看Ericsson’s Developer’s Zone 来得到开发工具。R380是一个非常好的模拟器,在 Symbian 不需要注册就可以下载。Ericsson 没有公开的为R320的模拟器。
   Motorola 有一个平台叫做 Mobile Internet eXchange 或者 MIX 。Mobile Application Development Kit 已经开发出一个开发平台,即为WAP也为Motorola的 VoxML。在注册后,可以在下面的网址找到数据包。
  
http://www.motorola.com/MIMS/MSPG/cgi-bin/spn_madk.cgi. 
   WAPmine 是一个独立的应用,叫做 WAPPage 是一个所见即所得的编辑工具。而且有一个XML树型控件来编辑WML标签。
   如果在开发公共应用程序时,想在很多设备上测试你的程序,就像在不同的浏览器上测试HTML页面一样。注意在不同的WML浏览器上的差别,可能比在不同的HTML浏览器上的差别要大。
  22. 如何编写和测试WML页面?
   现在有很多SDK。AnywhereYouGo.com有WAP SDK和IDE列表,可以下载一个来用。任何文本编辑器都可以书写一个简单的WML页面,当然HTML编辑器也可以(特别是那些支持个人定义标签的),例如:Allaire Homesite (
http://www.allaire.com )。可以使用SDK来做简单的测试,但是对于大的项目可能要困难些。AnywhereYouGo.com已经建立一套基于Web的工具来帮助WAP测试。
  23. 哪儿可以在找到WML的测试工具?
   首先确定WML代码是正确的,然后再使用WML测试工具。
   有一个非常好的测试工具在Zygo Communications(
http://wap.z-y-g-o.com/tools/),测试工具是用Perl写的。里面还有其他的工具可供下载。
  24. 如何操作WML页面?
   操作WML页面或者卡片,最简单的办法是通过现有的网关。大多数移动电话提供者将功能都放在主页上,在上面可以通过WAP设备操作。网关的链接一般叫做“Go to URL”。当选择以后,WAP设备将通过网关操作指定的普通IP或者URL。在这种情况下,网关读取从WAP设备发送给网关的WML内容,就像PC浏览器读取内容的过程一样。
   有些营运商选择不让他们的用户操作其他的站点。这个就像Internet Service Provider只允许用户操作ISP自己的站点。像这样的做法是不明智的,这样会发现自己的用户去其他地方了。
   如果要坚持这种方法,可以通过ISP拨号或者使用一个公共的网关来取得其他的WAP资源。
  25. 有没有一个友好的方式来管理WML内容?
   还没有。虽然Oracale正在开发数据库驱动的文档服务,被称为Panama,可以支持WAP分发。
  26. 如何防止用户代理cache页面?
   如果用户使用ASP,应该加入一行<%Response.expires=-1%> ,这个将阻止Cache。
  27. 怎样防止从Cache中读取WML页面?
   当WML页面下载到WAP设备后,它将保存在WAP设备内存中一段时间,直到这个时间过期。在这之后,页面将从服务器下载,而不是从WAP设备的缓存读取。这个过程被称做Cache。
   但是有些时候不想让页面从缓存中读取,而是从服务器端读取。一个典型的例子就是当服务器的内容不断在更新的时候,通过在HTTP头中加入一定的cache信息,来告诉WAP设备该页面将不存储在缓存中。
   可以在服务器端生成HTTP头,或者使用PHP、ASP、Perl或者其他服务端开发语言。这一行不能被包括在页面里,既然是HTTP的信息头,就不是WML元素。
   对于静态页面,或许没有使用服务器端脚本语言,许多浏览器支持META标签来控制浏览器的Cache。看本部分的最后的例子。
   将下面代码加入到HTTP头中,页面将马上过期:
  Expires: Mon, 26 Jul 1997 05:00:00 GMT
  Last-Modified: DD. month YYYY HH:MM:SS GMT
  Cache-Control: no-cache, must-revalidate
  Pragma: no-cache
   第一行告诉微型浏览器,页面已经过期一段时间了。第二行告诉浏览器页面最后一次修改的时间。DD应该换成当天的日期,month YY HH MM SS等等类推。第三行和第四行有同样的效果。告诉浏览器页面不被Cache(第三行适用于 HTTP 1.1,第四行适用于HTTP 1.0)。
   下面的是PHP的一个例子:
  <?
  // set the correct MIME type
   header("Content-type: text/vnd.wap.wml");
  // expires in the past
   header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
  // Last modified, right now
   header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); 
  // Prevent caching, HTTP/1.1
   header("Cache-Control: no-cache, must-revalidate");
  // Prevent caching, HTTP/1.0
   header("Pragma: no-cache");
   ?>
   下面是使用WebClasses(VB)的例子。使用"Response.Expires=-1",防止Cache。
  Private Sub WebClass_Start()
   ‘Set correct MIME type
   Response.ContentType = "text/vnd.wap.wml"
  
   ‘Make sure no caching
   Response.Expires = -1
   Response.AddHeader "Pragma", "no-cache"
   Response.AddHeader "Cache-Control", "no-cache, must-revalidate"
  
   ‘Use basicwml(my own) as template
   Set NextItem = basicwml
   End Sub 
   这里有一个ASP的例子,同样使用“Response.Expires=-1”防止Cache。
  <%
   Response.ContentType = "text/vnd.wap.wml"
   Response.Expires = -1
   Response.AddHeader "Pragma", "no-cache"
   Response.AddHeader "Cache-Control", "no-cache, must-revalidate"
  %> 
   最后是使用META的例子:
  <?xml version="1.0"?>
  <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
  "
http://www.wapforum.org/DTD/wml_1.1.xml">
   <wml>
   <head>
   <meta forua="true" http-equiv="Cache-Control" content="max-age=0"/>
   </head>
   <card id="alwaysexpire">
   <p>This deck will never be stored in the cache</p>
   </card>
   </wml>
   下面的页面是在经过86400秒(24 hours)后过期。
  <?xml version="1.0"?>
  <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
  "
http://www.wapforum.org/DTD/wml_1.1.xml">
   <wml>
   <head>
   <meta forua="true" http-equiv="Cache-Control" content="max-age=86400"/>
   </head>
   <card id="expire1day">
   <p>This card will live in the cache for a day</p>
   </card>
   </wml>
   有些浏览器例如:UP.Simulator如果可以通过“返回”达到另外一个卡片,那么它将不会重新装载卡片。为了强制这个更新动作,用户必须在META标签中使用must-revalidate 参数。
   <meta forua="true" http-equiv="Cache-Control" content="must-revalidate"/>
  28. 如何防止变量被保存在Cache中?
   变量保存在Cache中,这样变量还可以再利用。例如当用户返回到上一个输入卡片,他不需要重新输入,只需要改变需要改变的地方。但是在某些情况下这会造成一些问题。例如以WAP聊天系统,有些变量用了一遍又一遍,但是需要不同的内容。有些浏览器,例如:Nokia 7110,就会存在类似的在该清除的时候无法清除的问题。
   在WML中,<card>标签有一个参数叫做newcontext。
   当newcontext="true" 时清除所有的变量。但是这样也清除了所有导航的历史记录,这意味着back按钮不再工作。
   为了清除变量,可以告诉浏览器将变量设为空:
  <setvar name="one_variable" value=""/>
  <setvar name="another_variable" value=""/>
   但是,不是每个时候都有效果。在某些情况下必须使用一个难以想象的方法来清空变量。就是使用 onenterforward 事件。
  <onevent type="onenterforward">
   <refresh>
   <setvar name="one_variable" value=""/>
   <setvar name="another_variable" value=""/>
   </refresh>
  </onevent>
  29. 怎么能够知道请求是从WML浏览器来的还是HTML浏览器来的?
   既然要利用已经存在的为HTML浏览器编写的代码,就需要知道请求是从HTML浏览器还是从WML浏览器过来的。同样地,如果想重新引导的HTML浏览器直接到相应的HTML文档上,WML浏览器到WML页面上,以下的PHP代码就可以做到这些。
  <?
  // Because this script sends out HTTP header information,
  // the first characters in the file must be the <? PHP tag.
  // relative URL to your HTML file
   $htmlredirect = "/html/my_htmlpage.html";
  // ABSOLUTE URL to your WML file 
   $wmlredirect = "
http://wap.mysite.com/wml/my_wmldeck.wml";
   if(strpos(strtoupper($HTTP_ACCEPT),"VND.WAP.WML") > 0)
  {// Check whether the browser/gateway says it accepts WML.
   $br = "WML";
   }
   else {
   $browser=substr(trim($HTTP_USER_AGENT),0,4);
   if($browser=="Noki" || // Nokia phones and emulators
   $browser=="Eric" || // Ericsson WAP phones and emulators
   $browser=="WapI" || // Ericsson WapIDE 2.0
   $browser=="MC21" || // Ericsson MC218
   $browser=="AUR " || // Ericsson R320
   $browser=="R380" || // Ericsson R380
   $browser=="UP.B" || // UP.Browser
   $browser=="WinW" || // WinWAP browser
   $browser=="UPG1" || // UP.SDK 4.0
   $browser=="upsi" || // another kind of UP.Browser ??
   $browser=="QWAP" || // unknown QWAPPER browser
   $browser=="Jigs" || // unknown JigSaw browser
   $browser=="Java" || // unknown Java based browser
   $browser=="Alca" || // unknown Alcatel-BE3 browser (UP based?)
   $browser=="MITS" || // unknown Mitsubishi browser
   $browser=="MOT-" || // unknown browser (UP based?)
   $browser=="My S" || // unknown Ericsson devkit browser ?
  $browser=="WAPJ" || // Virtual WAPJAG
www.wapjag.de
  $browser=="fetc" || // fetchpage.cgi Perl script from www.wapcab.de
  $browser=="ALAV" || // yet another unknown UP based browser ?
   $browser=="Wapa") // another unknown browser (Web based "Wapalyzer"?)
   {
   $br = "WML";
   }
   else {
   $br = "HTML";
   }
   }
   if($br == "WML") {
  // Force the browser to load the WML file instead
   header("302 Moved Temporarily");
   header("Location: ".$wmlredirect);
   exit;
   }
   else {
  // Force the browser to load the HTML file instead
   header("302 Moved Temporarily");
   header("Location: ".$htmlredirect);
   exit;
   }
   ?> 
   这个判断是在服务端完成的, PHP代码将首先查看网关是否接收text/vnd.wap.vml MIME类型。如果不是,将检测前面的字符,查看是否为WML浏览器。如果不符合,那么就假设为HTML浏览器。如果有新的WML浏览器,那么ID字符串也要增加。
   这个代码基于Robert Whitinger(
robert@wapsight.com)的代码,使用了Don Amaro(donamaro.concepcion@nl.unisys.com)提供的列表。
   注意:由于只需要四个字符串就可以辨别,因此例如:"WapIDE-SDK/2.0;(R320s(Arial))" 可以使用“WapI”来代替是可行的做法,也是足够的。
   同样的功能也可以通过ASP来解决。先判断请求的是“/index.wml” 或者 “/index.html” 和所需要的MIME类型。另外以下的脚本辨别的方式和上面不一样。另外还需要网关告诉服务器它能接收 的text/vnd.wap.wml MIME类型。该例子如下所示:
  <%
  Response.Buffer = TRUE
   Dim IsWap
   httpAccept = LCase(Request.ServerVariables("HTTP_ACCEPT"))
   if Instr(httpAccept,"wap") then
   IsWap=1
   Else Response.Redirect "/index.html" : Response.Flush : Response.End
  End if
  %>
  <%Response.ContentType = "text/vnd.wap.wml"%><?xml version="1.0"?>
  <%Response.Flush%>
  <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
  "
http://www.wapforum.org/DTD/wml_1.1.xml">
   <wml>
   <card id="redirect">
   <onevent type="onenterforward">
   <go href="/index.wml"/>
   </onevent>
   <p>
   <a href="/index.wml">enter</a>
   </p>
   </card>
   </wml>
   <%Response.Flush:Response.End%>
  30. 如何判断访问者是来自哪个浏览器或者移动电话?
   可以通过检查HTTP_USER_AGENT标签来判断。例如试着使用Microsoft Internet Explorer访问一个站点的时候,HTTP_USER_AGENT将返回:Mozilla/4.0 (compatible;MSIE 5.0; Windows 98);在同样的情况下使用Nokia 7110访问这个站点,HTTP_USER_AGENT就会是:Nokia7110/1.0(04.73)。据此可以判断用户代理是什么类型的。
  31. 可以得到用户代理的电话号码吗?
   不可以,除非网关支持这个特点,WAP没有办法知道用户的电话号码。
  32. 可以通过WML使得可以用WAP设备进行拨号吗?
   WAP的电话功能可以使用Wireless Telephony Application Interface(WTAI)。
   例如:
   WMLScript: WTAPublic.MakeCall("9287787"); 
   但是第一代的WAP设备不支持这个功能。
  33. 能够从WAP设备中读取数据吗,例如:电话号码?
   这里有一些通过HTTP的信息,但是十分有限。既然只有网关发送过来少量的信息,像WAP设备的号码可能无法读取。同时,在某些国家这还涉及到个人隐私的问题。
   基本上丢弃的内容就是WAP网关传送给HTTP服务器的内容。这不同于WAP网关到网关。Phone.com的UP.Link网关是一个最好的例子。因为它在HTTP头中返回一个字符串叫做 UP_X_SUBNO,里面包含了电话号码。Ericsson网关将传送一个辨别设备用的字符串,但是在明文中没有电话号码。
   每次WAP设备向HTTP服务器请求一个URL,WAP网关就会将信息传送给HTTP服务器。
   以下的PHP脚本显示了从网关过来的所有HTTP头的信息。可以使用WML浏览器进行测试。(
http://wap.colorline.no/clientinfo.html)。其他的例子也可以在下面的UTL中找到:http://wap.colorline.no/demos.html
   第一个部分是取得所有的标准HTTP头信息。第二个部分是提取一个内容。
  <?
   header("Content-type: text/vnd.wap.wml");
   echo("<?xml version=\"1.0\"?>\n");
  echo("<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\"
  \"
http://www.wapforum.org/DTD/wml_1.1.xml\">\n\n");
   echo("<!—Code written in Microsoft NOTEPAD.EXE à \n");
  ?>
  <wml>
   <card id="init" title="Client Info">
   <p>
   <?
   // First part – standard HTTP stuff
   $headers = getallheaders();
   while (list($header, $value) = each($headers)) {
   echo strtoupper($header). ": ". $value. "<br/>\n";       
   }
   // Second part
  // IP address of the client side
   echo("REMOTE_ADDR: ".$REMOTE_ADDR. "<br/>\n");
  // Port at the client side
   echo("REMOTE_PORT: ".$REMOTE_PORT. "<br/>\n");
  // Name of authenticated user
   echo("REMOTE_USER: ".$REMOTE_USER. "<br/>\n");
  // Gateway Interface type
   echo("GATEWAY_INTERFACE: ".$GATEWAY_INTERFACE. "<br/>\n");
  // Protocol used by the server
   echo("SERVER_PROTOCOL: ".$SERVER_PROTOCOL. "<br/>\n");
  // Request Method
   echo("REQUEST_METHOD: ".$REQUEST_METHOD. "<br/>\n");
  // Connection type
   echo("HTTP_CONNECTION: ".$HTTP_CONNECTION. "<br/>\n");
  // Host it connected via (proxy)
   echo("HTTP_VIA: ".$HTTP_VIA. "<br/>\n");
   ?>
   </p>
   </card>
  </wml> 
   Henrik Gemal (
gemal@dk.net)也有一个在线的基于WML的工具BrowserSpy,来显示更多关于HTTP头的信息、服务器环境和用户的浏览器等等。有关这个工具的详细情况可以浏览http://wap.gemal.dk/
   Werner Forkel 提交了一个Perl的脚本,可以显示电话号码(如果有)。可以在以下位置测试:http://wap.colorline.no/wap-faq/apps/subnotest.wml,同样也收集在:http://wap.colorline.no/demos.html.
   这些程序只适合某个网关。如果要测试其他的网关,可能就显示不出电话号码。因此电话号码不能作为ID号来处理。至少因为不是一个全球的标准。
  34. 有没有办法连接到电话号码?
   在某些情况下,当在显示了一连串的号码之后,需要中断功能连接到一个电话号码上并拨号。例如:执行一个 dial:12345678 就非常像 mailto: 标签。
   越来越多的浏览器都支持这个功能,但还不是所有。Phone.com, Mitsubishi 和 Ericsson 已经在浏览器中集成了这个基于Wireless Telephony Interface specifications (WTAI)的功能。 WTAI将允许以下的URL将关闭连接并且拨号:
   <go href="wtai://wp/mc;+4712345678">Make a call to +47-12345678</go> 
   Nokia 7110 已经有个功能叫做“Use Number”。它可以通过WML卡片查找一个类似于电话号码的列表,然后用户可以选择进行呼叫。注意用户必须分离这些数字以便它能正常工作。
  35. 使用GET或者POST方式能传送多少字符?
   使用GET或者POST方式所能传送的字符数目,不同的设备有不同的限制。一个GET通过UTL传送变量,能传送的数据总量比使用POST方式所能传送的数据要小。例如,Nokia 7110似乎对每个GET 限制在512个字节左右,但是POST最大可以达到一个编译后卡片的大小(约1300字节)。UP.SDK 4.0将GET请求限制在970左右,最大可以达到一个编译后卡片的大小。
   显然,卡片有时候保存了要发送给服务器的参数的内容,既然编译后的卡片大小有限制,那么肯定要影响到整个所能传输的数据。
   在POST和GET之间没有太多的区别。比如这个没有很好地使用GET的例子。
  <input type="text" name="var1" format="*N"/>
   <p>
   <anchor>Send it
   <go href="somescript.cgi?variable=$(var1)" method="get"/>
   </anchor>
   </p>
   下面仍然是一个使用GET的请求,但是使用了<postfield>来传送参数。这个代码就漂亮多了。既然可以定义为GET,同样也很容易转成POST。
  <input type="text" name="var1" format="*N"/>
   <p>
   <anchor>Send it
   <go href="somescript.cgi" method="get">
   <postfield name="variable" value="$(var1)"/>
   </go>
   </anchor>
   </p>
   直接改为POST:
  <input type="text" name="var1" format="*N"/>
   <p>
   <anchor>Send it
   <go href="somescript.cgi" method="post">
   <postfield name="variable" value="$(var1)"/>
   </go>
   </anchor>
   </p>
   最好是做测试找到到底能传输多少数据。这里有个测试程序:
  
http://wap.colorline.no/wap-faq/apps/putsize.php3
   这个程序也可以在下面的URL中找到:
http://wap.colorline.no/demos.html
   该程序将产生一个卡片包含一个变量,里面包含了一定数量的字符X。用户可以选择传输是使用GET还是POST。在传输之后,脚本将要显示接收到的字符个数。
   脚本生成一个页面来测试使用GET或者POST方式到底能发送多少个字符:
  <?xml version="1.0"?>
  <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
  "
http://www.wapforum.org/DTD/wml_1.1.xml">
  <wml>
   <head>
   <meta forua="true" http-equiv="Cache-Control" content="max-age=0"/>
   <meta forua="true" http-equiv="Cache-Control" content="must-revalidate"/>
   </head>
   <card>
   <do type="prev" label="Back">
   <go href="putsize.php3"/>
   </do>
   <p>
   <anchor>GET data
   <go method="get" href="putsize.php3">
   <postfield name=\"a\"
  value="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/>
   </go>
   </anchor>
   </p>
   </card>
  </wml>
  36. 如何同HTML站点一样POST/CGI,返回表单数据到服务器?
   如果使用:
   <go method="post" href="mycgi.cgi"> 
   并且使用:
   <postfield name="fieldname" value="$NameOfInputField"/> 
   就可以POST数据给CGI程序了。
  37. POST无法工作是怎么回事?
   有很多说POST参数将会丢失的流言,特别是在Nokia 7110。就笔者所知,还没有哪个Nokia 7110有这样的问题。这个问题主要是存在于网关或接收方。
   测试显示Nokia SDK 2.18,当使用内建网关的时候,使用POST会出现问题。甚至当method 设置成“POST”的时候,服务器那边还是将POST请求作为 GET。
   当使用POST的URL时 ,Nokia SDK 将会崩溃。在某些情况下URL的最后的字符将被删除。
   POST Test页面将简单的POST的两个变量叫做“var1”和“var2”来显示整个变量的内容和HTTP头的内容。如果不能看到正确的变量内容,肯定有问题。检查HTTP头中的application/x-www-form-urlencoded(注意!需要在变量中输入一些内容)。
   这个方法解决了Nokia SDK 2.18的问题,可以把它配置到任意的公共网关来测试。笔者推荐使用 wapHQ 网关。
   在其他的情况下,POST确实不工作,问题可能是HTTP头在服务器端解释的时候有问题。脚本语言,例如:ASP、Java或是CGI等等都是通过查看在HTTP头中的 application/x-www-form-urlencoded 完全匹配的字符串。在某些情况下字符串可能有附加的数据,例如:charset="utf8" 。既然服务器端不是精确的匹配,它就不会查看HTTP头,因此POST就变量丢失了。
   注意这不是浏览器的问题,在HTTP头加入字符集描述,将造成脚本语言方面的错误。
   为了检测有关网关或浏览器的问题,仍使用上面的POST Test页面来测试。同样查看application/x-www-form-urlencoded 的输出,检查有没有附加的字符在结尾部分,如果有,那么这就是服务器端的问题。
   解决这个问题的方案很复杂,它随用户使用的脚本描述语言不同而不同,而且需要操作原代码。简单地说,解决方案就是需要人工读取HTTP头,不要使用脚本语言已经写好的读取方式。
   这里有一个用ASP编写的解决方法。它显示了如何在POST中抓取数据。用户需要从二进制数据中发现需要的变量。
  Dim lngToalByteCount
  Dim vntRequestData
   lngTotalByteCount = Request.TotalBytes
   vntRequestData = Request.BinaryRead(lngTotalByteCount)
   全部的代码,就应该像下面的代码:
  <%@ Language=VBScript %>
  <%
   Dim Temp, i, sPost, sWMLDeck
   ‘Converts the binary data to a string.
   For i = 1 To Request.TotalBytes
   Temp = Request.BinaryRead(1)
   sPost = sPost & Chr(AscB(Temp))
   Next
   ‘Parses out the values of the POSTED variables (in this
   ‘example myvar1 and myvar2).
   Dim sVar1, sVar2
   sVar1 = getVar("myvar1", sPost)
   sVar2 = getVar("myvar2", sPost)
   ‘Writes the WML Deck displaying the POSTED Variables
   sWMLDeck = "<?xml version=""1.0""?>" & vbCrLf
   sWMLDeck = sWMLDeck & "<!DOCTYPE wml PUBLIC ""-//WAPFORUM//DTD WML 1.1//EN"" "
   sWMLDeck = sWMLDeck & """
http://www.wapforum.org/DTD/wml_1.1.xml"">" & vbCrLf
   sWMLDeck = sWMLDeck & vbCrLf & "<wml>" & vbCrLf & vbTab
   sWMLDeck = sWMLDeck & "<card id=""main"" title=""POST TEST"">" & vbCrLf
   sWMLDeck = sWMLDeck & vbTab & vbTab & "<p>" & vbCrLf
   sWMLDeck = sWMLDeck & vbTab & vbTab & vbTab & "myVar1: " & sVar1 & "<br/>" & vbCrLf
   sWMLDeck = sWMLDeck & vbTab & vbTab & vbTab & "myVar2: " & sVar2 & vbCrLf
   sWMLDeck = sWMLDeck & vbTab & vbTab & "</p>" & vbCrLf & vbTab
   sWMLDeck = sWMLDeck & "</card>" & vbCrLf & ">/wml>"
   Response.ContentType = "text/vnd.wap.wml"
   Response.Write(sWMLDeck)
   ‘Quick function for picking out the values of the POSTed variables.
   ’sKey is the variable name, sRaw is the POST string.
   Private Function getVar(sKey, sRaw)
   Dim sRetVal
  If InStr(sRaw, sKey) Then
  sRetVal = Mid(sRaw, InStr(sRaw, sKey) + Len(sKey) + 1)
  If InStr(sRetVal, "&") Then
  sRetVal = Mid(sRetVal, 1, InStr(sRetVal, "&") – 1)
  End If
  End If
  getVar = sRetVal
   End Function
  %>
  38. 为什么META标签不工作?
   浏览器不支持默认的meta标签,例如:
   <meta http-equiv="refresh" content="1;http://somewhere.com/"> 
   虽然有少量网关支持非常有限的META标记。但是测试显示,如果使用了它们,网关就会出问题。例如某网关不支持普通的HTTP Cache控制,如果要实现Cache控制只好使用特殊的META标记。显然从其他网关来的用户就可能不支持这个META。注意:不要使用META tags。肯定有其他的方式来完成你的想法。
   最常使用的META是:
   <meta http-equiv="refresh" content="1;http://somewhere.com/"> 
   这个告诉浏览器重新装入指定的WML页面。WML中已经包含了一个<ontimer>。
  39. 为什么服务器接收不到用户发送的参数?
   用户输入的参数或者其他参数可以像在HTML中一样通过提交方式发送到服务器。在HTML中这个是<FORM>,POST或者GET。
   首先知道要知道POST和GET的区别。对于POST浏览器将生成一个数据包将变量名和它们的内容捆绑在一起,并发送到服务器。对于GET,它其实是一个URL请求,变量名和内容都包含在URL中。
   对于WAP环境,要求是非常严格的,必须要根据协议来操作。虽然以下的URL
   "/cgi-bin/somescript?username=john&telephone=123-123-1234&occupation=banana+bender"
   可以在HTML环境中工作,但是在WAP环境中无法工作。以上的部分编码将使得保护的变量内容被误解。特殊的空格(在 banana 和 bender )被转成 “+”。 URL就根本没有空格。
   以上的URL在WAP中无法工作的主要原因是用来分割每个变量和变量内容的 & (与号)没有转义。正确的格式应该是:
   "/cgi-bin/somescript?username=john&amp;telephone=123-123-1234&amp;occupation=banana+bender"
   在这里 & 被名字实体所替换。为了解释更清楚些,请看下面的代码:
  <card id="input" title="Gimme some data">
   <p><input type="text" name="username" format="M*m"/></p>
   <p><input type="text" name="occupation" format="M*m"/></p>
  <p>
  <anchor>Send this
  <go href="/cgi-bin/somescript?username=$(username)
  &amp;occupation=$(occupation)"/>
  </anchor>
  </p>
   注意这不是真正的WAP协议,专门的字符应该转义,否则将得到不可预料的结果。
  40. 为什么在HTTP中的Referer看不见?
   当HTML浏览器从一个URL到另外一个URL的时候,它默认地会发送一个叫做 Referer的 HTTP头给服务器,告诉它在浏览这个页面之前的那个页面。这是一个非常有用的特点,在WAP环境中同样也有。但是既然这个信息来自用户代理(浏览器)、WAP设备,通常为了节约带宽和时间,就被省略了。
   为了使用 Referer ,应该使用新的URL标签例如: <a>,<go>等等,并且加入参数:sendreferer。
   <go href="/somedir/somedeck.wml" sendreferer="true"/> 
   这样就会把参考的URL发送到服务器。
  41. 如果没有找到URL,有可能重新将用户引导到另外一个WML卡片或者页面吗?
   是的。但这是服务器端的特点,与客户端没有关系。
  42. 为什么普通的HTTP 302重新导向不好使?
   这的确是一个事实。核心的问题是在服务端的脚本语言,而不是在服务端语言和服务器之间。
   所谓的302 Found HTTP反应是指服务器告诉用户代理,它所需要的资源在另外的地方可以找到。302反应可能包括一个人们可理解的信息,如果在这种情况下“ Content-type: ”就被设置了。笔者所测试过的服务器,即使没有内容也都加了“Content-type:”。默认的 “Content-type:” 是text/html。当然有些网关不喜欢这个类型。
   以下的例子已经经过测试,在Apache和Microsoft Internet Information Server都可以工作。如果使用其他的Web Server,或者其他的脚本语言,需要能转换这些简单的脚本。关键的工作是十分简单的,除非需要,不用告诉服务器整个HTTP头。大多数Web Server将自动完成这个HTTP头,使得用户代理可以理解。
   所有的代码例子可以在线测试。如果它们能够工作,用户将被重新引导到
http://wap.colorline.no/clientinfo.html ,在那儿将产生一个WML页面来显示所有的HTTP头。
   PHP 代码测试可以在
http://wap.colorline.no/wap-faq/apps/302test.php3中找到。
  <?
   header("Location:
http://wap.colorline.no/clientinfo.html");
   header("Content-type: text/vnd.wap.wml";
  ?> 
   Perl测试代码可以在
http://wap.colorline.no/cgi-bin/302test.pl中找到。 
  print "Location:
http://wap.colorline.no/clientinfo.html\n";
  print "Content-type: text/vnd.wap.wml\n"; 
   ASP测试代码可以在
http://www.colorline.no/302test.asp中找到。 (注意不同的URL): 
  <%
   Response.Redirect = "
http://wap.colorline.no/clientinfo.html";
   Response.ContentType = "text/vnd.wap.wml";
   Response.Flush
   Response.End
  %>
  43. 可能在WML中实现ASP Session吗?
   不可能。可以把信息存储在临时变量中模拟Session。Session是保存在用户PC上的“cookies”中。目前的WAP设备不支持“cookies”。不过下一代的设备和WML可能支持“cookies”。
  44. WAP支持Session吗?
   在HTML中,一个十分普遍的“处理”用户的方法就是为每个用户分配一个“session”。这个有时候是通过指定一个独一无二的cookies来实现的。然而WAP的资源非常有限,因此session的处理必须以不同的方式来处理。
   Alex Kriegel 提供了一个安装在 WAPlinks的Custom Session Object包。这个zip文件中包含了VB类的文件和编译过的Dll文件,还有相关的文档。这些可以在
http://www.waplinks.com/customsessionobject.zip下载。
   另外一种方法是使用 PHPlib ,它是使用 PHP 编写的。
   Tarique (
tarique@nagpur.dot.net.in) 提供了如何使用PHPlib来验证和处理每个WAP用户。有相关的文件和注释可以到下面地址下载:
  
http://wap.colorline.no/wap-faq/archive/phplib_wml.zip
  45. 可以在WAP中使用Cookies吗?
   在理论上是可以的,但不是所有的WAP设备都支持。另一个方法来管理会话是使用隐藏的fields(包含会话标识,无论是POST或者GET方式)。
  46. WAP支持Cookies吗?
   普通的HTTP Cookies是作为WAP的扩展来实现的。无论你以前听到什么,Cookies的支持将越来越好。实际上Phone.com的 UP.Link网关已经支持这个功能有一段时间了。
   可以使用以下的脚本语言检测Cookie-support,:
  
http://wap.colorline.no/wap-faq/apps/cookietest.php3
   脚本在http://wap.colorline.no/demos.html也可以得到。
   当第一次看见卡片的时候,记数器应该为0。所有的Cache都被关闭。卡片通过在URL中随机地加入变量来强制自己加载(笔者不推荐这种强制加载办法)。当点击增加计数连接,页面将重新加载,卡片就再次出现,并且记数器变成1。
   在脚本中,Cookie的名字被称做 TestCookie,它有很长的生命期,因此可以隔好几天再来查看记数器,它将是上一次的数值。这要求你能使用与上一次访问所使用的WAP环境一样,否则将把你的数值清0。
   如果记数装置一直都是0,那么cookie 就没有能传送到你的Web Server。这个脚本也能表示Cookie是否被传送。
   另外,这个脚本同样还显示HTTP头中的HTTP_VIA 和 HTTP_USER_AGENT 。这些能够得到所使用的网关和模式。一些网关使用HTTP_VIS标识自己,而另外一些使用HTTP_USER_AGENT,还有一些则让程序无法知道。
   下面是它的PHP代码。一个标准的 PHP setcookie() 函数只有在这种脚本语言中才会出现。函数只是简单地设置cookie,并且PHP变量 $HTTP_COOKIE_VARS 用来读取cookie。
  <?
  if(isset($HTTP_COOKIE_VARS["TestCookie"]))
  {// Check if TestCookie is set
   $cookieset = "set";
  // Read the Cookie
   $cookieid = $HTTP_COOKIE_VARS["TestCookie"];
  // and increase its value
   $cookieid++;
    }
   else {
  // cookie was not set
   $cookieset = "not set";
  // start counter at zero
   $cookieid = 0;
   }
  // apply the Cookie to the HTTP header
  setcookie("TestCookie",$cookieid);
  // set the content type for WML
   header("Content-type: text/vnd.wap.wml");
  // disable ALL caching
   header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
   header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
   header("Cache-Control: no-cache, must-revalidate");          
   header("Pragma: no-cache");                                  
   echo("<?xml version=\"1.0\"?>\n");
  echo("<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\"
  \"
http://www.wapforum.org/DTD/wml_1.1.xml\">\n\n");
  echo("<!– This application attempts to test the capabilities of a WAP gateway to support     cookies –>\n");
   echo("<!– App by
Espen.Lyngaas@colorline.no (c) 2000 –>\n");
  // Generate random value for reload forcing
   $random = mt_rand(100000,999999);
  ?>
   <wml>
   <head>
  // Even more cache disabling
  <meta forua="true" http-equiv="Cache-Control" content="must-revalidate"/> 
   </head>
   <card id="init" title="CookieTest">
   <p>
   Cookie "TestCookie" was <?echo($cookieset)?>.
   Value is currently "<?echo($cookieid)?>"
   </p>
   <p>
  <anchor>
  Increase value
  <go method="get" href="<?echo($PHP_SELF)?>?random=<?echo($random)?>"/>
  </anchor>
  </p>
  <p>Gateway: 
   <?
   if(isset($HTTP_VIA))
  { // Is there something in the HTTP_VIA variable?
   echo($HTTP_VIA);
   }
   else {
   if(isset($HTTP_USER_AGENT))
  { // Is there something in the HTTP_USER_AGENT variable?
   echo($HTTP_USER_AGENT);
   }
   else {
  // Absolutely no identifier was found
   echo("Unknown");
   }
   }
   ?>
   </p>
   </card>
   </wml>
  47. 如何使用WAP设备发送E-Mail?
   在HTML中有一个默认的E-Mail机制:“ mailto:” 。但在WML中不好使,因此E-Mails必须通过WML表单来解决。例如:
  <wml>
    <card id="edit" title="Email Editor">
   <p>From: <input type="text" name="from" format="*M"/></p>
   <p>To: <input type="text" name="to" format="*M"/></p>
   <p>Subject: <input type="text" name="subject" format="*M"/></p>
   <p>Message body: <input type="text" name="body" format="*M"/></p>
   <p>
   <anchor>Send this mail
   <go method="post" href="
http://some.host/mailhandler"?action=send/">
   <postfield name="from" value="$(from)"/>
   <postfield name="to" value="$(to)"/>
   <postfield name="subject" value="$(subject)"/>
   <postfield name="body" value="$(body)"/>
   </go>
   </anchor>
   </p>
   </card>
  </wml> 
   在代码中的
http://some.host/mailhandler是一个CGI程序,它是服务端的脚本程序,将提交的表单转换成E-Mail格式并发送出去。
   如果想使用一个类似于发信的过程,就需要编辑变量名。另外发送的数据是有限的,长信息可能需要打断。
   为了演示它是如何工作的,下面的 PHP 脚本可以用来处理Mail。它将格式化WML页面,告诉用户是否发出信件。在真实的应用中,应该加入检测,例如:E-Mail的合法格式。
  <?
  // Tell the client that this is a WML deck
   header("Content-type: text/vnd.wap.wml");
   echo("<?xml version=\"1.0\"?>\n");
   echo("<!DOCTYPE wml PUBLIC \"-//WAPFORUM//DTD WML 1.1//EN\"
  \"
http://www.wapforum.org/DTD/wml_1.1.xml\">\n");
  // The name of your mail server
   $mailer = "wap.colorline.no";
  // Format the from field
   $from = $from." (WAP user at ".$mailer.")";
  // Add the from field and some character handling to the extra headers
   $extraheaders = $from."\nContent-Type: text/plain;
  charset=iso-8859-1\nContent-Transfer-Encoding: 8bit";
  // Start sending out the WML deck
   echo("<wml>\n");
   if(mail($to,$subject,$body,$extraheaders))
  {// Use PHP’s internal mail functionality
  // Mail was successfully sent
   echo("<card id=\"sent\" title=\"Mail sent\">\n");
   echo("<p>Mail was sent successfully</p>\n");
   echo("</card>\n");
   }
   else {
  // The mail could not be sent
   echo("<card id=\"notsent\" title=\"Mail failed\">\n"); 
   echo("<p>Unable to send mail</p>\n");
   echo("</card>\n");
   }
   echo("</wml>\n");
  ?>
   因为安全性的原因,以上的代码没有演示程序。
  48. 可以在模拟器上操作本地的页面,却没有办法访问Internet上的,有什么问题吗?
   大多数模拟器和工具都可以浏览Internet、Intranet和本机的页面。但是在访问一些大公司页面的时候,必须通过代理服务器来取得进入Internet的权限。如果必须通过这个代理服务器来取得HTML页面,那么你的WAP模拟器也会取得权限来访问Internet。
   注意到有些模拟器不支持代理服务器,但是大多数是支持的。在模拟器里面设置这些是很简单的。用户所做的只需要将代理主机的名字、IP地址和端口号输入就可以了。如果没有找到,你可以在 systems/network 管理器里面设置这些,也可以检查 Netscape/IE的设置。
   在某些情况下,代理服务器使用 userid 和 password 来取得进入Internet的权限。有些模拟器支持用户代理服务器,用户应该能告诉模拟器相关的代理配置。
   在极少的情况下,使用代理服务器(如Microsoft Proxy Server,)的用户会遇到更多的问题。这个代理服务器只接受一种验证方式(userid/password)。这种验证被称做 NTLM ,并且是某种 MS 的验证方式。几乎很少有模拟器支持这种方式。因此最好是避免使用它,或者让管理员使用“Basic Authentication”方式以避免更多的麻烦。
  49. 什么是PUSHing,它是如何工作的?
   PUSH被加入到WAP 1.2,而且只在WAP 1.2中才存在。简单地来说,PUSH提供了另外一种从服务器向用户发送数据的方式。PULLing是从客户端请求信息,然后接收它;PUSH意味着服务器可以向用户发送数据,而不需要用户来请求。
   内容或者应用服务器无法向用户代理直接发送数据,必须使用一种叫做Push Proxy 的网关。PPG 是基于Internet的Push Initiator (内容或者应用服务器) 与移动用户之间的。在Internet一边,使用Push Access Protocol,在移动网络一边使用Push Over-the-Air Protocol。
   当前只有 WAP 1.2 开发平台支持 PUSH, 例如 Nokia Toolkit 2.0。 Nokia Toolkit 2.0 only 内部支持PUSHing,意味着用户可以从工具包的界面将消息推送到模拟器。如果想试着到一个外部的Push Proxy Gateway, 工具包就崩溃了。从readme文件中知道,PUSHing 还没有经过完整的测试。
  50. WAP模拟器说text/html不支持,但是用户的MIME设置是正确的,为什么?
   当使用服务端的脚本语言,例如ASP、PHP或者Perl,来生成WML输出,或者从HTTP服务器提供WML页面的时候。记住HTTP一般默认的显示是HTML,其MIME类型是text/html。
   如果HTTP服务器或者服务器脚本有错误,错误的信息将使用HTML显示,因此微型浏览器是不能显示错误信息的。
   一个开发工具/模拟器可以让用户看到从HTTP服务器过来的代码。例如,在Nokia SDK中,这个功能被称做View Source。通过看代码可以知道HTTP服务器到底发送了些什么内容。也可以使用普通的浏览器来查看任何HTML格式的错误信息。
  51. 在哪儿有Visio移动电话的模板库?
   目前唯一知道的就是它包含在 Nokia 7110 中。
  52. 有没有其他有用的WML内容服务列表?
   这里至少有一个。
   对于Unix用户,
http://pwot.co.uk/wml/中有Thomas Neill (ponder@pwot.co.uk)提供的WML工具,包括WML二进制编译和反编译。
   Angus 和 Zygo WAP(
angus@z-y-g-o.com)已经开发出了一个Perl工具包。它还在为管道式的WML编译器工作。
  53. XML到XSL的转换可以应用到WML和WAP吗?
   既然WML实际上是XML,并且XSL将WML转换成其他不同的XML文档,那么问题的答案是显然的:XSL也可以应用到WML。可以参考Luca Passani的文章《WebTechniques》。这个文章在网络上的地址是:
  
http://www.webtechniques.com/archives/2000/03/passani/
   它推荐看一下叫做《在 Apache下Cocoon计划的实现》这篇文章。“Cocoon 是一个依赖于新的W3C技术(例如DOM,XML,和XSL)框架。Cocoon计划在于改变Wen信息创建,生成和提供的方式。文档内容、风格和逻辑经常因为个人或者工作组的不同而不同。 Cocoon目标在于将这三层分离,允许三层次之间进行独立的设计,创建和管理,减少相互之间的影响,增加工作的可复用性以及缩短上市的时间。Web内容的产生大多数是基于HTML的,但是HTML并不能将三者分离开来,混合着各种格式标签,程序逻辑等等。而Cocoon计划将要改变这种情况,允许内容,逻辑和风格相互分离。使用XML来保存,但是使用XSL来将它们混合。”
   基本上来说,Cocoon将解读HTTP头,判断使用的是什么浏览器,然后使用不同的风格来选择正确的页面,使用XSL进行混合。
  54. 想让用户只要简单地按下一个按钮就能够转跳到其他卡片而不是通过选择URL,这个可能吗?
   不,不可能。
  55. 如何避免一个行的中断以便可以在一行中输入多个链接?
   在Nokia 7110中,不可能做到这一点,每个链接都占据自己的一行。

]]>
http://blog.donews.com/afxtruelover/archive/2006/03/25/791273.aspx/feed 0
Session详解[转载] http://blog.donews.com/afxtruelover/archive/2006/03/23/783396.aspx http://blog.donews.com/afxtruelover/archive/2006/03/23/783396.aspx#comments Thu, 23 Mar 2006 01:06:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/03/23/783396.aspx 一、术语session
在我的经验里,session这个词被滥用的程度大概仅次于transaction,更加有趣的是transaction与session在某些语境下的含义是相同的。

session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话时从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个 session。有时候我们可以看到这样的话“在一个浏览器会话期间,…”,这里的会话一词用的就是其本义,是指从一个浏览器窗口打开到关闭这个期间 ①。最混乱的是“用户(客户端)在一次会话期间”这样一句话,它可能指用户的一系列动作(一般情况下是同某个具体目的相关的一系列动作,比如从登录到选购商品到结账登出这样一个网上购物的过程,有时候也被称为一个transaction),然而有时候也可能仅仅是指一次连接,也有可能是指含义①,其中的差别只能靠上下文来推断②。

然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义, “面向连接”指的是在通信双方在通信之前要先建立一个通信的渠道,比如打电话,直到对方接了电话通信才能开始,与此相对的是写信,在你把信发出去的时候你并不能确认对方的地址是否正确,通信渠道不一定能建立,但对发信人来说,通信已经开始了。“保持状态”则是指通信的一方能够把一系列的消息关联起来,使得消息之间可以互相依赖,比如一个服务员能够认出再次光临的老顾客并且记得上次这个顾客还欠店里一块钱。这一类的例子有“一个TCP session”或者 “一个POP3 session”③。

而到了web服务器蓬勃发展的时代,session在web开发语境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器之间保持状态的解决方案④。有时候session也用来指这种解决方案的存储结构,如“把xxx保存在session 里”⑤。由于各种用于web开发的语言在一定程度上都提供了对这种解决方案的支持,所以在某种特定语言的语境下,session也被用来指代该语言的解决方案,比如经常把Java里提供的javax.servlet.http.HttpSession简称为session⑥。

鉴于这种混乱已不可改变,本文中session一词的运用也会根据上下文有不同的含义,请大家注意分辨。
在本文中,使用中文“浏览器会话期间”来表达含义①,使用“session机制”来表达含义④,使用“session”表达含义⑤,使用具体的“HttpSession”来表达含义⑥

二、HTTP协议与状态保持
HTTP 协议本身是无状态的,这与HTTP协议本来的目的是相符的,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没有必要纪录彼此过去的行为,每一次请求之间都是独立的,好比一个顾客和一个自动售货机或者一个普通的(非会员制)大卖场之间的关系一样。

然而聪明(或者贪心?)的人们很快发现如果能够提供一些按需生成的动态信息会使web变得更加有用,就像给有线电视加上点播功能一样。这种需求一方面迫使HTML逐步添加了表单、脚本、DOM等客户端行为,另一方面在服务器端则出现了CGI规范以响应客户端的动态请求,作为传输载体的HTTP协议也添加了文件上载、 cookie这些特性。其中cookie的作用就是为了解决HTTP协议无状态的缺陷所作出的努力。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。

让我们用几个例子来描述一下cookie和session机制之间的区别与联系。笔者曾经常去的一家咖啡店有喝5杯咖啡免费赠一杯咖啡的优惠,然而一次性消费5杯咖啡的机会微乎其微,这时就需要某种方式来纪录某位顾客的消费数量。想象一下其实也无外乎下面的几种方案:
1、该店的店员很厉害,能记住每位顾客的消费数量,只要顾客一走进咖啡店,店员就知道该怎么对待了。这种做法就是协议本身支持状态。
2、发给顾客一张卡片,上面记录着消费的数量,一般还有个有效期限。每次消费时,如果顾客出示这张卡片,则此次消费就会与以前或以后的消费相联系起来。这种做法就是在客户端保持状态。
3、发给顾客一张会员卡,除了卡号之外什么信息也不纪录,每次消费时,如果顾客出示该卡片,则店员在店里的纪录本上找到这个卡号对应的纪录添加一些消费信息。这种做法就是在服务器端保持状态。

由于HTTP协议是无状态的,而出于种种考虑也不希望使之成为有状态的,因此,后面两种方案就成为现实的选择。具体来说cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上它还有其他选择。

三、理解cookie机制
cookie机制的基本原理就如上面的例子一样简单,但是还有几个问题需要解决:“会员卡”如何分发;“会员卡”的内容;以及客户如何使用“会员卡”。

正统的cookie分发是通过扩展HTTP协议来实现的,服务器通过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的cookie。然而纯粹的客户端脚本如JavaScript或者VBScript也可以生成cookie。

而cookie 的使用是由浏览器按照一定的原则在后台自动发送给服务器的。浏览器检查所有存储的cookie,如果某个cookie所声明的作用范围大于等于将要请求的资源所在的位置,则把该cookie附在请求资源的HTTP请求头上发送给服务器。意思是麦当劳的会员卡只能在麦当劳的店里出示,如果某家分店还发行了自己的会员卡,那么进这家店的时候除了要出示麦当劳的会员卡,还要出示这家店的会员卡。

cookie的内容主要包括:名字,值,过期时间,路径和域。
其中域可以指定某一个域比如.google.com,相当于总店招牌,比如宝洁公司,也可以指定一个域下的具体某台机器比如www.google.com或者froogle.google.com,可以用飘柔来做比。
路径就是跟在域名后面的URL路径,比如/或者/foo等等,可以用某飘柔专柜做比。
路径与域合在一起就构成了cookie的作用范围。
如果不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览器会话期的 cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里,当然这种行为并不是规范规定的。如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。

存储在硬盘上的cookie 可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存里的cookie,不同的浏览器有不同的处理方式。对于IE,在一个打开的窗口上按 Ctrl-N(或者从文件菜单)打开的窗口可以与原窗口共享,而使用其他方式新开的IE进程则不能共享已经打开的窗口的内存cookie;对于 Mozilla Firefox0.8,所有的进程和标签页都可以共享同样的cookie。一般来说是用javascript的window.open打开的窗口会与原窗口共享内存cookie。浏览器对于会话cookie的这种只认cookie不认人的处理方式经常给采用session机制的web应用程序开发者造成很大的困扰。

下面就是一个goolge设置cookie的响应头的例子
HTTP/1.1 302 Found
Location: http://www.google.com/intl/zh-CN/
Set-Cookie: PREF=ID=0565f77e132de138:NW=1:TM=1098082649:LM=1098082649:S=KaeaCFPo49RiA_d8; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com
Content-Type: text/html



image
这是使用HTTPLook这个HTTP Sniffer软件来俘获的HTTP通讯纪录的一部分

image
浏览器在再次访问goolge的资源时自动向外发送cookie

image
用Firefox可以很容易的观察现有的cookie的值
使用HTTPLook配合Firefox可以很容易的理解cookie的工作原理。

image
IE也可以设置在接受cookie前询问

四、理解session机制

session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。

当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 – 称为 session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个 session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 session id将被在本次响应中返回给客户端保存。

保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个cookie的名字都是类似于SEEESIONID,而。比如weblogic对于web应用程序生成的cookie,JSESSIONID= ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764,它的名字就是 JSESSIONID。

由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,一种是作为URL路径的附加信息,表现形式为http://…../xxx;jsessionid= ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764
另一种是作为查询字符串附加在URL后面,表现形式为http://…../xxx?jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764
这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。
为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id。

另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。比如下面的表单

<form name="testform" action="/xxx">
<input type="text">
</form>



在被传递给客户端之前将被改写成

<form name="testform" action="/xxx">
<input type="hidden" name="jsessionid" value="ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764">
<input type="text">
</form>



这种技术现在已较少应用,笔者接触过的很古老的iPlanet6(SunONE应用服务器的前身)就使用了这种技术。
实际上这种技术可以简单的用对action应用URL重写来代替。

在谈论session机制的时候,常常听到这样一种误解“只要关闭浏览器,session就消失了”。其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个session,否则服务器会一直保留,程序一般都是在用户做log off的时候发个指令去删除session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分session机制都使用会话cookie来保存session id,而关闭浏览器后这个 session id就消失了,再次连接服务器时也就无法找到原来的session。如果服务器设置的cookie被保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的session id发送给服务器,则再次打开浏览器仍然能够找到原来的session。

恰恰是由于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把session删除以节省存储空间。

五、理解javax.servlet.http.HttpSession
HttpSession是Java平台对session机制的实现规范,因为它仅仅是个接口,具体到每个web应用服务器的提供商,除了对规范支持之外,仍然会有一些规范里没有规定的细微差异。这里我们以BEA的Weblogic Server8.1作为例子来演示。

首先,Weblogic Server提供了一系列的参数来控制它的HttpSession的实现,包括使用cookie的开关选项,使用URL重写的开关选项,session持久化的设置,session失效时间的设置,以及针对cookie的各种设置,比如设置cookie的名字、路径、域, cookie的生存时间等。

一般情况下,session都是存储在内存里,当服务器进程被停止或者重启的时候,内存里的session也会被清空,如果设置了session的持久化特性,服务器就会把session保存到硬盘上,当服务器进程重新启动或这些信息将能够被再次使用, Weblogic Server支持的持久性方式包括文件、数据库、客户端cookie保存和复制。

复制严格说来不算持久化保存,因为session实际上还是保存在内存里,不过同样的信息被复制到各个cluster内的服务器进程中,这样即使某个服务器进程停止工作也仍然可以从其他进程中取得session。

cookie生存时间的设置则会影响浏览器生成的cookie是否是一个会话cookie。默认是使用会话cookie。有兴趣的可以用它来试验我们在第四节里提到的那个误解。

cookie的路径对于web应用程序来说是一个非常重要的选项,Weblogic Server对这个选项的默认处理方式使得它与其他服务器有明显的区别。后面我们会专题讨论。

关于session的设置参考[5] http://e-docs.bea.com/wls/docs70/webapp/weblogic_xml.html#1036869

六、HttpSession常见问题
(在本小节中session的含义为⑤和⑥的混合)

1、session在何时被创建
一个常见的误解是以为session在有客户端访问时就被创建,然而事实是直到某server端程序调用 HttpServletRequest.getSession(true)这样的语句时才被创建,注意如果JSP没有显示的使用 <% @page session="false"%> 关闭session,则JSP文件在编译成Servlet时将会自动加上这样一条语句 HttpSession session = HttpServletRequest.getSession(true);这也是JSP中隐含的 session对象的来历。

由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它。

2、session何时被删除
综合前面的讨论,session在下列情况下被删除a.程序调用HttpSession.invalidate();或b.距离上一次收到客户端发送的session id时间间隔超过了session的超时设置;或c.服务器进程被停止(非持久session)

3、如何做到在浏览器关闭时删除session
严格的讲,做不到这一点。可以做一点努力的办法是在所有的客户端页面里使用javascript代码window.oncolose来监视浏览器的关闭动作,然后向服务器发送一个请求来删除session。但是对于浏览器崩溃或者强行杀死进程这些非常规手段仍然无能为力。

4、有个HttpSessionListener是怎么回事
你可以创建这样的listener去监控session的创建和销毁事件,使得在发生这样的事件时你可以做一些相应的工作。注意是session的创建和销毁动作触发listener,而不是相反。类似的与HttpSession有关的listener还有 HttpSessionBindingListener,HttpSessionActivationListener和 HttpSessionAttributeListener。

5、存放在session中的对象必须是可序列化的吗
不是必需的。要求对象可序列化只是为了session能够在集群中被复制或者能够持久保存或者在必要时server能够暂时把session交换出内存。在 Weblogic Server的session中放置一个不可序列化的对象在控制台上会收到一个警告。我所用过的某个iPlanet版本如果 session中有不可序列化的对象,在session销毁时会有一个Exception,很奇怪。

6、如何才能正确的应付客户端禁止cookie的可能性
对所有的URL使用URL重写,包括超链接,form的action,和重定向的URL,具体做法参见[6]
http://e-docs.bea.com/wls/docs70/webapp/sessions.html#100770

7、开两个浏览器窗口访问应用程序会使用同一个session还是不同的session
参见第三小节对cookie的讨论,对session来说是只认id不认人,因此不同的浏览器,不同的窗口打开方式以及不同的cookie存储方式都会对这个问题的答案有影响。

8、如何防止用户打开两个浏览器窗口操作导致的session混乱
这个问题与防止表单多次提交是类似的,可以通过设置客户端的令牌来解决。就是在服务器每次生成一个不同的id返回给客户端,同时保存在session里,客户端提交表单时必须把这个id也返回服务器,程序首先比较返回的id与保存在session里的值是否一致,如果不一致则说明本次操作已经被提交过了。可以参看《J2EE核心模式》关于表示层模式的部分。需要注意的是对于使用javascript window.open打开的窗口,一般不设置这个id,或者使用单独的id,以防主窗口无法操作,建议不要再window.open打开的窗口里做修改操作,这样就可以不用设置。

9、为什么在Weblogic Server中改变session的值后要重新调用一次session.setValue
做这个动作主要是为了在集群环境中提示Weblogic Server session中的值发生了改变,需要向其他服务器进程复制新的session值。

10、为什么session不见了
排除session正常失效的因素之外,服务器本身的可能性应该是微乎其微的,虽然笔者在iPlanet6SP1加若干补丁的Solaris版本上倒也遇到过;浏览器插件的可能性次之,笔者也遇到过3721插件造成的问题;理论上防火墙或者代理服务器在cookie处理上也有可能会出现问题。
出现这一问题的大部分原因都是程序的错误,最常见的就是在一个应用程序中去访问另外一个应用程序。我们在下一节讨论这个问题。

七、跨应用程序的session共享

常常有这样的情况,一个大项目被分割成若干小项目开发,为了能够互不干扰,要求每个小项目作为一个单独的web应用程序开发,可是到了最后突然发现某几个小项目之间需要共享一些信息,或者想使用session来实现SSO(single sign on),在session中保存login的用户信息,最自然的要求是应用程序间能够访问彼此的session。

然而按照Servlet规范,session的作用范围应该仅仅限于当前应用程序下,不同的应用程序之间是不能够互相访问对方的session的。各个应用服务器从实际效果上都遵守了这一规范,但是实现的细节却可能各有不同,因此解决跨应用程序session共享的方法也各不相同。

首先来看一下Tomcat是如何实现web应用程序之间session的隔离的,从 Tomcat设置的cookie路径来看,它对不同的应用程序设置的cookie路径是不同的,这样不同的应用程序所用的session id是不同的,因此即使在同一个浏览器窗口里访问不同的应用程序,发送给服务器的session id也可以是不同的。

image
image

根据这个特性,我们可以推测Tomcat中session的内存结构大致如下。
image

笔者以前用过的iPlanet也采用的是同样的方式,估计SunONE与iPlanet之间不会有太大的差别。对于这种方式的服务器,解决的思路很简单,实际实行起来也不难。要么让所有的应用程序共享一个session id,要么让应用程序能够获得其他应用程序的session id。

iPlanet中有一种很简单的方法来实现共享一个session id,那就是把各个应用程序的cookie路径都设为/(实际上应该是/NASApp,对于应用程序来讲它的作用相当于根)。

<session-info>
<path>/NASApp</path>
</session-info>



需要注意的是,操作共享的session应该遵循一些编程约定,比如在session attribute名字的前面加上应用程序的前缀,使得 setAttribute("name", "neo")变成setAttribute("app1.name", "neo"),以防止命名空间冲突,导致互相覆盖。

在Tomcat中则没有这么方便的选择。在Tomcat版本3上,我们还可以有一些手段来共享session。对于版本4以上的Tomcat,目前笔者尚未发现简单的办法。只能借助于第三方的力量,比如使用文件、数据库、JMS或者客户端cookie,URL参数或者隐藏字段等手段。

我们再看一下Weblogic Server是如何处理session的。
image
image

从截屏画面上可以看到Weblogic Server对所有的应用程序设置的cookie的路径都是/,这是不是意味着在Weblogic Server中默认的就可以共享session了呢?然而一个小实验即可证明即使不同的应用程序使用的是同一个session,各个应用程序仍然只能访问自己所设置的那些属性。这说明Weblogic Server中的session的内存结构可能如下


对于这样一种结构,在 session机制本身上来解决session共享的问题应该是不可能的了。除了借助于第三方的力量,比如使用文件、数据库、JMS或者客户端 cookie,URL参数或者隐藏字段等手段,还有一种较为方便的做法,就是把一个应用程序的session放到ServletContext中,这样另外一个应用程序就可以从ServletContext中取得前一个应用程序的引用。示例代码如下,

应用程序A

context.setAttribute("appA", session); 



应用程序B

contextA = context.getContext("/appA");
HttpSession sessionA = (HttpSession)contextA.getAttribute("appA");



值得注意的是这种用法不可移植,因为根据ServletContext的JavaDoc,应用服务器可以处于安全的原因对于context.getContext("/appA");返回空值,以上做法在Weblogic Server 8.1中通过。

那么Weblogic Server为什么要把所有的应用程序的cookie路径都设为/呢?原来是为了SSO,凡是共享这个session的应用程序都可以共享认证的信息。一个简单的实验就可以证明这一点,修改首先登录的那个应用程序的描述符weblogic.xml,把cookie路径修改为/appA 访问另外一个应用程序会重新要求登录,即使是反过来,先访问cookie路径为/的应用程序,再访问修改过路径的这个,虽然不再提示登录,但是登录的用户信息也会丢失。注意做这个实验时认证方式应该使用FORM,因为浏览器和web服务器对basic认证方式有其他的处理方式,第二次请求的认证不是通过 session来实现的。具体请参看[7] secion 14.8 Authorization,你可以修改所附的示例程序来做这些试验。

八、总结
session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的经验,而是始终需要具体情况具体分析。
摘要:虽然session机制在web应用程序中被采用已经很长时间了,但是仍然有很多人不清楚session机制的本质,以至不能正确的应用这一技术。本文将详细讨论session的工作机制并且对在Java web application中应用session机制时常见的问题作出解答。

]]>
http://blog.donews.com/afxtruelover/archive/2006/03/23/783396.aspx/feed 1
Tomcat性能调较[转载] http://blog.donews.com/afxtruelover/archive/2006/03/21/778524.aspx http://blog.donews.com/afxtruelover/archive/2006/03/21/778524.aspx#comments Tue, 21 Mar 2006 03:19:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/03/21/778524.aspx 一. 引言

  性能测试与分析是软件开发过程中介于架构和调整的一个广泛并比较不容易理解的领域,更是一项较为复杂的活动。就像下棋游戏一样,有效的性能测试和分析只能在一个良好的计划策略和具备了对不可预料事件的处理能力的条件下顺利地完成。一个下棋高手赢得比赛靠的不仅仅是对游戏规则的认识,更是靠他的自己的能力和不断地专注于分析自己对手的实力来更加有效地利用和发挥规则的作用。同样一个优秀的性能测试和分析人员将要面对的是来自一个全新的应用程序和环境下带来的整个项目的挑战。本文中作者结合自己的使用经验和参考文档,对Tomcat性能方面的调整做一简要的介绍,并给出Tomcat性能的测试、分析和调整优化的一些方法。


二. 测量Web服务器的性能

  测量web服务器的性能是一项让人感到畏缩的任务,但是我们在这里将给出一些需要注意的地方并且指点你了解其中更多的细节性的内容。它不像一些简单的任务,如测量CPU的速率或者是测量程序占用CPU的比例,web服务器的性能优化中包括许调整许多变量来达到目标。许多的测量策略中都包含了一个看似简]]>
一. 引言

  性能测试与分析是软件开发过程中介于架构和调整的一个广泛并比较不容易理解的领域,更是一项较为复杂的活动。就像下棋游戏一样,有效的性能测试和分析只能在一个良好的计划策略和具备了对不可预料事件的处理能力的条件下顺利地完成。一个下棋高手赢得比赛靠的不仅仅是对游戏规则的认识,更是靠他的自己的能力和不断地专注于分析自己对手的实力来更加有效地利用和发挥规则的作用。同样一个优秀的性能测试和分析人员将要面对的是来自一个全新的应用程序和环境下带来的整个项目的挑战。本文中作者结合自己的使用经验和参考文档,对Tomcat性能方面的调整做一简要的介绍,并给出Tomcat性能的测试、分析和调整优化的一些方法。


二. 测量Web服务器的性能

  测量web服务器的性能是一项让人感到畏缩的任务,但是我们在这里将给出一些需要注意的地方并且指点你了解其中更多的细节性的内容。它不像一些简单的任务,如测量CPU的速率或者是测量程序占用CPU的比例,web服务器的性能优化中包括许调整许多变量来达到目标。许多的测量策略中都包含了一个看似简单的浏览实际上是在向服务器发送大量的请求,我们称之为客户端的程序,来测量响应时间。客户端和服务器端是在同一台机器上吗?服务器在测试的时候还运行着其它的什么程序吗?客户端和服务器端的通讯是通过局域网,100baseT,10baseT还是使用调制解调器?客户端是否一直重复请求相同的页面,还是随机地访问不同的页面?(这些影响到了服务缓存的性能)客户端发送请求的有规律的还是突发的?你是在最终的配置环境下运行服务的还是在调试的配置环境下运行服务的?客户端请求中包含图片还是只有HTML页面?是否有请求是通过servlets和JSP的,CGI程序,服务端包含(Server-Side Includes ,SSI是一个可以让你使用动态HTML文件的技术)?所有这些都将是我们要关心的,并且几乎我们不可能精确地把所有的问题都清楚地列出来。

  1.压力测试工具

  “工欲善其事,必先利其器”,压力测试只有借助于一些工具才可得以实施。

  大多数web压力测试工具的实现原理都是通过重复的大量的页面请求来模拟多用户对被测系统的并发访问,以此达到产生压力的目的。产生压力的手段都是通过录制或者是编写压力脚本,这些脚本以多个进程或者线程的形式在客户端运行,这样通过人为制造各种类型的压力,我们可以观察被测系统在各种压力状况下的表现,从而定位系统瓶颈,作为系统调优的基础。目前已经存在的性能测试工具林林总总,数量不下一百种,从单一的开放源码的免费小工具如 Aapache 自带的 web 性能测试工具 Apache Benchmark、开源的Jmeter 到大而全的商业性能测试软件如 Mercury 的 LoadRunner 等等。任何性能测试工具都有其优缺点,我们可以根据实际情况挑选用最合适的工具。您可以在这里找到一些web压力测试工具http://www.softwareqatest.com/qatweb1.html#LOAD

  这里我们所使用的工具要支持web应用服务认证才可以,要支持接收发送cookies,不仅如此Tomcat支持多种认证方式,比如基本认证、基于表单的认证、相互认证和客户端认证,而一些工具仅仅支持HTTP基本认证。真实地模拟用户认证是性能测试工具的一个重要的部分,因为认证机制将对一个web站点的性能特征产生重要的影响。基于你在产品中使用的不同的认证方式,你需要从上面的工具列表中选择使用这种特性的测试工具。

  Apache Benchmark和http_load是命令行形式的工具,非常易于使用。Apache Benchmark可以模仿单独的URL请求并且重复地执行,可以使用不同的命令行参数来控制执行迭代的次数,并发用户数等等。它的一个特点是可以周期性地打印出处理过程的信息,而其它工具只能给出一个全局的报告。

  2.压力测试工具介绍

  1) Apache Benchmark

  下面是运行Apache Benchmark的例子,响应时间非常长是因为它运行在一个配置非常低的系统上(Pentium 233)。在这里我们用它来访问一个URL,模拟127个并发用户重复执行1000次。


Root$ ab -k -n 1000 -c 127 -k http://tomcathost:8080/examples/date/date.jsp
This is ApacheBench, Version 2.0.36 <$Revision: 1.1 $> apache-2.0
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
Benchmarking tomcathost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Finished 1000 requests
Server Software: Apache
Server Hostname: tomcathost
Server Port: 8080
Document Path: /examples/date/date.jsp
Document Length: 701 bytes
Concurrency Level: 127
Time taken for tests: 53.162315 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Non-2xx responses: 1000
Keep-Alive requests: 0
Total transferred: 861000 bytes
HTML transferred: 701000 bytes
Requests per second: 18.81[#/sec] (mean)
Time per request: 6.752(mean)
Time per request: 0.053(mean, across all concurrent requests)
Transfer rate: 15.80>Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 51 387.5 0 2999
Processing: 63 6228 2058.4 6208 12072
Waiting: 17 4236 1855.2 3283 9193
Total: 64 6280 2065.0 6285 12072
Percentage of the requests served within a certain time (ms)
50% 6285
66% 6397
75% 6580
80% 9076
90% 9080
95% 9089
98% 9265
99% 12071
100% 12072 (longest request)

  2) Apache JMeter 中请求响应时间(图略)
  3) Mercury LoadRunner测试实时监控(图略)

  这三个工具是所有类似性能测试工具的典型代表,可以根据你自己的需要选择不同的测试工具。这里不对以上工具做详细的介绍,如果您对这些测试工具感兴趣的话可以参阅附加资料。

  3.性能评测技巧

  1) 由于评测时要用到系统时钟,所以当进行测试时不要运行无关的进程或程序,以免影响测试结果;

  2) 如果对自己的程序进行了修改,并试图改善它的性能,那么在修改前后应分别测试一下代码的执行时间;

  3) 尽量在完全一致的环境中进行每一次测试;

  4) 如果可能,应设计一个不依赖于任何用户输入的测试,测试中使用的数据应完全一致,避免用户的不同的反应或者数据的问题导致测试结果出现误差;

  5) 有可能您还需要考虑是在运行着的系统(包括操作系统和被测试的系统)上继续进行测试还是重新启动系统后再进行测试;

  6) 对于有些系统第一次使用可能要进行初始化,这种工作在系统使用过程中仅进行一次,所以有必要在重启系统后先进行一次初始化工作,然后进行性能测试。

三. 外部环境的调整

  在Tomcat和应用程序进行了压力测试后,如果您对应用程序的性能结果不太满意,就可以采取一些性能调整措施了,当然了前提是应用程序没有问题,我们这里只讲Tomcat的调整。由于Tomcat的运行依赖于JVM,所以在这里我们把Tomcat的调整可以分为两类来详细描述:

  外部环境调整

  调整非Tomcat组件,例如Tomcat运行的操作系统和运行Tomcat的java虚拟机。

  自身调整

  修改Tomcat自身的参数,调整Tomcat配置文件中的参数。

  下面我们将详细讲解外部环境调整的有关内容,Tomcat自身调整的内容将在第2部分中阐述。

  1.JAVA虚拟机性能优化

  Tomcat本身不能直接在计算机上运行,需要依赖于硬件基础之上的操作系统和一个java虚拟机。您可以选择自己的需要选择不同的操作系统和对应的JDK的版本(只要是符合Sun发布的Java规范的),但我们推荐您使用Sun公司发布的JDK。确保您所使用的版本是最新的,因为Sun公司和其它一些公司一直在为提高性能而对java虚拟机做一些升级改进。一些报告显示JDK1.4在性能上比JDK1.3提高了将近10%到20%。

  可以给Java虚拟机设置使用的内存,但是如果你的选择不对的话,虚拟机不会补偿。可通过命令行的方式改变虚拟机使用内存的大小。如下表所示有两个参数用来设置虚拟机使用内存的大小。
参数
描述

-Xms<size>
JVM初始化堆的大小

-Xmx<size>
JVM堆的最大值


  这两个值的大小一般根据需要进行设置。初始化堆的大小执行了虚拟机在启动时向系统申请的内存的大小。一般而言,这个参数不重要。但是有的应用程序在大负载的情况下会急剧地占用更多的内存,此时这个参数就是显得非常重要,如果虚拟机启动时设置使用的内存比较小而在这种情况下有许多对象进行初始化,虚拟机就必须重复地增加内存来满足使用。由于这种原因,我们一般把-Xms和-Xmx设为一样大,而堆的最大值受限于系统使用的物理内存。一般使用数据量较大的应用程序会使用持久对象,内存使用有可能迅速地增长。当应用程序需要的内存超出堆的最大值时虚拟机就会提示内存溢出,并且导致应用服务崩溃。因此一般建议堆的最大值设置为可用内存的最大值的80%。

  Tomcat默认可以使用的内存为128MB,在较大型的应用项目中,这点内存是不够的,需要调大。

  Windows下,在文件{tomcat_home}/bin/catalina.bat,Unix下,在文件{tomcat_home}/bin/catalina.sh的前面,增加如下设置:

  JAVA_OPTS=’-Xms【初始化内存大小】 -Xmx【可以使用的最大内存】’

  需要把这个两个参数值调大。例如:

  JAVA_OPTS=’-Xms256m -Xmx512m’

  表示初始化内存为256MB,可以使用的最大内存为512MB。

  另外需要考虑的是Java提供的垃圾回收机制。虚拟机的堆大小决定了虚拟机花费在收集垃圾上的时间和频度。收集垃圾可以接受的速度与应用有关,应该通过分析实际的垃圾收集的时间和频率来调整。如果堆的大小很大,那么完全垃圾收集就会很慢,但是频度会降低。如果你把堆的大小和内存的需要一致,完全收集就很快,但是会更加频繁。调整堆大小的的目的是最小化垃圾收集的时间,以在特定的时间内最大化处理客户的请求。在基准测试的时候,为保证最好的性能,要把堆的大小设大,保证垃圾收集不在整个基准测试的过程中出现。

  如果系统花费很多的时间收集垃圾,请减小堆大小。一次完全的垃圾收集应该不超过 3-5 秒。如果垃圾收集成为瓶颈,那么需要指定代的大小,检查垃圾收集的详细输出,研究 垃圾收集参数对性能的影响。一般说来,你应该使用物理内存的 80% 作为堆大小。当增加处理器时,记得增加内存,因为分配可以并行进行,而垃圾收集不是并行的。

  2.操作系统性能优化

  这里说的操作系统是指运行web服务器的系统软件,当然,不同的操作系统是为不同的目的而设计的。比如OpenBSD是面向安全的,因此在它的内核中有许多的限制来防止不同形式的服务攻击(OpenBSD的一句座右铭是“默认是最安全的”)。这些限制或许更多地用来运行活跃的web服务器。

  而我们常用的Linux操作系统的目标是易用使用,因此它有着更高的限制。使用BSD内核的系统都带有一个名为“Generic”的内核,表明所有的驱动器都静态地与之相连。这样就使系统易于使用,但是如果你要创建一个自定义的内核来加强其中某些限制,那就需要排除不需要的设备。Linux内核中的许多驱动都是动态地加载的。但是换而言之,内存现在变得越来越便宜,所以因为加载额外的设备驱动就显得不是很重要的。重要的是要有更多的内存,并且在服务器上腾出更多的可用内存。

  小提示:虽然现在内存已经相当的便宜,但还是尽量不要购买便宜的内存。那些有牌子的内存虽然是贵一点,但是从可靠性上来说,性价比会更高一些。

  如果是在Windows操作系统上使用Tomcat,那么最好选择服务器版本。因为在非服务器版本上,最终用户授权数或者操作系统本身所能承受的用户数、可用的网络连接数或其它方面的一些方面都是有限制的。并且基于安全性的考虑,必须经常给操作系统打上最新的补丁。

  3.Tomcat与其它web服务器整合使用

  虽然tomcat也可以作web服务器,但其处理静态html的速度比不上apache,且其作为web服务器的功能远不如apache,因此我们想把apache和tomcat集成起来,将html与jsp的功能部分进行明确分工,让tomcat只处理jsp部分,其它的由apache,IIS等这些web服务器处理,由此大大节省了tomcat有限的工作“线程”。

  4.负载均衡

  在负载均衡的思路下,多台服务器为对称方式,每台服务器都具有同等的地位,可以单独对外提供服务而无须其他服务器的辅助。通过负载分担技术,将外部发送来的请求按一定规则分配到对称结构中的某一台服务器上,而接收到请求的服务器都独立回应客户机的请求。

  提供服务的一组服务器组成了一个应用服务器集群(cluster),并对外提供一个统一的地址。当一个服务请求被发至该集群时,根据一定规则选择一台服务器,并将服务转定向给该服务器承担,即将负载进行均衡分摊。

  通过应用负载均衡技术,使应用服务超过了一台服务器只能为有限用户提供服务的限制,可以利用多台服务器同时为大量用户提供服务。当某台服务器出现故障时,负载均衡服务器会自动进行检测并停止将服务请求分发至该服务器,而由其他工作正常的服务器继续提供服务,从而保证了服务的可靠性。

  负载均衡实现的方式大概有四种:第一是通过DNS,但只能实现简单的轮流分配,不能处理故障,第二如果是基于MS IIS,Windows 2003 server本身就带了负载均衡服务,第三是硬件方式,通过交换机的功能或专门的负载均衡设备可以实现,第四种是软件方式,通过一台负载均衡服务器进行,上面安装软件。使用Apache Httpd Server做负载平衡器,Tomcat集群节点使用Tomcat就可以做到以上第四种方式。这种方式比较灵活,成本相对也较低。另外一个很大的优点就是可以根据应用的情况和服务器的情况采取一些策略。

四. 自身调整

  本节将向您详细介绍一些加速可使Tomcat实例加速运行的技巧和方法,无论是在什么操作系统或者何种Java虚拟机上。在有些情况下,您可能没有控制部署环境上的操作系统或者Java虚拟机。在这种情况下,您就需要逐行了解以下的的一些建议,然而你应该在修改后使之生效。我认为以下方法是Tomcat性能自身调整的最佳方式。

  1.禁用DNS查询

  当web应用程序向要记录客户端的信息时,它也会记录客户端的IP地址或者通过域名服务器查找机器名转换为IP地址。DNS查询需要占用网络,并且包括可能从很多很远的服务器或者不起作用的服务器上去获取对应的IP的过程,这样会消耗一定的时间。为了消除DNS查询对性能的影响我们可以关闭DNS查询,方式是修改server.xml文件中的enableLookups参数值:
Tomcat4

<Connector className="org.apache.coyote.tomcat4.CoyoteConnector" port="80" minProcessors="5" maxProcessors="75" enableLookups="false" redirectPort="8443" acceptCount="100" debug="0" connectionTimeout="20000" useURIValidationHack="false" disableUploadTimeout="true" />

Tomcat5

<Connector port="80" maxThreads="150" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" redirectPort="8443" acceptCount="100" debug="0" connectionTimeout="20000" disableUploadTimeout="true"/>

  除非你需要连接到站点的每个HTTP客户端的机器名,否则我们建议在生产环境上关闭DNS查询功能。可以通过Tomcat以外的方式来获取机器名。这样不仅节省了网络带宽、查询时间和内存,而且更小的流量会使日志数据也会变得更少,显而易见也节省了硬盘空间。对流量较小的站点来说禁用DNS查询可能没有大流量站点的效果明显,但是此举仍不失为一良策。谁又见到一个低流量的网站一夜之间就流量大增呢?

  2.调整线程数

  另外一个可通过应用程序的连接器(Connector)进行性能控制的的参数是创建的处理请求的线程数。Tomcat使用线程池加速响应速度来处理请求。在Java中线程是程序运行时的路径,是在一个程序中与其它控制线程无关的、能够独立运行的代码段。它们共享相同的地址空间。多线程帮助程序员写出CPU最大利用率的高效程序,使空闲时间保持最低,从而接受更多的请求。

  Tomcat4中可以通过修改minProcessors和maxProcessors的值来控制线程数。这些值在安装后就已经设定为默认值并且是足够使用的,但是随着站点的扩容而改大这些值。minProcessors服务器启动时创建的处理请求的线程数应该足够处理一个小量的负载。也就是说,如果一天内每秒仅发生5次单击事件,并且每个请求任务处理需要1秒钟,那么预先设置线程数为5就足够了。但在你的站点访问量较大时就需要设置更大的线程数,指定为参数maxProcessors的值。maxProcessors的值也是有上限的,应防止流量不可控制(或者恶意的服务攻击),从而导致超出了虚拟机使用内存的大小。如果要加大并发连接数,应同时加大这两个参数。web server允许的最大连接数还受制于操作系统的内核参数设置,通常Windows是2000个左右,Linux是1000个左右。

  在Tomcat5对这些参数进行了调整,请看下表:
属性名
描述

maxThreads
Tomcat使用线程来处理接收的每个请求。这个值表示Tomcat可创建的最大的线程数。

acceptCount
指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。

connnectionTimeout
网络连接超时,单位:毫秒。设置为0表示永不超时,这样设置有隐患的。通常可设置为30000毫秒。

minSpareThreads
Tomcat初始化时创建的线程数。

maxSpareThreads
一旦创建的线程超过这个值,Tomcat就会关闭不再需要的socket线程。


  最好的方式是多设置几次并且进行测试,观察响应时间和内存使用情况。在不同的机器、操作系统或虚拟机组合的情况下可能会不同,而且并不是所有人的web站点的流量都是一样的,因此没有一刀切的方案来确定线程数的值。

  3.加速JSP编译速度

  当第一次访问一个JSP文件时,它会被转换为Java serverlet源码,接着被编译成Java字节码。你可以控制使用哪个编译器,默认情况下,Tomcat使用使用命令行javac进行使用的编译器。也可以使用更快的编译器,但是这里我们将介绍如何优化它们。

  另外一种方法是不要把所有的实现都使用JSP页面,而是使用一些不同的java模板引擎变量。显然这是一个跨越很大的决定,但是事实证明至少这种方法是只得研究的。如果你想了解更多有关在Tomcat可使用的模板语言,你可以参考Jason Hunter和William Crawford合著的《Java Servlet Programming 》一书(O’Reilly公司出版)。

  在Tomcat 4.0中可以使用流行而且免费的Jikes编译器。Jikes编译器的速度要由于Sun的Java编译器。首先要安装Jikes(可访问http://oss.software.ibm.com/pub/jikes 获得更多的信息),接着需要在环境变量中设置JIKESPATH包含系统运行时所需的JAR文件。装好Jikes以后还需要设置让JSP编译servlet使用Jikes,需要修改web.xml文件中jspCompilerPlugin的值:
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>
org.apache.jasper.servlet.JspServlet
</servlet-class>
<init-param>
<param-name>logVerbosityLevel</param-name>
<param-value>WARNING</param-value>
</init-param>
<init-param>
<param-name>jspCompilerPlugin</param-name>
<param-value>
org.apache.jasper.compiler.JikesJavaCompiler
</param-value>
</init-param>
<init-param>
<!– <param-name>
org.apache.catalina.jsp_classpath
</param-name> –>
<param-name>classpath</param-name>
<param-value>
/usr/local/jdk1.3.1-linux/jre/lib/rt.jar:
/usr/local/lib/java/servletapi/servlet.ja
r</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>

  在Tomcat 4.1(或更高版本),JSP的编译由包含在Tomcat里面的Ant程序控制器直接执行。这听起来有一点点奇怪,但这正是Ant有意为之的一部分,有一个API文档指导开发者在没有启动一个新的JVM的情况下,使用Ant。这是使用Ant进行Java开发的一大优势。另外,这也意味着你现在能够在Ant中使用任何javac支持的编译方式,这里有一个关于Apache Ant使用手册的javac page列表。使用起来是容易的,因为你只需要在 元素中定义一个名字叫“compiler”,并且在value中有一个支持编译的编译器名字,示例如下:
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>
org.apache.jasper.servlet.JspServlet
</servlet-class>
<init-param>
<param-name>logVerbosityLevel</param-name>
<param-value>WARNING</param-value>
</init-param>
<init-param>
<param-name>compiler</param-name>
<param-value>jikes</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>


Ant可用的编译器
名称
别名
调用的编译器

classic
javac1.1, javac1.2
Standard JDK 1.1/1.2 compiler

modern
javac1.3, javac1.4
Standard JDK 1.3/1.4 compiler

jikes
   The Jikes compiler

JVC Microsoft
Microsoft command-line compiler from the Microsoft SDK for Java/Visual J++

KJC    The kopi compiler

GCJ    The gcj compiler (included as part of gcc)

SJ Symantec
Symantec’s Java compiler

extJavac
   Runs either the modern or classic compiler in a JVM of its own


  由于JSP页面在第一次使用时已经被编译,那么你可能希望在更新新的jsp页面后马上对它进行编译。实际上,这个过程完全可以自动化,因为可以确认的是新的JSP页面在生产服务器和在测试服务器上的运行效果是一样的。

  在Tomcat4的bin目录下有一个名为jspc的脚本。它仅仅是运行翻译阶段,而不是编译阶段,使用它可以在当前目录生成Java源文件。它是调试JSP页面的一种有力的手段。

  可以通过浏览器访问再确认一下编译的结果。这样就确保了文件被转换成serverlet,被编译了可直接执行。这样也准确地模仿了真实用户访问JSP页面,可以看到给用户提供的功能。也抓紧这最后一刻修改出现的bug并且修改它J

  Tomcat提供了一种通过请求来编译JSP页面的功能。例如,你可以在浏览器地址栏中输入http://localhost:8080/examples/jsp/dates/date.jsp?jsp_precompile=true,这样Tomcat就会编译data.jsp而不是执行它。此举唾手可得,不失为一种检验页面正确性的捷径。

  4. 其它

  前面我们提到过操作系统通过一些限制手段来防止恶意的服务攻击,同样Tomcat也提供了防止恶意攻击或禁止某些机器访问的设置。

  Tomcat提供了两个参数供你配置:RemoteHostValve 和RemoteAddrValve。

  通过配置这两个参数,可以让你过滤来自请求的主机或IP地址,并允许或拒绝哪些主机/IP。与之类似的,在Apache的httpd文件里有对每个目录的允许/拒绝指定。

  例如你可以把Admin Web application设置成只允许本地访问,设置如下:
<Context path="/path/to/secret_files" …>
<Valve className="org.apache.catalina.valves.RemoteAddrValve"

allow="127.0.0.1" deny=""/>
</Context>

  如果没有给出允许主机的指定,那么与拒绝主机匹配的主机就会被拒绝,除此之外的都是允许的。与之类似,如果没有给出拒绝主机的指定,那么与允许主机匹配的主机就会被允许,除此之外的都是拒绝的。

五. 容量计划

  容量计划是在生产环境中使用Tomcat不得不提的提高性能的另一个重要的话题。如果你没有对预期的网络流量下的硬件和带宽做考虑的话那么无论你如何做配置修改和测试都无济于事。

  这里先对提及的容量计划作一个简要的定义:容量计划是指评估硬件、操作系统和网络带宽,确定应用服务的服务范围,寻求适合需求和软件特性的软硬件的一项活动。因此这里所说的软件不仅包括Tomcat,也包括与Tomcat结合使用的任何第三方web服务器软件。

  如果在购买软硬件或部署系统前你对容量计划一无所知,不知道现有的软硬件环境能够支撑多少的访问量,甚至更糟直到你已经交付并且在生产环境上部署产品后才意识到配置有问题时再进行变更可能为时已晚。此时只能增加硬件投入,增加硬盘容量甚至购买更好的服务器。如果事先做了容量计划那么就不会搞的如此焦头烂额了。

  我们这里只介绍与Tomcat相关的内容。

  首先为了确定Tomcat使用机器的容量计划,你应该从一下列表项目种着手研究和计划:

  1. 硬件

  采用什么样的硬件体系?需要多少台计算机?使用一个大型的,还是使用多台小型机?每个计算机上使用几个CPU?使用多少内存?使用什么样的存储设备,I/O的处理速度有什么要求?怎样维护这些计算机?不同的JVM在这些硬件上运行的效果如何(比如IBM AIX系统只能在其设计的硬件系统上运行)?

  2. 网络带宽

  带宽的使用极限是多少?web应用程序如何处理过多的请求?

  3. 服务端操作系统

  采用哪种操作系统作为站点服务器最好?在确定的操作系统上使用哪个JVM最好?例如,JVM在这种系统上是否支持本地多线程,对称多处理?哪种系统可使web服务器更快、更稳定,并且更便宜。是否支持多CPU?

  4. Tomcat容量计划

  以下介绍针对Tomcat做容量计划的步骤:

  1) 量化负载。如果站点已经建立并运行,可以使用前面介绍的工具模仿用户访问,确定资源的需求量。

  2) 针对测试结果或测试过程中进行分析。需要知道那些请求造成了负载过重或者使用过多的资源,并与其它请求做比较,这样就确定了系统的瓶颈所在。例如:如果servlet在查询数据库的步骤上耗用较长的时间,那么就需要考虑使用缓冲池来降低响应时间。

  3) 确定性能最低标准。例如,你不想让用户花20秒来等待结果页面的返回,也就是说甚至在达到访问量的极限时,用户等待的时间也不能超过20秒种(从点击链接到看到返第一条返回数据)。这个时间中包含了数据库查询时间和文件访问时间。同类产品性能在不同的公司可能有不同的标准,一般最好采取同行中的最低标准或对这个标准做出评估。

  4) 确定如何合理使用底层资源,并逐一进行测试。底层资源包括CPU、内存、存储器、带宽、操作系统、JVM等等。在各种生产环境上都按顺序进行部署和测试,观察是否符合需求。在测试Tomcat时尽量多采用几种JVM,并且调整JVM使用内存和Tomcat线程池的大小进行测试。同时为了达到资源充分合理稳定地使用的效果,还需针对测试过程中出现的硬件系统瓶颈进行处理确定合理的资源配置。这个过程最为复杂,而且一般由于没有可参考的值所以只能靠理论推断和经验总结。

  5) 如果通过第4步的反复测试如果达到了最优的组合,就可以在相同的生产环境上部署产品了。

  此外应牢记一定要文档化你的测试过程和结果,因为此后可能还会进行测试,这样就可以拿以前的测试结果做为参考。另外测试过程要反复多次进行,每次的条件可能都不一样,因此只有记录下来才能进行结果比较和最佳条件的选择。

  这样我们通过测试找到了最好的组合方式,各种资源得到了合理的配置,系统的性能得到了极大的提升。


六. 附加资料

  很显然本文也很难全面而详尽地阐述性能优化过程。如果你进行更多研究的话可能会把性能调优做的更好,比如Java程序的性能调整、操作系统的调整、各种复杂环境与应用系统和其它所有与应用程序相关的东西。在这里提供一些文中提到的一些资源、文中提到的相关内容的链接以及本文的一些参考资料。

  1. Web性能测试资料及工具

  1) Jmeter Wiki首页,Jmeter为一个开源的100%Java开发的性能测试工具
  http://wiki.apache.org/jakarta-jmeter/

  2) Apache Benchmark使用说明
  http://httpd.apache.org/docs-2.0/programs/ab.html

  3) 一些Java相关测试工具的介绍,包含可以与Tomcat集成进行测试的工具
  http://blog.csdn.net/wyingquan/

  4) LoadRunner® 是一种预测系统行为和性能的工业标准级负载测试工具。它通过模拟数据以千万计用户来实施并发负载来对整个企业架构进行测试,来帮助您更快的查找和发现问题。
  http://www.mercury.com/us/products/performance-center/loadrunner/


  2. 文中介绍的相关内容的介绍

  1) Apache 2.x + Tomcat 4.x做负载均衡,描述了如何利用jk配置集群的负载均衡。
  http://raibledesigns.com/tomcat/index.html

  2) 容量计划的制定,收集了许多有关制定web站点容量计划的例子:
  http://www.capacityplanning.com/

  3) 评测Tomcat5负载平衡与集群,
  http://www.javaresearch.org/article/showarticle.jsp?column=556&thread=19777

  4) Apache与Tomcat的安装与整合之整合篇
  http://www.javaresearch.org/article/showarticle.jsp?column=23&thread=18139

  5) 性能测试工具之研究,介绍了性能测试工具的原理与思路
  http://www.51testing.com/emagzine/no2_2.htm

  6) Java的内存泄漏
  http://www.matrix.org.cn/resource/article/409.html

  7) Web服务器和应用程序服务器有什么区别?
  http://www.matrix.org.cn/resource/article/1429.html

  8) 详细讲解性能中数据库集群的问题
  http://www.theserverside.com/articles/article.tss?l=db_break

]]>
http://blog.donews.com/afxtruelover/archive/2006/03/21/778524.aspx/feed 1
Java(TM) Logging API 簡介[转载] http://blog.donews.com/afxtruelover/archive/2006/03/19/775294.aspx http://blog.donews.com/afxtruelover/archive/2006/03/19/775294.aspx#comments Sun, 19 Mar 2006 01:54:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/03/19/775294.aspx

Java(TM) Logging API 簡介


Cheng-Yu Chang

Medical Image Processing and Neural Network Lab

EE, NCKU, Taiwan 70101, ROC.

2002/09/17


寫在前面

一般我們在撰寫程式或開發系統時,debug 往往是免不了需要的。網路上也常常有人詢問到,哪種 <]]>

Java(TM) Logging API 簡介


Cheng-Yu Chang

Medical Image Processing and Neural Network Lab

EE, NCKU, Taiwan 70101, ROC.

2002/09/17


寫在前面

一般我們在撰寫程式或開發系統時,debug 往往是免不了需要的。網路上也常常有人詢問到,哪種 debug 工具會比較好。通常,在傳統上我們都會選擇使用 System.out.println() 來幫助我們顯示一些 debug 用的資訊。不過在 JavaTM 2, Standard Edition (J2SETM) 1.4 版本中,java.util.logging 這個新加入的 package 為我們提供了一些 class interface 來協助我們產生 debug 用的資訊,方便我們診斷出不管是在系統開發或是系統管理上所發生的問題。


Logging API 的基本架構

那麼到底 Java Logging API 有何神奇的功能可以協助我們 debug 呢?讓我們先來看看傳統 debug 用的一個範例。

/**
 * @(#)TraditionalDebugDemo.java 2002/09/17
 *
 * 傳統 debug 方式的簡單範例
 *
 * @authot
 *         Cheng-Yu Chang
 * @license
 *         GPL(GNU GENERAL PUBLIC LICENSE)
 *         http://www.gnu.org/copyleft/gpl.html
 *
 * */  

import java.io.*;

public class TraditionalDebugDemo {
       public static void main (String[] args) {
              if(args.length != 0) {
                    for(int i = 0;i < args.length;i++) {
                            System.out.println(args[i]);
                    }
              }
              else {
                   System.err.println("沒有輸入參數!");
              }
       }
}

從上面的範例可以看到,我們執行程式若是沒有輸入參數的話就透過 System.err.println("沒有輸入參數!");把錯誤的訊息產生,而若是有輸入參數的話就透過 System.out.println(args[i]);依序把內容印出來。其結果如下所示:

C:\>java TraditionalDebugDemo
沒有輸入參數!

C:\>java TraditionalDebugDemo 1 2 3 4
1
2
3
4

 

接下來,讓我們使用 Logging API 將這個範例重新改寫一下。


Logging API 中的 Logger

/**
 * @(#)LoggingDebugDemo.java 2002/09/17
 *
 * 使用 Logging API debug 的簡單範例
 *
 * @authot
 *         Cheng-Yu Chang
 * @license
 *         GPL(GNU GENERAL PUBLIC LICENSE)
 *         http://www.gnu.org/copyleft/gpl.html
 *
 * */

import java.io.*;
import java.util.logging.*;

public class LoggingDebugDemo {
       private static Logger myLogger = Logger.getLogger("LoggingDebugDemo");

       public static void main (String[] args) {
              if(args.length != 0) {
                    for(int i = 0;i < args.length;i++) {
                            myLogger.info(args[i]);
                    }
              }
              else {
                   myLogger.warning("沒有輸入參數!");
              }
       }
}

當我們要使用 Logging API 的時候,我們必須要 import java.util.logging.*;這個套件。接著可以看到我們建立了一個 Logger 物件,這個物件主要是用來紀錄我們應用程式或是系統所發出來的訊息用的。接著可以看到我們使用 myLogger.info(args[i]);來顯示我們輸入的參數,使用 myLogger.warning("沒有輸入參數!");來顯示沒有輸入參數時候的錯誤訊息。倘若我們執行這個程式的話,我們可以看到以下的輸出畫面:

C:\>java LoggingDebugDemo
2002/9/17
上午 11:54:17 LoggingDebugDemo main
警告: 沒有輸入參數!

C:\>java LoggingDebugDemo 1 2 3 4
2002/9/17
上午 11:56:10 LoggingDebugDemo main
資訊: 1
2002/9/17
上午 11:56:10 LoggingDebugDemo main
資訊: 2
2002/9/17
上午 11:56:10 LoggingDebugDemo main
資訊: 3
2002/9/17
上午 11:56:10 LoggingDebugDemo main
資訊: 4

 

從上面的結果我們可以發現,當我們使用傳統的 System.out.println() 方式的話,除了程式開法者本身之外,並沒有辦法分辨目前訊息真正的涵義,也就是到底只是一般的訊息顯示,亦或是某個部分發生了錯誤。但是當我們使用 Logging API 來幫我們產生紀錄訊息的話,我們可以明顯的看到發生的時間、在哪個類別發生、在類別中的哪個 method 當中產生、以及產生什麼樣子的訊息類型都可以一目了然。

那麼,大家也許會問,為什麼我們想要顯示的訊息,前面會有「警告」或是「資訊」的字眼出現呢?其關鍵在於我們使用的 myLogger.info(args[i]);還有 myLogger.warning("沒有輸入參數!");這兩部分。在 Logging APILevel 類別當中定義了許許多多的不同程度的紀錄方式,分別如下列所示:


SEVERE 這是最高程度的紀錄方式,紀錄著非常重要的訊息(例如程式的錯誤)。
WARNING 表示一些警告訊息。
INFO 表示執行期間的一些訊息。
CONFIG 關於設定檔的一些訊息。
FINE 紀錄一些 debug 專用的詳細訊息。
FINER 再更進一步的詳細訊息。
FINEST 這是最低程度的紀錄方式,表示紀錄最詳細的訊息。
ALL 特殊情況,表示所有程度的訊息都應該被紀錄下來。
OFF 特殊情況,表示取消訊息的紀錄。

為什麼要定義這麼多種程度的訊息紀錄方式呢?其實這表示這些訊息的重要性,通常我們在開發系統,撰寫程式上,無庸置疑最重要的訊息一定就是有危險的錯誤訊息了,一旦發生錯誤訊息,可能程式執行到這個地方就因為某些原因就停止了,那麼這種程度的訊息不管如何就一定要顯示出來告訴我們。而其他例如 Level.FINE 這種程度的訊息,就表示它可有可無,並沒有代表很深刻的涵義,在這部分我們可以用下面的例子來說明:


Logging API 中的 Level

/**
 * @(#)LevelDebugDemo.java 2002/09/17
 *
 * 使用 Logging API LEVEL debug 的簡單範例
 *
 * @authot
 *         Cheng-Yu Chang
 * @license
 *         GPL(GNU GENERAL PUBLIC LICENSE)
 *         http://www.gnu.org/copyleft/gpl.html
 *
 * */

import java.io.*;
import java.util.logging.*;

public class LevelDebugDemo {
       private static Logger myLogger = Logger.getLogger("LevelDebugDemo");

       public static void main (String[] args) {
              if(args.length != 0) {
                    for(int i = 0;i < args.length;i++) {
                            int j = i % 6;
                            switch(j) {
                                      case 0:
                                           myLogger.warning("WARNING Log!");
                                           break;
                                      case 1:
                                           myLogger.info("INFO Log!");
                                           break;
                                      case 2:
                                           myLogger.config("CONFIG Log!");
                                           break;
                                      case 3:
                                           myLogger.fine("FINE Log!");
                                           break;
                                      case 4:
                                           myLogger.finer("FINER Log!");
                                           break;
                                      case 5:
                                           myLogger.finest("FINEST Log!");
                                           break;
                                      default:
                                              break;
                            }
                    }
              }
              else {
                   myLogger.severe("沒有輸入參數!");
              }
       }
}

其執行結果如下所示:

C:\>java LevelDebugDemo
2002/9/17
下午 03:34:47 LevelDebugDemo main
嚴重的: 沒有輸入參數!

C:\>java LevelDebugDemo 1 2 3 4 5 6
2002/9/17
下午 03:35:02 LevelDebugDemo main
警告: WARNING Log!
2002/9/17
下午 03:35:03 LevelDebugDemo main
資訊: INFO Log!

 

相信大家看到上面的結果一定會覺得很奇怪,明明輸入了 6 個參數,照道理來說應該會有 6 項紀錄顯示出來才對,為什麼只有顯示兩種呢?其原因在於我們之前所提到的-「程度的不同」。在預設值當中,低於 Level.INFO 的值是不會被顯示出來的。所以我們只能看到 Level.INFO(資訊)程度的訊息。那麼,我們要怎麼改變程度呢?這個時候我們就要來看看 Logger API 實際上運作的情形了。當我們建立一個 Logger 物件之後,其最後的輸出是透過 Handler 類別來輸出的。

J2SE 提供了下列五種 Handler 類別讓我們來使用:


StreamHandler 將紀錄以 OutputStream 方式輸出。
ConsoleHandler 將紀錄以 System.err 方式輸出。
FileHandler 將紀錄導向一個檔案輸出。
SocketHandler 將紀錄傳送到遠端 TCP 連線輸出。
MemoryHandler 將紀錄暫存在記憶體當中。

現在我們將 LevelDebugDemo.java 改寫一下,加入 FileHandler 變成下面這樣:


Logging API 中的  Handler

 

/**
 * @(#)LevelDebugDemo2.java 2002/09/17
 *
 * 使用 Logging API LEVEL debug 的簡單範例
 *
 * @authot
 *         Cheng-Yu Chang
 * @license
 *         GPL(GNU GENERAL PUBLIC LICENSE)
 *         http://www.gnu.org/copyleft/gpl.html
 *
 * */

import java.io.*;
import java.util.logging.*;

public class LevelDebugDemo2 {
       private static Logger myLogger = Logger.getLogger("LevelDebugDemo2");       

       public static void main (String[] args) {
              try {
                  FileHandler h = new FileHandler("%h/myLogger.txt");
                  myLogger.addHandler(h);
                  h.setFormatter(new SimpleFormatter());
              }
              catch(IOException e) {myLogger.severe("FileHandler Error!");}

              myLogger.setLevel(Level.ALL);

              if(args.length != 0) {
                    for(int i = 0;i < args.length;i++) {
                            int j = i % 6;
                            switch(j) {
                                      case 0:
                                           myLogger.warning("WARNING Log!");
                                           break;
                                      case 1:
                                           myLogger.info("INFO Log!");
                                           break;
                                      case 2:
                                           myLogger.config("CONFIG Log!");
                                           break;
                                      case 3:
                                           myLogger.fine("FINE Log!");
                                           break;
                                      case 4:
                                           myLogger.finer("FINER Log!");
                                           break;
                                      case 5:
                                           myLogger.finest("FINEST Log!");
                                           break;
                                      default:
                                              break;
                            }
                    }
              }
              else {
                   myLogger.severe("沒有輸入參數!");
              }
       }
}

其中 FileHandler %h 參數是表示 user.home(使用者根目錄)這項系統項目,其他常用的參數還有 %t%g 等等,分別表示「系統暫存目錄」與「檔案依序編號」的意思。舉例來說 "%t/myLogger%g.txt" 的話就表示會依序產生 myLogger0.txt, myLogger1.txt等檔案,其位置位於系統暫存目錄當中。當執行完這個程式之後,會發現在我們使用者的根目錄之下會多出一個 myLogger.txt的檔案,其內容如下所示:

2002/9/17 下午 04:38:31 LevelDebugDemo2 main
警告: WARNING Log!
2002/9/17
下午 04:38:31 LevelDebugDemo2 main
資訊: INFO Log!
2002/9/17
下午 04:38:31 LevelDebugDemo2 main
配置: CONFIG Log!
2002/9/17
下午 04:38:31 LevelDebugDemo2 main
細緻: FINE Log!
2002/9/17
下午 04:38:31 LevelDebugDemo2 main
更細緻: FINER Log!
2002/9/17
下午 04:38:31 LevelDebugDemo2 main
最細緻: FINEST Log!

這時候我們可以發現所有程度的訊息都完整的顯示出來了,這是因為我們把訊息顯示程度設定成為全部顯示myLogger.setLevel(Level.ALL);的緣故。在這邊我們要另外注意到的一件事情是,若是我們把 h.setFormatter(new SimpleFormatter()); 這行註解掉的話,我們可以發現檔案輸出變成下面這個樣子:

<?xml version="1.0" encoding="MS950" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
<date>2002-09-17T16:51:19</date>
<millis>1032252679457</millis>
<sequence>0</sequence>
<logger>LoggingDebugDemo</logger>
<level>WARNING</level>
<class>LevelDebugDemo2</class>
<method>main</method>
<thread>10</thread>
<message>WARNING Log!</message>
</record>
˙

˙[中間省略]

˙

<date>2002-09-17T16:51:19</date>
<millis>1032252679697</millis>
<sequence>5</sequence>
<logger>LoggingDebugDemo</logger>
<level>FINEST</level>
<class>LevelDebugDemo2</class>
<method>main</method>
<thread>10</thread>
<message>FINEST Log!</message>
</record>
</log>

這是因為 FileHandler 預設的輸出模式是 XML 的格式,J2SE Logging API 提供了兩種輸出模式,一種是 SimpleFormatter,也就是之前我們看到的那種模式。另外一種 XMLFotmatter的話就是像我們後來看到的這種 XML 架構樣式了。


總結

到目前為止我們簡單介紹了如何使用 JavaTM 2, Standard Edition (J2SETM) 1.4 版本中新增加的 Logging API,明白 Logging API 當中 Logger, Handler, Formatter所代表的意義(如下圖所示)。往後我們要紀錄系統開發資訊的話,可以紀錄的更加明確、有意義。

]]> http://blog.donews.com/afxtruelover/archive/2006/03/19/775294.aspx/feed 0
[转帖]图片的淡出效果 http://blog.donews.com/afxtruelover/archive/2006/03/12/765197.aspx http://blog.donews.com/afxtruelover/archive/2006/03/12/765197.aspx#comments Sun, 12 Mar 2006 15:29:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/03/12/765197.aspx 控制图片的Alpha通道,比如

    

try {
            image=Image.createImage("/123.png");//载入原图
        }
        catch]]> 控制图片的Alpha通道,比如

    

try {
            image=Image.createImage("/123.png");//载入原图
        }
        catch (IOException e) { }
        int[] argb=new int[image.getWidth()*image.getHeight()];//产生图片数据数组
        image.getRGB(argb,0,image.getWidth(),0,0,image.getWidth(),image.getHeight());//得到ARGB矩阵
        for(int i=0;i<argb.length;i++){
            argb[i]&=0xa0ffffff;//设置每个象素的alpha通道值为a0,Alpha通道的值为0–100,想要渐变,让它从0慢慢到100就OK了

imageChange=Image.createRGBImage(argb,image.getWidth(),image.getHeight(),true);//产生新的图片

}

try {
            image=Image.createImage("/123.png");//载入原图
        }
        catch (IOException e) { }
        int[] argb=new int[image.getWidth()*image.getHeight()];//产生图片数据数组
        image.getRGB(argb,0,image.getWidth(),0,0,image.getWidth(),image.getHeight());//得到ARGB矩阵
        for(int i=0;i<argb.length;i++){
            argb[i]&=0xa0ffffff;//设置每个象素的alpha通道值为a0,Alpha通道的值为0–100,想要渐变,让它从0慢慢到100就OK了

imageChange=Image.createRGBImage(argb,image.getWidth(),image.getHeight(),true);//产生新的图片

}

]]> http://blog.donews.com/afxtruelover/archive/2006/03/12/765197.aspx/feed 0
应聘Java笔试时可能出现问题及其答案(转) http://blog.donews.com/afxtruelover/archive/2006/03/11/763665.aspx http://blog.donews.com/afxtruelover/archive/2006/03/11/763665.aspx#comments Sat, 11 Mar 2006 11:10:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/03/11/763665.aspx 在寻找这些答案的过程中,我将相关答案记录下来,就形成了以下这些东西。需要说明的是以下答案肯定有很多不完整甚至错误的地方,需要各位来更正与完善它,千万不要扔我的鸡蛋啊。
希望本文能够给即将奔赴笔试考场的同仁些许帮助,更希望更多的人加入到收集整理笔试题与完善答案的这些工作中来,为大家更好的获得工作机会做一点贡献。
在此感谢前面两文的作者的对笔试题目的收集与整理。
如有任何意见与建议请通过QQ:6045306,Mail:huijunzi@21cn.com与我联系。
Java基础方面:

1、作用域public,private,protected,以及不写时的区别
答:区别如下:
作用域 当前类 同一package 子孙类 其他package
public √ √ √ &rad]]>
前段时间因为要参加一个笔试,在准备期间在网上找到了两条关于笔试题目的文章,其中一篇为<<有感:应聘Java笔试时可能出现问题>>,还有一篇忘了名字,读后深受启发。
在寻找这些答案的过程中,我将相关答案记录下来,就形成了以下这些东西。需要说明的是以下答案肯定有很多不完整甚至错误的地方,需要各位来更正与完善它,千万不要扔我的鸡蛋啊。
希望本文能够给即将奔赴笔试考场的同仁些许帮助,更希望更多的人加入到收集整理笔试题与完善答案的这些工作中来,为大家更好的获得工作机会做一点贡献。
在此感谢前面两文的作者的对笔试题目的收集与整理。
如有任何意见与建议请通过QQ:6045306,Mail:huijunzi@21cn.com与我联系。
Java基础方面:

1、作用域public,private,protected,以及不写时的区别
答:区别如下:
作用域 当前类 同一package 子孙类 其他package
public √ √ √ √
protected √ √ √ ×
friendly √ √ × ×
private √ × × ×
不写时默认为friendly

2、ArrayList和Vector的区别,HashMap和Hashtable的区别
答:就ArrayList与Vector主要从二方面来说.
一.同步性:Vector是线程安全的,也就是说是同步的,而ArrayList是线程序不安全的,不是同步的
二.数据增长:当需要增长时,Vector默认增长为原来一培,而ArrayList却是原来的一半
就HashMap与HashTable主要从三方面来说。
一.历史原因:Hashtable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现
二.同步性:Hashtable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的
三.值:只有HashMap可以让你将空值作为一个表的条目的key或value

3、char型变量中能不能存贮一个中文汉字?为什么?
答:是能够定义成为一个中文的,因为java中以unicode编码,一个char占16个字节,所以放一个中文是没问题的

4、多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么?
答:多线程有两种实现方法,分别是继承Thread类与实现Runnable接口
同步的实现方面有两种,分别是synchronized,wait与notify

5、继承时候类的执行顺序问题,一般都是选择题,问你将会打印出什么?
答:父类:
package test;
public class FatherClass
{
public FatherClass()
{
System.out.println("FatherClass Create");
}
}
子类:
package test;
import test.FatherClass;
public class ChildClass extends FatherClass
{
public ChildClass()
{
System.out.println("ChildClass Create");
}
public static void main(String[] args)
{
FatherClass fc = new FatherClass();
ChildClass cc = new ChildClass();
}
}
输出结果:
C:\>java test.ChildClass
FatherClass Create
FatherClass Create
ChildClass Create

6、内部类的实现方式?
答:示例代码如下:
package test;
public class OuterClass
{
private class InterClass
{
public InterClass()
{
System.out.println("InterClass Create");
}
}
public OuterClass()
{
InterClass ic = new InterClass();
System.out.println("OuterClass Create");
}
public static void main(String[] args)
{
OuterClass oc = new OuterClass();
}
}
输出结果:
C:\>java test/OuterClass
InterClass Create
OuterClass Create
再一个例题:
public class OuterClass {
private double d1 = 1.0;
//insert code here
}
You need to insert an inner class declaration at line 3. Which two inner class declarations are

valid?(Choose two.)
A. class InnerOne{
public static double methoda() {return d1;}
}
B. public class InnerOne{
static double methoda() {return d1;}
}
C. private class InnerOne{
double methoda() {return d1;}
}
D. static class InnerOne{
protected double methoda() {return d1;}
}
E. abstract class InnerOne{
public abstract double methoda();
}
说明如下:
一.静态内部类可以有静态成员,而非静态内部类则不能有静态成员。 故 A、B 错
二.静态内部类的非静态成员可以访问外部类的静态变量,而不可访问外部类的非静态变量;return d1 出错。

故 D 错
三.非静态内部类的非静态成员可以访问外部类的非静态变量。 故 C 正确
四.答案为C、E

7、垃圾回收机制,如何优化程序?
希望大家补上,谢谢

8、float型float f=3.4是否正确?
答:不正确。精度不准确,应该用强制类型转换,如下所示:float f=(float)3.4

9、介绍JAVA中的Collection FrameWork(包括如何写自己的数据结构)?
答:Collection FrameWork如下:
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
Map
├Hashtable
├HashMap
└WeakHashMap
Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)
Map提供key到value的映射

10、Java中异常处理机制,事件机制?

11、JAVA中的多形与继承?
希望大家补上,谢谢

12、抽象类与接口?
答:抽象类与接口都用于抽象,但是抽象类(JAVA中)可以有自己的部分实现,而接口则完全是一个标识(同时有多重继承的功能)。

13、Java 的通信编程,编程题(或问答),用JAVA SOCKET编程,读服务器几个字符,再写入本地显示?
答:Server端程序:
package test;
import java.net.*;
import java.io.*;

public class Server
{
private ServerSocket ss;
private Socket socket;
private BufferedReader in;
private PrintWriter out;
public Server()
{
try
{
ss=new ServerSocket(10000);
while(true)
{
socket = ss.accept();
String RemoteIP = socket.getInetAddress().getHostAddress();
String RemotePort = ":"+socket.getLocalPort();
System.out.println("A client come in!IP:"+RemoteIP+RemotePort);
in = new BufferedReader(new

InputStreamReader(socket.getInputStream()));
String line = in.readLine();
System.out.println("Cleint send is :" + line);
out = new PrintWriter(socket.getOutputStream(),true);
out.println("Your Message Received!");
out.close();
in.close();
socket.close();
}
}catch (IOException e)
{
out.println("wrong");
}
}
public static void main(String[] args)
{
new Server();
}
};
Client端程序:
package test;
import java.io.*;
import java.net.*;

public class Client
{
Socket socket;
BufferedReader in;
PrintWriter out;
public Client()
{
try
{
System.out.println("Try to Connect to 127.0.0.1:10000");
socket = new Socket("127.0.0.1",10000);
System.out.println("The Server Connected!");
System.out.println("Please enter some Character:");
BufferedReader line = new BufferedReader(new

InputStreamReader(System.in));
out = new PrintWriter(socket.getOutputStream(),true);
out.println(line.readLine());
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println(in.readLine());
out.close();
in.close();
socket.close();
}catch(IOException e)
{
out.println("Wrong");
}
}
public static void main(String[] args)
{
new Client();
}
};

14、用JAVA实现一种排序,JAVA类实现序列化的方法(二种)? 如在COLLECTION框架中,实现比较要实现什么样的接口?
答:用插入法进行排序代码如下
package test;
import java.util.*;
class InsertSort
{
ArrayList al;
public InsertSort(int num,int mod)
{
al = new ArrayList(num);
Random rand = new Random();
System.out.println("The ArrayList Sort Before:");
for (int i=0;i<num ;i++ )
{
al.add(new Integer(Math.abs(rand.nextInt()) % mod + 1));
System.out.println("al["+i+"]="+al.get(i));
}
}
public void SortIt()
{
Integer tempInt;
int MaxSize=1;
for(int i=1;i<al.size();i++)
{
tempInt = (Integer)al.remove(i);
if(tempInt.intValue()>=((Integer)al.get(MaxSize-1)).intValue())
{
al.add(MaxSize,tempInt);
MaxSize++;
System.out.println(al.toString());
} else {
for (int j=0;j<MaxSize ;j++ )
{
if

(((Integer)al.get(j)).intValue()>=tempInt.intValue())
{
al.add(j,tempInt);
MaxSize++;
System.out.println(al.toString());
break;
}
}
}
}
System.out.println("The ArrayList Sort After:");
for(int i=0;i<al.size();i++)
{
System.out.println("al["+i+"]="+al.get(i));
}
}
public static void main(String[] args)
{
InsertSort is = new InsertSort(10,100);
is.SortIt();
}
}
JAVA类实现序例化的方法是实现java.io.Serializable接口
Collection框架中实现比较要实现Comparable 接口和 Comparator 接口

15、编程:编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串。 但是要保证汉字不被截半个,如“我ABC”4,应该截为“我AB”,输入“我ABC汉DEF”,6,应该输出为“我ABC”而不是“我ABC+汉的半个”。
答:代码如下:
package test;

class SplitString
{
String SplitStr;
int SplitByte;
public SplitString(String str,int bytes)
{
SplitStr=str;
SplitByte=bytes;
System.out.println("The String is:´"+SplitStr+"´;SplitBytes="+SplitByte);
}
public void SplitIt()
{
int loopCount;


loopCount=(SplitStr.length()%SplitByte==0)?(SplitStr.length()/SplitByte):(SplitStr.length()/Split

Byte+1);
System.out.println("Will Split into "+loopCount);
for (int i=1;i<=loopCount ;i++ )
{
if (i==loopCount){


System.out.println(SplitStr.substring((i-1)*SplitByte,SplitStr.length()));
} else {


System.out.println(SplitStr.substring((i-1)*SplitByte,(i*SplitByte)));
}
}
}
public static void main(String[] args)
{
SplitString ss = new SplitString("test中dd文dsaf中男大3443n中国43中国人

0ewldfls=103",4);
ss.SplitIt();
}
}

16、JAVA多线程编程。 用JAVA写一个多线程程序,如写四个线程,二个加1,二个对一个变量减一,输出。
希望大家补上,谢谢

17、STRING与STRINGBUFFER的区别。
答:STRING的长度是不可变的,STRINGBUFFER的长度是可变的。如果你对字符串中的内容经常进行操作,特别是内容要修改时,那么使用StringBuffer,如果最后需要String,那么使用StringBuffer的toString()方法

Jsp方面

1、jsp有哪些内置对象?作用分别是什么?
答:JSP共有以下9种基本内置组件(可与ASP的6种内部组件相对应):
 request 用户端请求,此请求会包含来自GET/POST请求的参数
response 网页传回用户端的回应
pageContext 网页的属性是在这里管理
session 与请求有关的会话期
application servlet 正在执行的内容
out 用来传送回应的输出
config servlet的构架部件
page JSP网页本身
exception 针对错误网页,未捕捉的例外

2、jsp有哪些动作?作用分别是什么?
答:JSP共有以下6种基本动作
jsp:include:在页面被请求的时候引入一个文件。
jsp:useBean:寻找或者实例化一个JavaBean。
jsp:setProperty:设置JavaBean的属性。
jsp:getProperty:输出某个JavaBean的属性。
jsp:forward:把请求转到一个新的页面。
jsp:plugin:根据浏览器类型为Java插件生成OBJECT或EMBED标记

3、JSP中动态INCLUDE与静态INCLUDE的区别?
答:动态INCLUDE用jsp:include动作实现
<jsp:include page="included.jsp" flush="true" />它总是会检查所含文件中的变化,适合用于包含动态页面,并且可以带参数
静态INCLUDE用include伪码实现,定不会检查所含文件的变化,适用于包含静态页面
<%@ include file="included.htm" %>

4、两种跳转方式分别是什么?有什么区别?
答:有两种,分别为:
<jsp:include page="included.jsp" flush="true">
<jsp:forward page= "nextpage.jsp"/>
前者页面不会转向include所指的页面,只是显示该页的结果,主页面还是原来的页面。执行完后还会回来,相当于函数调用。并且可以带参数.后者完全转向新页面,不会再回来。相当于go to 语句。

Servlet方面

1、说一说Servlet的生命周期?
答:servlet有良好的生存期的定义,包括加载和实例化、初始化、处理请求以及服务结束。这个生存期由javax.servlet.Servlet接口的init,service和destroy方法表达。

2、Servlet版本间(忘了问的是哪两个版本了)的不同?
希望大家补上,谢谢

3、JAVA SERVLET API中forward() 与redirect()的区别?
答:前者仅是容器中控制权的转向,在客户端浏览器地址栏中不会显示出转向后的地址;后者则是完全的跳转,浏览器将会得到跳转的地址,并重新发送请求链接。这样,从浏览器的地址栏中可以看到跳转后的链接地址。所以,前者更加高效,在前者可以满足需要时,尽量使用forward()方法,并且,这样也有助于隐藏实际的链接。在有些情况下,比如,需要跳转到一个其它服务器上的资源,则必须使用sendRedirect()方法。

4、Servlet的基本架构
public class ServletName extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
}
}

Jdbc、Jdo方面

1、可能会让你写一段Jdbc连Oracle的程序,并实现数据查询.
答:程序如下:
package hello.ant;
import java.sql.*;
public class jdbc
{
String dbUrl="jdbc:oracle:thin:@127.0.0.1:1521:orcl";
String theUser="admin";
String thePw="manager";
Connection c=null;
Statement conn;
ResultSet rs=null;
public jdbc()
{
try{
Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
c = DriverManager.getConnection(dbUrl,theUser,thePw);
conn=c.createStatement();
}catch(Exception e){
e.printStackTrace();
}
}
public boolean executeUpdate(String sql)
{
try
{
conn.executeUpdate(sql);
return true;
}
catch (SQLException e)
{
e.printStackTrace();
return false;
}
}
public ResultSet executeQuery(String sql)
{
rs=null;
try
{
rs=conn.executeQuery(sql);
}
catch (SQLException e)
{
e.printStackTrace();
}
return rs;
}
public void close()
{
try
{
conn.close();
c.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public static void main(String[] args)
{
ResultSet rs;
jdbc conn = new jdbc();
rs=conn.executeQuery("select * from test");
try{
while (rs.next())
{
System.out.println(rs.getString("id"));
System.out.println(rs.getString("name"));
}
}catch(Exception e)
{
e.printStackTrace();
}
}
}

2、Class.forName的作用?为什么要用?
答:调用该访问返回一个以字符串指定类名的类的对象。

3、Jdo是什么?
答:JDO是Java对象持久化的新的规范,为java data object的简称,也是一个用于存取某种数据仓库中的对象的标准化API。JDO提供了透明的对象存储,因此对开发人员来说,存储数据对象完全不需要额外的代码(如JDBC API的使用)。这些繁琐的例行工作已经转移到JDO产品提供商身上,使开发人员解脱出来,从而集中时间和精力在业务逻辑上。另外,JDO很灵活,因为它可以在任何数据底层上运行。JDBC只是面向关系数据库(RDBMS)JDO更通用,提供到任何数据底层的存储功能,比如关系数据库、文件、XML以及对象数据库(ODBMS)等等,使得应用可移植性更强。

4、在ORACLE大数据量下的分页解决方法。一般用截取ID方法,还有是三层嵌套方法。
答:一种分页方法
<%
int i=1;
int numPages=14;
String pages = request.getParameter("page") ;
int currentPage = 1;
currentPage=(pages==null)?(1):{Integer.parseInt(pages)}
sql = "select count(*) from tables";
ResultSet rs = DBLink.executeQuery(sql) ;
while(rs.next()) i = rs.getInt(1) ;
int intPageCount=1;
intPageCount=(i%numPages==0)?(i/numPages):(i/numPages+1);
int nextPage ;
int upPage;
nextPage = currentPage+1;
if (nextPage>=intPageCount) nextPage=intPageCount;
upPage = currentPage-1;
if (upPage<=1) upPage=1;
rs.close();
sql="select * from tables";
rs=DBLink.executeQuery(sql);
i=0;
while((i<numPages*(currentPage-1))&&rs.next()){i++;}
%>
//输出内容
//输出翻页连接
合计:<%=currentPage%>/<%=intPageCount%><a href="List.jsp?page=1">第一页</a><a

href="List.jsp?page=<%=upPage%>">上一页</a>
<%
for(int j=1;j<=intPageCount;j++){
if(currentPage!=j){
%>
<a href="list.jsp?page=<%=j%>">[<%=j%>]</a>
<%
}else{
out.println(j);
}
}
%>
<a href="List.jsp?page=<%=nextPage%>">下一页</a><a href="List.jsp?page=<%=intPageCount%>">最后页

</a>


Xml方面

1、xml有哪些解析技术?区别是什么?
答:有DOM,SAX,STAX等
DOM:处理大型文件时其性能下降的非常厉害。这个问题是由DOM的树结构所造成的,这种结构占用的内存较多,而且DOM必须在解析文件之前把整个文档装入内存,适合对XML的随机访问SAX:不现于DOM,SAX是事件驱动型的XML解析方式。它顺序读取XML文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过在其回调事件中写入处理代码来处理XML文件,适合对XML的顺序访问
STAX:Streaming API for XML (StAX)

2、你在项目中用到了xml技术的哪些方面?如何实现的?
答:用到了数据存贮,信息配置两方面。在做数据交换平台时,将不能数据源的数据组装成XML文件,然后将XML文件压缩打包加密后通过网络传送给接收者,接收解密与解压缩后再同XML文件中还原相关信息进行处理。在做软件配置时,利用XML可以很方便的进行,软件的各种配置参数都存贮在XML文件中。

3、用jdom解析xml文件时如何解决中文问题?如何解析?
答:看如下代码,用编码方式加以解决
package test;
import java.io.*;
public class DOMTest
{
private String inFile = "c:\\people.xml";
private String outFile = "c:\\people.xml";
public static void main(String args[])
{
new DOMTest();
}
public DOMTest()
{
try
{
javax.xml.parsers.DocumentBuilder builder =


javax.xml.parsers.DocumentBuilderFactory.newInstance().newDocumentBuilder();
org.w3c.dom.Document doc = builder.newDocument();
org.w3c.dom.Element root = doc.createElement("老师");
org.w3c.dom.Element wang = doc.createElement("王");
org.w3c.dom.Element liu = doc.createElement("刘");
wang.appendChild(doc.createTextNode("我是王老师"));
root.appendChild(wang);
doc.appendChild(root);
javax.xml.transform.Transformer transformer =
javax.xml.transform.TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(javax.xml.transform.OutputKeys.ENCODING, "gb2312");
transformer.setOutputProperty(javax.xml.transform.OutputKeys.INDENT, "yes");


transformer.transform(new javax.xml.transform.dom.DOMSource(doc),
new

javax.xml.transform.stream.StreamResult(outFile));
}
catch (Exception e)
{
System.out.println (e.getMessage());
}
}
}

4、编程用JAVA解析XML的方式.
答:用SAX方式解析XML,XML文件如下:
<?xml version="1.0" encoding="gb2312"?>
<person>
<name>王小明</name>
<college>信息学院</college>
<telephone>6258113</telephone>
<notes>男,1955年生,博士,95年调入海南大学</notes>
</person>
事件回调类SAXHandler.java
import java.io.*;
import java.util.Hashtable;
import org.xml.sax.*;
public class SAXHandler extends HandlerBase
{
private Hashtable table = new Hashtable();
private String currentElement = null;
private String currentValue = null;
public void setTable(Hashtable table)
{
this.table = table;
}
public Hashtable getTable()
{
return table;
}
public void startElement(String tag, AttributeList attrs)
throws SAXException
{
currentElement = tag;
}
public void characters(char[] ch, int start, int length)
throws SAXException
{
currentValue = new String(ch, start, length);
}
public void endElement(String name) throws SAXException
{
if (currentElement.equals(name))
table.put(currentElement, currentValue);
}
}
JSP内容显示源码,SaxXml.jsp:
<HTML>
<HEAD>
<TITLE>剖析XML文件people.xml</TITLE>
</HEAD>
<BODY>
<%@ page errorPage="ErrPage.jsp"
contentType="text/html;charset=GB2312" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.Hashtable" %>
<%@ page import="org.w3c.dom.*" %>
<%@ page import="org.xml.sax.*" %>
<%@ page import="javax.xml.parsers.SAXParserFactory" %>
<%@ page import="javax.xml.parsers.SAXParser" %>
<%@ page import="SAXHandler" %>
<%
File file = new File("c:\\people.xml");
FileReader reader = new FileReader(file);
Parser parser;
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
SAXHandler handler = new SAXHandler();
sp.parse(new InputSource(reader), handler);
Hashtable hashTable = handler.getTable();
out.println("<TABLE BORDER=2><CAPTION>教师信息表</CAPTION>");
out.println("<TR><TD>姓名</TD>" + "<TD>" +
(String)hashTable.get(new String("name")) + "</TD></TR>");
out.println("<TR><TD>学院</TD>" + "<TD>" +
(String)hashTable.get(new String("college"))+"</TD></TR>");
out.println("<TR><TD>电话</TD>" + "<TD>" +
(String)hashTable.get(new String("telephone")) + "</TD></TR>");
out.println("<TR><TD>备注</TD>" + "<TD>" +
(String)hashTable.get(new String("notes")) + "</TD></TR>");
out.println("</TABLE>");
%>
</BODY>
</HTML>

EJB方面

1、EJB2.0有哪些内容?分别用在什么场合? EJB2.0和EJB1.1的区别?
答:规范内容包括Bean提供者,应用程序装配者,EJB容器,EJB配置工具,EJB服务提供者,系统管理员。这里面,EJB容器是EJB之所以能够运行的核心。EJB容器管理着EJB的创建,撤消,激活,去活,与数据库的连接等等重要的核心工作。JSP,Servlet,EJB,JNDI,JDBC,JMS…..

2、EJB与JAVA BEAN的区别?
答:Java Bean 是可复用的组件,对Java Bean并没有严格的规范,理论上讲,任何一个Java类都可以是一个Bean。但通常情况下,由于Java Bean是被容器所创建(如Tomcat)的,所以Java Bean应具有一个无参的构造器,另外,通常Java Bean还要实现Serializable接口用于实现Bean的持久性。Java Bean实际上相当于微软COM模型中的本地进程内COM组件,它是不能被跨进程访问的。Enterprise Java Bean 相当于DCOM,即分布式组件。它是基于Java的远程方法调用(RMI)技术的,所以EJB可以被远程访问(跨进程、跨计算机)。但EJB必须被布署在诸如Webspere、WebLogic这样的容器中,EJB客户从不直接访问真正的EJB组件,而是通过其容器访问。EJB容器是EJB组件的代理,EJB组件由容器所创建和管理。客户通过容器来访问真正的EJB组件。

3、EJB的基本架构
答:一个EJB包括三个部分:
Remote Interface 接口的代码
package Beans;
import javax.ejb.EJBObject;
import java.rmi.RemoteException;
public interface Add extends EJBObject
{
//some method declare
}
Home Interface 接口的代码
package Beans;
import java.rmi.RemoteException;
import jaax.ejb.CreateException;
import javax.ejb.EJBHome;
public interface AddHome extends EJBHome
{
//some method declare
}
EJB类的代码
package Beans;
import java.rmi.RemoteException;
import javax.ejb.SessionBean;
import javx.ejb.SessionContext;
public class AddBean Implements SessionBean
{
//some method declare
}

J2EE,MVC方面

1、MVC的各个部分都有那些技术来实现?如何实现?
答:MVC是Model-View-Controller的简写。"Model" 代表的是应用的业务逻辑(通过JavaBean,EJB组件实现), "View" 是应用的表示面(由JSP页面产生),"Controller" 是提供应用的处理过程控制(一般是一个Servlet),通过这种设计模型把应用逻辑,处理过程和显示逻辑分成不同的组件实现。这些组件可以进行交互和重用。

2、应用服务器与WEB SERVER的区别?
希望大家补上,谢谢


3、J2EE是什么?
答:Je22是Sun公司提出的多层(multi-diered),分布式(distributed),基于组件(component-base)的企业级应用模型(enterpriese application model).在这样的一个应用系统中,可按照功能划分为不同的组件,这些组件又可在不同计算机上,并且处于相应的层次(tier)中。所属层次包括客户层(clietn tier)组件,web层和组件,Business层和组件,企业信息系统(EIS)层。

4、WEB SERVICE名词解释。JSWDL开发包的介绍。JAXP、JAXM的解释。SOAP、UDDI,WSDL解释。
答:Web Service描述语言WSDL
SOAP即简单对象访问协议(Simple Object Access Protocol),它是用于交换XML编码信息的轻量级协议。
UDDI 的目的是为电子商务建立标准;UDDI是一套基于Web的、分布式的、为Web Service提供的、信息注册中心的实现标准规范,同时也包含一组使企业能将自身提供的Web Service注册,以使别的企业能够发现的访问协议的实现标准。


5、BS与CS的联系与区别。
希望大家补上,谢谢

6、STRUTS的应用(如STRUTS架构)
答:Struts是采用Java Servlet/JavaServer Pages技术,开发Web应用程序的开放源码的framework。 采用Struts能开发出基于MVC(Model-View-Controller)设计模式的应用构架。 Struts有如下的主要功能:
一.包含一个controller servlet,能将用户的请求发送到相应的Action对象。
二.JSP自由tag库,并且在controller servlet中提供关联支持,帮助开发员创建交互式表单应用。
三.提供了一系列实用对象:XML处理、通过Java reflection APIs自动处理JavaBeans属性、国际化的提示和消息。

设计模式方面

1、开发中都用到了那些设计模式?用在什么场合?
答:每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心。通过这种方式,你可以无数次地使用那些已有的解决方案,无需在重复相同的工作。主要用到了MVC的设计模式。用来开发JSP/Servlet或者J2EE的相关应用。简单工厂模式等。


2、UML方面
答:标准建模语言UML。用例图,静态图(包括类图、对象图和包图),行为图,交互图(顺序图,合作图),实现图,

JavaScript方面

1、如何校验数字型?
var re=/^\d{1,8}$|\.\d{1,2}$/;
var str=document.form1.all(i).value;
var r=str.match(re);
if (r==null)
{
sign=-4;
break;
}
else{
document.form1.all(i).value=parseFloat(str);
}


CORBA方面

1、CORBA是什么?用途是什么?
答:CORBA 标准是公共对象请求代理结构(Common Object Request Broker Architecture),由对象管理组织 (Object Management Group,缩写为 OMG)标准化。它的组成是接口定义语言(IDL), 语言绑定(binding:也译为联编)和允许应用程序间互操作的协议。 其目的为:
用不同的程序设计语言书写
在不同的进程中运行
为不同的操作系统开发


LINUX方面

1、LINUX下线程,GDI类的解释。
答:LINUX实现的就是基于核心轻量级进程的"一对一"线程模型,一个线程实体对应一个核心轻量级进程,而线程之间的管理在核外函数库中实现。
GDI类为图像设备编程接口类库。

]]>
http://blog.donews.com/afxtruelover/archive/2006/03/11/763665.aspx/feed 0
[转帖]游戏Loaading的设计 http://blog.donews.com/afxtruelover/archive/2006/03/04/752129.aspx http://blog.donews.com/afxtruelover/archive/2006/03/04/752129.aspx#comments Sat, 04 Mar 2006 00:38:00 +0000 truelover http://blog.donews.com/afxtruelover/archive/2006/03/04/752129.aspx 为什么很多游戏要加入Loading滚动条呢?加入Loading状态并不是为了使软件显得更专业美观,而是为了保证程序的运行内存不溢出。通常计算机/手机的存储系统分为:cup 的缓存,磁盘(或者手机中的存储用的的FLASH RAM或者其他类型的可以持久保存的存储系统),运行内存。我们知道通常NOKIA S40的heap size为200KB大小,而通常我们加入程序和3张128*128的图片之后内存就趋于崩溃了,再加入声音和地图,程序的运算内存就显得太不够了。一般来讲,很多游戏仅仅在运行的时候把所有的资源一次性读入heap m]]> 为什么很多游戏要加入Loading滚动条呢?加入Loading状态并不是为了使软件显得更专业美观,而是为了保证程序的运行内存不溢出。通常计算机/手机的存储系统分为:cup 的缓存,磁盘(或者手机中的存储用的的FLASH RAM或者其他类型的可以持久保存的存储系统),运行内存。我们知道通常NOKIA S40的heap size为200KB大小,而通常我们加入程序和3张128*128的图片之后内存就趋于崩溃了,再加入声音和地图,程序的运算内存就显得太不够了。一般来讲,很多游戏仅仅在运行的时候把所有的资源一次性读入heap memory这样,我们在模拟器看到程序运行的状况就非常接近崩溃的边缘,如果不小心加入了新的图片,可能就没有足够的运算内存了。

         我们如何解决heap size不够的事情呢?手机是不能够改变其heap size的,我们只有想办法控制heap memory的使用。最直观的做法就是:存储内存与运算内存的优化使用,当运算内存需要资源时从存储内存中调用,需要新的资源时,就把不需要的释放掉。下面我就结合一段代码解释我们是如何制作Loading状态的。

         众所周知,Java是内置多线程的,我们可以使用两个线程来解决loading的问题,一个读资源的线程,一个绘制资源的线程。程序代码:

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

/**
* Loading演示
* @author gaogao
* */
class MainCanvas
    extends Canvas
    implements Runnable {

//程序状态
  static final int LOADING = 0;
  static final int GAMEING = 1;

//程序状态控制器
  int state = LOADING;

//主线程
  Thread thread = null;
//是否loading完毕,
  boolean isLoaded = false;

//内部类,新开读取资源的 线程
  class Loading
      implements Runnable {
    //内线程
    Thread innerThread = null;

    public Loading() {
      innerThread = new Thread(this);
      innerThread.start();
    }

    int counter = 100;
    public void run() {
      //模拟读取资源
      //把下面的东西改成读取资源的代码即可
      while (counter > 0) {
        counter–;
        try {
          Thread.sleep(20);
        }
        catch (Exception ex) {}
      }
      //loading结束
      isLoaded = true;
    }
  }

  Loading loading = null;

  public MainCanvas() {
    loading = new Loading();
    thread = new Thread(this);
    thread.start();

  }

  int loadingCounter = 0;

//绘制..
  public void paint(Graphics g) {
    g.setColor(0);
    g.fillRect(0, 0, getWidth(), getHeight());
    switch (state) {
      case LOADING: {
        g.setColor(0XFFFFFF);

        g.drawString("LOADING" + ">>>>>".substring(0, loadingCounter),
                     getWidth() >> 1, getHeight() >> 1,
                     Graphics.HCENTER | Graphics.TOP);

        loadingCounter = ++loadingCounter % 5;

      }
      break;
      case GAMEING: {
        g.setColor(0XFFFFFF);
        g.drawString("GAME", getWidth() >> 1, getHeight() >> 1,
                     Graphics.HCENTER | Graphics.TOP);
      }
      break;
    }
  }

  public void run() {
    while (true) {
      try {
        Thread.sleep(100);
      }
      catch (Exception ex) {

      }
      if (isLoaded) {
        loading = null;
        state = GAMEING;
      }
      repaint(0, 0, getWidth(), getHeight());
      serviceRepaints();
    }
  }
}

public class Main
    extends MIDlet {
  MainCanvas mc;

  public void startApp() {

    if (mc == null) {
      mc = new MainCanvas();
      Display disp = Display.getDisplay(this);
      disp.setCurrent(mc);
    }
  }

  public void destroyApp(boolean bool) {}

  public void pauseApp() {}

]]>
http://blog.donews.com/afxtruelover/archive/2006/03/04/752129.aspx/feed 0