(更正说明:由于框架后来改动,主启动模块“appCenter”主要用于框架启动,不在作为用户模块,因此本文章中的“appCenter”名称可改为任意用户模块名称,功能不变。本案例中的urlmap缓存配置可取消<afterMsgTable>项,其他不变。——20190102)
在之前的文章中我大概介绍过我的消息编程web框架,代码与流程比较简单,因此本来没有写这个入门的打算。因为今天偶然想了解下springmvc框架,找了个比较实用、详细的文章打算初步了解下应用过程。可是看了不到三分钟内实在无法看下去,因为它太复杂、太繁琐了。它定义了许多必须理解、学习、标准的类,什么handler、拦截器、控制器、moduleview等等,你要是应用它就必须先掌握这些类,而且也必须使用这些类。繁琐的准则和标准无疑会提高人们的学习成本 。框架本身是个工具,工具的特点是帮助人,而不是束缚人,是提高效率,而不是要花更大的成本去学习。想到这,我有了写这个消息对象编程web框架使用入门想法。现在我们看消息框架如何使用的。
在我的消息框架里没有定义什么控制类、拦截类的,也就是没有基于功能的对象区分,你可以任意根据自己的需求去写,用在任意地方。前提是你的类继承消息对象基本模块TLBaseModule(或抽象类接口 IObjcec)就行。
现在我们一步步的讲解如何三分钟实现一个简单的页面:hello 统一消息对象!
第一步、url到模块的映射
这在一般mvc框架中为urldispatch,将url映射或分发到控制器。,我们当然可以实现自动的解析url映射,但是自动缺乏灵活性。在我的框架中,映射通过手工配置,这样控制灵活,比如不同的url映射到一个模块,而参数还不同,或者随时可取消、更改一个映射。我们来看具体配置 。实现url转换的模块是TLWUrlMap,它的配置文件是 urlmMap_config.xml。在我的设计中,每个模块单独自己的配置文件,这样配置文件会很多,但调理清晰,多不等于复杂。
<?xml version="1.0" encoding="UTF-8" ?>
<moduleConfig>
<params>
<verison value="1"/>
<defaultClientUser value="webUser"/>
</params>
<url-mapping>
<url value="/*">
<msg destination="appCenter" default="index" index="index" />
</url>
<url value="/index">
<msg destination="appCenter" action="index"/>
</url>
<url value="/hello">
<msg destination="appCenter" action="index"/>
</url>
</url-mapping>
</moduleConfig>
上面是配置文件 ,对于url-mapping下的每一项是一个url映射,因为我们是消息编程,每个url是映射到一个消息,这个消息自身包含了具体的模块和方法,例如
<url value="/index">
<msg destination="appCenter" action="index" />
</url>
这表示 url为/index 映射到目的模块为appCenter 、方法为index 的消息,urlmap模块将执行这个消息。为表示灵活性,上面还定义了/hello 也映射到同一消息,也就是说/index ,/hello 执行的是同一个模块。
urlmap映射的查询顺序是首先根据url全径检索,如果没有定义这个url的具体映射,则到/* 下检索。对于映射:
<url value="/*">
<msg destination="appCenter" default="index" index="index" />
</url>
定义该目录下的映射关系,该目录所有的url统一映射到appCenter模块,默认是 index方法(如果没有输入路径),其中/index映射到index方法,如果在该目录下再加一个 url ,例如/user 要映射到usermodule方法,那我们可以定义映射 user="usermodule"。
对于配置中检索不到的url映射,框架输出无此url,不在继续执行。
定义完映射后,我们就可以编写对应的映射模块了。做这个配置半分钟足够了。
第二步、编写对应的应用模块
上面url映射到appCenter模块中,我们现在编写模块实现。“appcenter ”为模块名字,具体的类名可以任意,只要我们在工厂配置中指定即可,我们定义qqWebCenter类。如果模块需要输入与输出,那么要继承TLWServModule,该对象封装了一些输入、输出方法,方便使用 。这里TLWAPPCenter也是继承TLWServModule。
public class qqWebCenter extends TLWAPPCenter {
public qqWebCenter(){
super();
}
public qqWebCenter(String name ){
super(name);
}
public qqWebCenter(String name , TLObjectFactory modulefactory){
super(name,modulefactory);
}
@Override
protected TLMsg mycheckMsgAction(Object fromWho, TLMsg msg) {
TLMsg returnMsg=null;
switch (msg.getAction()) {
case "index":
index(fromWho,msg);
break;
default:
putMsg("error",creatOutMsg().setAction("setError").setParam("content","no action"));
}
return returnMsg ;
}
protected void index(Object fromWho, TLMsg msg) {
outData odata = creatOutDataMsg("index");
odata.addData("message","hello 统一消息编程!");
putOutData(odata);
}
}
在qqWebCenter类中,我们定义了url映射到的方法 index(),在index方法中:
首先我们创建了输出数据对象 odata ,然后给这输出数据添加数据,也就是我们要输出的内容。最后输出数据。
数据如何输出的呢,这里面没有体现,也没有体现对应的html模板,因为如何输出数据是输出接口的责任。我看许多框架,在模块方法里定义view模板,我认为逻辑是错误的。先不说模板写在代码里就太僵化,主要是数据模板与数据是对应关系,而不能与处理模块的方法成为对应关系。后面我们可以看到随着更换输出接口而产生不同的输出格式。现在数据如何与模板对应的呢,在上面创建数据对象的时候:
outData odata = creatOutDataMsg("index");
传入的参数“index” 为数据标识,或称为数据id,通过这个数据id来实现与模板的对应,数据id可任意定义。实现中,为保持数据唯一,数据Id默认是模块的名字及传入的参数组合,因此上面代码隐含定义了该数据的标识id为 :appCenter.index。
这个模块方法用了三条语句,一分钟吧。
第三步、配置模板
完成了模块编码、用户数据,现在最后一步——输出模板。目前我通过封装velocity来输出页面。为什么用它来做输出接口?因为我还没有时间学习其他的,它可以就用么。因为输出接口是独立的,因此我们可以包装、配置任何输出接口。
我们先编写模板,模板遵守velocity语法,这里就输出一个变量,因此模板很简单,模板起名appCenter.index.vm,名字与数据id一致是为了方便。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Title</title>
</head>
<body>
$message
</body>
</html>
然后我们配置数据与模板的对应,在velocity的配置文件中velocity_config.xml(包装后的velocity接口配置文件)定义数据与模板的对应关系:
<?xml version="1.0" encoding="UTF-8" ?>
<moduleConfig>
<params>
<resource.loader value="file"/>
<input.encoding value="UTF-8"/>
<output.encoding value="UTF-8"/>
<resource.loader value="file"/>
<file.resource.loader.class value="org.apache.velocity.runtime.resource.loader.FileResourceLoader"/>
</params>
<templates>
<dataid name="appCenter.index" template="appCenter.index.vm" />
</templates>
</moduleConfig>
项目 <dataid name="appCenter.index" template="appCenter.index.vm" /> 指明数据appCenter.index对应的模板是 appCenter.index.vm。
这个html模板和增加一条配置1分钟吧。
第四步、配置模块工厂
在第二步时候我们编写了模块,模块需要加到模块工厂里才能被使用,在工厂配置文件中 增加一项:
<module name="appCenter" classfile=".application.qinqin.web.qqWebCenter" configfile="appConfig.xml"/>
该项指出appCenter对应的类,模块工厂会根据调用自动创建。注:这里我们用的是框架启动主模块appCenter,这是框架必备的。url不一定要映射到启动主模块里,任何模块都可以。
仅仅增加一行配置,半分钟。
好了,我们来看输出结果:
我们没有定义继承什么控制类、view类 ,也没有需要任何servlet、jsg代码,三分钟轻松实现了。
现在我们在进一步!
上面页面没有用户输入,现在我们提供一个用户输入 username ,页面输出:username 说 “hello 统一消息对象!”。
为简便,我们不在写提交页面,直接在url里写上用户变量 /index?username=xiaoming 。
对于用户输入变量的获得 可以用以下两个方式:
1、我们在模块里可主动获取
String username = getUserData("username");
2、我们在urlmap配置里面定义变量,由框架自动获取。
我们现选择第二种,在配置里面定义,修改urlmap配置
<url value="/index">
<msg destination="appCenter" action="index" clientVars="username" />
</url>
在url 映射消息中增加一个项ClientVars,定义用户输入的变量名,如果多个变量,则用分号;间隔。
修改模块方法增加获取变量代码:
protected void index(Object fromWho, TLMsg msg) {
String username = (String) msg.getParam("username");
outData odata = creatOutDataMsg("index");
odata.addData("username",username);
odata.addData("message","hello 统一消息编程!");
putOutData(odata);
}
通过urlmap自动获得的变量,自动添加到函数参数 msg中,直接提取即可。
最后修改模板增加username变量:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Title</title>
</head>
<body>
$username 说:$message
</body>
</html>
现在我们看输出结果:
非常完美,轻松实现了!
革命继续!
我们现在用的html页面输出,要改成json格式输出,因为公司开发出了手机端app,传输数据是json。前面我们说过输出是接口的事情,与数据模块无关,因此我们更改接口就可,不用更改任何代码,更改urlmap配置:
<url value="/index">
<msg destination="appCenter" action="index" clientVars="username" clientUser="tokenUser" />
</url>
在配置中 <defaultClientUser value="webUser"/>定义默认用户类型。在上面url映射中,我们增加了一个 clientUser项,告诉该url对应的用户是tokenUser类型,从而改变了用户类型,在模块工厂中 tokenUser类型对应的client接口是json输出,现在我们看结果:
很神奇,没有更改任何代码,输出自动为json格式了。
我们再继续下!
回到开始的html页面,根据业务发展流量增大,页面需要缓存。在原模块代码中加缓存要改代码,不仅麻烦,而且违反业务逻辑。我们还是通过更改配置项来实现。
<?xml version="1.0" encoding="UTF-8" ?>
<moduleConfig>
<params>
<verison value="1"/>
<defaultClientUser value="webUser"/>
</params>
<params>
<verison value="1"/>
<defaultClientUser value="webUser"/>
</params>
<beforeMsgTable>
<action value="toServlet">
<msg action="getCacheBeforeUrlMap" destination="servletCache" useInputMsg="false" usePreReturnMsg="false"></msg>
</action>
</beforeMsgTable>
<afterMsgTable>
<action value="toServlet">
<msg action="writeCacheAfterUrlMap" destination="servletCache" useInputMsg="false" usePreReturnMsg="false"></msg>
</action>
</afterMsgTable>
<url-mapping>
<url value="/*">
<msg destination="appCenter" default="index" index="index" />
</url>
<url value="/index">
<msg destination="appCenter" action="index" clientVars="username"/>
</url>
<url value="/hello">
<msg destination="appCenter" action="index"/>
</url>
</url-mapping>
</moduleConfig>
在urlmap配置中,我们增加了beforeMsgTable、afterMsgTable 两项,意思是在执行模块方法前、后,先执行该项的消息。相当于springmvc的拦截器。beforeMsgTable 用于读缓存,afterMsgTable用于写缓存。在消息目的模块 servletCache的配置文件中,我们配置模块对应的缓存项:
<?xml version="1.0" encoding="UTF-8" ?>
<params>
<verison value="1"/>
<cacheClass value="fileCache" />
</params>
<moduleConfig>
<modulesForCache>
<module name="appCenter" index="appCenter.index"/>
</modulesForCache>
<caches>
<cache name="appCenter.index" cacheKey="index" cacheValueName="content" valueType="string" exptime="180" />
</caches>
</moduleConfig>
modulesForCache 项中,name指明要缓存的模块 ,index="appCenter.index" 定义方法 index对应的缓存名字
在caches项中,是对缓存具体的配置,定义了缓存的名称、缓存值的类型、过期时间。
在params项中,cacheClass定义了所用缓存工具为fileCache,为文件缓存。我们可以更改为其他缓存,如ehache。目前我只包装了这两个缓存工具。
下面我们看实现,刷新两次页面,看输出日志:
第一次访问,框架调用了 velocity 模板接口velocityOutInterface输出页面。
第二次访问时启动servletCache,直接读取缓存 输出,输出接口是 directOutInterface。通过缓存输出时间缩小的到6毫秒。第一次输出为107毫秒,一方面是没有缓存,另一方面还是模块第一次实例化。第二次访问时,模块已经在内存实例化。
增加、取消缓存更改配置即可。而更改配置无需重新启动server,所有的配置动态更新,在前面文章我阐述过。
现在我们总结下消息框架的开发过程:
1、映射url
在在映射中自动提取输入变量,设置用户类型,各种前后拦截(所有模块都可以)
2、编写处理模块
3、编写模板,如果需要。
对比springmvc ,没有必须继承、使用的类、遵守的规则,我们无需过于的学习框架本身。同时开发快捷,配置灵活。