webx源码svn和构建其应用

本文全部来自互联网

svn check out :

http://code.taobao.org/svn/webx

 

----------------------------------------

创建简单的WEBX应用
1.前言
阿里巴巴网站所采用的WEB应用框架是Webx。本文将指导你建立一个简单的Webx应用。
在尝试本文的例子之前,
1.    请先阅读《新人指南》,并按照其指示,在你的电脑上安装适当的软件。
2.    请再阅读《创建简单的WEB应用》,并熟悉Servlet API中的基本概念,以及创建、布署、调试WEB应用的技巧。
2.准备 – 创建一个新项目
为了能进行本文的例子,请用antx创建一个新的工程项目。
mkdir workshop-2
cd workshop-2
antx gen:web
这样,你就会得到一个项目的初始目录:
workshop-2
│  project.jelly
│  project.xml

├─docs

└─src
    ├─descriptors
    │  └─web
    │          web.xml
    │
    ├─java
    └─webroot
        │
        └─WEB-INF
然后,请修改几个文件。第一个文件是:project.xml。这个文件指出了项目的名称、依赖哪些jar包等信息。
<?xml version="1.0" encoding="GB2312"?>
<project id="workshop/webapp-2">
    <build>
        <dependencies>
            <include uri="toolkit/webx/turbine" version="2.0"/>
        </dependencies>
    </build>
</project>
第二个要修改的文件是:project.jelly。这个文件指出了该项目默认build成一个WAR包。
<?xml version="1.0" encoding="GB2312"?>
<project default="war">
</project>
改完这两个文件以后,就可以生成eclipse项目了。请在项目目录下执行:
antx eclipse
这样便生成了.classpath和.project这两个eclipse相关的文件。请打开Eclipse并导入这个项目。
至此,你就可以在eclipse下开发项目了。
3.创建第一个Webx应用
3.1.创建、布署、运行
利用Webx框架来创建WEB应用,是一件比较简单的事情。
下面,我们将创建一个在屏幕上显示Hello World的Webx应用。与其说“创建”不如说“配置”,因为显示Hello World页面,并不需要写一行程序。
3.1.1.创建web.xml
首先,请修改src/descriptors/web目录下的web.xml文件:
<?xml version="1.0" encoding="GB2312"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
    <servlet>
        <servlet-name>WebxController</servlet-name>
        <servlet-class>com.alibaba.webx.WebxControllerServlet</servlet-class>
        <init-param>
            <param-name>initAllServices</param-name>
            <param-value>true</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>WebxController</servlet-name>
        <url-pattern>*.htm</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>WebxController</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>
这个web.xml中定义了一个被称为Webx controller的servlet,并把它映射到*.htm和*.do两个URL中。Webx Controller Servlet的定义中,包含一个initAllServices参数。将initAllServices设置成true,会让webx在启动时就初始化所有的services,以便及早发现初始化异常。
3.1.2.创建log4j.xml
虽然不是必须的,但是我还是建议你配置一下日志系统。妥善地配置好日志系统,有助于你在将来的开发、调试中发现错误。请在src/webroot/WEB-INF/目录下,创建一个log4j.xml,内容如下:
<?xml version="1.0" encoding="GB2312"?>
<!DOCTYPE log4j:configuration SYSTEM "http://toolkit.alibaba-inc.com/dtd/log4j/log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    <!-- ===================================================================== -->
    <!--  以下是appender的定义                                                 -->
    <!-- ===================================================================== -->
    <appender name="WORKSHOP" class="org.apache.log4j.FileAppender">
        <param name="file" value="${loggingRoot}/${localHost}/workshop.log"/>
        <param name="append" value="false"/>
        <param name="encoding" value="GB2312"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d [%X{requestURIWithQueryString}] %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
    <appender name="WORKSHOP-ERROR" class="org.apache.log4j.ConsoleAppender">
        <param name="target" value="System.err"/>
        <param name="encoding" value="GB2312"/>
        <param name="threshold" value="warn"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d [%X{requestURIWithQueryString}] %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
    <appender name="RESOURCE" class="org.apache.log4j.FileAppender">
        <param name="file" value="${loggingRoot}/${localHost}/workshop-resource.log"/>
        <param name="append" value="false"/>
        <param name="encoding" value="GB2312"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d [%X{requestURIWithQueryString}] %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
    <appender name="FILTER" class="org.apache.log4j.FileAppender">
        <param name="file" value="${loggingRoot}/${localHost}/workshop-filter.log"/>
        <param name="append" value="true"/>
        <param name="encoding" value="GB2312"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
    <appender name="APACHE" class="org.apache.log4j.FileAppender">
        <param name="file" value="${loggingRoot}/${localHost}/workshop-apache.log"/>
        <param name="append" value="true"/>
        <param name="encoding" value="GB2312"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d [%X{requestURIWithQueryString}] %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
    <appender name="VELOCITY" class="org.apache.log4j.FileAppender">
        <param name="file" value="${loggingRoot}/${localHost}/workshop-velocity.log"/>
        <param name="append" value="true"/>
        <param name="encoding" value="GBK"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/>
        </layout>
    </appender>
    <!-- ===================================================================== -->
    <!--  以下是logger的定义                                                   -->
    <!-- ===================================================================== -->
    <logger name="com.alibaba.webx.filter" additivity="false">
        <level value="WARN"/>
        <appender-ref ref="FILTER"/>
    </logger>
    <logger name="com.alibaba.service.VelocityService" additivity="false">
        <level value="WARN"/>
        <appender-ref ref="VELOCITY"/>
    </logger>
    <logger name="com.alibaba.service.resource" additivity="false">
        <level value="DEBUG"/>
        <appender-ref ref="RESOURCE"/>
    </logger>
    <logger name="com.alibaba.service.ResourceLoaderService" additivity="false">
        <level value="DEBUG"/>
        <appender-ref ref="RESOURCE"/>
    </logger>
    <logger name="org.apache.commons.beanutils">
        <level value="error"/>
    </logger>
    <logger name="org.apache.commons.digester">
        <level value="error"/>
    </logger>
    <logger name="org.apache" additivity="false">
        <level value="WARN"/>
        <appender-ref ref="APACHE"/>
    </logger>
    <!-- ===================================================================== -->
    <!--  Root logger的定义                                                    -->
    <!-- ===================================================================== -->
    <root>
        <level value="DEBUG"/>
        <appender-ref ref="WORKSHOP"/>
        <appender-ref ref="WORKSHOP-ERROR"/>
    </root>
</log4j:configuration>
这个配置文件有点长 —— 但你完全可以按照你的意愿来自由地修改这个文件。配置的方法请参见Log4j的文档。
3.1.3.创建webx.xml
每个webx应用,都需要有一个webx.xml配置文件。这个文件定义了Webx所用到的所有services的配置。请在src/webroot/WEB-INF/目录下创建webx.xml:
<?xml version="1.0" encoding="GB2312"?>
<!DOCTYPE configuration PUBLIC "-//ALIBABA//DTD Services Configuration V1.0//EN"
        "http://toolkit.alibaba-inc.com/dtd/toolkit/service/services.dtd">
<configuration>
    <services>
        <service name="PipelineService">
            <property name="pipeline.default.descriptor" value="/WEB-INF/pipeline.xml"/>
        </service>
    </services>
</configuration>
在这个文件中,目前只配置了一个Service —— PipelineService(管道服务)。PipelineService是Webx的核心服务,提供了webx请求执行的流程。
3.1.4.创建pipeline.xml
接下来,让我们创建/WEB-INF/pipeline.xml管道配置文件。
<pipeline>
    <valve class="com.alibaba.service.pipeline.TryCatchFinallyValve">
        <try>
            <valve class="com.alibaba.turbine.pipeline.SetLoggingContextValve"/>
            <valve class="com.alibaba.turbine.pipeline.SetLocaleValve"
 defaultLocale="zh_CN" defaultCharset="GBK"/>
            <valve class="com.alibaba.turbine.pipeline.AnalyzeURLValve"/>
            <valve class="com.alibaba.turbine.pipeline.ChooseValve" label="processModule">
                <when extension="jsp, vm">
                    <valve class="com.alibaba.turbine.pipeline.PerformActionValve" actionParam="action"/>
                    <valve class="com.alibaba.turbine.pipeline.PerformScreenTemplateValve"/>
                </when>
                <when extension="do">
                    <valve class="com.alibaba.turbine.pipeline.PerformActionValve" actionParam="action"/>
                    <valve class="com.alibaba.turbine.pipeline.PerformScreenValve"/>
                </when>
            </valve>
            <valve class="com.alibaba.turbine.pipeline.RedirectTargetValve" goto="processModule"/>
        </try>
        <catch>
            <valve target="error.vm" class="com.alibaba.turbine.pipeline.SetErrorPageValve"/>
            <valve class="com.alibaba.turbine.pipeline.PerformScreenTemplateValve"/>
        </catch>
        <finally>
            <valve class="com.alibaba.turbine.pipeline.SetLoggingContextValve" action="cleanup"/>
        </finally>
    </valve>
</pipeline>
这个文件的意思过会再解释。让我们先创建一个页面。
3.1.5.创建页面模板
请在src/webroot目录下,创建如下子目录:
src/webroot
└─templates
  ├─control
  ├─layout
  └─screen
然后在templates/screen目录下,创建一个hello.vm如下:
<html>
<head>
  <title>Hello!</title>
</head>
<body>
  <p>你好,世界!</p>
  <p>现在时间:$date</p>
</body>
</html>
3.1.6.布署、运行
至此,所有的文件和目录都已经创建好了。我们可以对它打包和发布,请在workshop-2目录下执行下列命令:
antx
antxexpand target\workshop-webapp-2.war \tomcat\webapps\workshop
其中\tomcat为你安装tomcat的目录。
启动tomcat,打开浏览器看一下效果:
 
这个页面是不是有问题?是的,这里显示的当前时间为$date。先别急,我们会稍后解决这个问题。
3.2.审视Webx应用的结构
这是一个简单的应用。由于它是基于标准的JavaEE Web应用规范,因此它的目录结构也没有什么特殊的地方:
workshop-2

├─templates
│  ├─control
│  ├─layout
│  └─screen
│          hello.vm

└─WEB-INF
    │  log4j.xml
    │  pipeline.xml
    │  web.xml
    │  webx.xml
    │
    ├─classes
    └─lib
            *.jar
通常我们会把所有配置文件都放在WEB-INF目录下。除了标准的web.xml以外,我们增加了webx.xml、pipeline.xml和log4j.xml等几个文件。随着应用变得复杂,我们后面还会创建少量新的配置文件。
通常我们总是把模板放在templates目录下,不过只要你喜欢,你完全可以改成其它的目录名。Vm后缀的为Velocity的模板,jsp后缀的为JSP模板,ftl后缀的为Freemarker模板。
3.3.Webx执行的流程
当你在浏览器里敲入http://localhost:8080/workshop/hello.htm时,在Webx中发生了哪些事呢?
很简单,首先,根据web.xml中的映射,*.htm被映射到Webx Controller Servlet。因此Webx Controller Servlet会被激活来处理这个请求。而Webx Controller Servlet只做了一件事,就是:执行pipeline。
所谓pipeline,即管道,它是由一个或多个“阀门Valve”构成的。我们可以想象,水从管道的一头流入,从管道另一头流出,其中经过很多个阀门。事实上,阀门可以控制水流的方向、甚至改变水分子的组成结构。这真是一个不寻常的管道。
管道是由PipelineService来创建并管理的。在上面的例子中,PipelineService读取/WEB-INF/pipeline.xml来创建管道。在请求被处理的过程中,这个管道做了哪些事呢?让我们逐个简介一下。
3.3.1.TryCatchFinallyValve
<valve class="com.alibaba.service.pipeline.TryCatchFinallyValve">
    <try> …… </try>
    <catch> …… </catch>
    <finally> …… </finally>
</valve>
这个阀门类似于Java中的try-catch-finally结构。它将整个管道分成了三个分支:try分支、catch分支、finally分支。
1.    首先,try分支会被执行。
2.    在执行过程中,如果发生异常,就会转入到catch分支。通常可以在这里做错误处理的工作。
3.    无论发生异常与否,finally分支都会被执行。通常可以在这里做一些扫尾工作。
3.3.2.SetLoggingContextValve
<valve class="com.alibaba.service.pipeline.TryCatchFinallyValve">
    <try>
        <valve class="com.alibaba.turbine.pipeline.SetLoggingContextValve"/>
    </try>
    <catch>
        ……
    </catch>
    <finally>
        <valve class="com.alibaba.turbine.pipeline.SetLoggingContextValve" action="cleanup"/>
    </finally>
</valve>
这是一个可写可不写的阀门,但是写了它以后,有助于我们在日志中发现错误的根源。
下面是我们的log4j日志文件中的一行错误信息:
2007-01-31 13:36:05,509 [/workshop/hello.htm] ERROR screen.Error - Error occurred while processing the HTTP request
发现了吗?Log4j把导致错误的请求的URL也记录在案了!这是怎么做到的呢?原来,log4j有一个叫做MDC的功能,它利用ThreadLocal变量保存当前线程的上下文信息。回看我们的log4j.xml配置:
<appender ……>
    ……
    <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="%d [%X{requestURIWithQueryString}] %-5p %c{2} - %m%n"/>
    </layout>
</appender>
其中,%X{requestURIWithQueryString}就是引用了MDC中的内容。现在的问题是,MDC何时被设置?当然是在请求开始的时候设置。SetLoggingContextValve做的就是这件事。最后,别忘了,当请求结束时,我们必须在finally分支中,用SetLoggingContextValve清除MDC的内容。
MDC中可以包含哪些内容呢?事实上可以包含任何内容。SetLoggingContextValve设置了如下内容:
%X{参数名}    说明    示例
method    请求的方法    GET、POST
requestURL    完整URL,不含query string    http://localhost:8080/workshop/hello.htm
requestURLWithQueryString    完整URL,包含query string    http://localhost:8080/workshop/hello.htm?id=1
requestURI    不包含host信息的URI,不含query string    /workshop/hello.htm
requestURIWithQueryString    不包含host信息的URI,包含query string    /workshop/hello.htm?id=1
queryString    Query string    id=1&submit=true
remoteHost    用户主机名    127.0.0.1
remoteAddr    用户IP地址    127.0.0.1
userAgent    用户浏览器类型    Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
referrer    用户是从哪个页面点击到该页的    http://localhost:8080/workshop/hello.htm
事实上,我们完全可以扩展SetLoggingContextValve,以实现更多的功能,例如:在日志中显示当前登录的用户名。SetLoggingContextValve使日志信息变得对我们更有意义。
3.3.3.SetLocaleValve
SetLocaleValve被用来确定请求的输入/输出字符集编码、地域信息。
<valve class="com.alibaba.turbine.pipeline.SetLocaleValve"
defaultLocale="zh_CN"
defaultCharset="GBK"/>
    地域信息(Locale) —— 会影响ResourceBundle、表单出错提示信息、邮件生成等和Locale相关的组件的执行结果。在上面的配置中,默认将采用zh_CN(即中国大陆)作为当前请求的Locale。
    字符集编码(Charset) —— 用来作为输出页面时所用的编码(即Content Type中指定的charset,输出字符集编码),和解析表单或query string时所用的编码(即输入字符集编码)。在上面的配置上,默认使用“GBK”作为字符集编码。
在正常情况下,浏览器会用Content Type中指定的charset来提交表单,因此输入、输出编码是一致的。然而,有些情况下,输入、输出编码会不一致。比如,通过外部系统提交的信息(如贸易通、旺旺、合作网站),其字符集编码未必和网站一致。SetLocaleValve提供了一种特别的参数,来处理这种特殊情况。
SetLocaleValve的处理逻辑是这样的:
1.    一般情况下,SetLocaleValve将根据其配置中的defaultLocale和defaultCharset来确定当前locale和charset。该charset将被用于解析参数和输出页面。
2.    如果URL中包含类似这样的参数:_lang=zh_HK:UTF-8,这意味着用户要求进入中国香港版(zh_HK)网页,并使用UTF-8作为默认的charset。SetLocaleValve会把这个值保存在cookie或session中。这样后继的页面不用指定_lang参数,也会使用(zh_HK,UTF-8)地域和字符集编码。
3.    如果URL中包含参数:_lang=default,这意味着用户要求回到系统默认的locale和charset,即valve参数中所指定的defaultLocale和defaultCharset。在本例中,分别为zh_CN和GBK。
4.    如果URL中包含参数:_input_charset=UTF-8,这意味着必须用UTF-8来解码参数。需要注意的是,这个值可能和valve配置中指定的defaultCharset不同。SetLocaleValve将仍然以前面所说的规则来确定页面的输出字符集编码。外部系统(如贸易通、旺旺、合作网站)通过HTTP方式向系统发送请求时,最好指定该参数,而不是假设网站的默认charset。这样,当网站的默认charset发生改变时,也不会影响到外部系统。
3.3.4.AnalyzeURLValve
AnalyzeURLValve用来分析URL的成分。
<valve class="com.alibaba.turbine.pipeline.AnalyzeURLValve"/>
有不少初学者误以为URL和物理文件之间是一一对应的。例如:http://localhost:8080/workshop/hello.htm代表workshop目录下有一个hello.htm文件。这是不一定的。事实上,你应该把URL看作一个参数。一个URL实际对应的操作是什么,可以由应用程序自己来决定。
以http://localhost:8080/workshop/hello.htm为例,URL的分析分为下面几步:
1.    /workshop被称为Context Path。应用服务器看到/workshop,就把这个请求交给workshop应用来接管。
2.    /hello.htm被称为Servlet Path。因为我们在web.xml中把*.htm映射到Webx Controller Servlet,所以Webx Controller Servlet就接管了这个请求。
3.    Webx Controller Servlet激活pipeline,继而调用AnalyzeURLValve来分析/hello.htm是什么意思。根据webx默认的映射规则,/hello.htm被转换成/hello.vm。
经过上述转换,最后得到的结果被称为target。在这个例子中,target的值为/hello.vm。
有人想当然认为/hello.vm是代表Velocity模板,事实上,这也是不一定的。到目前为止,只能称它为“target”。在后续的valve中,我们会将/hello.vm解释为Velocity模板。
3.3.5.ChooseValve
ChooseValve用来作条件分支。
<valve class="com.alibaba.turbine.pipeline.ChooseValve">
    <when extension="jsp, vm">
        ……
    </when>
    <when extension="do">
        ……
    </when>
    <otherwise>
        ……
    </otherwise>
</valve>
当<when>中的条件被满足时,这个when所包含的分支就被执行。如果所有when都不满足,就执行otherwise分支。
<when>标签支持多种条件:
1.    如果target的后缀为jsp或vm,那么就执行该分支。
<when extension="jsp, vm">
2.    如果target的值以/images/开头,那么就执行分支。
<when target="/images/**">
3.    如果path的值以/member/images/开头,那么就执行分支。和target不同,path是URL去掉contextPath之后的剩余部分,而target是由AnalyseURLValve翻译而来的。
<when path="/member/images/**">
在我们的例子中,/hello.vm匹配了<when extension=”jsp,vm”>分支。在这个分支里,/hello.vm被视作screen模板,并作进一步处理。
3.3.6.PerformActionValve
PerformActionValve用来执行action。
<valve class="com.alibaba.turbine.pipeline.PerformActionValve" actionParam="action"/>
参数actionParam的值指明了valve将根据什么参数来确定action的名称。
例如,假如URL为http://localhost:8080/workshop/hello.htm?action=myAction,那么valve将会执行MyAction类。
有关action的细节,我们将在后面展现。
3.3.7.PerformScreenTemplateValve
PerformScreenTemplateValve用来执行screen和模板。
<valve class="com.alibaba.turbine.pipeline.PerformScreenTemplateValve"/>
假如target的值为:/xxx/yyy/hello.vm,那么,valve会:
1.    在/templates/screen目录下,找到/xxx/yyy/hello.vm模板。
2.    依次查找screen类:
a)    xxx.yyy.Hello        (如果找不到,尝试下一个)
b)    xxx.yyy.Default    (如果找不到,尝试下一个)
c)    xxx.Default        (如果找不到,尝试下一个)
d)    Default            (如果找不到,尝试下一个)
e)    TemplateScreen    (系统默认screen,不可能找不到的)
3.    执行screen,并渲染screen模板。
4.    如果存在layout布局,则渲染layout,并将screen放于布局中。
关于layout布局,后面会讲到。
3.3.8.PerformScreenValve
和前一个valve不同,PerformScreenValve不关心模板,只关心screen类。
<valve class="com.alibaba.turbine.pipeline.PerformScreenValve"/>
假如target的值为:/xxx/yyy/hello.do,那么,valve会:
1.    查找screen类:xxx.yyy.Hello
2.    执行screen类。
3.    如果存在layout布局,则渲染layout,并将screen放于布局中。
3.3.9.RedirectTargetValve
<valve class="……" label="myLabel">
……
<valve class="com.alibaba.turbine.pipeline.RedirectTargetValve" goto="myLabel"/>
RedirectTargetValve实现了内部重定向。
如果在screen、action里调用了rundata.setRedirectTarget(),那么,RedirectTargetValve会将控制转向到标记了label的valve上,从而实现内部的循环。在上面的例子中,PerformActionValve和PerformScreenTemplateValve(或PerformScreenValve)被重新执行了,但target的值因setRedirectTarget()而改变,因而实现了内部重定向。
3.3.10.SetErrorPageValve
<valve class="com.alibaba.turbine.pipeline.SetErrorPageValve" target="error.vm"/>
SetErrorPageValve的工作是显示错误页面error.vm。
4.增加布局
在实际的应用中,一组相关的页面会有类似的布局。而布局往往和页面所要表现的业务逻辑显得无关,例如显示菜单、标题、版权等信息。
现在,我们在前面的例子中,增加“布局”的功能。
 
4.1.创建布局
为了创建类似图中所示的布局,我们需要在templates/layout目录下创建一个default.vm:
<html>
<head>
  <title>Hello!</title>
  <style type="text/css">
  <!--
    .border { border: 1px dashed #000000; }
  -->
  </style>
</head>
<body>
<table width="100%"  border="0" cellspacing="10" cellpadding="20">
<tr>
  <td colspan="2" class="border">$control.setTemplate("header.vm")</td>
</tr>
<tr>
  <td width="20%" class="border">$control.setTemplate("menu.vm")</td>
  <td class="border">$screen_placeholder</td>
</tr>
<tr>
  <td colspan="2" class="border">$control.setTemplate("bottom.vm")</td>
</tr>
</table>
</body>
</html>
在这个布局中,我们看到有两种特别的标记:
1.    $screen_placeholder —— 这个标记将被screen的内容所取代。在本例中,即templates/screen/hello.vm的内容。
2.    $control.setTemplate(“…”) —— 这个标记将被control的内容所取代。Control是一种可重用的页面片段,它的模板在templates/control目录下。
4.2.创建control模板
在这个layout中,我们引用了三个control:header.vm、menu.vm和bottom.vm。我们需要在templates/control目录下创建这三个文件。内容分别如下:
header.vm
<h1>标题、Logo</h1>
menu.vm
<ul>
<li>菜单</li>
<li>菜单</li>
<li>菜单</li>
<li>菜单</li>
<li>菜单</li>
</ul>
bottom.vm
<h1>版权信息</h1>
4.3.修改screen模板
使用layout以后,screen模板不再充当显示完整页面的角色,而是只显示和业务相关的部分页面。因此,我们需要稍微修改一下screen模板,去掉其内容中头尾的部分。因为这部分内容,已经写在layout模板中了。
<html>
<head>
  <title>Hello!</title>
</head>
<body>
  <p>你好,世界!</p>
  <p>现在时间:$date</p>
</body>
</html>
重新启动应用服务器。现在,你应该看到一个完全不同的页面了。
 
4.4.查找布局的规则
刚才我们创建了default.vm布局。需要强调的是,这个布局不仅仅对hello.vm有效,而且对所有页面都有效。不信的话,你可以在templates/screen目录下创建一个新的screen试试看。不需要重启机器,你就可以看到效果!所有的页面现在都被“框”在这个布局里面了。
假如我的大部分页面的布局是相同的,但是有一两个页面例外,我该怎么做呢?有办法。Webx的布局规则很灵活,足够应付大部分的场景。假如我的screen模板名为/xxx/yyy/hello.vm,那么Webx将依次查找下面的layout模板:
1.    /xxx/yyy/hello.vm        (如果找不到,尝试下一个)
2.    /xxx/yyy/default.vm    (如果找不到,尝试下一个)
3.    /xxx/default.vm        (如果找不到,尝试下一个)
4.    /default.vm            (如果找不到,尝试下一个)
5.    (如果上面所有都找不到,就显示不带layout的screen)
如果你希望对/hello.vm指定layout,你可以在templates/layout目录下,为其特别创建一个和screen同名的layout模板,就可以了。Webx假设同一目录下的模板,在功能上是相关的,因此布局也是相似的。因此你不仅可以在layout根目录下设置default.vm,也可以在各级子目录下设置default.vm。
4.5.指定页面标题
细心的你也许已经注意到了,采用layout的机制以后,页面的title是由layout来控制的,而不是由screen来控制的。因为screen的模板中已经不包含<html><head><title/></head></html>标签了。但这样是不合逻辑的,因为不同的screen可能会有不同的标题。除了标题,还有Java Script、CSS等很多内容,都应该由screen来控制。怎么解决这个矛盾呢?
Webx提供了一个HtmlPageAttributeTool工具类来解决这个问题。
class HtmlPageAttributeTool {

    addBodyAttribute(String, String);
    addScript(String);
    addStyle(String);
    addStyleSheet(String);
    addStyleSheet(String, String, String, String);
   
    setAlinkColor(String);
    setBackground(String);
    setBgColor(String);
    setDescription(String);
    setHttpEquiv(String, String);
    setKeywords(String);
    setLinkColor(String);
    setTextColor(String);
    setTitle(String);
    setVlinkColor(String);
   
    getBodyAttributes();
    getDefaultDoctype();
    getHttpEquivs();
    getMetaTags();
    getScripts();
    getStyles();
    getStyleSheets();
    getTitle();

}
你可以用$page这个特殊的变量来引用这个对象,并调用其方法修改页面title,甚至页面颜色、CSS、JavaScript等全局页面属性。让我们修改一下templates/screen/hello.vm:
$page.setTitle("新标题!")

<p>你好,世界!</p>
<p>现在时间:$date</p>
注意,在$page中所设置的属性并不会自动出现在页面中。我们还需要修改一下layout模板做配合。修改后的templates/layout/default.vm模板如下:
<html>
<head>
  #showHead ("Hello!")
  ……
</head>
<body #bodyAttributes ()>
……
</body>
</html>
在这个layout模板中,我们使用了两个Velocity宏:#showHead和#bodyAttributes。显然,我们需要在Velocity中定义这些宏。为此,我们还需要创建一个文件:templates/macros.vm,内容如下:
#** -------------------------------------------
 * 显示所有headers
 * -------------------------------------------- *#
#macro (showHead $defaultTitle)

#showTitle ($defaultTitle)
#showMetaTags ()
#showHttpEquiv ()
#showStylesheets ()
#showJavascripts ()

#end

#** -------------------------------------------
 * 显示标题,如果未提供标题,则使用默认值
 * -------------------------------------------- *#
#macro (showTitle $defaultTitle)

#if( $page.title != "" )
<title>$page.title</title>
#else
<title>$!defaultTitle</title>
#end

#end

#** -------------------------------------------
 * 显示meta tags
 * -------------------------------------------- *#
#macro (showMetaTags)

#foreach($metaTag in $page.metaTags.keySet())
<meta name="$metaTag" content="$page.metaTags.get($metaTag)">
#end

#end

#** -------------------------------------------
 * 显示meta http-equiv
 * -------------------------------------------- *#
#macro (showHttpEquiv)

#foreach($httpEquiv in $page.httpEquivs.keySet())
<meta http-equiv="$httpEquiv" content="$page.httpEquivs.get($httpEquiv)">
#end

#end

#** -------------------------------------------
 * 显示stylesheets
 * -------------------------------------------- *#
#macro (showStylesheets)

#foreach( $styleSheet in $page.styleSheets )
<link rel="stylesheet" href="$styleSheet.Url"
  #if($styleSheet.Type != "" ) type="$styleSheet.Type" #end
  #if($styleSheet.Media != "") media="$styleSheet.Media" #end
  #if($styleSheet.Title != "") title="$styleSheet.Title" #end>
#end

#end

#** -------------------------------------------
 * 显示javascripts
 * -------------------------------------------- *#
#macro (showJavascripts)

#foreach( $script in $page.scripts )
<script type="text/javascript" src="$script" language="JavaScript"></script>
#end

#end


#** -------------------------------------------
 * 显示body attributes
 * -------------------------------------------- *#
#macro (bodyAttributes)

#foreach( $attributeName in $page.bodyAttributes.keySet() )
    $attributeName="$page.bodyAttributes.get($attributeName)"
#end

#end
最后,我们需要修改一下webx.xml,在VelocityService中装载这个宏文件:
<configuration>
    <services>
        ……
        <service name="VelocityService">
            <property name="input.encoding" value="GBK"/>
            <property name="velocimacro.library" value="macros.vm"/>
        </service>
    </services>
</configuration>
虽然创建Velocity宏的步骤略显复杂,但是这样的工作在每个项目中只需要做一次就可以了。
重启应用服务器,你就会看到新的页面标题。
5.页面驱动
到目前为止,我们已经创建了相当复杂的页面,但我们却没有为此写一行程序。而且在创建页面的过程中,大部分修改都能立即生效 —— 而不需要重启服务器。这种接近于所见即所得的方式,非常贴近一个真实网站的开发流程。在真实的网站项目中,产品设计师首先设计的内容就是页面 —— 确定页面的数量、功能、布局。与此同时,美术设计师会配合产品设计师对页面做美化。直到最后,Java工程师才会介入,开始编写程序。
和其它大多数的Web框架不同,Webx是以页面为驱动的 —— 它允许你先把页面做出来,在必要时才让Java工程师介入。最重要的是,产品设计师制作页面时,基本上不需要Java工程师的介入。他们只需要了解一些书写模板和HTML的技巧,以及前面所讲的页面布局的知识就可以了。
和普通HTML页面不同的是,Webx页面是真正的动态页面。在前面的Velocity模板中,我们已经使用了好几个变量:$control、$page。事实上,还有更多现成的变量可用,它们都是预设在系统内部的常用工具。而且我们还可以创建新的工具。
下面是在页面中可用的工具:
工具名    类名    作用域    说明
$arrayUtil    ArrayUtil    全局    静态工具类,参见相应类的API文档。
其中最常用的当属$stringUtil和$stringEscapeUtil。例如:

## 判断字符串为空串
#if ($stringUtil.isEmpty($var)) … #end

## 进行HTML编码
<input type=”hidden” name=”myField”
      value=”$stringEscapeUtil.escapeHtml($var)”/>

## 进行JavaScript编码
<script language=”JavaScript”>
var myvar="$stringEscapeUtil.escapeJavaScript($var)";
</script>
$classLoaderUtil    ClassLoaderUtil    全局   
$classUtil    ClassUtil    全局   
$enumUtil    EnumUtil    全局   
$exceptionUtil    ExceptionUtil    全局   
$fileUtil    FileUtil    全局   
$localeUtil    LocaleUtil    全局   
$mathUtil    MathUtil    全局   
$messageUtil    MessageUtil    全局   
$objectUtil    ObjectUtil    全局   
$streamUtil    StreamUtil    全局   
$stringEscapeUtil    StringEscapeUtil    全局   
$stringUtil    StringUtil    全局   
$systemUtil    SystemUtil    全局   
           
$control    ControlTool    Request    创建control,例如:
$control.setTemplate(“header.vm”)
$control.setModule(“myControl”)
$form    FormTool    Request    处理表单,后文会讲到
$page    HtmlPageAttributeTool    Request    设置页面属性
$rundata    RunData    Request    取得request、response相关的数据,例如:
$rundata.parameters.id 取得?id=123参数的值
$uri    URIBrokerTool    Request    生成URL,后文会讲到
所谓全局作用域,意思是这种工具仅当系统启动时被初始化,随后被所有页面重复使用。这种工具必须是无状态的。
和全局作用域相对的,是Request作用域。这种作用域的工具会在每次请求时创建和初始化,因此可以在它里面保存与当前请求相关的数据。但这种工具不能被多个请求共享。
如果我所需要的功能不在上面的列表中,怎么办呢?没关系,你可以创建你自己的工具。
在创建这个工具之间,先问自己一个问题:这个工具中是否包含仅和当前request相关的数据?如果是,那么它是一个Request作用域的工具,否则,它是一个全局作用域的工具。你需要修改webx.xml,以增加新的工具:
<configuration>
<services>
    ……
        <service name="PullService">
            <property name="tool.request"> <!-- 或tool.global -->
                <property name="date" value="java.util.Date"/>
            </property>
        </service>
    </services>
</configuration>
在这个配置中,我们指定了一个request作用域的新对象:java.util.Date。之所以设置成request而不是global作用域,是因为我们希望每次请求,对象都被更新。
重启服务器,我们就可以看到页面中显示出了当前的时间。
 
6.验证表单
现在,让我们做一个新的功能:注册用户。
每个用户需要填写下面的内容,以成为我们的会员:
字段名    描述    限定
ID    唯一用户ID    必填项,
由字母、数字、下划线构成,
最少4个字符,最多不超过10个字符。
Password    用户验证密码    必填项,
由字母、数字、下划线构成,
最少4个字符,最多不超过10个字符,
为了安全,密码不得与ID相同。
Password Confirm    密码必须填两遍,以防输错    必填项,
必须和Password相同。
Name    用户名    必填项,
可由任何字符构成,包括中文,
不超过20个字符。
Email    电子邮件地址    可选项,
必须符合电子邮件地址的规范。
Description    个人描述/心情故事    可选项
在Webx中做这样的表单验证相当简单 —— 不需要写一行程序。
6.1.创建form.xml
首先,你需要在WEB-INF/目录下创建一个新的文件:form.xml,内容如下:
<?xml version="1.0" encoding="GB18030"?>
<form>
    <group name="register">
        <field name="id" displayName="用户ID">
            <required-validator>
                <message>必须填写${displayName}</message>
            </required-validator>
            <regexp-validator pattern="^[A-Za-z_][A-Za-z_0-9]*$">
                <message>${displayName}必须由字母、数字、下划线构成</message>
            </regexp-validator>
            <length-validator minLength="4" maxLength="10">
                <message>${displayName}最少必须由${minLength}个字组成,最多不能超过${maxLength}个字</message>
            </length-validator>
        </field>
        <field name="password" displayName="密码">
            <required-validator>
                <message>必须填写${displayName}</message>
            </required-validator>
            <regexp-validator pattern="^[A-Za-z_][A-Za-z_0-9]*$">
                <message>${displayName}必须由字母、数字、下划线构成</message>
            </regexp-validator>
            <length-validator minLength="4" maxLength="10">
                <message>${displayName}最少必须由${minLength}个字组成,最多不能超过${maxLength}个字</message>
            </length-validator>
            <compare-validator notEqualTo="id">
                <message>${displayName}不能与${id.displayName}相同</message>
            </compare-validator>
        </field>
        <field name="passwordConfirm" displayName="密码验证">
            <required-validator>
                <message>必须填写${displayName}</message>
            </required-validator>
            <compare-validator equalTo="password">
                <message>${displayName}必须和${password.displayName}相同</message>
            </compare-validator>
        </field>
        <field name="name" displayName="名字">
            <required-validator>
                <message>必须填写${displayName}</message>
            </required-validator>
            <length-validator maxLength="20">
                <message>${displayName}最多不能超过${maxLength}个字</message>
            </length-validator>
        </field>
        <field name="email" displayName="电子邮件">
            <email-validator>
                <message>${displayName}必须是合法的E-mail地址</message>
            </email-validator>
        </field>
        <field name="description" displayName="心情故事"/>
    </group>
</form>
这个文件定义了用户要填写的表单的信息,它包括如下内容:
1.    文件以<form>标签开始。
2.    <form>由一个或多个<group>组成,每个group代表一组要验证的字段。每个group有一个唯一的名字。
3.    <group>由一个或多个<field>组成,每个field代表一个要验证的字段。每个field有一个在group范围内唯一的名字。
4.    <field>由零个或多个<validator>组成,每个validator代表一个验证条件。如果条件不满足,validator中的message信息将被显示出来。
6.2.修改webx.xml
为了使用刚刚创建的form.xml,我们需要修改一下webx.xml文件:
<configuration>
    <services>
        ……
        <service name="FormService">
            <property name="form.descriptors">
                <value>/WEB-INF/form.xml</value>
            </property>
        </service>
    </services>
</configuration>
6.3.创建register.vm模板
最后,我们来写screen模板:register.vm。在templates/screen目录下创建这个文件:
#macro (registerMessage $field)
    #if (!$field.valid) <div style="color: red; font-weight: bold;">$field.message</div> #end
#end

<form action="" method="post">

  #set ($group = $form.register.defaultInstance)

  <strong>用户注册</strong><hr/>

  <table border="0" cellpadding="0" cellspacing="10">
    <tr>
      <td>用户ID:</td>
      <td>
        <input type="text" name="$group.id.key" value="$!group.id.value" size="20"/>
      </td>
      <td>
        #registerMessage ($group.id)
      </td>
    </tr>
    <tr>
      <td>密码: </td>
      <td>
        <input type="password" name="$group.password.key" value="$!group.password.value" size="30"/>
      </td>
      <td>
        #registerMessage ($group.password)
      </td>
    </tr>
    <tr>
      <td>再输一遍密码: </td>
      <td>
        <input type="password" name="$group.passwordConfirm.key" value="$!group.passwordConfirm.value" size="30"/>
      </td>
      <td>
        #registerMessage ($group.passwordConfirm)
      </td>
    </tr>
    <tr>
      <td>用户名:</td>
      <td>
        <input type="text" name="$group.name.key" value="$!group.name.value" size="30"/>
      </td>
      <td>
        #registerMessage ($group.name)
      </td>
    </tr>
    <tr>
      <td>电子邮件:</td>
      <td>
        <input type="text" name="$group.email.key" value="$!group.email.value" size="30"/>
      </td>
      <td>
        #registerMessage ($group.email)
      </td>
    </tr>
    <tr>
      <td>心情故事:</td>
      <td>
        <textarea name="$group.description.key" cols="30" rows="10">$!group.description.value</textarea>
      </td>
      <td>
        #registerMessage ($group.description)
      </td>
    </tr>
  </table>
 
  <hr/><input type="submit" value="  注 册  "/>

</form>
创建一个表单验证的模板,包含以下几个步骤:
1.    取得form.xml中定义的group实例:
  #set ($group = $form.register.defaultInstance)
其中,“register”为group的名字。
2.    创建HTML控件,包括:文本框、textarea、密码框等。但不管什么控件,都有一个name和一个value。而我们的任务是设置name和value,例如:
  <input type="text" name="$group.id.key" value="$!group.id.value" size="20"/>
其中,“id”为group中field的名字。请别忘了在$group.id.value中加上感叹号。因为当value值为null时,Velocity会直接显示成“$group.id.value”。而“$!group.id.value”就可以避免这种情形。
3.    显示错误信息
  #if (!$group.id.valid) $group.id.message #end
为了简化,我们写了一个Velocity宏:registerMessage,从而把这句话简化成:
  #registerMessage ($group.id)
这样,我们就完成了这个表单。重启服务器,在浏览器上试一下:输入一些错误的值,看看会有什么反应。
 
7.创建程序:Action
是时候创建程序代码了。到目前为止,虽然我们没有写一行程序,但系统也工作得挺好。然而,有很多事情是做不了的,例如:从数据库中读数据,或向数据库中写数据。
在Register注册用户的例子中,当我们正确地填写了所有的字段,并提交时,数据不会自动存到数据库里去。现在我们就来写一段程序,将数据保存到数据库中。不过为了读写数据库,我们还需要写业务层、数据访问层的代码 —— 这些,我们将在另一篇教程中说明。在数据库可以被操作之前,我们先写一些空的代码。将来数据库就绪了,我们再把真实的代码填进去。
7.1.创建model类:SiteUser
为了将表单中,用户所填写的信息写入数据库,我们需要创建一个简单的model类。Model就是M-V-C中的Model。不过我们所要创建的model类的用处更广泛 —— 它不仅在WEB层有用,而且也能在我们将来要介绍的业务层、数据访问层中有用。
Model类是一个形式特别简单的普通Java Bean。Model类不应该包含任何业务逻辑 —— model只能作为信息的载体。请创建这个类:com.alibaba.workshop2.model.SiteUser。
package com.alibaba.workshop2.model;

import org.apache.commons.lang.builder.ToStringBuilder;

public class SiteUser {
    private String id;
    private String password;
    private String name;
    private String email;
    private String description;

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
    }
}
这个类简单到只包含一些属性以及相关的setter、getter。因此在实践中,可以由代码生成器为我们代劳,来生成这些简单而乏味的类。
在SiteUser类的最后,我们利用Apache Commons-lang工具包中的ToStringBuilder工具来生成对象的字符串描述。这是为了调试的直观和方便。
7.2.创建action类:SiteUserAction
请创建SiteUserAction类:
package com.alibaba.workshop2.module.action;

import com.alibaba.service.form.Form;
import com.alibaba.service.form.FormService;
import com.alibaba.service.form.Group;
import com.alibaba.service.template.TemplateContext;

import com.alibaba.turbine.module.action.TemplateAction;
import com.alibaba.turbine.service.rundata.RunData;

import com.alibaba.webx.WebxException;

import com.alibaba.workshop2.model.SiteUser;

public class SiteUserAction extends TemplateAction {
    public void doRegister(RunData rundata, TemplateContext context)
            throws WebxException {
        FormService forms = (FormService) getWebxComponent().getService(FormService.SERVICE_NAME);
        Form        form  = forms.getForm(rundata);

        if (form.isValid()) {
            Group    group = form.getGroup("register");
            SiteUser user  = new SiteUser();

            group.setProperties(user);

            // FIXME! 保存到数据库中。
            System.out.println("Registered user: " + user);
        }
    }
}
一般情况下,一个Action会从TemplateAction派生。在这个类里面,我们做了几件事:
1.    取得FormService
Action的作用是将用户填写并提交的数据保存到数据库中。所以我们首先要从表单中取得用户填写的数据。我们前面已经配置了FormService,并用它来验证用户的输入。现在我们要取得FormService来取得用户的输入。取得FormService的方法是:
FormService forms = (FormService) getWebxComponent().getService(FormService.SERVICE_NAME);
然而,还有更简单的方法来取得FormService,即:被动注入。注入的方法有两种。第一种是通过setter方法来注入:
public class SiteUserAction extends TemplateAction {
    private FormService forms;

    public void setFormService(FormService forms) {
        this.forms = forms;
    }

    public void doRegister(RunData rundata, TemplateContext context)
            throws WebxException {
        Form form = forms.getForm(rundata);

        ……
    }
}
第二种方法是通过getter方法来注入:
public abstract class SiteUserAction extends TemplateAction {
    protected abstract FormService getFormService();

    public void doRegister(RunData rundata, TemplateContext context)
            throws WebxException {
        Form form = getFormService().getForm(rundata);

        ……
    }
}
为了方便,以后我们将一直使用第二种注入方法。
2.    取得form
取得FormService以后,我们将从中取得form对象。Form是一个或多个Group的集合。
Form form  = forms.getForm(rundata);
3.    判断form是否合法
只有当form验证合法,我们才可以将它保存到数据库里。下面的语句判断form是否合法。
if (form.isValid()) {
    ……
}
4.    从form中取得group
每个form都可以包含一个或多个group。Group被定义在form.xml中。每个group都有一个唯一的名字。我们必须取得名为“register”的group,才能取得用户填写的数据。
Group group = form.getGroup("register");
5.    将用户填写的数据转入model对象
SiteUser user  = new SiteUser();

group.setProperties(user);
6.    将对象保存到数据库中
由于目前数据库还没有就绪,所以我们暂时不能做这件事。不过我们可以把user对象打印出来,以证明代码的正确性。将来数据库准备好时,我们再用实际的业务逻辑代码替换这里的打印语句。
// FIXME! 保存到数据库中。
System.out.println("Registered user: " + user);
由于ToStringBuilder的功效,我们可以在标准输出看到user对象内部的所有用户数据:用户ID、名字、密码、E-mail等等。
不知你是否注意到语句前面的FIXME注释。这是Sun推荐我们做的特殊注释。在Eclipse集成环境中,Task视图会列出项目中所有的这类特殊注释。
 
由于我们将来必须回过头来修改这段代码,使之访问真正的数据库,因此这种注释技术可以防止我们忘记这件事。类似的注释还包括:
1.    // TODO —— 表示尚未完成的待办事项。
2.    // XXX  —— 表示被注释的代码虽然实现了功能,但是实现方案有待商榷,希望将来能改进。
3.    // FIXME —— 表示被注释的代码需要被修正。
上述所有注释都会被eclipse task视图所收集。在项目发布前,检查一下task视图是一个很好的习惯。
7.3.修改screen模板:register.vm
必须在screen模板中,做少量修改:
……
<form action="" method="post">
  <input type="hidden" name="action" value="site_user_action"/>
  ……
  <hr/><input type="submit" name="event_submit_do_register" value="  注 册  "/>

</form>
其中,hidden字段action=site_user_action告诉Webx(实际上是PerformActionValve),调用哪个action类(SiteUserAction)。而event_submit_do_register按钮则告诉Webx,执行action类中的doRegister方法。
7.4.修改webx.xml
可是,系统怎样找到SiteUserAction类呢?
你需要修改一下webx.xml配置,告诉系统到何处去找类:
<configuration>
    <services>
        ……
        <service name="ModuleLoaderService">
            <property name="module.packages">
                <value>com.alibaba.workshop2.module</value>
            </property>
        </service>
    </services>
</configuration>
这段配置告诉Webx:当action=site_user_action时,就执行com.alibaba.workshop2.module.action.SiteUserAction类。
最后,请重新启动服务器,并在浏览器上查看register页面,填入符合验证规则的内容,并提交,看看控制台上会不会打印出SiteUser对象的内容。
8.创建程序:Screen
包括Action在内,Webx有三种可编程的模块(Module):
1.    Action —— 用来处理用户提交的数据。
2.    Screen —— 用来表现主体页面的内容。
3.    Control —— 用来表现部分页面的内容。
前面我们已经创建了好几个screen页面的模板,以及control的模板。然而没有程序的模板是“死”的,因为你无法将数据库中的数据、业务逻辑的状态显示在页面中。但我们可以对screen和control创建Java程序,让它把模板做“活”。
下面,我们来创建一个viewUser的页面,它包括一个viewUser.vm的screen模板和一个ViewUser.java的screen类。这个页面的功能是从“数据库”里查得的用户数据,并显示到页面中。
8.1.创建screen模板:viewUser.vm
首先,让我们创建screen模板:templates/screen/viewUser.vm。内容如下:
<strong>用户信息</strong><hr/>

<table border="0" cellpadding="0" cellspacing="10">
<tr>
  <td>用户ID:</td>
  <td>$user.id</td>
</tr>
<tr>
  <td>密码: </td>
  <td>$stringUtil.repeat("*", $user.password.length())</td>
</tr>
<tr>
  <td>用户名:</td>
  <td>$user.name</td>
</tr>
<tr>
  <td>电子邮件:</td>
  <td>$user.email</td>
</tr>
<tr>
  <td>心情故事:</td>
  <td>$!user.description</td>
</tr>
</table>
 
<hr/>
这个screen模板将user对象中的内容显示在页面中。其中,
    password字段被显示成若干个“*”号。
    由于description字段的值可能为空,为了防止velocity显示不正确,应当加上“!”:$!user.description。
不需要重启服务器,请访问:http://localhost:8080/workshop/view_user.htm来观看效果。你会注意到,除了description字段以外,其它所有的字段都被显示成:$user…这样的形式。原因是:$user对象不存在。这正是screen类要做的事情。
8.2.创建screen类:ViewUser.java
ViewUser screen的作用,是从数据库中取得指定用户的信息,并将它显示到screen模板中。然而目前数据库的代码尚未

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值