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

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

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

    Springweb 表現(xiàn)層集成技術(shù)


    13.1. 簡介

    Spring之所以出色的一個原因就是將表現(xiàn)層從MVC的框架中分離出來。例如,通過配置就可以讓Velocity或者XSLT來代替已經(jīng)存在的JSP頁面。本章介紹和Spring集成的一些主流表現(xiàn)層技術(shù),并簡要描述如何集成新的表現(xiàn)層。這里假設(shè)你已經(jīng)熟悉第 12.5 節(jié) “視圖與視圖解析”,那里介紹了將表現(xiàn)層集成到MVC框架中的基本知識。
    13.2. 和JSP & JSTL一起使用Spring

    Spring 為JSP和JSTL提供了一組方案(順便說一下,它們都是最流行的表現(xiàn)層技術(shù)之一)。使用JSP或JSTL需要使用定義在 WebApplicationContext里的標準的視圖解析器。此外,你當然也需要寫一些JSP頁面來顯示頁面。這里描述一些Spring為方便 JSP開發(fā)而提供的額外功能。
    13.2.1. 視圖解析器

    就象和Spring集成的其他表現(xiàn)層技術(shù)一樣,對于JSP頁面你需要一個視圖解析器來解析。最常用的JSP視圖解析器是InternalResourceViewResolver和ResourceBundleViewResolver。它們被定義在WebApplicationContext里:

    # The ResourceBundleViewResolver:
    <bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
        <property name="basename"><value>views</value></property>
    </bean>

    # And a sample properties file is uses (views.properties in WEB-INF/classes):
    welcome.class=org.springframework.web.servlet.view.JstlView
    welcome.url=/WEB-INF/jsp/welcome.jsp

    productList.class=org.springframework.web.servlet.view.JstlView
    productList.url=/WEB-INF/jsp/productlist.jsp

    你可以看到ResourceBundleViewResolver需要一個屬性文件來把視圖名稱映射到 1)類和 2) URL。 通過ResolverBundleViewResolver,你可以用一個解析器來解析兩種類型的視圖。

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass"><value>org.springframework.web.servlet.view.JstlView</value></property>
        <property name="prefix"><value>/WEB-INF/jsp/</value></property>
        <property name="suffix"><value>.jsp</value></property>
    </bean>

    InternalResourceBundleViewResolver可以配置成使用JSP頁面。作為好的實現(xiàn)方式,強烈推薦你將JSP文件放在WEB-INF下的一個目錄中,這樣客戶端就不會直接訪問到它們。
    13.2.2. 普通JSP頁面和JSTL

    當你使用Java標準標簽庫(Java Standard Tag Library)時,你必須使用一個特殊的類,JstlView,因為JSTL在使用象I18N這樣的功能前需要一些準備工作。
    13.2.3. 其他有助于開發(fā)的標簽

    正如前面幾章所提到的,Spring可以將請求參數(shù)綁定到命令對象上。為了更容易地開發(fā)含有數(shù)據(jù)綁定的JSP頁面,Spring定義了一些專門的標簽。所有的Spring標簽都有HTML轉(zhuǎn)義功能來決定是否使用字符轉(zhuǎn)義。

    標簽庫描述符(TLD)和庫本身都包含在spring.jar里。更多有關(guān)標簽的信息可以訪問http://www.springframework.org/docs/taglib/index.html.
    13.3. Tiles的使用

    Tiles象其他表現(xiàn)層技術(shù)一樣,可以集成在使用Spring的Web應(yīng)用中。下面大致描述一下過程。
    13.3.1. 所需的庫文件

    為了使用Tiles,你必須將需要的庫文件包含在你的項目中。下面列出了這些庫文件。

        *

          struts version 1.1
        *

          commons-beanutils
        *

          commons-digester
        *

          commons-logging
        *

          commons-lang

    這些文件以從Spring中獲得。
    13.3.2. 如何集成Tiles

    為了使用Tiles,你必須用定義文件(definition file)來配置它(有關(guān)于定義(definition)和其他Tiles概念,請參考http://jakarta.apache.org/struts)。在Spring中,這些都可以使用TilesConfigurer在完成。下面是ApplicationContext配置的片段。

    <bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles.TilesConfigurer">
        <property name="factoryClass">
            <value>org.apache.struts.tiles.xmlDefinition.I18nFactorySet</value>
        </property>
        <property name="definitions">
            <list>
                <value>/WEB-INF/defs/general.xml</value>
                <value>/WEB-INF/defs/widgets.xml</value>
                <value>/WEB-INF/defs/administrator.xml</value>
                <value>/WEB-INF/defs/customer.xml</value>
                <value>/WEB-INF/defs/templates.xml</value>
            </list>
        </property>
    </bean>

    你可以看到,有五個文件包含定義,它們都存放在WEB-INF/defs目錄中。當初始化WebApplicationContext時,這些文件被讀取,并且初始化由factoryClass屬性指定的定義工廠(definitons factory)。在這之后,你的Spring Web應(yīng)用就可以使用在定義文件中的tiles includes內(nèi)容。為了使用這些,你必須得和其他表現(xiàn)層技術(shù)一樣有一個ViewResolver。有兩種可以選擇,InternalResourceViewResolver和ResourceBundleViewResolver。
    13.3.2.1.  InternalResourceViewResolver

    InternalResourceViewResolver用viewClass屬性指定的類實例化每個它解析的視圖。

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="requestContextAttribute"><value>requestContext</value></property>
        <property name="viewClass">
            <value>org.springframework.web.servlet.view.tiles.TilesView</value>
        </property>
    </bean>

    13.3.2.2.  ResourceBundleViewResolver

    必須提供給ResourceBundleViewResolver一個包含viewnames和viewclassess屬性的屬性文件。

    <bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
        <property name="basename"><value>views</value></property>
    </bean>

        ...
        welcomeView.class=org.springframework.web.servlet.view.tiles.TilesView
        welcomeView.url=welcome (<b>this is the name of a definition</b>)
            
        vetsView.class=org.springframework.web.servlet.view.tiles.TilesView
        vetsView.url=vetsView (<b>again, this is the name of a definition</b>)
           
        findOwnersForm.class=org.springframework.web.servlet.view.JstlView
        findOwnersForm.url=/WEB-INF/jsp/findOwners.jsp
        ...

    你可以發(fā)現(xiàn),當使用ResourceBundleViewResolver,你可以使用不同的表現(xiàn)層技術(shù)。
    13.4. Velocity

    Velocity是Jakarta項目開發(fā)的表現(xiàn)層技術(shù)。有關(guān)與Velocity的詳細資料可以在http://jakarta.apache.org/velocity找到。這一部分介紹如何集成Velocity到Spring中。
    13.4.1. 所需的庫文件

    在使用Velocity之前,你需要在你的Web應(yīng)用中包含兩個庫文件,velocity-1.x.x.jar 和commons-collections.jar 。一般它們放在WEB-INF/lib目錄下,以保證J2EE服務(wù)器能夠找到,同時把它們加到你的classpath中。當然假設(shè)你也已經(jīng)把spring.jar放在你的WEB-INF/lib目錄下!最新的Velocity和commnons collections的穩(wěn)定版本由Spring框架提供,可以從/lib/velocity和/lib/jakarta-commons目錄下獲取。
    13.4.2. 分發(fā)器(Dispatcher Servlet)上下文

    你的Spring DispatcherServlet配置文件(一般是WEB-INF/[servletname]-servlet.xml)應(yīng)該包含一個視圖解析器的bean定義。我們也可以再加一個bean來配置Velocity環(huán)境。我指定DispatcherServlet的名字為‘frontcontroller’,所以配置文件的名字反映了DispatcherServlet的名字

    下面的示例代碼顯示了不同的配置文件

    <!-- ===========================================================-->
    <!-- View resolver. Required by web framework.                  -->
    <!-- ===========================================================-->
    <!--
      View resolvers can be configured with ResourceBundles or XML files.  If you need
      different view resolving based on Locale, you have to use the resource bundle resolver,
      otherwise it makes no difference.  I simply prefer to keep the Spring configs and
      contexts in XML.  See Spring documentation for more info.
    -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
        <property name="cache"><value>true</value></property>
        <property name="location"><value>/WEB-INF/frontcontroller-views.xml</value></property>
    </bean>

    <!-- ===========================================================-->
    <!-- Velocity configurer.                                       -->
    <!-- ===========================================================-->
    <!--
      The next bean sets up the Velocity environment for us based on a properties file, the
      location of which is supplied here and set on the bean in the normal way.  My example shows
      that the bean will expect to find our velocity.properties file in the root of the
      WEB-INF folder.  In fact, since this is the default location, it's not necessary for me
      to supply it here.  Another possibility would be to specify all of the velocity
      properties here in a property set called "velocityProperties" and dispense with the
      actual velocity.properties file altogether.
    -->
    <bean
        id="velocityConfig"
        class="org.springframework.web.servlet.view.velocity.VelocityConfigurer"
        singleton="true">
        <property name="configLocation"><value>/WEB-INF/velocity.properties</value></property>         
    </bean>

    13.4.3. Velocity.properties

    這個屬性文件用來配置Velocity,屬性的值會傳遞給Velocity運行時。其中只有一些屬性是必須的,其余大部分屬性是可選的 - 詳細可以查看Velocity的文檔。這里,我僅僅演示在Spring的MVC框架下運行Velocity所必需的內(nèi)容。
    13.4.3.1. 模版位置

    大部分屬性值和Velocity模版的位置有關(guān)。Velocity模版可以通過classpath或文件系統(tǒng)載入,兩種方式都有各自的優(yōu)缺點。從 classpath載入具有很好的移植性,可以在所有的目標服務(wù)器上工作,但你會發(fā)現(xiàn)在這種方式中,模版文件會把你的java包結(jié)構(gòu)弄亂(除非你為模版建立獨立樹結(jié)構(gòu))。從classpath載入的另一個重要缺點是在開發(fā)過程中,在源文件目錄中的任何改動常常會引起WEB-INF/classes下資源文件的刷新,這將導(dǎo)致開發(fā)服務(wù)器重啟你的應(yīng)用(代碼的即時部署)。這可能是令人無法忍受的。一旦完成大部分的開發(fā)工作,你可以把模版文件存在在jar中,并把它放在WEB-INF/lib目錄下中。
    13.4.3.2. velocity.properties示例

    這個例子將Velocity模版存放在文件系統(tǒng)的WEB-INF下,客戶瀏覽器是無法直接訪問到它們的,這樣也不會因為你開發(fā)過程中修改它們而引起Web應(yīng)用重啟。它的缺點是目標服務(wù)器可能不能正確解析指向這些文件的路徑,尤其是當目標服務(wù)器沒有把WAR模塊展開在文件系統(tǒng)中。Tomcat 4.1.x/5.x,WebSphere 4.x和WebSphere 5.x支持通過文件系統(tǒng)載入模版。但是你在其他類型的服務(wù)器上可能會有所不同。

    #
    # velocity.properties - example configuration
    #


    # uncomment the next two lines to load templates from the
    # classpath (i.e. WEB-INF/classes)
    #resource.loader=class
    #class.resource.loader.class=org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader

    # comment the next two lines to stop loading templates from the
    # file system
    resource.loader=file
    file.resource.loader.class=org.apache.velocity.runtime.resource.loader.FileResourceLoader


    # additional config for file system loader only.. tell Velocity where the root
    # directory is for template loading.  You can define multiple root directories
    # if you wish, I just use the one here.  See the text below for a note about
    # the ${webapp.root}
    file.resource.loader.path=${webapp.root}/WEB-INF/velocity


    # caching should be 'true' in production systems, 'false' is a development
    # setting only.  Change to 'class.resource.loader.cache=false' for classpath
    # loading
    file.resource.loader.cache=false

    # override default logging to direct velocity messages
    # to our application log for example.  Assumes you have
    # defined a log4j.properties file
    runtime.log.logsystem.log4j.category=com.mycompany.myapplication

    13.4.3.3. Web應(yīng)用的根目錄標記

    上面在配置文件資源載入時,使用一個標記${webapp.root}來代表Web應(yīng)用在文件系統(tǒng)中的根目錄。這個標記在作為屬性提供給Velocity之前,會被Spring的代碼解釋成和操作系統(tǒng)有關(guān)的實際路徑。這種文件資源的載入方式在一些服務(wù)器中是不可移植的。如果你認為可移植性很重要,可以給VelocityConfigurer定義不同的 “appRootMarker”,來修改根目錄標記本身的名字。Spring的文檔對此有詳細表述。
    13.4.3.4. 另一種可選的屬性規(guī)范

    作為選擇,你可以用下面的內(nèi)嵌屬性來代替Velocity配置bean中的“configLocation”屬性,從而直接指定Velocity屬性。

    <property name="velocityProperties">
        <props>
            <prop key="resource.loader">file</prop>
            <prop key="file.resource.loader.class">org.apache.velocity.runtime.resource.loader.FileResourceLoader</prop>
            <prop key="file.resource.loader.path">${webapp.root}/WEB-INF/velocity</prop>
            <prop key="file.resource.loader.cache">false</prop>
        </props>
    </property>

    13.4.3.5. 缺省配置(文件資源載入)

    注意從Spring 1.0-m4起,你可以不使用屬性文件或內(nèi)嵌屬性來定義模版文件的載入,你可以把下面的屬性放在Velocity配置bean中。

    <property name="resourceLoaderPath"><value>/WEB-INF/velocity/</value></property>

    13.4.4. 視圖配置

    配置的最后一步是定義一些視圖,這些視圖和Velocity模版一起被顯示。視圖被定義在Spring上下文文件中。正如前面提到的,這個例子使用XML文件定義視圖bean,但是也可以使用屬性文件(ResourceBundle)來定義。視圖定義文件的名字被定義在WEB-INF/frontcontroller-servlet.xml文件的ViewResolver的bean中。

    <!--
      Views can be hierarchical, here's an example of a parent view that
      simply defines the class to use and sets a default template which
      will normally be overridden in child definitions.
    -->
    <bean id="parentVelocityView" class="org.springframework.web.servlet.view.velocity.VelocityView">
        <property name="url"><value>mainTemplate.vm</value></property>       
    </bean>

    <!--
      - The main view for the home page.  Since we don't set a template name, the value
      from the parent is used.
    -->
    <bean id="welcomeView" parent="parentVelocityView">
      <property name="attributes">
          <props>
              <prop key="title">My Velocity Home Page</prop>
          </props>
      </property>    
    </bean> 

    <!--
      - Another view - this one defines a different velocity template.
    -->
    <bean id="secondaryView" parent="parentVelocityView">
        <property name="url"><value>secondaryTemplate.vm</value></property> 
        <property name="attributes">
            <props>
                <prop key="title">My Velocity Secondary Page</prop>
            </props>
        </property>   
    </bean>

    13.4.5. 創(chuàng)建Velocity模版

    最后,你需要做的就是創(chuàng)建Velocity模版。我們定義的視圖引用了兩個模版,mainTemplate.vm和secondaryTemplate.vm。屬性文件velocity.proeprties定義這兩個文件被放在WEB-INF/velocity/下。如果你在velocity.properties中選擇通過classpath載入,它們應(yīng)該被放在缺省包的目錄下,(WEB-INF/classes),或者WEB-INF/lib下的jar文件中。下面就是我們的‘secondaryView’看上去的樣子(簡化了的HTML文件)。

    ## $title is set in the view definition file for this view.
    <html>
        <head><title>$title</title></head>
        <body>
            <h1>This is $title!!</h1>

            ## model objects are set in the controller and referenced
            ## via bean properties o method names.  See the Velocity
            ## docs for info

            Model Value: $model.value
            Model Method Return Value: $model.getReturnVal()

        </body>
    </html>

    現(xiàn)在,當你的控制器返回一個ModelAndView包含“secondaryView”時,Velocity就會工作,將上面的頁面轉(zhuǎn)化為普通的HTML頁面。
    13.4.6. 表單處理

    Spring提供一個標簽庫給JSP頁面使用,其中包含了<spring:bind>標簽。這個標簽主要使表單能夠顯示在web層或業(yè)務(wù)層中的Validator驗證時產(chǎn)生的出錯消息。這種行為可以被Velocity宏和其他的Spring功能模擬實現(xiàn)。
    13.4.6.1. 驗證錯誤

    通過表單驗證而產(chǎn)生的出錯消息可以從屬性文件中讀取,這有助于維護和國際化它們。Spring以它自己的方式處理這些,關(guān)于它的工作方式,你可以參考MVC指南或javadoc中的相關(guān)內(nèi)容。為了訪問這些出錯消息,需要把RequestContext對象暴露給VelocityContext中的Velocity模版。修改你在views.properties或views.xml中的模版定義,給一個名字到它的attributes里(有了名字就可以被訪問到)。

    <bean id="welcomeView" parent="parentVelocityView">
        <property name="requestContextAttribute"><value>rc</value></property> 
        <property name="attributes">
            <props>
                <prop key="title">My Velocity Home Page</prop>
            </props>
        </property>    
    </bean>

    在我們前面例子的基礎(chǔ)上,上面的例子將RequestContext屬性命名為rc。這樣從這個視圖繼承的所有Velocity視圖都可以訪問$rc。
    13.4.6.2. Velocity的宏

    接下來,需要定義一個Velocity宏。既然宏可以在幾個Velocity模版(HTML表單)中重用,那么完全可以把宏定義在一個宏文件中。創(chuàng)建宏的詳細信息,參考Velocity文檔。

    下面的代碼應(yīng)該放在你的Velocity模版根目錄的VM_global_library.vm文件中。

    #*
     * showerror
     *
     * display an error for the field name supplied if one exists
     * in the supplied errors object.
     *
     * param $errors the object obtained from RequestContext.getErrors( "formBeanName" )
     * param $field the field name you want to display errors for (if any)
     *
     *#
    #macro( showerror $errors $field )
        #if( $errors )
            #if( $errors.getFieldErrors( $field ))
                #foreach($err in $errors.getFieldErrors( $field ))
                    <span class="fieldErrorText">$rc.getMessage($err)</span><br />
                #end
            #end
        #end
    #end     

    13.4.6.3. 將出錯消息和HTML的域關(guān)聯(lián)起來

    最后,在你的HTML表單中,你可以使用和類似下面的代碼為每個輸入域顯示所綁定的出錯消息。

    ## set the following variable once somewhere at the top of
    ## the velocity template
    #set ($errors=$rc.getErrors("commandBean"))
    <html>
    ...
    <form ...>
        <input name="query" value="$!commandBean.query"><br>
        #showerror($errors "query")
    </form>
    ...
    </html>       

    13.4.7. 總結(jié)

    總之,下面是上面那個例子的文件目錄結(jié)構(gòu)。只有一部分被顯示,其他一些必要的目錄沒有顯示出來。文件定位出錯很可能是Velocity視圖不能工作的主要原因,其次在視圖中定義了錯誤的屬性也是很常見的原因。

    ProjectRoot
      |
      +- WebContent
          |
          +- WEB-INF
              |
              +- lib
              |   |
              |   +- velocity-1.3.1.jar
              |   +- spring.jar
              |
              +- velocity
              |   |
              |   +- VM_global_library.vm
              |   +- mainTemplate.vm
              |   +- secondaryTemplate.vm
              |
              +- frontcontroller-servlet.xml
              +- frontcontroller-views.xml
              +- velocity.properties

    13.5. XSLT視圖

    XSLT 一種用于XML文件的轉(zhuǎn)換語言,作為web應(yīng)用的一種表現(xiàn)層技術(shù)非常流行。如果你的應(yīng)用本身需要處理XML文件,或者你的數(shù)據(jù)模型很容易轉(zhuǎn)換成XML文件,XSLT就是一個不錯的選擇。下面介紹如何生成XML文檔用作模型數(shù)據(jù),以及如何在Spring應(yīng)用中使用XSLT轉(zhuǎn)換它們。
    13.5.1. My First Words

    這個Spring應(yīng)用的例子在控制器中創(chuàng)建一組單詞,并把它們加到數(shù)據(jù)模型的映射表中。這個映射表和我們XSLT視圖的名字一起被返回。關(guān)于Spring中Controller的詳細信息,參考第 12.3 節(jié) “控制器” 。 XSLT視圖會把這組單詞生成一個簡單XML文檔用于轉(zhuǎn)換。
    13.5.1.1. Bean的定義

    對于一個簡單的Spring應(yīng)用,配置是標準的。DispatcherServlet配置文件包含一個ViewResolver,URL映射和一個控制器bean..

    <bean id="homeController"class="xslt.HomeController"/>

    ..它實現(xiàn)了我們單詞的產(chǎn)生“邏輯”。
    13.5.1.2. 標準MVC控制器代碼

    控制器邏輯被封裝在AbstractController的子類中,包含象下面這樣的處理器方法。

    protected ModelAndView handleRequestInternal(
        HttpServletRequest req,
        HttpServletResponse resp)
        throws Exception {
           
        Map map = new HashMap();
        List wordList = new ArrayList();
           
        wordList.add("hello");
        wordList.add("world");
          
        map.put("wordList", wordList);
         
        return new ModelAndView("home", map);
    }

    到目前為止,我們沒有做任何XSLT特定的東西。模型數(shù)據(jù)的創(chuàng)建方式和其他Spring的 MVC應(yīng)用相同。現(xiàn)在根據(jù)不同的應(yīng)用配置,這組單詞被作為請求屬性交給JSP/JSTL處理,或者作為VelocityContext里的對象,交給 Velocity處理。為了使XSLT能處理它們,它們必須得轉(zhuǎn)換成某種XML文檔。有一些軟件包可以自動DOM化一個對象圖,但在Spring中,你可以用任何方式把你的模型轉(zhuǎn)換成DOM樹。這樣可以避免使XML轉(zhuǎn)換決定你模型數(shù)據(jù)結(jié)構(gòu),這在使用工具管理DOM化過程的時候是很危險的。
    13.5.1.3. 把模型數(shù)據(jù)轉(zhuǎn)換成XML文檔

    為了從我們的單詞列表或其他模型數(shù)據(jù)中創(chuàng)建DOM文檔,我們繼承org.springframework.web.servlet.view.xslt.AbstractXsltView。同時,我們必須實現(xiàn)抽象方法createDomNode()。傳給它的第一個參數(shù)就是我們的數(shù)據(jù)模型的Map。下面是我們這個應(yīng)用中HomePage類的源程序清單 - 它使用JDOM來創(chuàng)建XML文檔,然后在轉(zhuǎn)換成所需要的W3C節(jié)點,這僅僅是因為我發(fā)現(xiàn)JDOM(和Dom4J)的API比W3C的API簡單。

    package xslt;

    // imports omitted for brevity

    public class HomePage extends AbstractXsltView {

        protected Node createDomNode(
            Map model, String rootName, HttpServletRequest req, HttpServletResponse res
        ) throws Exception {
           
            org.jdom.Document doc = new org.jdom.Document();
            Element root = new Element(rootName);
            doc.setRootElement(root);

            List words = (List) model.get("wordList");
            for (Iterator it = words.iterator(); it.hasNext();) {
                String nextWord = (String) it.next();
                Element e = new Element("word");
                e.setText(nextWord);
                root.addContent(e);
            }

            // convert JDOM doc to a W3C Node and return
            return new DOMOutputter().output( doc );
        }

    }

    13.5.1.3.1. 添加樣式表參數(shù)

    你的視圖子類可以定義一些name/value組成的參數(shù),這些參數(shù)將被加到轉(zhuǎn)換對象中。參數(shù)的名字必須符合你在XSLT模版中使用<xsl:param name="myParam">defaultValue</xsl:param>格式定義的參數(shù)名。為了指定這些參數(shù),可以從AbstractXsltView中重載方法getParameters(),并返回包含name/value組合的Map。
    13.5.1.3.2. 格式化日期和貨幣

    不象JSTL和Velocity,XSLT對和本地化相關(guān)的貨幣和日期格式化支持較弱。Spring為此提供了一個幫助類,讓你在createDomNode()中使用,從而獲得這些支持。詳細請參考org.springframework.web.servlet.view.xslt.FormatHelper的javadoc。
    13.5.1.4. 定義視圖屬性

    下面是單視圖應(yīng)用的屬性文件views.properties(如果你使用基于XML的視圖解析器,比如上面例子中的Velocity,它等價于XML定義),如我們的“My First Words”..

    home.class=xslt.HomePage
    home.stylesheetLocation=/WEB-INF/xsl/home.xslt
    home.root=words

    這兒,你可以看到視圖是如何綁定在由屬性“.class”定義的HomePage類上的,HomePage類處理數(shù)據(jù)模型的DOM化操作。屬性 “stylesheetLocation”指定了將XML文檔轉(zhuǎn)換成HTML文檔時所需要的XSLT文件,而最后一個屬性“.root”指定了XML文檔根節(jié)點的名字。它被上面的HomePage類作為第二個參數(shù)傳遞給createDomNode方法。
    13.5.1.5. 文檔轉(zhuǎn)換

    最后,我們定義了XSLT的代碼來轉(zhuǎn)換上面的XML文檔。在views.properties文件中指定了這個XSLT文件home.xslt存放在war文件里的WEB-INF/xsl下。

    <?xml version="1.0"?>

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="text/html" omit-xml-declaration="yes"/>

        <xsl:template match="/">
            <html>
                <head><title>Hello!</title></head>
                <body>

                    <h1>My First Words</h1>
                    <xsl:for-each select="wordList/word">
                        <xsl:value-of select="."/><br />
                    </xsl:for-each>

                </body>
            </html>
        </xsl:template>

    </xsl:stylesheet>

    13.5.2. 總結(jié)

    下面的WAR文件結(jié)構(gòu)簡單列了一些上面所提到的文件和它們在WAR文件中的位置。

    ProjectRoot
      |
      +- WebContent
          |
          +- WEB-INF
              |
              +- classes
              |    |
              |    +- xslt
              |    |   |
              |    |   +- HomePageController.class
              |    |   +- HomePage.class
              |    |
              |    +- views.properties
              |
              +- lib
              |   |
              |   +- spring.jar
              |
              +- xsl
              |   |
              |   +- home.xslt
              |
              +- frontcontroller-servlet.xml

    當然,你還需要保證XML解析器和XSLT引擎在classpath中可以被找到。JDK 1.4會缺省提供它們,并且大多數(shù)J2EE容器也會提供它們,但是這也是一些已知的可能引起錯誤的原因。
    13.6. 文檔視圖 (PDF/Excel)
    13.6.1. 簡介

    HTML頁面并不總是向用戶顯示數(shù)據(jù)輸出的最好方式,Spring支持從數(shù)據(jù)動態(tài)生成PDF或Excel文件,并使這一過程變得簡單。文檔本身就是視圖,從服務(wù)器以流的方式加上內(nèi)容類型返回文檔,客戶端PC只要運行電子表格軟件或PDF瀏覽軟件就可以瀏覽。

    為了使用Excel電子表格,你需要在你的classpath中加入‘poi’庫文件,而對PDF文件,則需要iText.jar文件。它們都包含在Spring的主發(fā)布包中。
    13.6.2. 配置和安裝

    基于文檔的視圖的處理方式和XSLT視圖幾乎完全相同,下面的部分將以前面的例子為基礎(chǔ),演示了XSLT例子中的控制器是如何使用相同的數(shù)據(jù)模型生成PDF文檔和Excel電子表格(它們可以在Open Office中打開或編輯)。
    13.6.2.1. 文檔視圖定義

    首先,讓我們來修改一下view.properties文件(或等價的xml定義),給兩種文檔類型都添加一個視圖定義。加上剛才XSLT視圖例子的內(nèi)容,整個文件如下。

    home.class=xslt.HomePage
    home.stylesheetLocation=/WEB-INF/xsl/home.xslt
    home.root=words

    xl.class=excel.HomePage

    pdf.class=pdf.HomePage

    如果你添加你的數(shù)據(jù)到一個模版電子表格,必須在視圖定義的‘url’屬性中指定模版位置。
    13.6.2.2. 控制器代碼

    我們用的控制器代碼和前面XSLT例子中用的一樣,除了視圖的名字。當然,你可以干得巧妙一點,使它基于URL參數(shù)或者其他邏輯-這證明了Spring的確在分離視圖和控制器方面非常出色!
    13.6.2.3. 用于Excel視圖的視圖子類化

    正入我們在XSLT例子中做的,為了在生成輸出文檔的過程中實現(xiàn)定制的行為,我們將繼承合適的抽象類。對于Excel,這包括提供一個org.springframework.web.servlet.view.document.AbstractExcelView的子類,并實現(xiàn)buildExcelDocument方法。

    下面是一個我們Excel視圖的源程序清單,它在電子表格中每一行的第一列中顯示模型map中的單詞。

    package excel;

    // imports omitted for brevity

    public class HomePage extends AbstractExcelView {

        protected void buildExcelDocument(
            Map model,
            HSSFWorkbook wb,
            HttpServletRequest req,
            HttpServletResponse resp)
            throws Exception {
       
            HSSFSheet sheet;
            HSSFRow sheetRow;
            HSSFCell cell;

            // Go to the first sheet
            // getSheetAt: only if wb is created from an existing document
            //sheet = wb.getSheetAt( 0 );
            sheet = wb.createSheet("Spring");
            sheet.setDefaultColumnWidth((short)12);

            // write a text at A1
            cell = getCell( sheet, 0, 0 );
            setText(cell,"Spring-Excel test");

            List words = (List ) model.get("wordList");
            for (int i=0; i < words.size(); i++) {
                cell = getCell( sheet, 2+i, 0 );
                setText(cell, (String) words.get(i));

            }
        }
    }

    如果你現(xiàn)在修改控制器使它返回xl作為視圖的名字(return new ModelAndView("xl", map);),并且運行你的應(yīng)用,當你再次對該頁面發(fā)起請求時,Excel電子表格被創(chuàng)建,自動下載。
    13.6.2.4. 用于PDF視圖的視圖子類化

    單詞列表的PDF版本就更為簡單了。這次,需要象下面一樣繼承org.springframework.web.servlet.view.document.AbstractPdfView,并實現(xiàn)buildPdfDocument()方法。

    package pdf;

    // imports omitted for brevity

    public class PDFPage extends AbstractPdfView {

        protected void buildPdfDocument(
            Map model,
            Document doc,
            PdfWriter writer,
            HttpServletRequest req,
            HttpServletResponse resp)
            throws Exception {
           
            List words = (List) model.get("wordList");
           
            for (int i=0; i<words.size(); i++)
                doc.add( new Paragraph((String) words.get(i)));
       
        }
    }

    同樣修改控制器,使它通過return new ModelAndView("pdf", map);返回一個pdf視圖;并在你的應(yīng)用中重新載入該URL。這次就會出現(xiàn)一個PDF文檔,顯示存儲在模型數(shù)據(jù)的map中的單詞。
    13.7. Tapestry

    Tapestry是Apache Jakarta項目(http://jakarta.apache.org/tapestry)下的一個面向組件的web應(yīng)用框架。Spring框架是圍繞輕量級容器概念建立的J2EE應(yīng)用框架。雖然Spring它自己的web表現(xiàn)層功能也很豐富,但是使用Tapestry作為web表現(xiàn)層,Spring容器作為底層構(gòu)建的J2EE應(yīng)用有許多獨特的優(yōu)勢。這一節(jié)將詳細介紹使用這兩種框架的最佳實現(xiàn)。這里假設(shè)你熟悉Tapestry和Spring框架的基礎(chǔ)知識,這里就不再加以解釋了。對于Tapestry和Spring框架的一般性介紹文檔,可以在它們的網(wǎng)站找到。
    13.7.1. 架構(gòu)

    一個由Tapestry和Spring構(gòu)建的典型分層的J2EE應(yīng)用包括一個上層的Tapestry表現(xiàn)層和許多底部層次構(gòu)成,它們存在于一個或多個Spring應(yīng)用上下文中。

        *

          用戶界面層:

          - 主要關(guān)注用戶界面的內(nèi)容

          - 包含某些應(yīng)用邏輯

          - 由Tapestry提供

          - 除了通過Tapestry提供的用戶界面,這一層的代碼訪問實現(xiàn)業(yè)務(wù)層接口的對象。實現(xiàn)對象由Spring應(yīng)用上下文提供。
        *

          業(yè)務(wù)層:

          - 應(yīng)用相關(guān)的“業(yè)務(wù)”代碼

          - 訪問域?qū)ο螅⑶沂褂肕apper API從某種數(shù)據(jù)存儲(數(shù)據(jù)庫)中存取域?qū)ο?br>
          - 存在在一個或多個Spring上下文中

          - 這層的代碼以一種應(yīng)用相關(guān)的方式操作域模型中的對象。它通過這層中的其它代碼和Mapper API工作。這層中的對象由某個特定的mapper實現(xiàn)通過應(yīng)用上下文提供。

          - 既然這層中的代碼存在在Spring上下文中,它由Spring上下文提供事務(wù)處理,而不自己管理事務(wù)。
        *

          域模型:

          - 問題域相關(guān)的對象層次,這些對象處理和問題域相關(guān)的數(shù)據(jù)和邏輯

          - 雖然域?qū)ο髮哟伪粍?chuàng)建時考慮到它會被某種方式持久化,并且為此定義一些通用的約束(例如,雙向關(guān)聯(lián)),它通常并不知道其它層次的情況。因此,它可以被獨立地測試,并且在產(chǎn)品和測試這兩種不同的mapping實現(xiàn)中使用。

          - 這些對象可以是獨立的,也可以關(guān)聯(lián)Spring應(yīng)用上下文以發(fā)揮它的優(yōu)勢,例如隔離,反向控制,不同的策略實現(xiàn),等等。
        *

          數(shù)據(jù)源層:

          - Mapper API(也稱為Data Access Objects):是一種將域模型持久化到某種數(shù)據(jù)存儲(一般是數(shù)據(jù)庫,但是也可以是文件系統(tǒng),內(nèi)存,等等)的API。

          - Mapper API實現(xiàn):是指Mapper API的一個或多個特定實現(xiàn),例如,Hibernate的mapper,JDO的mapper,JDBC的mapper,或者內(nèi)存mapper。

          - mapper實現(xiàn)存在在一個或多個Spring應(yīng)用上下文中。一個業(yè)務(wù)層對象需要應(yīng)用上下文的mapper對象才能工作。
        *

          數(shù)據(jù)庫,文件系統(tǒng),或其它形式的數(shù)據(jù)存儲:

          - 在域模型中的對象根據(jù)一個或多個mapper實現(xiàn)可以存放在不止一個數(shù)據(jù)存儲中

          - 數(shù)據(jù)存儲的方式可以是簡單的(例如,文件系統(tǒng)),或者有它域模型自己的數(shù)據(jù)表達(例如,一個數(shù)據(jù)庫中的schema)。但是它不知道其它層次的情況。

    13.7.2. 實現(xiàn)

    真正的問題(本節(jié)所需要回答的),是Tapestry頁面是如何訪問業(yè)務(wù)實現(xiàn)的,業(yè)務(wù)實現(xiàn)僅僅是定義在Spring應(yīng)用上下文實例中的bean。
    13.7.2.1. 應(yīng)用上下文示例

    假設(shè)我們以xml格式定義的下面的應(yīng)用上下文:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
            "http://www.springframework.org/dtd/spring-beans.dtd">
     
    <beans>
     
        <!-- ========================= GENERAL DEFINITIONS ========================= -->
     
        <!-- ========================= PERSISTENCE DEFINITIONS ========================= -->
     
        <!-- the DataSource -->
        <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
            <property name="jndiName"><value>java:DefaultDS</value></property>
            <property name="resourceRef"><value>false</value></property>
        </bean>
     
        <!-- define a Hibernate Session factory via a Spring LocalSessionFactoryBean -->
        <bean id="hibSessionFactory"
            class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
            <property name="dataSource"><ref bean="dataSource"/></property>
        </bean>
     
        <!--
         - Defines a transaction manager for usage in business or data access objects.
         - No special treatment by the context, just a bean instance available as reference
         - for business objects that want to handle transactions, e.g. via TransactionTemplate.
         -->
        <bean id="transactionManager"
            class="org.springframework.transaction.jta.JtaTransactionManager">
        </bean>
     
        <bean id="mapper"
            class="com.whatever.dataaccess.mapper.hibernate.MapperImpl">
            <property name="sessionFactory"><ref bean="hibSessionFactory"/></property>
        </bean>
      
        <!-- ========================= BUSINESS DEFINITIONS ========================= -->
     
        <!-- AuthenticationService, including tx interceptor -->
        <bean id="authenticationServiceTarget"
            class="com.whatever.services.service.user.AuthenticationServiceImpl">
            <property name="mapper"><ref bean="mapper"/></property>
        </bean>
        <bean id="authenticationService"
            class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
            <property name="transactionManager"><ref bean="transactionManager"/></property>
            <property name="target"><ref bean="authenticationServiceTarget"/></property>
            <property name="proxyInterfacesOnly"><value>true</value></property>
            <property name="transactionAttributes">
                <props>
                    <prop key="*">PROPAGATION_REQUIRED</prop>
                </props>
            </property>
        </bean> 
     
        <!-- UserService, including tx interceptor -->
        <bean id="userServiceTarget"
            class="com.whatever.services.service.user.UserServiceImpl">
            <property name="mapper"><ref bean="mapper"/></property>
        </bean>
        <bean id="userService"
            class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
            <property name="transactionManager"><ref bean="transactionManager"/></property>
            <property name="target"><ref bean="userServiceTarget"/></property>
            <property name="proxyInterfacesOnly"><value>true</value></property>
            <property name="transactionAttributes">
                <props>
                    <prop key="*">PROPAGATION_REQUIRED</prop>
                </props>
            </property>
        </bean> 
     
     </beans>

    在Tapestry應(yīng)用中,我們需要載入這個應(yīng)用上下文,并允許Tapestry頁面訪問authenticationService和 userService這兩個bean,它們分別實現(xiàn)了AuthenticationService接口和UserService接口。
    13.7.2.2. 在Tapestry頁面中獲取bean

    在這點上,web應(yīng)用可以調(diào)用Spring的靜態(tài)工具方法WebApplicationContextUtils.getApplicationContext(servletContext)來獲取應(yīng)用上下文,參數(shù)servletContext是J2EE Servlet規(guī)范定義的標準ServletContext。因此,頁面獲取例如UserService實例的一個簡單方法就象下面的代碼:

        WebApplicationContext appContext = WebApplicationContextUtils.getApplicationContext(
            getRequestCycle().getRequestContext().getServlet().getServletContext());
        UserService userService = appContext.getBean("userService");
        ... some code which uses UserService

    這個方法可以工作。將大部分邏輯封裝在頁面或組件基類的一個方法中可以減少很多冗余。然而,這在某些方面違背了Spring所倡導(dǎo)的反向控制方法,而應(yīng)用中其它層次恰恰在使用反向控制,因為你希望頁面不必向上下文要求某個名字的bean,事實上,頁面也的確對上下文一無所知。

    幸運的是,有一個方法可以做到這一點。這是因為Tapestry已經(jīng)提供一種方法給頁面添加聲明屬性,事實上,以聲明方式管理一個頁面上的所有屬性是首選的方法,這樣Tapestry能夠?qū)傩缘纳芷谧鳛轫撁婧徒M件生命周期的一部分加以管理。
    13.7.2.3. 向Tapestry暴露應(yīng)用上下文

    首先我們需要Tapestry頁面組件在沒有ServletContext的情況下訪問ApplicationContext;這是因為在頁面/組件生命周期里,當我們需要訪問ApplicationContext時,ServletContext并不能被頁面很方便地訪問到,所以我們不能直接使用WebApplicationContextUtils.getApplicationContext(servletContext)。一個方法就是實現(xiàn)一個特定的Tapestry的IEngine來暴露它:

    package com.whatever.web.xportal;
    ...
    import ...
    ...
    public class MyEngine extends org.apache.tapestry.engine.BaseEngine {
     
        public static final String APPLICATION_CONTEXT_KEY = "appContext";
     
        /**
         * @see org.apache.tapestry.engine.AbstractEngine#setupForRequest(org.apache.tapestry.request.RequestContext)
         */
        protected void setupForRequest(RequestContext context) {
            super.setupForRequest(context);
        
            // insert ApplicationContext in global, if not there
            Map global = (Map) getGlobal();
            ApplicationContext ac = (ApplicationContext) global.get(APPLICATION_CONTEXT_KEY);
            if (ac == null) {
                ac = WebApplicationContextUtils.getWebApplicationContext(
                    context.getServlet().getServletContext()
                );
                global.put(APPLICATION_CONTEXT_KEY, ac);
            }
        }
    }

    這個engine類將Spring應(yīng)用上下文作為“appContext”屬性存放在Tapestry應(yīng)用的“Global”對象中。在Tapestry應(yīng)用定義文件中必須保證這個特殊的IEngine實例在這個Tapestry應(yīng)用中被使用。例如,

    file: xportal.application:
     
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE application PUBLIC
        "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
        "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
    <application
        name="Whatever xPortal"
        engine-class="com.whatever.web.xportal.MyEngine">
    </application>

    13.7.2.4. 組件定義文件

    現(xiàn)在在我們的頁面或組件定義文件(*.page或*.jwc)中,我們僅僅添加property-specification元素從ApplicatonContext中獲取bean,并為這些bean創(chuàng)建頁面或組件屬性。例如:

        <property-specification name="userService"
                                type="com.whatever.services.service.user.UserService">
            global.appContext.getBean("userService")
        </property-specification>
        <property-specification name="authenticationService"
                                type="com.whatever.services.service.user.AuthenticationService">
            global.appContext.getBean("authenticationService")
        </property-specification>

    在property-specification中定義的OGNL表達式使用上下文中的bean來指定屬性的初始值。整個頁面定義文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE page-specification PUBLIC
        "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
        "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
        
    <page-specification class="com.whatever.web.xportal.pages.Login">
     
        <property-specification name="username" type="java.lang.String"/>
        <property-specification name="password" type="java.lang.String"/>
        <property-specification name="error" type="java.lang.String"/>
        <property-specification name="callback" type="org.apache.tapestry.callback.ICallback" persistent="yes"/>
        <property-specification name="userService"
                                type="com.whatever.services.service.user.UserService">
            global.appContext.getBean("userService")
        </property-specification>
        <property-specification name="authenticationService"
                                type="com.whatever.services.service.user.AuthenticationService">
            global.appContext.getBean("authenticationService")
        </property-specification>
      
        <bean name="delegate" class="com.whatever.web.xportal.PortalValidationDelegate"/>
     
        <bean name="validator" class="org.apache.tapestry.valid.StringValidator" lifecycle="page">
            <set-property name="required" expression="true"/>
            <set-property name="clientScriptingEnabled" expression="true"/>
        </bean>
     
        <component id="inputUsername" type="ValidField">
            <static-binding name="displayName" value="Username"/>
            <binding name="value" expression="username"/>
            <binding name="validator" expression="beans.validator"/>
        </component>
      
        <component id="inputPassword" type="ValidField">
            <binding name="value" expression="password"/>
           <binding name="validator" expression="beans.validator"/>
           <static-binding name="displayName" value="Password"/>
           <binding name="hidden" expression="true"/>
        </component>
     
    </page-specification>

    13.7.2.5. 添加抽象訪問方法

    現(xiàn)在在頁面或組件本身的Java類定義中,我們所需要做的是為我們定義的屬性添加抽象getter方法。當Tapestry真正載入頁面或組件時,Tepestry會對類文件作一些運行時的代碼處理,添加已定義的屬性,掛接抽象getter方法到新創(chuàng)建的域上。例如:

        // our UserService implementation; will come from page definition
        public abstract UserService getUserService();
        // our AuthenticationService implementation; will come from page definition
        public abstract AuthenticationService getAuthenticationService();

    這個例子的login頁面的完整Java類如下:

    package com.whatever.web.xportal.pages;
     
    /**
     *  Allows the user to login, by providing username and password.
     *  After succesfully logging in, a cookie is placed on the client browser
     *  that provides the default username for future logins (the cookie
     *  persists for a week).
     */
    public abstract class Login extends BasePage implements ErrorProperty, PageRenderListener {
     
        /** the key under which the authenticated user object is stored in the visit as */
        public static final String USER_KEY = "user";
      
        /**
         * The name of a cookie to store on the user's machine that will identify
         * them next time they log in.
         **/
        private static final String COOKIE_NAME = Login.class.getName() + ".username"; 
        private final static int ONE_WEEK = 7 * 24 * 60 * 60;
     
        // --- attributes
     
        public abstract String getUsername();
        public abstract void setUsername(String username);
     
        public abstract String getPassword();
        public abstract void setPassword(String password);
     
        public abstract ICallback getCallback();
        public abstract void setCallback(ICallback value);
       
        public abstract UserService getUserService();
     
        public abstract AuthenticationService getAuthenticationService();
     
        // --- methods
     
        protected IValidationDelegate getValidationDelegate() {
            return (IValidationDelegate) getBeans().getBean("delegate");
        }
     
        protected void setErrorField(String componentId, String message) {
            IFormComponent field = (IFormComponent) getComponent(componentId);
            IValidationDelegate delegate = getValidationDelegate();
            delegate.setFormComponent(field);
            delegate.record(new ValidatorException(message));
        }
     
        /**
         *  Attempts to login.
         *
         *  <p>If the user name is not known, or the password is invalid, then an error
         *  message is displayed.
         *
         **/
        public void attemptLogin(IRequestCycle cycle) {
        
            String password = getPassword();
     
            // Do a little extra work to clear out the password.
     
            setPassword(null);
            IValidationDelegate delegate = getValidationDelegate();
     
            delegate.setFormComponent((IFormComponent) getComponent("inputPassword"));
            delegate.recordFieldInputValue(null);
     
            // An error, from a validation field, may already have occured.
     
            if (delegate.getHasErrors())
                return;
     
            try {
                User user = getAuthenticationService().login(getUsername(), getPassword());
               loginUser(user, cycle);
            }
            catch (FailedLoginException ex) {
                this.setError("Login failed: " + ex.getMessage());
                return;
            }
        }
     
        /**
         *  Sets up the {@link User} as the logged in user, creates
         *  a cookie for their username (for subsequent logins),
         *  and redirects to the appropriate page, or
         *  a specified page).
         *
         **/
        public void loginUser(User user, IRequestCycle cycle) {
        
            String username = user.getUsername();
     
            // Get the visit object; this will likely force the
            // creation of the visit object and an HttpSession.
     
            Map visit = (Map) getVisit();
            visit.put(USER_KEY, user);
     
            // After logging in, go to the MyLibrary page, unless otherwise
            // specified.
     
            ICallback callback = getCallback();
     
            if (callback == null)
                cycle.activate("Home");
            else
                callback.performCallback(cycle);
     
            // I've found that failing to set a maximum age and a path means that
            // the browser (IE 5.0 anyway) quietly drops the cookie.
     
            IEngine engine = getEngine();
            Cookie cookie = new Cookie(COOKIE_NAME, username);
            cookie.setPath(engine.getServletPath());
            cookie.setMaxAge(ONE_WEEK);
     
            // Record the user's username in a cookie
     
            cycle.getRequestContext().addCookie(cookie);
     
            engine.forgetPage(getPageName());
        }
      
        public void pageBeginRender(PageEvent event) {
            if (getUsername() == null)
                setUsername(getRequestCycle().getRequestContext().getCookieValue(COOKIE_NAME));
        }
    }

    13.7.3. 小結(jié)

    在這個例子中,我們用聲明的方式將定義在Spring的ApplicationContext中業(yè)務(wù)bean能夠被頁面訪問。頁面類并不知道業(yè)務(wù)實現(xiàn)從哪里來,事實上,也很容易轉(zhuǎn)移到另一個實現(xiàn),例如為了測試。這樣的反向控制是Spring框架的主要目標和優(yōu)點,在這個Tapestry應(yīng)用中,我們在J2EE棧上自始至終使用反向控制。

    posted on 2007-06-13 09:53 chenguo 閱讀(744) 評論(0)  編輯  收藏 所屬分類: Spring Dev

    <2025年5月>
    27282930123
    45678910
    11121314151617
    18192021222324
    25262728293031
    1234567

    導(dǎo)航

    統(tǒng)計

    留言簿

    隨筆分類(1)

    文章分類(52)

    好友 小山的博客

    最新隨筆

    最新評論

    主站蜘蛛池模板: 亚洲综合熟女久久久30p| 又黄又爽一线毛片免费观看 | 中国一级特黄的片子免费 | 在线免费观看色片| 99999久久久久久亚洲| 毛片免费观看网站| 亚洲第一综合天堂另类专| 国产成人精品高清免费| 免费精品国产自产拍在线观看| 国产美女精品久久久久久久免费| 精品无码专区亚洲| 亚洲国产成人久久综合一区77| 国产精品玖玖美女张开腿让男人桶爽免费看 | 一本岛v免费不卡一二三区| 亚洲精品亚洲人成在线观看下载| 一级毛片a免费播放王色电影| 国产91精品一区二区麻豆亚洲 | 久久青青成人亚洲精品| 88av免费观看入口在线| 狠狠色香婷婷久久亚洲精品| 在线jlzzjlzz免费播放| jizz免费观看| 亚洲视频网站在线观看| 免费看AV毛片一区二区三区| 免费人成网上在线观看| 亚洲av无码潮喷在线观看| 无码中文在线二区免费| 美女露隐私全部免费直播| 亚洲国产成人片在线观看 | 国产在线19禁免费观看| 国产成人免费AV在线播放| 亚洲欧洲国产视频| 亚洲爽爽一区二区三区| 亚洲视频免费一区| 菠萝菠萝蜜在线免费视频| 久久久亚洲精品视频| 免费视频淫片aa毛片| 中文字幕高清免费不卡视频| 亚洲成人黄色网址| 亚洲中文久久精品无码ww16| 国产桃色在线成免费视频|