<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    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 劉璐 閱讀(704) | 評論 (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 劉璐 閱讀(733) | 評論 (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 劉璐 閱讀(311) | 評論 (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 劉璐 閱讀(278) | 評論 (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 劉璐 閱讀(338) | 評論 (0)編輯 收藏

    主站蜘蛛池模板: 亚洲欧洲国产综合| 亚洲免费福利视频| 日本卡1卡2卡三卡免费| 久久精品乱子伦免费| 亚洲伊人久久精品影院| 亚洲最大福利视频| 久久亚洲AV无码精品色午夜麻| 激情内射亚洲一区二区三区爱妻| 亚洲gv白嫩小受在线观看| 亚洲一级毛片免费看| 中文字幕在线免费视频| 无码人妻一区二区三区免费视频 | 午夜宅男在线永久免费观看网| 亚洲av无码专区在线观看素人| 亚洲精品自产拍在线观看动漫| 亚洲av一综合av一区| 最好2018中文免费视频| 青娱乐免费在线视频| 女人被弄到高潮的免费视频| 久久亚洲精品成人| 成人av片无码免费天天看| 日美韩电影免费看| 亚洲av永久无码嘿嘿嘿| 99久久人妻精品免费二区| 伊人久久大香线蕉亚洲| 一级毛片在线免费视频| heyzo亚洲精品日韩| 久久久久久久综合日本亚洲| 国产亚洲综合久久| 四虎影视精品永久免费网站| 亚洲av无码一区二区三区人妖 | 国产精品亚洲w码日韩中文| 亚洲图片校园春色| 啦啦啦完整版免费视频在线观看| 亚洲国产精品久久久久婷婷软件| 久久国产乱子精品免费女| 亚洲黑人嫩小videos| 成年人网站在线免费观看| 在线观看亚洲免费| 在线a亚洲v天堂网2019无码| 久久免费视频99|