我们的web框架是在tomcat环境上开发的,也是基于前面所述的消息对象编程框架或模板。整个应用模式与前面介绍的普通环境下的一样,所以理解了前面的案例,那么也很容易理解这个了。cn.tianlong.java包下的servletutils 为web框架,servletdemo为测试代码。配置在WEB-INF/conf 下
虽然是在tomcat上开发的框架,但我对tomcat和servlet/jsp编程没有任何经验,也没有写过一行代码,只是大概看了看《Tomcat与Java Web开发技术详解(第2版)》(孙卫琴)这本书。不过这不影响我们的框架开发,因为我们的框架基本没有用到servlet或jsp。我们通过一个过滤器filter来启动模块工厂,然后就与普通环境编程一样了。下面是大概流程
对于web框架与其他应用的唯一不同,我觉得就是多线程。在前面我们介绍的案例中都是单线程。对于web访问,每一个web访问需求启动一个线程。当然我们可以对每一个线程进行工厂、框架启动,但是这非常没有效率,因为线程销毁后,工厂、框架要销毁,下一个访问来还要重新建立,而重新建立对象要花许多时间。因此在过滤器中,我们先建立个工厂池,其实也是框架池,可以初始化一定数量的工厂,当一个访问需求来时从工厂池取出工厂,无需建立,等需求完毕后归还工厂。下面我们看过滤器的配置和代码。
首先在tamcat的web站点配置文件web.xml文件中配置框架过滤器:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<filter>
<filter-name>tlfilter</filter-name>
<filter-class>cn.tianlong.java.servletutils.TLFilterWithFactoryPool</filter-class>
</filter>
<filter-mapping>
<filter-name>tlfilter</filter-name>
<url-pattern>/tlobject/*</url-pattern>
</filter-mapping>
</web-app>
根据文件配置,对于web站点下/tlobject/* 所有访问通过TLFilterWithFactoryPool过滤。目录/tlobject 可根据自己情况定义。看TLFilterWithFactoryPool代码:
public class TLFilterWithFactoryPool implements Filter, IObject {
protected String configFile;
protected String configDir;
protected List<TLWModulefactory> factoryPool;
protected int poolNumber = 200;
protected int maxPoolNumber = 300;
protected TLMsg getAppMsg = new TLMsg()
.setAction(TLObjectFactory.FACTORY_GETMODULE)
.setParam(FACTORY_MODULENAME, "appCenter");
protected TLMsg startAppMsg = new TLMsg().setAction("start");
protected ServletContext context;
protected TLWModulefactory bootFactory;
public void init(FilterConfig config) throws ServletException {
Filter.super.init(config);
context = config.getServletContext();
configDir = context.getRealPath("/WEB-INF/conf");
configFile = configDir + File.separator + "appConfig.xml";
TLWModulefactory.setConfigDir(configDir);
bootFactory = new TLWModulefactory("moduleFactory");
TLMsg msg = new TLMsg().setAction("getModule")
.setParam(FACTORY_MODULENAME, "appConfig");
TLWAppConfig appConfig = (TLWAppConfig) putMsg(bootFactory, msg).getParam(INSTANCE);
if (appConfig.getParams().get("iniPoolNumber") != null)
poolNumber = Integer.parseInt((String) appConfig.getParams().get("iniPoolNumber"));
if (appConfig.getParams().get("maxPoolNumber") != null)
maxPoolNumber = Integer.parseInt((String) appConfig.getParams().get("maxPoolNumber"));
registInfactory(bootFactory, "servletContext", context);
bootFactory.boot();
initFactoryPool();
setModuleConfig();
}
private void setModuleConfig(){
TLMsg mmsg = new TLMsg().setAction("getModule")
.setParam(FACTORY_MODULENAME, "monitorConfig");
TLMonitorConfigForThread monitorConfig = (TLMonitorConfigForThread) putMsg(bootFactory, mmsg).getParam(INSTANCE);
if(monitorConfig !=null)
{
TLMsg smsg = new TLMsg().setAction("setFactoryPool")
.setParam("factoryPool", factoryPool);
putMsg(monitorConfig,smsg);
}
}
public void destroy() {
bootFactory.destroyModule();
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
// chain.doFilter(req, resp);
long startTime = System.currentTimeMillis();
TLWModulefactory modulefactory = getFreeFactory();
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
registInfactory(modulefactory, "servletRequest", request);
registInfactory(modulefactory, "servletResponse", response);
MyAPPCenter appCenter = (MyAPPCenter) putMsg(modulefactory, getAppMsg).getParam(TLObjectFactory.FACTORY_MODULEINSTANCE);
putMsg(appCenter, startAppMsg);
modulefactory.setNoUse();
if (factoryPool.size() < maxPoolNumber)
factoryPool.add(modulefactory);
Long nowTime = System.currentTimeMillis();
Long runtime = nowTime - startTime;
TLLog.setLog(TLFilterWithFactoryPool.class, "运行时间:" + runtime, LogLevel.INFO);
}
protected void registInfactory(TLWModulefactory modulefactory, String name, Object object) {
TLMsg registInFactoryMsg = new TLMsg().setAction("registInFactory")
.setParam(FACTORY_MODULENAME, name)
.setParam(INSTANCE, object);
putMsg(modulefactory, registInFactoryMsg);
}
protected void initFactoryPool() {
factoryPool = Collections.synchronizedList(new ArrayList<TLWModulefactory>());
for (int i = 0; i < poolNumber; i++) {
TLWModulefactory modulefactory = new TLWModulefactory("moduleFactory");
modulefactory.setNoUse();
registInfactory(modulefactory, "servletContext", context);
factoryPool.add(modulefactory);
}
}
protected TLWModulefactory getFreeFactory() {
TLWModulefactory factory = getFreeFactoryFromPool();
if (factory != null)
return factory;
factory = new TLWModulefactory("moduleFactory");
factory.setUse();
registInfactory(factory, "servletContext", context);
TLLog.setLog(TLFilterWithFactoryPool.class, " 产生新工厂", LogLevel.WARN);
return factory;
}
protected synchronized TLWModulefactory getFreeFactoryFromPool() {
if (factoryPool.size() == 0)
return null;
TLWModulefactory factory = factoryPool.get(0);
if (factory != null) {
factoryPool.remove(0);
TLLog.setLog(TLFilterWithFactoryPool.class, " 取出一个工厂 " + factoryPool.size(), LogLevel.WARN);
return factory;
} else
return null;
}
@Override
public String getName() {
return null;
}
@Override
public TLMsg putMsg(IObject toWho, TLMsg msg) {
return toWho.getMsg(toWho, msg);
}
@Override
public TLMsg getMsg(Object fromWho, TLMsg msg) {
return null;
}
}
在过滤器的初始化函数init中,首先设置文件目录、配置文件等项。然后初始化了一个工厂。这个工厂的目的是对一些公用的模块进行boot初始化,这些模块对于所有线程都是公用的,比如cache、数据库链接等。然后initFactoryPool()初始化工厂池。setModuleConfig()函数设置配置文件监听模块,既前面介绍的监听配置文件是否改变而动态加载的模块。由于web框架是多线程的,因此这里的配置监听模块与前面介绍的不同,要处理多工厂。
在过滤器运行函数doFilter中,对于每个连接取出一个工厂,然后通过这个工厂启动appCenter:
putMsg(appCenter, startAppMsg);
对于每个http变量,如request、response
,放到工厂里面,当成模块一样处理,其他模块用到时,直接从工厂里面取即可。
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
registInfactory(modulefactory, "servletRequest", request);
registInfactory(modulefactory, "servletResponse", response);
在appCenter中,基本类是TLWAPPCenter,我们看它的启动消息行为:
protected void start(Object fromWho, TLMsg msg) {
startUrlToServlet();
}
protected void startUrlToServlet() {
putMsg("urlMap",createMsg().setAction("doWithUrl"));
}
appCenter启动urlMap模块,并传递消息"doWithUrl" ,urlMap模块对url进行解析,完成url与消息msg的转换,因为我们的模块都是对消息的处理。urlMap的配置文件,也就是描述url对应消息msg的文件:
<?xml version="1.0" encoding="UTF-8" ?>
<moduleConfig>
<!-- 目录相对于filter配置目录下-->
<url-mapping>
<url value="/*" >
<!-- 对于目录设置*,该目录下只能为同一个servmodule处理 。
map方式: 该目录后下url=“方法” ,如/home 对应 index 方法。没有map的方法无法访问-->
<msg destination="appCenter" default="index"
bd="baidu" home="index" input="input" content="content"
getmsg="getmsg" ve="velocity" http="http" />
</url>
<url value="/baidu" >
<msg action="baidu" destination="appCenter" />
</url>
<url value="/service" >
<msg action="service" destination="msgMap" />
</url>
<url value="/index" >
<msg action="index" destination="appCenter" />
</url>
<url value="/test/*" >
<msg destination="servletTest1" getuser="user" index="index" />
</url>
<url value="/db/*" >
<msg destination="servletDbTest" find="find" findall="findall"
query="query" dbmodle="dbmodle"/>
</url>
</url-mapping>
</moduleConfig>
配置逻辑比较清晰。url为/baidu 转到appCenter的baidu行为。对于"/db/*" ,db目录下所有的url都转给servletDbTest,其中/db/find 对应消息行为find, /db/dbmodle 对应消息行为为dbmodle 。对于根目录"/*"下的访问,都转给appCenter,其中bd也转向baidu。这样通过/baidu、/bd访问的是一个内容。/home也转向index行为,这样/home、 /index 都访问index行为。从这里看出我们可以灵活配置url对应的消息模块及处理方法。
看到这里,有同学发现没有,我们模块有名字,通过名字直接访问模块,模块的方法也有名字,消息msg的action其实就是模块方法的名字。以往我们调用一个对象的方法很困难,首先要有这个对象的实例,然后才能调用方法。如果我们希望另一个对象也执行这个对象的方法,我们可以将实例传递过去,但怎么告诉对方执行那个方法呢?可能只有把方法名传递过去,然后对方通过反射的办法找出这个方法再调用。而在这里我们完全不用,我们只要告诉对方方法的名字就行了,对方就很容易通过名字执行方法。想想这是不是我们现实的场景,人们之间互动都是传递概念,而不是传递实物。领导说“你让小王过来”,领导绝对不是把小王拉到我们面前说“这个人,你让他过来”,同样小王当听到“过来”这个方法的名字时,他自动开始走的行为,我们不用在小王面前表演如何过来或去控制小王行走。
TLWUrlMap完成url到msg的转换后将消息发送到对应的模块,由模块进行处理。下面我们看框架主配置app_config.xml,主要看主要配置:
<appConfig>
<params>
<name value="demo"/>
<verison value="1"/>
<!-- 模块默认包前缀 -->
<package value="cn.tianlong.java"/>
<charset value="text/html;charset=utf-8"/>
<!-- 框架url路径 -->
<tlobjectPath value="/tlobject"/>
<!-- 默认输出接口,volectiy -->
<outInterface value="velocity"/>
<!-- 默认输入接口,volectiy -->
<inInterface value="driectFromUserInterface"/>
<!-- 工厂池初始数量-->
<iniPoolNumber value="50"/>
<!-- 工厂池最大数量-->
<maxPoolNumber value="300"/>
<!-- 打开全msg日志
<msgLog value="enable"></msgLog>
-->
</params>
。。。。。
<factoryBoot>
<msg action="getModule" moduleName="log" usePreReturnMsg="false" />
<msg action="getModule" moduleName="monitorConfig" usePreReturnMsg="false" />
<msg action="getModule" moduleName="webServiceCustomerPool" usePreReturnMsg="false" />
<msg action="getModule" moduleName="cache" usePreReturnMsg="false" />
<msg action="getModule" moduleName="dbserver1" usePreReturnMsg="false" />
<msg action="getModule" moduleName="dbserver2" usePreReturnMsg="false" />
<msg action="getModule" moduleName="abMsgtable" usePreReturnMsg="false" />
</factoryBoot>
在 <params>中定义了web的一些参数设置,如果工厂池数量、最大数量等。我们注意下面两项
<!-- 默认输出接口,volectiy -->
<outInterface value="velocity"/>
<!-- 默认输入接口,volectiy -->
<inInterface value="driectFromUserInterface"/>
输出给用户可以有不同的格式,默认输出是velocity模块,对应模块类TLWVelocityOutInterface,如想更换输出,更改配置即可。同样如果我们想对输入处理也更换输入接口即可。我们看在模块中如何获得用户输入和给用户输出:
看appCenter中一个行为
private void input(Object fromWho, TLMsg msg) {
String username = getUserData("username");
String [] inputname ={"username","passwd"};
TLMsg input=getUserData(inputname);
String username1= (String) input.getParam("username");
if(username1==null)
username1="";
String passwd = (String) input.getParam("passwd");
if(passwd==null)
passwd="";
outData odata = creatOutDataMsg();
odata.addData("用户输入:");
odata.addData("username",username);
odata.addData("inputUsername",username1);
odata.addData("inputPasswd",passwd);
putOutData(odata);
}
根据上面的url转msg配置,url为/input 对应上面这个行为。对于input?username=tianlong&&passwd=12356。可以单独取出某个输入变量,如:
String username = getUserData("username")
也可以用数组的方式统一取出输入变量,转换到一个msg中的参数
String [] inputname ={"username","passwd"};
TLMsg input=getUserData(inputname);
输出时,先创建输出变量:
outData odata = creatOutDataMsg();
然后对输出变量赋值,最后输出变量
putOutData(odata);
输出模块为velocity模块,会根据velocity模块的配置文件查找html模板 ,velocity模块配置文件:
<templates>
<dataid name="appCenter.velocity" template="appCenter.velocity.vm" />
<dataid name="servletTest1.index" template="servletTest1.index.vm" />
<dataid name="servletTest1.user" template="servletTest1.user.vm" />
<dataid name="servletDbTest.db.user" template="servletDbTest.db.user.vm" />
</templates>
当我们创造一个输出变量时,默认有个变量id,在配置中,这个变量id对应相应的velocity模板。如果更换html模板,则更改配置即可。没有对应模板的输出数据则直接输出。上面那个/input输出数据没有对应的模板,因此直接输出,我们看结果:
在input行为中,我们没有用到jsp或servlet技术,就是获得、处理、输出,所有这一切都是在框架中实现。现在我们看看web的日志:
从日志中可以清晰的看到框架流程。首先取出工厂,启动一个appCenter,启动urlMap并解析url,最后输出。我们发现上面许多解析配置,说明这模块刚创建。因为初始化工厂时,仅仅工厂实例化,其他模块没有实例化。第一次运行实例化后,则模块保存在工厂里,下次运行无需再创建,这和servlet是一样的。对于保存在工厂里的模块,要注意实例变量,因为模块为公共的。如果需要模块每次实例化,那么在配置文件中设置非单例模式即可,见前面的配置文件设置说明。上面看到启动了abMsgtable、mslog,这是appcenter初始化信息执行的,可以取消,目前为实验。下面我们看下第二次访问情况,因为工厂池设置了50个,那么起码50次访问后才能全部初始化。我们用ab压力测试工具访问50次后再看看。
这时我们发现没有解析配置的日志提示了,说明模块实例都已经初始化完备了,每次访问直接调用了,运行时间由开始的14ms 减小到1ms。当然不是所有的模块都实例化,只有该url用到的实例化。
理论上每次用完一个工厂然后返回到工厂池,实际我发现某些情况代码错误,比如空值之类可能导致该线程崩溃或出错而使工厂没有返回到工厂池,最后工厂池消耗为空,则不断地创建新工厂,降低效率。
通过上面大概介绍,看到关于这个web框架的设计体现了以下的原则:
1、灵活性
模块可随意配置,例如介绍配置文件中的输入、输出接口模块。如果输出模板不用velocity模板采用其他模板,则根据接口规则设计其他模板模块即可,不影响其他模块。
2、层次透明性
一个层次做的工作不应该涉及其他层次。相对于应用模块,servlet属于底层,应用模块获得url参数,不该操作servlet层。应用模块有自己的getindata和putoutdata。相对于应用模块,输出模块也是底层,也不应该直接操作输出模块。输出模块自行分析应用模块来的数据而采取如何输出,这在velocity配置文件中得到体现,对于应用模块的输出只负责定义了数据id,输出模块怎么输出这个数据是输出的事情。我看有些框架直接把html模板名定义到应用模块里,这就是混淆了层次。
3、效率
一个框架在遵守逻辑性的前提下要有效率。对于这个框架实现,公共模块我们放到session池里,对于一般模块我们放到工厂池里。通过上面演示看出,通过这种对象的存储,效率极大提高。
该框架我只在本机电脑进行了测试,具体实用如何也不知道,有兴趣的同学可以测试及和其他框架比较。
该框架还有许多可开发的地方,目前只是基本。