Skip to content
Songli Huang edited this page Nov 24, 2022 · 48 revisions

1 开始

1.1 创建maven的web工程

确保机器上已经安装maven(建议版本maven2),并已经把mvn命令配置到命令行环境变量中。 进入你要创建的工程目录,比如 cd /Users/yourname/yourprojects 输入命令

mvn archetype:generate

mvn会列表已经注册的项目类型模板(这个列表出现可能比较慢,请稍微等待一下)。它会提示一个默认的数字,我这里是344,代表最常用的maven工程模板,其实就是普通的jar工程,但是我们需要web工程,从上面列表中找到该数字对应的列表,一般往下找几行,就找到我们需要的了,(相关列表输出部分如下)

344: remote -> org.apache.maven.archetypes:maven-archetype-quickstart (An archetype which contains a sample Maven project.)
345: remote -> org.apache.maven.archetypes:maven-archetype-site (An archetype which contains a sample Maven site which demonstrates some of the supported document types like
    APT, XDoc, and FML and demonstrates how to i18n your site. This archetype can be layered
    upon an existing Maven project.)
346: remote -> org.apache.maven.archetypes:maven-archetype-site-simple (An archetype which contains a sample Maven site.)
347: remote -> org.apache.maven.archetypes:maven-archetype-webapp (An archetype which contains a sample Maven Webapp project.)

命令行中输入 347 (这个值在你的机器上可能会有所不同), 后面根据提示完成web工程的创建,比如 groupId为myweb, artifactId也为myweb, package为com.mycompany.myweb. 完成之后会生成myweb文件夹,进入该文件夹,修改pom.xml,加入相关依赖。

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <groupId>wint</groupId>
            <artifactId>wint-framework</artifactId>
            <version>1.6.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.6.4</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.1</version>
        </dependency>
    </dependencies>

为了能方便jetty使用,也建议增加jetty插件,并把java的最小支持版本改成1.6,最终的pom.xml文件如下。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>myweb</groupId>
    <artifactId>myweb</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>myweb Maven Webapp</name>
    <url>http://maven.apache.org</url>

    <properties>
        <java.version>1.6</java.version>
        <java.encoding>utf-8</java.encoding>
        <project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <groupId>wint</groupId>
            <artifactId>wint-framework</artifactId>
            <version>1.6.5.4</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.6.4</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.1</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>myweb</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>7.6.16.v20140903</version>
            </plugin>
        </plugins>
    </build>
</project>

1.2 配置web.xml

wint可以作为Servlet配置,也可以作为Filter配置,如果作为Servlet的最简单配置如下:

<!-- dispatch servlet --> 	
<servlet>
    <servlet-class>wint.mvc.DispatcherServlet</servlet-class>
    <servlet-name>WintDispatcherServlet</servlet-name>
    <load-on-startup>1</load-on-startup>
    <init-param>
        <param-name>wint.app.package</param-name>
        <param-value>com.mycompany.myweb</param-value>
    </init-param>
</servlet>

<!-- servlet mapping -->
<servlet-mapping>
    <servlet-name>WintDispatcherServlet</servlet-name>
    <url-pattern>*.htm</url-pattern>
</servlet-mapping>

wint.app.package 参数值是你的项目package,按照约定,wint会去com.mycompany.myweb.web.action目录加载Action类。 上面简单的引入了wint,默认不需要任何配置文件,因为wint的理念就是约定胜于配置,后面你将会看到各种约定理念。

2 第一个Wint页面

2.1 建立模板页面

找myweb/src/main/webapp目录下,建立templates文件夹,再在templates下建立page文件夹,在page文件夹下建立文件helloFirst.vm,输入内容

<h1>hello</h1> this is my first wint page. 

最终路径是 webapp/templates/page/helleFirst.vm 。 这里的helloFirst.vm的后缀是.vm,表示将用velocity引擎渲染页面,这个就是约定。wint还支持freemark,如果将文件名改成helloFirst.ftl,那将用freemark引擎渲染页面。 由于刚才我们在pom.xml配置好了jetty插件,那么我们就可以直接使用了。

2.2 启动web应用

在命令行中输入 mvn jetty:run 启动你的工程吧。如果你在命令行中看到类似如下的输出,

2014-02-12 14:34:53.896:INFO:/:==================================================
2014-02-12 14:34:53.897:INFO:/:Wint framework initializing info:
2014-02-12 14:34:53.897:INFO:/:Magic Type: java
2014-02-12 14:34:53.897:INFO:/:Environment: dev
2014-02-12 14:34:53.897:INFO:/:wint.app.package = com.mycompany.myweb
2014-02-12 14:34:53.897:INFO:/:Wint framework has been initialized.
2014-02-12 14:34:53.897:INFO:/:==================================================

那么就说明启动成功啦。 打开浏览器,输入 http://127.0.0.1:8080/helloFirst.htm 访问一下看看吧!!!

3 映射规则

3.1 设计理念,先页面后逻辑

为了能更快更早的看到页面,wint的理念是先出页面,后写逻辑,可以在不定义Action的情况下,先创建页面,可以把原型稿或是视觉稿直接使用,再后面慢慢补充逻辑。 现在我们稍微调整以下helloFirst.vm来补充一下逻辑吧,调整内容如下

<h1>hello $!result</h1> this is my first wint page.
<form method="get">
    Your name: <input type="text" name="name" value="">
    <button type="submit">Submit</button>
</form>

3.2 构建Action类

先确保com.mycompany.myweb.web.action这个package已经创建,在改package下创建一个名为HelloFirst的类,增加名为execute的方法,注意方法参数,相关代码如下:

package com.mycompany.myweb.web.action;

import wint.mvc.flow.FlowData;
import wint.mvc.template.Context;

public class HelloFirst {

    public void execute(FlowData flowData, Context context) {
        String name = flowData.getParameters().getString("name");
        if (name != null) {
            context.put("result", name.toUpperCase());
        }
    }

}

这里信息量比较大,我们一个一个来。

  • 按照约定,helloFirst对应的action是com.mycompany.myweb.web.action.HelloFirst这个类的execute方法
  • 按照约定,只要有FlowData参数的execute其他重载形式(public方法)也可以作为目标,但是如果有多个重载存在,wint会检测到冲突而报错。
  • 上面代码中,execute方法两个参数,FlowData代表的数据流转的封装,包括请求和响应,跳转等的相关信息;Context代表了输出上下文,默认输出到模板的运行环境中。
  • FlowData接口有很多方法,其中 getParameters() 获取请求的queryString字段或是form表单的字段,你可以用getString,getInt,getLong等获取目标类型的值。
  • 最后如果name参数存在,则转成大写后放入context中,由于context的数据默认会穿到模板中,这样helloFirst.vm中的result就可以取到了。

好了,重新启动以下你的应用,看看效果吧。

3.3 Action的映射规则

按照约定,默认情况下,action的映射规则如下:

  • 规则化输入url,以下划线(_),连接符(-)组成的字符串统一转成驼峰风格,比如 /hello_first或是/hello-first都会转成/helloFirst
  • 如果带有路径的url,比如/foo/bar/hello-first,那么wint默认会支持两种策略查找Action。 策略1,寻找 com.mycompany.myweb.web.action.foo.bar.HelloFirst类及对应的execute方法,上面的例子就是遵循了这个策略。 策略2,寻找 com.mycompany.myweb.web.action.foo.Bar类及对应的helloFirst方法,方法参数规则也是需要必须带FlowData。 上述两种策略中任意一种都可以找到匹配到对应的Action,如果两个策略同时满足,wint会检测到冲突而报错。
  • 找到对应的Action后,会执行Action相应的方法,执行之后默认渲染 /foo/bar/helloFirst.xxx 类似的模板
  • 渲染的目标模板策略,按下面的优先级排序:1、rundata.setTarget方法,2、方法返回值或是@Action注解默认值,3、和Module同名
  • doAction类型,如果action是do开头的,比如/foo/bar/doCreateSth,那么就是doAction,这种类型用于做修改性的操作,可能会引起数据的变更,很多时候,为了防止重复提交,我们会有很多方案,其中一种方案就是操作后跳转到另外的页面,比如提示成功页。doAction的存在对这个事情做了强制,如果请求是doAction,那么不会渲染默认的do-xxx页面,所以必须手动设置,通过rundata.setTarget或是返回值,或是@Action注解,下面是一个doAction的例子
    @Action(defaultTarget="foo/bar/createSomeThing")
    public void doCreateSomething(FlowData flowData, Context context) {
        // do some thing
        boolean success = ...;
        if (success) {
            flowData.redirectTo("http://your-success-message-url");
        }
    }

4 参数

4.1 queryString, form表单请求参数

最直接的方法是调用FlowData.getParameters()获取各种类型的参数,另外对于表单的处理,还有很丰富的方法,参见后面的表单处理

4.2 文件上传

通过FlowData.getUploadFiles()获取,如果文件上传,默认依赖于commons-fileupload

4.3 位置参数

默认支持位置参数,由于用于可读性更高的URL,比如 /book/1234-2.htm,最终可以映射到Action为

public class Book {
    public void execute(FlowData flowData, Context context, int bookId, int page) {
        // bookId is 1234, page = 2
    }
}

映射规则如下:

  • 从第一个数字字符开始映射,每个参数间用 连接符(-),或是下划线(_),或是斜杠(/)分割
  • Action的方法中除去FlowData和Context类型之外的所有参数都按顺序映射,如果该值不存在或是转换失败,将用默认值填充(Object类型是null,其他类型是0或是false) 你也可以通过flowData.getArguments()方法获取所有位置参数。

5 URL

5.1 为什么要管理URL

很多时候我们都会采用字符串拼接的方式组成url,特别是在页面上,这个直接的方式固然有效,但也存在很多问题,比如一个相对路径变化后要去找所有的地方改,碰到特殊字符需要做urlencode,csrf漏洞等安全问题,URL重写规则,统一后缀管理,跨应用必须使用决定地址的时候如何管理绝对地址,包括css,js等静态资源等等各种问题。

5.2 wint中url管理

在/src/main/resources目录下创建名为 url.xml 的文件,输入类型的内容

<?xml version="1.0" encoding="utf-8"?>
<url-config>
    <url name="baseModule" >
        <server-url>http://127.0.0.1:8080</server-url>
    </url>
    <url name="adminModule" extends="baseModule">
        <path>admin</path>
    </url>
    <url name="staticServer">
        <server-url>http://statics.cdnxxxx.net</server-url>
    </url>
    <url name="jsServer" extends="staticServer">
        <path>js</path>
    </url>
    <url name="cssServer" extends="staticServer">
        <path>css</path>
    </url>
</url-config>

打开刚才的helloFirst.vm文件,修改成如下内容

<h1>hello $!result</h1> this is my first wint page.
<form method="get">
    Your name: <input type="text" name="name" value="">
    <button type="submit">Submit</button>
</form>

<ul>
    <li><a href="$baseModule.setTarget("helloFirst")">hello</a></li>
    <li><a href="$baseModule.setTarget("helloFirst").param('name', 'Pister')">hello, Pister</a></li>
    <li><a href="$baseModule.setTarget("helloFirst").args(111,222).param('name', 'Pister')">hello, Pister 111 222</a></li>
    <li><a href="$baseModule.setTarget("helloFirst").param('name', 'Pister').withToken()">hello, Pister</a></li>
</ul>

重新启动jetty服务后,再看看几个url的差别。正如你看到的那样,

  • baseModule名称是引用自url.xml里的名称,你当然也可以引用其他名称,比如adminModule等,至于url.xml这个文件名,也是默认约定的。
  • setTarget方法是一个url的目标,不需要写后缀,可以利用斜杠(/)区分url路径层级,比如 setTarget('foo/myProfile')等。
  • param方法就是组建queryString的key-value对,多个key-value对的时候可以连续写,比如 setTarget('foo/myProfile').param('param1', 'value1').param('param2', 'value2')
  • args是生成位置参数的,setTarget("helloFirst").args(111,222)将生成/hello-first/111-222.htm类似的URL。注意,args第一个参数必须是数字,想想为什么? :)
  • 最后来看看最后一个url,withToken()方法会再生成的url上带上一个token,不用的登录用户拥有不同的token,主要用来防止csrf漏洞,如果涉及到url会修改资源的时候请带上这个参数。

6 重定向

  • 内部重定向:你可以利用FlowData.forwordTo来实现内部重定向,forwordTo会共享当前的请求状态,比如flowData和context
  • FlowData.setTarget方法:在执行action前调用,则设置目标action;在执行了action内或是后调用,如果是doAction,相当于内部重定向,会执行目标和渲染模板,否则只设置渲染目标。
  • 外部重定向:FlowData.redirectTo有两个重载方法,第一个种是接受一个字符串,重定向到目标URL;第二种接受两个参数,分别是url.xml配置的名称和url目标,返回一个UrlBroker,你可以在UrlBroker对象上调用param等方法组装成你想要的url,比如
public class Book {
    public void detail(FlowData flowData, int bookId) {
        // do sth with bookId
    }

    public void doCreate(FlowData flowData) {
       boolean result = ... // create book result.
       int newBookId = ... // the book id just create.
       flowData.redirectTo('baseModule', 'book/detail').args(newBookId);
    }
  
}

当doCreate执行后,会跳转到 http://127.0.0.1:8080/book/detail/$newBookId.htm 页面

7 安全

7.1 xss

xss 目前的主要解决方案是针对特定场景的内容输出做转义,替换到特定的字符

7.1.1 wint对xss的默认方案

wint默认模板输出就是html转义的。在我们的例子 http://127.0.0.1:8080/helloFirst.htm 中,在name输入框中,输入<script>alert(123);</script>试试,看看结果是弹出一个对话框呢还是一行文本,看一下页面输出的html源代码验证一下吧。 :)

7.1.2 wint对其他场景的xss支持,除了html外,我们还会在其他地方碰到xss漏洞,比如js中,或是xml中等,我们可以利用内置的变量securityUtil在模板页面中使用相应的方法替换相应的字符,例如

$securityUtil.escapeJs($yourcontent);
$securityUtil.escapeXml($yourcontent);

由于默认输出会对html做处理,有时候我们想要原始的内容,不希望做转义,可以使用下面的方法

$securityUtil.rawText($yourcontent);

由于该方法会存在xss安全隐患,使用的时候要特别小心。 如果有时候想支持富文本,又想防止xss漏洞,建议自己写一个工具,思路是把要支持的html标签和css属性放入白名单,过滤其他标签。

7.2 csrf

csrf 目前的解决方案主要是在请求中加入和用户登录相关的token,并再服务器端校验该token。

7.2.1 直接使用token

wint页面中可以直接使用$securityUtil.csrfToken()获取一个token,可以在请求中直接使用,也可以在后面使用,比如ajax请求。wint中的token对于同一次用户登录是不变的(所以不能用于防止表单的重复提交)。

7.2.2 表单处理

表单可以直接用$securityUtil.tokenHtml生成一个token的隐藏域

7.2.3 链接处理

wint中的URL模板可以使用withToken()方法在链接中加入token

7.2.4 token的校验

wint默认针对所有的doAction会进行token检查,在开发环境下会提示缺少token参数,在其他环境下,返回http的403状态码。 如果想关闭此功能,或是对部分doAction不做校验,可以修改wint.xml完成,参见后面的wint.xml说明。

8 表单

8.1 传统的表单的处理方法

现在假如我们有一个新需求,要收集用户提交上来的信息,收集的字段为

  • 用户名:必填,有长度限制
  • 年龄:整数,有大小限制
  • 地址:可选,但有最大长度限制 我们先来看看不用form怎么做, 先需要建立一个输入页面,现在我们把页面建立在templates/page/user目录下,命名为addInfo.vm,最后路径是templates/page/user/addInfo.vm
<form action="$baseModule.setTarget('user/doAddInfo')" method="post">
    $securityUtil.tokenHtml
    <div>
        username <input type="text" name="username">
    </div>
    <div>
        age <input type="text" name="age">
    </div>
    <div>
        address <input type="text" name="address">
    </div>
    <div style="color: red">
        $!message
    </div>
    <div>
        <button type="submit">Save</button>
    </div>
</form>

正如你猜想的,该页面的访问路径是 http://127.0.0.1:8080/user/add-info.htm 该页面对应的Action可以是 com.mycompany.myweb.web.action.User.addInfo 或是 com.mycompany.myweb.web.action.user.AddInfo.execute 这里我们可以共用User类,所有可以采用前者。当然如果页面上没有任何业务上的输出,也可以不提供Action。 我们再在User类上也增加doAction方法,以便处理表单请求,最终com.mycompany.myweb.web.action.User类如下

package com.mycompany.myweb.web.action;

import wint.lang.utils.StringUtil;
import wint.mvc.flow.FlowData;
import wint.mvc.module.annotations.Action;
import wint.mvc.template.Context;

public class User {

    public void addInfo(FlowData flowData, Context context) {
        // do some sth
    }

    @Action(defaultTarget = "user/addInfo")
    public void doAddInfo(FlowData flowData, Context context) {
        String username = flowData.getParameters().getString("username");
        int age = flowData.getParameters().getInt("age");
        String address = flowData.getParameters().getString("address");

        // 校验各个字段是否满足条件
        if (StringUtil.isEmpty(username)) {
            context.put("message", "用户名不能为空");
            return;
        }
        if (username.length() > 16 || username.length() < 4) {
            context.put("message", "用户的长度必须在4~16个字符之间");
            return;
        }
        if (age > 255 || age < 1) {
            context.put("message", "无效的年龄范围");
            return;
        }
        if (!StringUtil.isEmpty(address) && address.length() > 80) {
            context.put("message", "地址的长度不能超过80个字符");
            return;
        }
        // 到这里终于成功了,我们这里在增加记录代码,比如保存到数据库等。

        // 处理成功后别忘记重定向到其他页面哦
        flowData.redirectTo("baseModule", "user/addUserSuccess");
    }
}

看到了吗,我们把每个字段的参数都取出来,然后判断检查逻辑,一旦不满足就返回,但是doAction没有指定视图,所以需要一个我们使用@Action注解指定了一个默认的page,这个效果相当于内部重定向,当校验失败后会执行/user/addInfo这个目标。由于是内部重定向,当然会共享context,所以把context中的message也会带过去,然后就会页面上显示出来了。 好了,现在我们重启以下服务,访问 http://127.0.0.1:8080/user/add-info.htm 看看效果吧。 好像功能是可以了,但是有很多问题存在

  • 输入的内容没有在输入框中保留下来
  • 错误信息放在的统一提示的地方,姑且不说用户体验的问题;就是一次只会显示一条错误,用户要在多次完善之后才能勉强填写完成
  • 看看我们的java类吧,太长太多的检查处理代码了
  • 如果碰到相似的业务领域如何复用代码,比如一个是新建书籍,一个是编辑书籍。

8.2 wint表单定义

现在我们利用wint的表单来完成上面的功能: 在/src/main/resources目录下建立一个名为form.xml的文件,输入如下内容

<?xml version="1.0" encoding="utf-8"?>

<forms>

    <form name="user.addInfo">
        <field name="username" type="string">
            <validator type="required" message="用户名不能为空"></validator>
            <validator type="string" message="用户名长度必须在${min}~${max}个字符之间">
                <param name="min" value="4"></param>
                <param name="max" value="16"></param>
            </validator>
        </field>

        <field name="age" type="int">
            <validator type="required" message="请输入您的年龄"></validator>
            <validator type="int" message="无效的年龄范围">
                <param name="min" value="1"></param>
                <param name="max" value="255"></param>
            </validator>
        </field>

        <field name="address" type="string">
            <validator type="string" message="地址的长度不能超过${max}个字符">
                <param name="max" value="80"></param>
            </validator>
        </field>
    </form>

</forms>

上面文件中定义了一个对表单的描述,包括有多少字段,每个字段有多少校验器,每个校验器的规则如何。还是按照约定,wint默认会加载名为form.xml的文件作为表单描述文件,每个form都以name做为标识,name的命名规则建议是: 领域.功能, 比如 user.addInfo, user.reg, book.create等等。 当存在大量表单时,集中存放在一个form.xml文件里不利于维护,wint支持把form文件独立出来,下面的例子就是把form文件处理出来了

<?xml version="1.0" encoding="utf-8"?>
<forms>
    <resource file="forms/user-form.xml" />
    <resource file="forms/book-form.xml" />
</forms>

8.2.1 表单域

wint支持多种校验器,内置支持的检验器如下:

  • required 必填字段
  • int或integer 32bit整数
  • long 64bit整数
  • number 浮点数或是整数
  • string 字符串
  • email 电子邮箱
  • regex 正则表达式
  • date 日期
  • enums 枚举
  • phone 电话号码
  • excludeChars 不能包含的字符
  • csrf (废弃)
  • boolean true or false

如果这些不能满足你的需求,你还可以自定义检验器,通过实现Validator接口或是继承AbstractValidator抽象类均可。 所有的检验器可以零个或多个param参数,用于更加丰富的描述校验器的规则;还可以在message字段中可以利用${名称}引用这些param

8.2.2 表单继承

有时候我们会碰到有些表单字段相似,但是又有少许区别,比如一个是新建XXX,一个是编辑XXX。固然你可以定义两个form,但是很多字段是重复的,维护起来不方便,wint为了解决这个问题,提供了一个表单继承的功能,可以通过extends的表单属性来实现继承,示例代码如下:

<?xml version="1.0" encoding="utf-8"?>

<forms>

    <form name="user.base">
        <field name="username" type="string">
            <validator type="required" message="用户名不能为空"></validator>
            <validator type="string" message="用户名长度必须在${min}~${max}个字符之间">
                <param name="min" value="4"></param>
                <param name="max" value="16"></param>
            </validator>
        </field>

        <field name="age" type="int">
            <validator type="required" message="请输入您的年龄"></validator>
            <validator type="int" message="无效的年龄范围">
                <param name="min" value="1"></param>
                <param name="max" value="255"></param>
            </validator>
        </field>

    </form>

    <form name="user.addInfo" extends="user.base">
        <field name="address" type="string">
            <validator type="string" message="地址的长度不能超过${max}个字符">
                <param name="max" value="80"></param>
            </validator>
        </field>
    </form>
</forms>

上面的示例中user.addInfo表单有3个字段,其中2个继承自user.base,1个来自自己定义。 wint来支持表单字段覆盖,比如在user.addInfo用重新定义了age字段,那么将覆盖user.base中的age字段。

8.3 表单使用

好了,我们回到之前的例子,当创建好了user.addInfo这个表单后,我们再调整userInfo.vm文件内容如下

#set($form = $formFactory.getForm('user.addInfo'))

#macro(errorMessage $msg)
    #if ("$!msg" != "")
        <div style="color: red">
            $msg
        </div>
    #end
#end

<form action="$baseModule.setTarget('user/doAddInfo')" method="post">
    $securityUtil.tokenHtml
    <div>
        username <input type="text" name="$form.username.name" value="$!form.username.value">
    </div>
    #errorMessage($form.username.message)

    <div>
        age <input type="text" name="$form.age.name" value="$!form.age.value">
    </div>
    #errorMessage($form.age.message)

    <div>
        address <input type="text" name="$form.address.name" value="$!form.address.value">
    </div>
    #errorMessage($form.address.message)

    <div>
        <button type="submit">Save</button>
    </div>

</form>

$formFactory是wint的内置变量,可以在运行获取表单信息。模板中所有的表单域都用了$form变量获取相关的内容,包括字段名。errorMessage是一个自定义的velocity宏,用来显示错误信息,更好的做法应该定义在一个通用的文件,而不是一个具体的模板中,这里为了演示方便就定义在一起了。接下去要调整java类了,我们先定义一个UserDO对象用来接收这些字段。

package com.mycompany.myweb.biz.domain;

public class UserDO {

    private String username;

    private int age;

    private String address;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

然后修改User这个Action调整如下

package com.mycompany.myweb.web.action;

import com.mycompany.myweb.biz.domain.UserDO;
import wint.mvc.flow.FlowData;
import wint.mvc.form.Form;
import wint.mvc.module.annotations.Action;
import wint.mvc.template.Context;

public class User {

    public void addInfo(FlowData flowData, Context context) {
        // do some sth
    }

    @Action(defaultTarget = "user/addInfo")
    public void doAddInfo(FlowData flowData, Context context) {
        Form form = flowData.getForm("user.addInfo");
        UserDO userDO = new UserDO();
        if (!form.apply(userDO)) {
            return;
        }
        // 这里已经把表单中的字段填入userDO对象中了

        // 处理成功后别忘记重定向到其他页面哦
        flowData.redirectTo("baseModule", "user/addUserSuccess");
    }
}

重新启动一下服务,在此访问http://127.0.0.1:8080/user/add-info.htm 看看效果吧。 :)

FlowData.getForm方法返回一个Form接口,该接口的apply方法的功能如下:

  • 先根据表单描述的定义校验表单
  • 如果检验有不通过的,把相关该字段记录下来,并把结果标记为false
  • 当全部检验成功时,把字段名填充到参数的对象中,按名称匹配,并自动完成类型转换 返回true

8.3.1 表单的hold

有时候我们需要在预先来页面的表单中填充数据,比如修改已经有信息等,那么这个时候表单的hold起作用了,我们调整一下User这个Action代码如下

package com.mycompany.myweb.web.action;

import com.mycompany.myweb.biz.domain.UserDO;
import wint.mvc.flow.FlowData;
import wint.mvc.form.Form;
import wint.mvc.module.annotations.Action;
import wint.mvc.template.Context;

public class User {

    public void addInfo(FlowData flowData, Context context) {
        Form form = flowData.getForm("user.addInfo");
        // 这个userDO一般可以从数据库中获取到,
        // 这里为了方便直接填充了数据
        UserDO userDO = new UserDO();
        userDO.setAddress("my address");
        userDO.setAge(33);
        userDO.setUsername("Pister");

        form.hold(userDO);
    }

    @Action(defaultTarget = "user/addInfo")
    public void doAddInfo(FlowData flowData, Context context) {
        Form form = flowData.getForm("user.addInfo");
        UserDO userDO = new UserDO();
        if (!form.apply(userDO)) {
            return;
        }
        // 这里已经把表单中的字段填入userDO对象中了

        // 处理成功后别忘记重定向到其他页面哦
        flowData.redirectTo("baseModule", "user/addUserSuccess");
    }
}

重启一下应用,再次访问 http://127.0.0.1:8080/user/add-info.htm 看看效果吧 :)

9 视图

wint支持的视图如下:

  • 模板视图(默认),执行完目标后渲染对应的模板。
  • json视图,把context内容中的数据渲染成json格式。
  • nop视图,wint框架不做任何输出,你需要通过调用FlowData.getWriter()或FlowData.getOutputStream()实现自定义输出。 不同视图的切换方式就是在action中调用 FlowData.setViewType(), ViewTypes中定义了wint支持的视图,下面代码表示输出的是一个json视图
public void foo(FlowData flowData, Context context) {
   flowData.setViewType(ViewTypes.JSON_VIEW_TYPE);
   context.put("name", "Pister");
   context.put("address", "hangzhou");
}

10 布局和挂件

到目前为止我们的templates目录下还只有一个直接目录——page,wint还可以支持另外2个直接目录layout和widget,它们的功能分别如下:

  • page 页面模板,url请求过来或是action执行后渲染对应的页面
  • layout 布局模板,当layout文件夹存在时,渲染page的时候先会按照规则去寻找对应layout,如果找不到,则只渲染page目标,否则的话 page渲染的结果当做一个变量插入到layout的模板中,这个变量默认的名字是$page_content
  • widget 页面挂件,可以在page或是在layout中通过 $widget.setTemplate('yourWidgetPath')来插入widget,实现功能和代码的分离及复用

10.1 布局文件寻找规则

以templates/page/foo/bar/helloFirst.vm的page页面为例,寻找他对应的layout文件的规则,优先级如下

    1. templates/layout/foo/bar/helloFirst.xxx
    1. templates/layout/foo/bar/default.xxx
    1. templates/layout/foo/default.xxx
    1. templates/layout/default.xxx 如果一个都没匹配到,就认为没有layout。

10.2 widget

10.2.1 widget传参

widget可以在页面上当做通用模块使用,当然也可以给它传递参数。

$widget.setTemplate("common.vm").addToContext('name1', 'value1').addToContext('name2', 'value2')

可以在common.vm里直接使用这些参数

10.2.2 widget逻辑

widget也可以像Action一样有自己的业务逻辑代码。在com.mycompany.myweb.web.widget这个package下,可以定义类似于Action的Widget逻辑,比如common对应的widget模块代码如下

package com.mycompany.myweb.web.widget;

import wint.mvc.flow.FlowData;
import wint.mvc.template.Context;

public class Common {

    public void execute(FlowData flowData, Context context) {
        // 这里可以执行逻辑
    }

}

当然,模板中传入的name1和name2的值也可以在context中获取到。

国际化

wint的多语言支持比较直接,3个步骤完成了:

    1. 定义一个类继承于 wint.help.biz.result.ResultCode
    1. 在该类中定义ResultCode类型的public static final成员
    1. 在i18n目录下构建与该类同名的properties文件,增加成员同名的属性
  • 举例:创建一个UserResultCodes类,包含了常量的定义
package com.mycompany.biz.resultcodes;

import wint.help.biz.result.ResultCode;

public class UserResultCodes extends ResultCode {
    public static final ResultCode USERNAME_EXIST = create();
    public static final ResultCode PASSWORD_INCORRECT = create();

}

创建一个对应简体中文的资源文件 i18n/com/mycompany/biz/resultcodes/UserResultCodes_zh_CN.properties 输入内容如下

USERNAME_EXIST=对不起,该用户名已经存在
PASSWORD_INCORRECT=对不起,密码错误

也可以再创建一个对应英文的资源文件 i18n/com/mycompany/biz/resultcodes/UserResultCodes_en_US.properties

USERNAME_EXIST=the username your input has been exist.
PASSWORD_INCORRECT=sorry, password is wrong. 

代码中直接访问 UserResultCodes.USERNAME_EXIST 常量即可,渲染到页面可以调用ResultCode的getMessage方法获取国际化的文本。如果找不到任何国际化的文本,将输出常量名。

wint.xml

wint.xml里面可以修改wint的核心服务,一般情况下不用去修改它。 wint工程默认是可以不提供wint.xml配置文件的,如果不提供将加载默认的wint-default.xml(该文件被打包在wint-${version}.jar中)。如果提供了wint.xml,那么wint.xml的配置将覆盖wint-default.xml定义的部分,对于wint-default.xml中有定义,但是wint.xml中未定义的内容,将不做覆盖。

wint初始化参数

wint参数有很多,都定义在wint.core.config.Constants.PropertyKeys类中,几乎所有的参数都有默认值,默认值定义在wint.core.config.Constants.Defaults类中。只有一个参数没有默认值,那就是wint.app.package,用来定义wint约定的package。 修改这些参数配置的地方是在web.xml中,增加标签即可,比如

<!-- 用于指定运行环境 -->
 <init-param>
        <param-name>wint.app.env</param-name>
        <param-value>test</param-value>
 </init-param>
<!-- 生成的url后缀 -->
<init-param>
        <param-name>wint.app.url.suffix</param-name>
        <param-value>.php</param-value>
 </init-param>

模板工具

很多时候我们要再模板上使用一些工具方法,比如格式化一个日期等,如果你想自定义一些工具,可以在wint.xml中配置一个全局类,wint默认已经内置了一些util,你也可以再做一些调整,比如增加:myUtil

	<object class="wint.mvc.tools.service.DefaultPullToolsService">
		<object name="tools">
			<object name="securityUtil" class="wint.lang.utils.SecurityUtil"/>
			<object name="systemUtil" class="wint.lang.utils.SystemUtil"/>
			<object name="wintUtil" class="wint.help.tools.WintUtil"/>
			<object name="dateUtil" class="wint.lang.utils.DateUtil"/>
			<object name="enumsUtil" class="wint.lang.enums.EnumsUtil"/>
			<object name="myUtil" class="com.yourcompany.MyUtil" />
		</object>
	</object>

页面中就可以调用myUtil的静态方法了。

spring集成

wint会按照约定检查是否要使用spring,检查有2个条件,如果这两个条件同时满足,才会使用spring,这两个条件是:

    1. org.springframework.context.ApplicationContext 这个类在classpath下存在
    1. wint.app.spring.context.file配置参数(默认是applicationContext.xml)的资源文件存在 如果使用spring,那么会从 wint.app.spring.context.file配置参数(默认是applicationContext.xml)对应的资源文件开始初始化。 所有的spring的bean都会自动注入到action中,在action中提供一个简单的setter方法就可以使用spring中的bean了。

自动生成工具

请移步wint自动化工具