WebWheel MVC框架(以下简称WMVC)可以用5个概念进行描述,从前往后依次是过滤器、过滤器组、动作类,结果类和结果解释器。对应4个java接口:Filter(过滤器)、FilterGroup(过滤器组)、Action(动作类)、ResultInterpreter(结果解释器),任何对象类型(继承自Object)都可以作为结果类。
public interface Action {
Object execute() throws Exception;
}
唯一的execute方法将被WMVC调用,它返回的对象即结果类对象。如果结果类对象为null,则WMVC将忽略此次http请求,并将请求继续专递给下一级的filter或者servlet(WMVC使用javax.servlet.Filter进行框架拦截)。
public interface ResultInterpreter<T> {
void init() throws Exception;
boolean render(T result, WebContext ctx, Action action) throws Exception;
void destroy();
}
init和destroy方法为生命期回调函数,将在WMVC启动和销毁时依次调用。
public interface Filter {
void init() throws Exception;
boolean process(FilterChain chain) throws Exception;
void destroy();
}
init和destroy方法为生命期回调函数,将在WMVC启动和销毁时依次调用。
public interface FilterChain {
// 获得当前的应用上下文
WebContext getWebContext();
// 获得当前http请求所映射的Action类型的实例,如果还未创建则创建,否则返回已有实例
Action getAction();
// 获得当前http请求所映射的Action类型,当仅需要获得Action类型而非Action实例时,
// 优先使用此方法,可以避免创建未创建的Action实例
Class extends Action> getActionClass();
// 让过滤器链继续
boolean go() throws Exception;
}
FilterGroup由若干个Filter组成,在其内部,这些Filter形成一个过滤器链队列,通过此FilterGroup的http请求将依次通过这些过滤器,其定义如下:
public interface FilterGroup {
FilterGroup append(Filter filter);
void bindAction(String path, Class<? extends Action> actionClass);
List<Filter> getFilters();
......//其他省略的方法,具体参见API文档
}
FilterGroup的实例为immutable对象。
public interface WebContext {
// 获得当前http请求的uri地址(相对servlet context的地址,如/xxx/yyy.html)
String getPath();
// 获得应用的入口类实例
Main getMain();
// 获得servlet上下文
ServletContext getContext();
// 获得当前http请求对象
HttpServletRequest getRequest();
// 获得当前http响应对象
HttpServletResponse getResponse();
}
在WMVC中,每一个web应用程序都需要一个主入口类,此类必须继承自cn.webwheel.Main抽象类,并且拥有默认无参数构造方法。
这个类将负责上述几个MVC元件的组装和应用程序的其他初始化操作。在WMVC中,没有任何的配置文件,只需要在web.xml中声明一个Filter,示例如下:
上面绿色的类名即为web应用程序的主入口类。<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- 设置WebWheel过滤器 -->
<filter>
<filter-name>WebWheel</filter-name>
<filter-class>cn.webwheel.WebWheelFilter</filter-class>
<!-- 设置应用程序的入口类,此类必须继承cn.webwheel.Main类 -->
<init-param>
<!-- 必须为WMVC设置一个程序入口类,通过名称为main的init-param的设置 -->
<param-name>main</param-name>
<!-- 入口类必须继承自cn.webwheel.Main抽象类 -->
<param-value>com.myfirstapp.WebMain</param-value>
</init-param>
</filter>
<!-- 将WebWheel过滤器映射到所有的地址 -->
<filter-mapping>
<filter-name>WebWheel</filter-name>
<url-pattern>/*</url-pattern>
<!-- 设置过滤类型 -->
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
</web-app>
cn.webwheel.Main主要定义如下:
abstract public class Main implements ObjectFactory {
// 当前应用程序的servlet上下文环境
protected ServletContext servletContext;
// 内置的根过滤器组
protected final FilterGroup root;
// cn.webwheel.ObjectFactory接口的默认实现,Action对象将通过这个方法创建
// 默认实现是使用Class.newIntance创建实例
public <T> T getInstanceOf(Class<T> cls) {...}
// 抽象初始化方法,Main的继承子类必须实现此方法,在其中对WMVC进行设置或进行其他的初始化操作
protected abstract void init() throws Exception;
// 当web应用销毁时,此方法将被调用,可在此进行系统清理操作
protected void destroy() {}
// 注册一个结果解释器类型到一个结果类型上去,当Action返回相应的结果类型时,
// 所对应的结果计时器将被实例化(通过Main.getInstanceOf方法),用以处理解释结果
public final <T> void registerResultType(Class<T> resultType, ResultInterpreter<? extends T> interpreter) {...}
}
一个使用WMVC的web应用程序大致会有一个如下的入口类:
public class WebMain extends Main {
protected void init() {
registerResultType(ResultA.class, new ResultAInterpreter());
registerResultType(ResultB.class, new ResultBInterpreter());
......
FilterGroup filterGroup = root.append(new FilterA()).append(new FilterB());
filterGroup.bindAction("/a.do", ActionA.class);
filterGroup.bindAction("/b.do", ActionB.class);
......
filterGroup.append(new FilterC()).bindAction("/c.do", ActionC.class);
......
}
}
至此,便是WMVC的全部,您已经可以马上使用它了,当然,您如果想更方便更快捷的开发web应用程序,请阅读下面的章节。
WebWheel DI框架(以下简称WDI)是一个轻巧的DI(IOC)框架。它在功能上实现了编译期的静态和半静态类型绑定,在实现上使用jdk6的JavaCompiler功能进行动态编译,通过不使用任何反射操作以达到高性能的对象创建过程。(请确定您在阅读本节之前已经熟悉DI概念)
WDI的核心接口Container定义如下:
public interface Container {
public <T> void bind(Key<T> key, Provider<? extends T> provider);
public <T> T getInstance(Key<T> key, String data) throws DIException;
public <T> Provider<? extends T> getProvider(Key<T> key);
public <T> Fetcher<T> createFetcher(Key<T> key, String data);
}
Container作为DI容器本身,提供了基本的类型配置和获取的功能。
Key类代表某一类型绑定的最小粒度单位,Key类对象含有两个值,一个是java类型,一个是字符串名称,两个值共同决定了对象创建方法。字符串名称提供了有益的类型补充作用,从而可实现同一java类型的不同绑定方法。
Provider接口代表着类型的创建方法,需要用户提供,其定义如下:
其中key参数为对应的类型键值,data为动态参数(稍后详述),nativeProvider为本地创建者,通过它可绕过用户Provider而直接获取容器创建的对象。
public interface Provider<T> {
T get(Key<T> key, String data, NativeProvider<?> nativeProvider);
}
在Container中,使用bind方法绑定一个类型到一个实例提供者,使用getInstance方法从DI容器中获得某种类型的一个实例,通过getProvider方法取得之前绑定在某种类型上的实例提供者,通过createFetcher方法创建一个对象延迟获取器。
在WDI中,对象创建的最精细控制依据三个要素:对象类型,静态参数,动态参数。静态参数和动态参数都是以字符串的形式出现的,对象类型是以java.lang.reflect.Type的形式出现的(目前支持Class和ParameterizedType)。
cn.webwheel.di.Desire注释描述了对象注入位置,它可以放置在公开的构造方法,公开的对象方法,公开的对象属性以及公开对象方法的参数之上。当类仅有一个公开构造方法时,此构造方法上的Desire注释可以省略。
Desire注释包含了两个字符串值:value为静态参数(与Key中的name对应),data为动态参数。Deisre注释所处位置所对应的对象类型和value参数共同构成了一个Key键值,以此来标识注入所使用的Provider。如果未在Container中配置对应此Key键值的Provider,则将直接使用NativeProvider创建对象完成注入。
例如,典型的代码为:
public interface HelloService {
void sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
public void sayHello(String name) {
System.out.println("hello " + name + "!");
}
}
public class HelloLogic {
//通过Desire标识注入位置,并指定静态参数为name,动态参数为abc
@Desire(value="name", data="abc")
public String name;
private HelloService service;
//在setter方法上标识注入
@Desire
public void setHelloService(HelloService service) {
this.service = service;
}
public void doLogic() {
service.sayHello(name);
}
}
public class HelloTest {
public static void main(String[] args) {
//DI容器配置
Container container = new ...
//这里绑定了HelloService接口到实现类HelloServiceImpl
container.bind(new Key <HelloService>(HelloService.class, Desire.DefaultName), new Provider<HelloService>() {
public HelloService get(Key<HelloService> key, String data, NativeProvider<?> nativeProvider) {
return (HelloService) nativeProvider.get(new Key(HelloServiceImpl.class, key.getName()));
}
});
//这里绑定静态参数为name的String类型,直接将动态参数作为对象返回值
container.bind(new Key<String>(String.class, "name"), new Provider<String>() {
public String get(Key<String> stringKey, String data, NativeProvider<?> nativeProvider) {
return data;
}
});
//测试代码
//HelloLogic未经过DI配置,所以会默认的从nativeProvider中创建
HelloLogic logic = container.getInstance(
new Key<HelloLogic>(HelloLogic.class, Desire.DefaultName), Desire.DefaultData);
//这里应该打印出Hello abc!
logic.doLogic();
}
}
关于NativeProvider,其get动作不会检查key值是否已在Container中进行了配置,而直接通过WDI规则(主要指Desire注释)创建新的对象, 当从头Container中获取一个未经过配置的对象时,即会调用NativeProvider进行实例创建。
在WDI中,有两个类实现了Container接口:DefaultContainer与RichContainer,
DefaultContainer实现了Container接口,并且以多层次的方式组织容器,从而可以隔离不同模块的对象创建方式(如果当前容器找不到合适的类型绑定,则递归的向上层容器查找)。
更加方便的是,DefaultContainer提供了一个方法:addNamedDesire,签名如下:
通过这个方法,可以将一个注释接口当作静态参数使用,增强了类型安全性。此注释接口必须含有名称为value类型为String的接口参数。此参数作为注入动态参数使用。例如:
public void addNamedDesire(String name, Class<? extends Annotation> anCls)
RichContainer是DefaultContainer的子类,为其提供更为丰富方便常用的方法。一般情况下,优先使用RichContainer。
public interface Flower {
void bloom();
}
public class Rose implements Flower {
public void bloom() {
System.out.println("beautiful rose");
}
}
//必须标识Retention为RUNTIME,并且拥有一个名为value类型为String的参数
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Red {
String value() default Desire.DefaultData;
}
public class FlowerTest {
//等同于 @Desire("red")
@Red
public Flower flower;
public static void main(String[] args) {
RichContainer container = new RichContainer();
//绑定静态参数为red的Flower类型到Rose实现之上
container.bind(Flower.class, "red", Rose.class);
//绑定静态参数red到Red注释上
container.addNamedDesire("red", Red.class);
//获取一个test实例
FlowerTest test = container.getInstance(FlowerTest.class);
//应打印出 beautiful rose
test.flower.bloom();
}
}
集成WMVC:使用WDI替换掉WMVC中Main类getInstanceOf的默认实现,则可使用WDI创建Action对象,将Action对象纳入WDI容器。 稍后将详述WDI与WMVC集成的出色表现。
WDI主要功能介绍至此,它在WebWheel框架中起着非常重要的作用,代码的组织都是在WDI容器中进行。当然,WDI在WebWheel框架中是独立的部分,随时可以拿出来单独使用。
WebWheel包含了一个很具有特色的模板引擎(以下简称为WTE),与当今市场上的模板引擎最大的不同之处就在于其最大限度的保留了html语言的原貌, 不会破坏html结构,对页面设计开发者和可视化编辑软件非常友好,同时其支持组件化的html模板组织方式,为html重用提供了有力的支持,并且它采取了编译执行的方式来运行, 性能上堪比jsp。
市场上模板引擎绝大多数都是采用自定义标签(struts、jsf、wicket等)或者内嵌脚本语言(jsp、freemarker、velocity等)进行内容动态控制和逻辑控制。 这两种方式实际上都会对html本身产生或多或少的侵入,以至于模板对于页面开发维护人员来讲,难于维护,甚至要学习这些模板技术才能进行模板修改。
WTE则另辟蹊径,通过对html友好的自定义标签属性来进行动态内容的逻辑控制,三种自定义标签属性可完备的实现所有的程序逻辑控制能力。
WTE将一个文本模板和一个java对象组装形成一个动态输出,每一个模板对应一个java对象。模板作为表现,java对象作为数据模型,这不但是二者的逻辑地位,也是WTE最佳实践的体现。
WTE API仅有一个接口和一个类组成,如下:
ComponentRenderer为渲染器接口,负责内容输出,它含有一个render方法,接受一个输出流、一个java对象和一个代理输出渲染器(稍后详述)作为参数。
它由ComponentRendererBuilder类生成,ComponentRendererBuilder为渲染器的创建者,它通过一个java类和模板文本进行构造。
public class ComponentRendererBuilder<T> {
public ComponentRendererBuilder(Class<T> componentClass, String template)
throws PlainTemplateParser.TemplateParserException {...}
public Class<? extends ComponentRenderer<T>> build() {...}
}
public interface ComponentRenderer<T> {
void render(Writer writer, T component, ComponentRenderer delegate) throws IOException;
}
WTE静态模板的格式与xml几乎相同,只是不受单根的限制,可以多根甚至无根,但一旦有标签就必须正确的关闭。
首先来看一个最简单的模板:
对应的java类:
<!-- template.html -->
<html>
<body>
Hello ${name}!
</body>
</html>
测试代码:
public class Model {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
此测试代码的输出结果应当是:
public class Test {
public static void main(String[] args) throws Exception {
//将模板文件读入字符串
String template = ...;
//创建渲染器的创建者,传入模型java类以及模板文本
ComponentRendererBuilder<Model> builder = new ComponentRendererBuilder<Model>(Model.class, template);
//创建渲染器类
Class<? extends ComponentRenderer<Model>> rendererClass = builder.build();
//实例化渲染器
ComponentRenderer<Model> renderer = rendererClass.newInstance();
//组装模型java对象
Model model = new Model();
model.setName("kobe2000");
//渲染输出到StringWriter,并打印到控制台
StringWriter writer = new StringWriter();
renderer.render(writer, model, null);
System.out.println(writer.toString());
}
}
<!-- template.html -->
<html>
<body>
Hello kobe2000!
</body>
</html>
如上所示,WTE模板中的动态输出通过${}标记实现,其内部为WTE表达式语言(以下简称WEL),表达式语法与java语言几乎相同,
不同之处在于调用方法时,如果方法的名称以get或is开头,则该方法可以省略get或is并将余下的名字首字母小写,如果方法的参数列表为空,
则可省略掉小括号,如:
WEL可支持的数值运算符和逻辑运算符包括:+ - * / % < <= > >= == != && || ?:,运算符的优先级与java语言表达式相同。支持数组取值操作。
WEL可支持的常量包括:字符串(使用单引号引用),数值常量,true,false,null。
java: a.foo()
WEL: a.foo() 或 a.foo
java: a.getLuck()
WEL: a.getLuck() 或 a.luck() 或 a.getLuck 或 a.luck
java: a.isFunny()
WEL: a.isFunny() 或 a.funny() 或 a.isFunny 或 a.funny
java: a.dowith(b.getHead()[0])
WEL: a.dowith(b.head[0]) 或 ...
WEL的求值是建立在一个树状的参考系之下的,每个WEL会对应一个参考对象,WEL会首先参考此对象进行求值,如果失败则递归的参考此参考对象的父级参考对象,直至根参考对象。 根参考对象就是模板所对应的java对象。
当WEL内遇到一个非常量语法单元时,将在参考系内由低向高寻找可以匹配的参考对象进行求值,例如表达式${foo()},当前的参考对象为类A的实例, 则首先在类A上查找是否有名为foo(或者getFoo、isFoo)并且参数列表为空的公开方法,如果存在这样的方法,则会在A的实例上运行foo方法, 并获得返回值作为表达式的结果,否则向上一级查找参考对象,例如B的实例,在其上是否有符合的方法,递归直至根参考对象,如果仍然失败的话, 则WTE忽略掉此表达式,而视之为普通的文本字符串。
如前所述,在WTE模板中,通过三类自定义标签属性控制动态内容,他们分别是以w: q:和v:为前缀的标签属性。
<div w:style="color:red" style="color:blue">I'm red</div>
<span w:title="hello">Hi</span>
<div w:tag="center">I will be in center</div>
<div w:content="new content">I'm not exist</div>
tag和content是两个特殊的名称,当它们被使用时,则代表动态输出是标签名和标签内容,上述的模板动态输出效果是:
<div style="color:red">I'm red</div> <!-- style属性值被替换 -->
<span title="hello">Hi</span> <!-- 增加title属性 -->
<center>I will be in center</center> <!-- 标签的名称变为center -->
<div>new content</div> <!-- 标签的内容变为new content -->
w:属性值不仅仅可填写文本,也可使用WEL进行动态输出控制,例如:
<div w:title="${getTitle()}" w:style="color:${color}">content</div>
模板对应的java类:
public class Page {
public String color = "#112233";
public String getTitle() {return "this is title";}
}
则输出结果将为:
<div title="this is title" style="color:#112233">content</div>
值得注意的是当表达式求值结果为null的时候,动态输出内容为空字符串,其他情况与String.valueOf方法的返回方式相同。
<body w:="${body}">
<div w:="${header}">
<h2>${title}</h2>
</div>
<div w:="${content}">
<div w:="${left}">
${menus}
</div>
<div w:="${right}">
${block}
</div>
</div>
</body>
body的下一级有header和content,content下一级又有left和right,它们的求值结果共同组成了这个树状的参考系,每个参考对象对应一个模板组件,
在每个模板组件下的WEL将参考所处的位置对应的参考对象依次向上求值,例如${body}表达式对应于模板对象,也就是根参考对象,${header}和${content}对应于${body}表达式的求值结果,
${title}对应于${header}的求值结果,${left}和${right}对应于${content}的求值结果,${menu}对应${left},${block}对应${right}。
<ul>
<li w:="${messages}">
[${_idx}]${this}(${length})
</li>
<li w:="messages">
[索引]名字(长度)
</li>
</ul>
对应的模板类为:
public class Page {
public List<String> messages;
}
${messages}表达式在参考对象Page实例下的求值结果是一个List对象,满足循环输出的要求,将产生类似这样的动态内容:
<ul>
<li>
[0]hello(5)
</li>
<li>
[1]webwheel(8)
</li>
<li>
[2]template(8)
</li>
</ul>
特别说明的,当同一循环模板表达式连续出现时,WTE将会只处理第一个模板组件,而忽略掉之后的连续模板组件,例如上述的li标签,
无论在后面跟多少个标记w:="messages"的li,效果都是一样的,这有利于模板的静态预览效果。_idx和this是两个特殊的关键字,
前者为当前在循环当中的数字索引值,从0开始,类型为int。后者代表当前的参考对象,循环或非循环模板组件中都可以使用,这个例子中就是List中每个String类型的元素。
<div w:="${-messages}">
${size}
</div>
这样div标签不再是一个循环渲染模板,div里面的区域也不再对应List中每个元素,而是将List对象作为参考对象,所以这时${size}可以正确的解释为List.size方法进行输出。
public interface ComponentRenderer<T> {
void render(Writer writer, T component, ComponentRenderer delegate) throws IOException;
}
其中的delegate参数将会负责外部组件的渲染工作,例如w:表达式为w:="${*story},则将会把story的求值结果(遵循循环模板相同的原则)传入delegate对象,将渲染当前标签的任务交给其完成。如下:
<body>
<div w:="${*story}">
I will disappear.
</div>
</body>
public class Delegate implements ComponentRenderer {
public void render(Writer writer, Object component, ComponentRenderer delegate) throws IOException {
Story story = (Story)component;
writer.write(story.getTitle() + " - " + story.getAuthor());
}
}
使出如此的模板和代理对象后,渲染结果将会如此:
<body>
阿里巴巴与四十大盗 - 阿拉伯人民
</body>
<div class="warp" q:class="${exp}"></div>
<input type="checkbox" q:checked="${exp}"/>
<div q:tag="${exp}">hahaha</div>
<span q:content=${exp}>hehehe</span>
<b q:="${exp}">fade away</b>
${exp}表达式决定是否输出对应的标签属性,与w:类似,tag和content为两个特殊值,指示是否输出标签本身和标签的内容,如果q:后面为空,
则代表是否输出整个标签(标签本身和内容)。判断的依据是表达式的求值结果是否等于其类型的默认值,例如对于数值类型,则0为假,非0为真,
对于布尔类型,false为假,true为真,对于对象类型,null为假,非null为真。上述模板根据表达式${exp}求值结果的不同分别会显示为:
<!-- exp为真 -->
<div class="warp"></div>
<input type="checkbox" checked="checked"/>
<div>hahaha</div>
<span>hehehe</span>
<b>fade away</b>
<!-- exp为假 -->
<div></div>
<input type="checkbox"/>
hahaha
<span></span>
值得注意的是,checkbox一例中,模板未出现checked属性,但是出现了q:checked,则这时如果动态输出checked属性,则其属性值将取属性名,这里即是checked文本。
<div v:book="${this}">
<span>${book.name}</span>
<ul>
<li w:="${book.authors}" v:authorName="${this.name}" v:no="${_idx+1}">
[${no}]${authorName}(${authorName.length})
</li>
<li w:="${book.authors}">
[2]kobe2000(8)
</li>
</ul>
</div>
在变量作用域内,WEL可直接引用其进行求值
在cn.webwheel.utils包中,包含了一些便利的工具将上面三种单一的框架集成起来,其中最关键的是DefaultMain类,它包含一个WDI容器, 并使用其创建Action对象(通过重写getInstanceOf方法),同时设置了一些类型的自动注入:Container、RichContainer、Main、ObjectFactory、 ServletContext,在Action实现类中如果需要这些对象可直接进行注入,而且通过DIFilter过滤器实现了与http请求相关的类型注入配置:WebContext、 HttpServletRequest、HttpServletResponse、HttpSession,通过WebParam和WebCookie注释接口的配合,实现了http参数和cookie参数的注入配置, http参数类型包括Map<String, String[]>、String、String[]、boolean、byte、byte[]、short、short[]、int、int[]、long、long[]、float、float[]、double、double[], cookie参数类型包括Cookie[]、Cookie、String、boolean、byte、short、int、long、float、double。
DefaultMain中还通过TemplateResult和TemplateResultInterpreter将WTE融合进来,提供bindAction/bindPage/bindComponent三个方法为http请求和模板组件进行映射。 而且DefaultMain中还提供了一个autoBind方法以约定的方式进行自动配置,节省映射配置代码。还有一个scanPlugins进行插件扫描。