MicroFish

Open & Open hits
随笔 - 33, 文章 - 2, 评论 - 4, 引用 - 0
数据加载中……

2007年3月26日

HSQL 学习笔记1(zz)

http://hi.baidu.com/wannachan/blog/item/8e82bf86fc5d663f67096ef1.html

HSQL 学习笔记

1.     hsql 学习
1.1.     学习目的
本文档是针对hSQL 数据库方面的基础学习,为了使项目组成员能够达到使用hSQL 数据库的目的。
1.2.     培训对象
开发人员
1.3.     常用词及符号说明
常用词:
hsql:一种免费的跨平台的数据库系统
E:\hsqldb:表示是在dos 命令窗口下面
1.4.     参考信息
doc\guide\guide.pdf

2.     HSQL
2.1.     HSQL 运行工具
java -cp ../lib/hsqldb.jar org.hsqldb.util.DatabaseManager
注意hsqldb.jar 文件的文件路径,最好能放到classpath 里面,或者放到当前路径下.
java -cp hsqldb.jar org.hsqldb.util.DatabaseManager

2.2.     运行数据库
启动方式: Server Modes and
In-Process Mode (also called Standalone Mode).

一个test 数据库会包含如下文件:
• test.properties
• test.script
• test.log
• test.data
• test.backup
test.properties 文件包含关于数据库的一般设置.
test.script   文件包含表和其它数据库,插入没有缓存表的数据.
test.log 文件包含当前数据库的变更.
test.data 文件包含缓存表的数据
test.backup 文件是最近持久化状态的表的数据文件的压缩备份文件
所有以上这个文件都是必要的,不能被删除.如果数据库没有缓存表,test.data 和test.backup 文件将不会存在.另外,除了以上文件HSQLDB 数据库可以链接到任何文本文件,比如cvs 文件.

当操作test 数据库的时候, test.log 用于保存数据的变更. 当正常SHUTDOWN,这个文件将被删除. 否则(不是正常shutdown),这个文件将用于再次启动的时候,重做这些变更.test.lck 文件也用于记录打开的数据库的事实, 正常SHUTDOWN,文件也被删除.在一些情况下,test.data.old 文件会被创建,并删除以前的.






2.3.     Server Mode
java -cp ../lib/hsqldb.jar org.hsqldb.Server -database.0 file:mydb -dbname.0 xdb

命令行方式:


启动数据,数据库文件mydb,数据库名称xdb

也可以在 server.properties 文件中定义启动的数据库,最多10个
例如: server.properties:
server.database.0=file:E:/hsqldb/data/mydb
server.dbname.0=xdb

server.database.1=file:E:/hsqldb/data/testdb
server.dbname.1=testdb

server.database.2=mem:adatabase
server.dbname.2=quickdb
启动命令: java -cp ../lib/hsqldb.jar org.hsqldb.Server
运行结果如下



java 测试程序:
package test;
import junit.framework.TestCase;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestConnect extends TestCase {
     Connection connection;
     protected void setUp()
     {        
         try {
             Class.forName("org.hsqldb.jdbcDriver" );
             connection = DriverManager.getConnection("jdbc:hsqldb:hsql://localhost/xdb","sa","");
            
            
         } catch (Exception e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
     }
     public void testselect()
     {
         Statement stmt=null;
         ResultSet rs=null;
         try {
             stmt = connection.createStatement();
             String sql ="select * from test";
             rs=stmt.executeQuery( sql);
             while(rs.next() )
             {
                 System.out.println("id="+rs.getString("id"));
                 System.out.println("name="+rs.getString("name"));
             }
            
         } catch (SQLException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
         finally
         {
             try {
                 rs.close() ;
                 stmt.close();
             } catch (SQLException e) {
                 // TODO Auto-generated catch block
                 e.printStackTrace();
             }            
         }    
        
     }
     protected void tearDown()
     {
         try {
             connection.close();
         } catch (Exception e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
     }

}
以上在eclipse 中测试通过.

2.4.     In-Process (Standalone) Mode
不需要启动server
connection = DriverManager.getConnection("jdbc:hsqldb:file:E:/hsqldb/data/mydb","sa","");
这样就可以连接数据库。
只能在一个jvm 中使用,不能在多个jvm 中使用。
这种模式是在相同的jvm 下作为你的应用程序的一部分,运行数据库引擎。对大多数应用程序,这种模式运行会相当快,作为数据,不需要转换和网络传输。

主要的缺点就是不可能从外面的应用程序访问到默认数据库,因此当你的应用运行时候,你不能通过别的工具检查数据库内容。在1.8.0 版本中,你可以在相同jvm 中的线程中运行数据库初始化,并提供外面访问你的进程内数据库。
     推荐在开发应用中使用这种方式。
连接串:
Windows: DriverManager.getConnection("jdbc:hsqldb:file:E:/hsqldb/data/mydb","sa","");
Unix: DriverManager.getConnection("jdbc:hsqldb:file:/opt/db/testdb","sa","");

2.5.     Memory-Only Databases
当随即访问内存,数据库不固定时,可以采用内存的方式运行数据库,由于没有数据写到硬盘上,这种方式使用在应用数据和applets 和特殊应用的内部进程中使用,URL:

Connection c = DriverManager.getConnection("jdbc:hsqldb:mem:aname", "sa", "");
2.6.     Using Multiple Databases in One JVM
2.7.     Different Types of Tables
HSQLDB 支持 TEMP 表和三种类型的持久表(MEMORY 表, CACHED 表,TEXT表)

当使用 CREATE TABLE   命令时,Memory 表时默认类型,它们的数据整体保存在内存当中,但是任何改变它们的结构或者内容,它们会被写到<dbname>.script 文件中。这个脚本文件在数据库下一次打开的时候被对出,内存表重新被创建内容,根temp 表不同,内存表时持久化的。

CACHED 表通过CREATE CACHED TABLE 命令建立. 只有部分的它们的数据或者索引被保存在内存中,允许大表占用几百兆的内存空间。例外一个优点,在数据库引擎中,启动大量数据的缓存表需要花费少量的时间,缺点是减慢了运行和使用Hsqldb 的速度。表相对小的时候,不要使用cache 表,在小表中使用内存数据库。

从版本 1.7.0 以后,支持text 表,使用 CSV (Comma Separated Value)   或者其它分隔符文本文件作为它们的数据源。你可以特殊指定一个存在的CSV 文件,例如从其它的数据或者程序中导出文件,作为TXT 表的数据源。 同时,你可以指定一个空文件,通过数据库引擎填充数据。TEXT 表将比cache 表更加效率高。Text 表可以指向不同的数据文件。

* memory-only databases 数据库只支持memory 表和cache 表,不支持text 表。
2.8.     约束和索引
HSQLDB 支持 PRIMARY KEY, NOT NULL, UNIQUE, CHECK and FOREIGN KEY 约束.





3.     sql 命令
3.1.     sql 支持
select top 1 * from test;
select limit 0 2 * from test;
DROP TABLE test IF EXISTS;
3.2.     Constraints and Indexes
主健约束:PRIMARY KEY
唯一约束:
唯一索引:
外健:
CREATE TABLE child(c1 INTEGER, c2 VARCHAR, FOREIGN KEY (c1, c2) REFERENCES parent(p1, p2));

3.3.     索引和查询速度
索引提高查询速度,比提高排序速度。
主健和唯一所列自动创建索引,否则需要自己创建CREATE INDEX command。
索引: 唯一索引和非唯一索引
多列的索引,如果只是使用后面的,不使用第一个,将不会条查询速度。

(TB is a very large table with only a few rows where TB.COL3 = 4)
SELECT * FROM TA JOIN TB ON TA.COL1 = TB.COL2 AND TB.COL3 = 4;
SELECT * FROM TB JOIN TA ON TA.COL1 = TB.COL2 AND TB.COL3 = 4;(faster)

原因是 TB.COL3 可以被快速的估计,如果TB 表放到前面(index on TB.COL3):
一般规则是把缩小条件的列的表放在前面

3.4.     使用where 还是join
使用 WHERE   条件链接表可能会降低运行速度.
下面的例子将会比较慢,即使使用了索引:
     SELECT ... FROM TA, TB, TC WHERE TC.COL3 = TA.COL1 AND TC.COL3=TB.COL2 AND TC.COL4 = 1
这个查询隐含TA.COL1 = TB.COL2 ,但是没有直接设定这个条件.如果 TA 和 TB 每个表都包含100 条记录,10000 组合将和 TC 关联,用于TC这个列的条件,尽管有索引在这个列上.使用JOIN 关键字, 在组合TC 之前,TA.COL1 = TB.COL2 条件直接并缩小组合 TA 和 TB 的行数, 在运行大数据量的表的结果是,将会很快:
     SELECT ... FROM TA JOIN TB ON TA.COL1 = TB.COL2 JOIN TC ON TB.COL2 = TC.COL3 WHERE TC.COL4 = 1
这个查询可以提高一大步,如果改变表的顺序, 所以 TC.COL1 = 1 将最先使用,这样更小的集合将组合在一起:
     SELECT ... FROM TC JOIN TB ON TC.COL3 = TB.COL2 JOIN TA ON TC.COL3 = TA.COL1 WHERE TC.COL4 = 1
以上例子,数据引擎自动应用于TC.COL4 = 1 组合小的集合于其它表关联. Indexes TC.COL4, TB.COL2   TA.COL1 都将使用索引,提高查询速度.
3.5.     Subqueries and Joins
使用join 和调整表的顺序提高效率.
例如:, 第二个查询的速度将更快一些(TA.COL1 和TB.COL3都有索引):
Example 2.2. Query comparison
     SELECT ... FROM TA WHERE TA.COL1 = (SELECT MAX(TB.COL2) FROM TB WHERE TB.COL3 = 4)

     SELECT ... FROM (SELECT MAX(TB.COL2) C1 FROM TB WHERE TB.COL3 = 4) T2 JOIN TA ON TA.COL1 = T2.C1
第二个查询将 MAX(TB.COL2) 与一个单记录表相关联. 并使用TA.COL1索引,这将变得非常快. 第一个查询是将 TA 表中的每一条记录不断地与MAX(TB.COL2)匹配.
3.6.     数据类型
TINYINT, SMALLINT, INTEGER, BIGINT, NUMERIC and DECIMAL (without a decimal point) are supported integral types and map to byte, short, int, long and BigDecimal in Java.

Integral Types:
TINYINT, SMALLINT, INTEGER, BIGINT, NUMERIC and DECIMAL
Other Numeric Types:
REAL, FLOAT or DOUBLE
Bit and Boolean Types:
     BOOLEAN: UNDEFINED,TRUE,FALSE  
NULL values are treated as undefined.
Storage and Handling of Java Objects
Sequences and Identity

Identity Auto-Increment Columns:
The next IDENTITY value to be used can be set with the
ALTER TABLE ALTER COLUMN <column name> RESTART WITH <new value>;
Sequences:
SELECT NEXT VALUE FOR mysequence, col1, col2 FROM mytable WHERE ...
    
3.7.     事务问题:
SET PROPERTY "sql.tx_no_multi_rewrite" TRUE

4.     Connections
通用驱动jdbc:hsqldb:   下列协议标识(mem: file: res: hsql: http: hsqls: https:)
Table 4.1. Hsqldb URL Components
Driver and Protocol     Host and Port     Database
jdbc:hsqldb:mem:
     not available     accounts

jdbc:hsqldb:mem:.
jdbc:hsqldb:file:
     not available     mydb
/opt/db/accounts
C:/data/mydb

数据库路径.
jdbc:hsqldb:res:
     not available     /adirectory/dbname

jars files are accessed in Java programs. The /adirectory above stands for a directory in one of the jars.
jdbc:hsqldb:hsql:
jdbc:hsqldb:hsqls:
jdbc:hsqldb:http:
jdbc:hsqldb:https:
     //localhost
//192.0.0.10:9500
//dbserver.somedomain.com
     /an_alias
/enrollments
/quickdb

别名在server.properties or webserver.properties文件中指定
     database.0=file:/opt/db/accounts
     dbname.0=an_alias

     database.1=file:/opt/db/mydb
     dbname.1=enrollments

     database.2=mem:adatabase
     dbname.2=quickdb
In the example below, the database files lists.* in the /home/dbmaster/ directory are associated with the empty alias:
     database.3=/home/dbmaster/lists
     dbname.3=
4.1.     Connection properties
Connection properties are specified either by establishing the connection via the:
     DriverManager.getConnection (String url, Properties info);
method call, or the property can be appended to the full Connection URL.
Table 4.2. Connection Properties
get_column_name     true     column name in ResultSet
This property is used for compatibility with other JDBC driver implementations. When true (the default), ResultSet.getColumnName(int c) returns the underlying column name
When false, the above method returns the same value as ResultSet.getColumnLabel(int column) Example below:
     jdbc:hsqldb:hsql://localhost/enrollments;get_column_name=false
                    
When a ResultSet is used inside a user-defined stored procedure, the default, true, is always used for this property.
ifexists     false     connect only if database already exists
Has an effect only with mem: and file: database. When true, will not create a new database if one does not already exist for the URL.
When false (the default), a new mem: or file: database will be created if it does not exist.
Setting the property to true is useful when troubleshooting as no database is created if the URL is malformed. Example below:
     jdbc:hsqldb:file:enrollments;ifexists=true
shutdown     false     shut down the database when the last connection is closed
This mimics the behaviour of 1.7.1 and older versions. When the last connection to a database is closed, the database is automatically shut down. The property takes effect only when the first connection is made to the database. This means the connection that opens the database. It has no effect if used with subsequent, simultaneous connections.
This command has two uses. One is for test suites, where connections to the database are made from one JVM context, immediately followed by another context. The other use is for applications where it is not easy to configure the environment to shutdown the database. Examples reported by users include web application servers, where the closing of the last connection conisides with the web app being shut down.


4.2.     Properties Files
大小写敏感 (e.g. server.silent=FALSE will have no effect, but server.silent=false will work).
属性文件和设定存储如下 :
Table 4.3. Hsqldb Server Properties Files
File Name     Location     Function
server.properties     the directory where the command to run the Server class is issued     settings for running HSQLDB as a database server communicating with the HSQL protocol
webserver.properties     the directory where the command to run the WebServer class is issued     settings for running HSQLDB as a database server communicating with the HTTP protocol
<dbname>.properties     the directory where all the files for a database are located     settings for each particular database
Properties files for running the servers are not created automatically. You should create your own files that contain server.property=value pairs for each property.
4.2.1.     Server and Web Server Properties
server.properties and webserver.properties 文件支持如下设定:
Table 4.4. Property File Properties
Value     Default     Description
server.database.0     test     the path and file name of the first database file to use
server.dbname.0     ""     lowercase server alias for the first database file
server.urlid.0     NONE     SqlTool urlid used by UNIX init script. (This property is not used if your are running Server/Webserver on a platform other than UNIX, or of you are not using our UNIX init script).
server.silent     true     no extensive messages displayed on console
server.trace     false     JDBC trace messages displayed on console
In 1.8.0, 每个服务器支持同时启动10个不同的数据库. The server.database.0 property defines the filename / path whereas the server.dbname.0 defines the lowercase alias used by clients to connect to that database. The digit 0 is incremented for the second database and so on. Values for the server.database.{0-9} property can use the mem:, file: or res: prefixes and properties as discussed above under CONNECTIONS. For example,
     database.0=mem:temp;sql.enforce_strict_size=true;
Values specific to server.properties are:
Table 4.5. Server Property File Properties
Value     Default     Description
server.port     9001     TCP/IP port used for talking to clients. All databases are served on the same port.
server.no_system_exit     true     no System.exit() call when the database is closed
Values specific to webserver.properties are:
Table 4.6. WebServer Property File Properties
Value     Default     Description
server.port     80     TCP/IP port used for talking to clients
server.default_page     index.html     the default web page for server
server.root     ./     the location of served pages
.<extension>     ?     multiple entries such as .html=text/html define the mime types of the static files served by the web server. See the source for WebServer.java for a list.
All the above values can be specified on the command line to start the server by omitting the server. prefix.
5.     SqlTool
Mem 数据库:
E:\hsqldb>java -jar ./lib/hsqldb.jar mem
Hsql Server:
(前提是xdb server 已经启动):
(java -cp ../lib/hsqldb.jar org.hsqldb.Server -database.0 file:mydb -dbname.0 xdb)
java -jar ./hsqldb.jar xdb

posted @ 2007-03-26 17:18 刘璐 阅读(684) | 评论 (0)编辑 收藏

(转)用DbUnit进行SqlMap单元测试- -

http://starrynight.blogdriver.com/starrynight/621943.html
DbUnit简介

为依赖于其他外部系统(如数据库或其他接口)的代码编写单元测试是一件很困难的工作。在这种情况下,有效的单元必须隔离测试对象和外部依赖,以便管理测试对象的状态和行为。

使用mock object对象,是隔离外部依赖的一个有效方法。如果我们的测试对象是依赖于DAO的代码,mock object技术很方便。但如果测试对象变成了DAO本身,又如何进行单元测试呢?

开源的DbUnit项目,为以上的问题提供了一个相当优雅的解决方案。使用DbUnit,开发人员可以控制测试数据库的状态。进行一个DAO单元测试之前,DbUnit为数据库准备好初始化数据;而在测试结束时,DbUnit会把数据库状态恢复到测试前的状态。

下面的例子使用DbUnit为iBATIS SqlMap的DAO编写单元测试。

准备测试数据
首先,要为单元测试准备数据。使用DbUnit,我们可以用XML文件来准备测试数据集。下面的XML文件称为目标数据库的Seed File,代表目标数据库的表名和数据,它为测试准备了两个Employee的数据。employee对应数据库的表名,employee_uid、start_date、first_name和last_name都是表employee的列名。

<?xml version="1.0" encoding="GB2312"?>
<dataset>
    <employee employee_uid="0001"
        start_date="2001-01-01"
        first_name="liutao"
        last_name="liutao" />
   
    <employee employee_uid="0002"
        start_date="2001-04-01"
        first_name="wangchuang"
        last_name="wangchuang" />
</dataset>

缺省情况下,DbUnit在单元测试开始之前删除Seed File中所有表的数据,然后导入Seed File的测试数据。在Seed File中不存在的表,DbUnit则不处理。
Seed File可以手工编写,也可以用程序导出现有的数据库数据并生成。

SqlMap代码
我们要测试的SqlMap映射文件如下所示:
<select id="queryEmployeeById" parameterClass="java.lang.String"
    resultClass="domain.Employee">
    select employee_uid as userId,
        start_date as startDate,
        first_name as firstName,
        last_name as lastName
    from EMPLOYEE where employee_uid=#value#
</select>
<delete id="removeEmployeeById" parameterClass="java.lang.String">
    delete from EMPLOYEE where employee_uid=#value#
</delete>
<update id="updateEmpoyee" parameterClass="domain.Employee">
    update EMPLOYEE
    set start_date=#startDate#,
    first_name=#firstName#,
    last_name=#lastName#
    where employee_uid=#userId#
</update>
<insert id="insertEmployee" parameterClass="domain.Employee">
    insert into employee (employee_uid,
        start_date, first_name, last_name)
        values (#userId#, #startDate#, #firstName#, #lastName#)
</insert>

编写DbUnit TestCase
为了方便测试,首先为SqlMap的单元测试编写一个抽象的测试基类,代码如下。

public abstract class BaseSqlMapTest extends DatabaseTestCase {
    protected static SqlMapClient sqlMap;

    protected IDatabaseConnection getConnection() throws Exception {
        return new DatabaseConnection(getJdbcConnection());
    }
    protected void setUp() throws Exception {
        super.setUp();
        init();
    }
    protected void tearDown() throws Exception {
        super.tearDown();
        getConnection().close();
        if (sqlMap != null) {
            DataSource ds = sqlMap.getDataSource();
            Connection conn = ds.getConnection();
            conn.close();
        }
    }
    protected void init() throws Exception {
        initSqlMap("sqlmap/SqlMapConfig.xml", null);
    }
    protected SqlMapClient getSqlMapClient() {
        return sqlMap;
    }
    protected void initSqlMap(String configFile, Properties props)
            throws Exception {
        Reader reader = Resources.getResourceAsReader(configFile);
        sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader, props);
        reader.close();
    }
    protected void initScript(String script) throws Exception {
        DataSource ds = sqlMap.getDataSource();
        Connection conn = ds.getConnection();
       
        Reader reader = Resources.getResourceAsReader(script);
        ScriptRunner runner = new ScriptRunner();
        runner.setStopOnError(false);
        runner.setLogWriter(null);
        runner.setErrorLogWriter(null);

        runner.runScript(conn, reader);
        conn.commit();
        conn.close();
        reader.close();
    }
    private Connection getJdbcConnection() throws Exception {
        Properties props = new Properties();
        props.load(Resources.getResourceAsStream("sqlmap/SqlMapConfig.properties"));
        Class driver = Class.forName(props.getProperty("driver"));
        Connection conn = DriverManager.getConnection(props.getProperty("url"),
                props.getProperty("username"), props.getProperty("password"));
        return conn;
    }
}

然后为每个SqlMap映射文件编写一个测试用例,extends上面的抽象类。如编写Employ.xml的测试用例如下,它覆盖了DbUnit的DatabaseTestCase类的getDataSet方法。

public class EmployeeDaoTest extends BaseSqlMapTest {
   
    protected IDataSet getDataSet() throws Exception {
        Reader reader = Resources.getResourceAsReader("config/employee_seed.xml");
        return new FlatXmlDataSet(reader);
    }
    public void testQueryEmpoyeeById() throws Exception {
        String id = "0001";
        Employee emp = (Employee)sqlMap.queryForObject("queryEmployeeById", id);
        assertNotNull(emp);
        assertEquals("0001", emp.getUserId());
        assertEquals("liutao", emp.getFirstName());
    }
    public void testRemoveEmployeeById() throws Exception {
        String id = "0001";
        int num = sqlMap.delete("removeEmployeeById", id);
        assertEquals(1, num);
       
        // 注意这里, 确认删除不能使用SqlMap的查询, 很奇怪!
        ITable table = getConnection().createQueryTable("removed",
                "select * from employee where employee_uid='0001'");
        assertEquals(0, table.getRowCount());
    }
    public void testUpdateEmployee() throws Exception {
        String id = "0002";
        Employee emp = (Employee)sqlMap.queryForObject("queryEmployeeById", id);
        emp.setLastName("wch");
        sqlMap.update("updateEmpoyee", emp);
       
        Employee emp1 = (Employee)sqlMap.queryForObject("queryEmployeeById", id);
        assertEquals("wch", emp1.getLastName());
    }
    public void testInsertEmployee() throws Exception {
        Employee emp = new Employee();
        emp.setUserId("0005");
        emp.setStartDate("2003-09-09");
        emp.setFirstName("macy");
        emp.setLastName("macy");
        sqlMap.insert("insertEmployee", emp);
       
        Employee emp1 = (Employee)sqlMap.queryForObject("queryEmployeeById", "0005");
        assertEquals(emp.getFirstName(), emp1.getFirstName());
        assertEquals(emp.getStartDate(), emp1.getStartDate());
    }
}

以上例子中的绿色代码部分使用ITable接口来查询已删除的数据。因为使用SqlMapClient.queryForObject方法查询,已删除的数据还存在,真奇怪(有时间再研究)。

DbUnit的断言
我们可以使用DbUnit的Assertion类的方法来比较数据是否相同。

public class Assertion {
    public static void assertEquals(ITable expected, ITable actual)
    public static void assertEquals(IDataSet expected, IDataSet actual)
}

DatabaseTestCase的getSetUpOperation和getTearDownOperation方法
缺省情况下,DbUnit执行每个测试前,都会执行CLEAN_INSERT操作,删除Seed File中所有表的数据,并插入文件的测试数据。你可以通过覆盖getSetUpOperation和getTearDownOperation方法改变setUp和tearDown的行为。

protected DatabaseOperation getSetUpOperation() throws Exception {
    return DatabaseOperation.REFRESH;
}
protected DatabaseOperation getTearDownOperation() throws Exception {
   
return DatabaseOperation.NONE;
}

REFRESH操作执行测试前并不执行CLEAN操作,只是导入文件中的数据,如果目标数据库数据已存在,DbUnit使用文件的数据来更新数据库。

使用Ant
上面的方法通过extends DbUnit的DatabaseTestCase来控制数据库的状态。而
使用DbUnit的Ant Task,完全可以通过Ant脚本的方式来实现。

<taskdef name="dbunit" classname="org.dbunit.ant.DbUnitTask"/>
<!-- 执行set up 操作 -->
<dbunit driver="org.hsqldb.jdbcDriver"
        url="jdbc:hsqldb:hsql://localhost/xdb"
        userid="sa" password="">
    <operation type="INSERT" src="employee_seed.xml"/>
</dbunit>
<!-- run all tests in the source tree -->
<junit printsummary="yes" haltonfailure="yes">
  <formatter type="xml"/>
  <batchtest fork="yes" todir="${reports.tests}">
    <fileset dir="${src.tests}">
      <include name="**/*Test*.java"/>
    </fileset>
  </batchtest>
</junit>
<!-- 执行tear down 操作 -->
<dbunit driver="org.hsqldb.jdbcDriver"
        url="jdbc:hsqldb:hsql://localhost/xdb"
        userid="sa" password="">
    <operation type="DELETE" src="employee_seed.xml"/>
</dbunit>

以上的Ant脚本把junit task放在DbUnit的Task中间,可以达到控制数据库状态的目标。

由此可知,DbUnit可以灵活控制目标数据库的测试状态,从而使编写SqlMap单元测试变得更加轻松。

本文抄袭了资源列表的“Effective Unit Test with DbUnit”,但重新编写了代码示例。

网上资源

1、DbUnit Framework

2、Effective Unit Testing with DbUnit

3、Control your test-environement with DbUnit and Anthill

posted @ 2007-03-26 17:16 刘璐 阅读(709) | 评论 (0)编辑 收藏

2007年3月8日

抽象类和接口的区别

abstract class和interface是Java语言中对于抽象类定义进行支持的两种机制,正是由于这两种机制的存在,才赋予了Java强大的面向对象能力。abstract class和interface之间在对于抽象类定义的支持方面具有很大的相似性,甚至可以相互替换,因此很多开发者在进行抽象类定义时对于abstract class和interface的选择显得比较随意。其实,两者之间还是有很大的区别的,对于它们的选择甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。本文将对它们之间的区别进行一番剖析,试图给开发者提供一个在二者之间进行选择的依据。  

理解抽象类  

abstract class和interface在Java语言中都是用来进行抽象类(本文中的抽象类并非从abstract class翻译而来,它表示的是一个抽象体,而abstract class为Java语言中用于定义抽象类的一种方法,请读者注意区分)定义的,那么什么是抽象类,使用抽象类能为我们带来什么好处呢?  

在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样。并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如:如果我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的。  

在面向对象领域,抽象类主要用来进行类型隐藏。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,因此它可以是不允许修改的;同时,通过从这个抽象体派生,也可扩展此模块的行为功能。熟悉OCP的读者一定知道,为了能够实现面向对象设计的一个最核心的原则OCP(Open-Closed Principle),抽象类是其中的关键所在。  


从语法定义层面看abstract class和interface  

在语法层面,Java语言对于abstract class和interface给出了不同的定义方式,下面以定义一个名为Demo的抽象类为例来说明这种不同。  

使用abstract class的方式定义Demo抽象类的方式如下:  

abstract class Demo {  
 abstract void method1();  
 abstract void method2();  
 …  
}  

使用interface的方式定义Demo抽象类的方式如下:  

interface Demo {  
 void method1();  
 void method2();  
 …  
}  

在abstract class方式中,Demo可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface方式的实现中,Demo只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。从某种意义上说,interface是一种特殊形式的abstract class。  

      从编程的角度来看,abstract class和interface都可以用来实现"design by contract"的思想。但是在具体的使用上面还是有一些区别的。  

首先,abstract class在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。也许,这是Java语言的设计者在考虑Java对于多重继承的支持方面的一种折中考虑吧。  

其次,在abstract class的定义中,我们可以赋予方法的默认行为。但是在interface的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,但是这会 增加一些复杂性,有时会造成很大的麻烦。  

在抽象类中不能定义默认行为还存在另一个比较严重的问题,那就是可能会造成维护上的麻烦。因为如果后来想修改类的界面(一般通过abstract class或者interface来表示)以适应新的情况(比如,添加新的方法或者给已用的方法中添加新的参数)时,就会非常的麻烦,可能要花费很多的时间(对于派生类很多的情况,尤为如此)。但是如果界面是通过abstract class来实现的,那么可能就只需要修改定义在abstract class中的默认行为就可以了。  

同样,如果不能在抽象类中定义默认行为,就会导致同样的方法实现出现在该抽象类的每一个派生类中,违反了"one rule,one place"原则,造成代码重复,同样不利于以后的维护。因此,在abstract class和interface间进行选择时要非常的小心。  


从设计理念层面看abstract class和interface  

上面主要从语法定义和编程的角度论述了abstract class和interface的区别,这些层面的区别是比较低层次的、非本质的。本小节将从另一个层面:abstract class和interface所反映出的设计理念,来分析一下二者的区别。作者认为,从这个层面进行分析才能理解二者概念的本质所在。  

前面已经提到过,abstarct class在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is a"关系,即父类和派生类在概念本质上应该是相同的(参考文献〔3〕中有关于"is a"关系的大篇幅深入的论述,有兴趣的读者可以参考)。对于interface 来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。为了使论述便于理解,下面将通过一个简单的实例进行说明。  

考虑这样一个例子,假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstract class或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示:  

使用abstract class方式定义Door:  

abstract class Door {  
 abstract void open();  
 abstract void close();  
}  

   
使用interface方式定义Door:  


interface Door {  
 void open();  
 void close();  
}  

   
其他具体的Door类型可以extends使用abstract class方式定义的Door或者implements使用interface方式定义的Door。看起来好像使用abstract class和interface没有大的区别。  

如果现在要求Door还要具有报警的功能。我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstract class和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略)?下面将罗列出可能的解决方案,并从设计理念层面对这些不同的方案进行分析。  

解决方案一:  

简单的在Door的定义中增加一个alarm方法,如下:  

abstract class Door {  
 abstract void open();  
 abstract void close();  
 abstract void alarm();  
}  

   
或者  

interface Door {  
 void open();  
 void close();  
 void alarm();  
}  

   
那么具有报警功能的AlarmDoor的定义方式如下:  

class AlarmDoor extends Door {  
 void open() { … }  
 void close() { … }  
 void alarm() { … }  
}  

   
或者  

class AlarmDoor implements Door {  
 void open() { … }  
 void close() { … }  
 void alarm() { … }  
}  

这种方法违反了面向对象设计中的一个核心原则ISP(Interface Segregation Priciple),在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变(比如:修改alarm方法的参数)而改变,反之依然。  

解决方案二:  

既然open、close和alarm属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:这两个概念都使用abstract class方式定义;两个概念都使用interface方式定义;一个概念使用abstract class方式定义,另一个概念使用interface方式定义。  

显然,由于Java语言不支持多重继承,所以两个概念都使用abstract class方式定义是不可行的。后面两种方式都是可行的,但是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。我们一一来分析、说明。  

如果两个概念都使用interface方式来定义,那么就反映出两个问题:1、我们可能没有理解清楚问题领域,AlarmDoor在概念本质上到底是Door还是报警器?2、如果我们对于问题领域的理解没有问题,比如:我们通过对于问题领域的分析发现AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为在这两个概念的定义上(均使用interface方式定义)反映不出上述含义。  

如果我们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。我们该如何来设计、实现来明确的反映出我们的意思呢?前面已经说过,abstract class在Java语言中表示一种继承关系,而继承关系在本质上是"is a"关系。所以对于Door这个概念,我们应该使用abstarct class方式来定义。另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。如下所示:  

abstract class Door {  
 abstract void open();  
 abstract void close();  
}  
interface Alarm {  
 void alarm();  
}  
class AlarmDoor extends Door implements Alarm {  
 void open() { … }  
 void close() { … }  
    void alarm() { … }  
}  

   
这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实abstract class表示的是"is a"关系,interface表示的是"like a"关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。

posted @ 2007-03-08 13:27 刘璐 阅读(293) | 评论 (0)编辑 收藏

2007年3月7日

关于session的详细解释

一、术语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

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


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

  使用Firefox可以很容易的观察现有的cookie的值

  使用HTTPLook配合Firefox可以很容易的理解cookie的工作原理。


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

  这是一个询问接受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传递回服务器。比如下面的表单



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




  这种技术现在已较少应用,笔者接触过的很古老的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也可以是不同的。

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

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

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

/NASApp

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


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

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

  从截屏画面上可以看到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机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的经验,而是始终需要具体情况具体分析。

posted @ 2007-03-07 10:50 刘璐 阅读(261) | 评论 (0)编辑 收藏

2007年3月6日

Java Web 开发

13. Tomcat与其他HTTP服务器集成
13.1 Tomcat与HTTP服务器集成的原理
 <!-- Define a non-SSL HTTP/1.1 Connector on port 8080 -->
    <Connector port="8080" maxHttpHeaderSize="8192"
               maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
               enableLookups="false" redirectPort="8443" acceptCount="100"
               connectionTimeout="20000" disableUploadTimeout="true" />
 
<!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009"
               enableLookups="false" redirectPort="8443" protocol="AJP/1.3" />
 
第一个连接监听8080端口,负责建立HTTP连接,在通过浏览器访问TOMCAT服务器的WEB应用时,使用的就是这个连接。
第二个连接器监听8009端口,负责和其他的HTTP服务器建立连接,在把TOMCAT与其他HTTP服务器集成时,就需要用到这个连接器。
 
13.2 在Windows下Tomcat与Apache服务器集成
1.install apache http server

2.copy jk into <APACHE_HOME>/modules
 
3.create workers.properties and copy it into <APACHE_HOME>/conf
workers.tomcat_home=C:\jakarta-tomcat  #让mod_jk模块知道Tomcat
workers.java_home=C:\j2sdk1.4.2  #让mod_jk模块知道j2sdk
ps=\  #指定文件路径分割符
worker.list=worker1
worker.worker1.port=8009  #工作端口,若没占用则不用修改
worker.worker1.host=localhost  #Tomcat服务器的地址
worker.worker1.type=ajp13  #类型
worker.worker1.lbfactor=1  #负载平衡因数
 
4.modify <APACHE_HOME>/conf/httpd.conf
# Using mod_jk2.dll to redirect dynamic calls to Tomcat
LoadModule jk_module modules\mod_jk_2.0.46.dll
JkWorkersFile "conf\workers.properties"
JkLogFile "logs\mod_jk2.log"
JkLogLevel debug
JkMount /*.jsp worker1
JkMount /helloapp/* worker1

5.test

6.banlance
(1)copy jk into <APACHE_HOME>/lib
 
(2)create workers.properties in <APACHE_HOME>/conf
ps=\  #指定文件路径分割符
worker.list=worker1,worker2,loadbalancer
worker.worker1.port=8009  #工作端口,若没占用则不用修改
worker.worker1.host=localhost  #Tomcat服务器的地址
worker.worker1.type=ajp13  #类型
worker.worker1.lbfactor=100  #负载平衡因数
worker.worker2.port=8009  #工作端口,若没占用则不用修改
worker.worker2.host=anotherhost  #Tomcat服务器的地址
worker.worker2.type=ajp13  #类型
worker.worker2.lbfactor=100  #负载平衡因数
worker.loadbalancer.type=lb
worker.loadbalancer.balanced_workers=worker1, worker2
 
(3)modify <APACHE_HOME>/conf/httpd.conf
# Using mod_jk2.dll to redirect dynamic calls to Tomcat
LoadModule jk_module modules\mod_jk_2.0.46.dll
JkWorkersFile "conf\workers.properties"
JkLogFile "logs\mod_jk2.log"
JkLogLevel debug
JkMount /*.jsp loadbalancer
JkMount /helloapp/* loadbalancer

(4)keep worker's name the same as jvmRoute property of <Engine> element in server.xml
eg. <Engine name="Catalina" defaultHost="localhost" debug="0" jmvRoute="worker1">

(5)test
 
if both of tomcat servers are running in the same server, at least the port of one of tomact servers should be modified.
shut down HTTP connection of tomcat server, cancel HTTP connector in server.xml
 
13.3 Tomcat与IIS服务器集成
1.prepare
(1)JK isapi_redirect.dll add it into <CATALINA_HOME>/bin

(2)create workers.properties in <CATALINA_HOME>/conf
workers.tomcat_home=C:\jakarta-tomcat  #让mod_jk模块知道Tomcat
workers.java_home=C:\j2sdk1.4.2  #让mod_jk模块知道j2sdk
ps=\  #指定文件路径分割符
worker.list=worker1
worker.worker1.port=8009  #工作端口,若没占用则不用修改
worker.worker1.host=localhost  #Tomcat服务器的地址
worker.worker1.type=ajp13  #类型
worker.worker1.lbfactor=1  #负载平衡因数
 
(3)create uriworkermap.properties in <CATALINA_HOME>/conf
/index.jsp=worker1
/helloapp/*=worker1
2.edit register
equals configurate attribuates of JK
 
3.add 'jakarta' vitural directory in IIS for JK
choose 脚本和可执行程序
 
4.把JK插件作为ISAPI筛选器加入IIS
 
14. MVC
14.1 用RequestDispatcher 实现MVC
1. 定义用以表示数据的bean
2. 使用一个servlet处理请求
– servlet读取请求参数,检查数据的缺失或异常等。
3. 填充bean
– 该servlet调用业务逻辑(与具体应用相关的代码)或数据访问代码得到最终的结果。得出的结果被放在第一步中定义的bean中。
4. 将bean存储在请求、会话或servlet的上下文中
– 该servlet调用请求、会话或servlet上下文对象的setAttribute存储表达请求结果的bean的引用。
5. 将请求转发到JSP页面
– 该servlet确定哪个JSP页面适合于处理当前的情形,并使用RequestDispatcher的forward方法将控制转移到那个页面。
6. 从bean中提取数据
– JSP页面使用jsp:useBean和与第4步匹配的位置访问之前存储的bean,然后使用jsp:getProperty输出bean的属性。
– JSP页面并不创建或修改bean;它只是提取并显示由servlet创建的数据。
 
RequestDispatcher dispatcher =
request.getRequestDispatcher(address);
dispatcher.forward(request, response);
 
14.2 jsp:useBean在MVC中的使用与在独立JSP页面中有什么不同
• JSP页面不应该创建对象
– 应该由servlet,而非JSP页面,创建所有的数据对象。因此,为了保证JSP页面不会创建对象,我们应该使用<jsp:useBean ... type="package.Class" />而不是<jsp:useBean ... class="package.Class" />
• JSP页面也不应该修改已有的对象
– 因此,我们应该只使用jsp:getProperty,不使用jsp:setProperty。
 
14.3 基于请求的数据共享
• Servlet
ValueObject value = new ValueObject(...);
request.setAttribute("key", value);
RequestDispatcher dispatcher =request.getRequestDispatcher("/WEB-INF/SomePage.jsp");
dispatcher.forward(request, response);
• JSP
<jsp:useBean id="key" type="somePackage.ValueObject" scope="request" />
<jsp:getProperty name="key" property="someProperty" />
 
14.4 基于会话的数据共享
• Servlet
ValueObject value = new ValueObject(...);
HttpSession session = request.getSession();
session.setAttribute("key", value);
RequestDispatcher dispatcher =request.getRequestDispatcher("/WEB-INF/SomePage.jsp");
dispatcher.forward(request, response);
• JSP
<jsp:useBean id="key" type="somePackage.ValueObject" scope="session" />
<jsp:getProperty name="key" property="someProperty" />
 
14.5 基于ServletContext的数据共享
• Servlet
synchronized(this) {
ValueObject value = new ValueObject(...);
getServletContext().setAttribute("key", value);
RequestDispatcher dispatcher =request.getRequestDispatcher("/WEB-INF/SomePage.jsp");
dispatcher.forward(request, response);
}
• JSP
<jsp:useBean id="key" type="somePackage.ValueObject" scope="application" />
<jsp:getProperty name="key" property="someProperty" />
 
15. Expression Language
15.1 启用表达式语言
• 仅能够用于支持JSP 2.0 (servlets 2.4) 的服务器中
–例如:Tomcat 5,而非Tomcat 4
• 必须在web.xml文件中使用JSP 2.0
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://java.sun.com/xml/ns/j2ee web-app_2_4.xsd"
version="2.4">

</web-app>
 
15.2 访问作用域变量
• ${varName}
– 表示在PageContext,HttpServletRequest,HttpSession,和ServletContext中,依照所列的顺序进行查找,输出这个属性名所对应的对象。
– PageContext不适用于MVC。
• 等价的形式
– ${name}
– <%= pageContext.findAttribute("name") %>
– <jsp:useBean id="name" type="somePackage.SomeClass" scope="...">
<%= name %>
 
15.3 访问bean的属性
• ${varName.propertyName}
– 表示查找给定名称的作用域变量,并输出指定的bean属性
• 等价的形式
– ${customer.firstName}
– <%@ page import="coreservlets.NameBean" %>
<%NameBean person =(NameBean)pageContext.findAttribute("customer");%>
<%= person.getFirstName() %>
 
15.4 访问集合
• ${attributeName[entryName]}
• Works for
– Array. Equivalent to
• theArray[index]
– List. Equivalent to
• theList.get(index)
– Map. Equivalent to
• theMap.get(keyName)
• Equivalent forms (for HashMap)
– ${stateCapitals["maryland"]}
– ${stateCapitals.maryland}
– But the following is illegal since 2 is not a legal var name
• ${listVar.2}
 
15.5 引用隐式对象(预定义变量名)
• pageContext. The PageContext object.
– E.g. ${pageContext.session.id}
• param and paramValues. Request params.
– E.g. ${param.custID}
• header and headerValues. Request headers.
– E.g. ${header.Accept} or ${header["Accept"]}
– ${header["Accept-Encoding"]}
• cookie. Cookie object (not cookie value).
– E.g. ${cookie.userCookie.value} or ${cookie["userCookie"].value}
• initParam. Context initialization param.
• pageScope, requestScope, sessionScope, appliationScope.
– Instead of searching scopes.
• Problem
– Using implicit objects usually works poorly with MVC model

posted @ 2007-03-06 17:19 刘璐 阅读(324) | 评论 (0)编辑 收藏