阅读更多
首先我们来看一个场景:

此图为sohu 首页的一部分。
这个页面比较典型,其实只要是复杂一些的页面通常都由许多不同模块的数据内容组成。如该页面就包含了新闻、体育、娱乐、视频等内容。每一个模块都需要单独查询,然后将结果填充到页面的各个部分。
如果由你来实现这个页面,你会怎么写呢?
通常,我们的写法是 (struts2.0):
public String index() {
//查询新闻
newsList = service.queryNewsList();
//查询体育
sportsList = service.querySportsList();
//查询视频
videoList = service.queryVideoList();
return SUCCESS;
}
这样是可以完成任务的,但是每一个查询都必须等待上一个查询结束后才能开始。假设查询比较耗时(不考虑缓存的因素):新闻查了20秒,体育查了10秒,视频查了5秒,则总页面至少需要 35 秒后才能打开。
如果老板对性能要求较高,且 SQL优化已经起不到帮助作用,那么还有没有其它办法可以提高程序的效率呢?
对了,最简单的就是在页面中使用 iframe,将页面拆分成若干个子页面并行加载。这样每个部分执行快慢并不影响页面的整体打开速度,从总体上讲页面的加载速度提高了。但是过多的使用iframe,会带来许多负面影响。如session问题、内存占用问题、样式问题等等,而且页面的可维护性也将变差。因此这并不是一种好的解决方案。
或者也可以采用 ajax 动态获取各个部分并填充页面。但这样大大增加了前端的代码量及实现难度。
现在我们的服务器往往都是4核、甚至是8核的了,我们能不能充分利用多核优势在后台并行运算结果并统一渲染页面呢?
思路1:当用户请求到 indexAction 的时候,启动n个子线程分别查询不同模块数据。最终当所有线程处理结束后,合并结果,渲染页面。
使用多线程,就必须解决线程同步、加解锁等等问题。需要大大增加代码量,且要求程序员水平较高。如果一个项目中,程序员水平参差不齐,那么最好别采用这种方案,否则不知哪里的出了问题就会影响整个项目的质量。
思路2:让框架去干脏活、累活。当用户请求到 indexAction 的时候,我们利用框架的功能,动态将该请求模拟成为n个子action的请求,并在子action结束时自动合并结果,渲染页面。这样我们就通过框架虚拟出了iframe 的效果,同时避免了页面中使用 iframe 带来的问题,且降低了程序的复杂度。
采用这种方案,原有的项目无需大改,程序员也可以用熟悉的方法去开发,无需关注多线程及数据合并等等问题,可谓一举多得。
现在我们来改造 NutzMVC 框架,使其能支持并行计算功能。
Nutz是一个国产的开源框架,融合了MVC、Ioc、AOP、Dao诸多功能,且设计合理,预留了多处扩展点,用户可以很容易的动态给它增、减功能。因此今天就拿它来开刀。
Nutz地址为:http://code.google.com/p/nutz/ 有兴趣的同学可以去看看。下面的内容假设您已经对 Nutz有所了解。
为了在程序中虚拟出对 action 请求,需要用到 UrlMapping 类的实例。这个类在系统启动时会自动创建,但默认为 private 直接获取不到。因此需要自定义一个加载器,将UrlMapping 实例开放出来。
这个类很简单,只需重载下 NutLoading 即可。
/**
* 缺省加载器的基础上公开 urlMaping 属性,使 MVC 运行时可以并行执行其它 URL
* @author Gongqin(gongqin@gmail.com)
*/
public class ProLoading extends NutLoading {
private static UrlMapping urlMaping = null;
@Override
public UrlMapping load(NutConfig config) {
urlMaping = super.load(config);
return urlMaping;
}
/**
* 返回 urlMaping 实例
* @return
*/
public static UrlMapping getUrlMaping() {
return urlMaping;
}
}
新增加一个注解,用于标识需要异步请求的子 action 地址
/**
* 声明一个需要异步请求的 url 地址
* 例如:
*
* @Asyn({"retb:/b", "retc:/c"})
*
* 对 /b 和 /c 请求的返回值会分别存入 req 的 retb 和 retc 的属性中。
*
* @author Gongqin(gongqin@gmail.com)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface Asyn {
/**
* 需要异步请求的 url,支持同时进行多个请求
* 每行的格式为 key:url 。运行完成后会将 key 做为主键存入 req 中去。如果没有写 key,则认为您已经自己把返回值做了处理
*/
String[] value();
/**
* 异步执行的子action最长处理时间,默认为 30 秒
* @return
*/
int timeout() default 30;
}
这里到了今天的核心:定制一个动作链处理器,用于解析注解,并异步执行子 action ,最后合并处理结果。
/**
* 异步执行的动作链处理器
* @author Gongqin(gongqin@gmail.com)
*/
public class AsynProcessor extends AbstractProcessor {
/**
* 解析 Asyn 注解并异步执行子 action,最后合并处理结果
*/
@Override
public void process(final ActionContext ac) throws Throwable {
Asyn asyn = ac.getMethod().getAnnotation(Asyn.class);
// 如果没有异步任务,则处理完毕后直接返回
if (null == asyn || null == asyn.value() || asyn.value().length == 0) {
doNext(ac);
return;
}
// 开始处理异步任务
ExecutorService executor = Executors.newCachedThreadPool();
List>> tasks = new ArrayList>>(asyn.value().length);
for (final String url : asyn.value()) {
//增加一个异步请求
tasks.add(executor.submit(new Callable>() {
@Override
public Pair