使用Apache Drill处理数据文件

安装Drill

官网下载tar包并解压即可,linux和windows都是如此。
注意:drill要求java版本是8或以上。

命令行使用Drill

最简单的方式是用embedded模式启动drill(启动后可以在浏览器里访问 http://localhost:8047 来管理drill):

bin/drill-embedded

这样就以嵌入模式启动了drill服务并同时进入了内置的sqline命令行。如果drill服务已经启动,则可以这样进入sqline命令行(参考):

bin/sqline -u jdbc:drill:drillbit=localhost

作为例子,用SQL语法查询一下drill自带的数据(命令行里的cp表示classpath,注意查询语句最后要加分号结尾):

apache drill> SELECT * FROM cp.`employee.json` LIMIT 3;

查询任意数据文件的内容:

apache drill> SELECT * FROM dfs.`/home/hao/apache-drill-1.16.0/sample-data/region.parquet`;

退出命令行用!quit

在java代码里使用Drill

下面是在java代码里使用Drill的例子代码,要注意的一点是,JDBC的URL是jdbc:drill:drillbit=localhost,而不是很多教程上说的jdbc:drill:zk=localhost

package com.acme;

import java.sql.*;

public class DrillJDBCExample {
    static final String JDBC_DRIVER = "org.apache.drill.jdbc.Driver";
    //static final String DB_URL = "jdbc:drill:zk=localhost:2181";
    static final String DB_URL = "jdbc:drill:drillbit=localhost"; //for embedded mode installation

    static final String USER = "admin";
    static final String PASS = "admin";

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try{
            Class.forName(JDBC_DRIVER);
            conn = DriverManager.getConnection(DB_URL,USER,PASS);

            stmt = conn.createStatement();
            /* Perform a select on data in the classpath storage plugin. */
            String sql = "select employee_id,first_name,last_name from cp.`employee.json`";
            ResultSet rs = stmt.executeQuery(sql);

            while(rs.next()) {
                int id  = rs.getInt("employee_id");
                String first = rs.getString("first_name");
                String last = rs.getString("last_name");

                System.out.print("ID: " + id);
                System.out.print(", First: " + first);
                System.out.println(", Last: " + last);
            }

            rs.close();
            stmt.close();
            conn.close();
        } catch(SQLException se) {
            //Handle errors for JDBC
            se.printStackTrace();
        } catch(Exception e) {
            //Handle errors for Class.forName
            e.printStackTrace();
        } finally {
            try{
                if(stmt!=null)
                    stmt.close();
            } catch(SQLException se2) {
            }
            try {
                if(conn!=null)
                    conn.close();
            } catch(SQLException se) {
                se.printStackTrace();
            }
        }
    }
}

让Drill访问数据库

根据要访问的数据库的不同,需要为Drill添加相应的驱动,方法见RDBMS Storage Plugin

利用Drill将csv格式转换为parquet格式

原理是在drill里创建一张格式为parquet的表,该表的路径(下例中的/parquet1)对应的是磁盘上的一个目录。

ALTER SESSION SET `store.format`='parquet';
ALTER SESSION SET `store.parquet.compression` = 'snappy';

CREATE TABLE dfs.tmp.`/parquet1` AS 
SELECT * FROM dfs.`/my/csv/file.csv`;

让drill支持.zip、.arc压缩格式

(暂缺)

参考资料

Drill in 10 Minutes
How to convert a csv file to parquet

JVM内存模型

JVM的内存分为堆(heap)和栈(thread stack)两类区域,分别存放不同数据,规则如下。

以下数据存放在heap中:

  • 所有对象(object)。但对象的method里的local variable存放在stack中。
  • 所有对象的member variable,不论是primitive或是指向一个对象。
  • 所有静态类的variable

以下数据存放在stack中(每个线程有自己的thread stack,互相不可见):

  • 当前线程执行过的所有方法(method)
  • 当前线程内所有local variable(method里的变量)。如果此变量指向一个对象,则变量存放在stack中而对象仍然存放在heap中。
  • 当前线程内所有primitive类型(如int, long, boolean)的local variable

java-memory-model-3

参考资料:

Java Memory Model

Java 内存区域和GC机制

写代码的代码:JET

用过EMF的人想必都对它的代码生成功能印象深刻吧,有没有想过这是怎样实现的呢?

代码生成一般是通过写好的模板,在用户输入一些限制条件后,由程序把这两者结合起来得到需要的代码。EMF也是这样,它内置了一些模板(放在 org.eclipse.emf.codegen.ecore里),我们通过Java Interface或XML Schema文件建立ecore模型后(这一步由org.eclipse.emf.codegen.ui支持),插件 org.eclipse.emf.codegen帮我们生成model、edit、editor和test这几个新的插件,这些生成的插件就可以被用来构 造应用程序了。

EMF使用JET(Java Emitter Templates)进行代码生成,JET使用的模板文件与JSP格式几乎完全一致,像JSP里有application这样的隐含变量一样,在JET模 板文件里可以使用argument这个隐含变量,它代表用户的输入参数(对EMF来说就是ecore模型)。下面是一个最简单的带有参数的模板文件 hello.txtjet,JET模板文件的扩展名一般是要生成文件类型再加上jet。

<%@ jet package="hello" class="GreetingTemplate" %>
Hello, <%=argument%>!

只要给这个模板文件一个参数,JET就能帮我们生成一个字符串,其内容是“Hello,参数!”。如果我们定义一个.javajet的模板,配合适当的参数,再把生成的字符串保存为文件,就得到了java代码,EMF的代码生成过程就是如此。

自己实现代码生成器的过程如下:创建一个Plugin项目,编写模板文件.javajet,一般放在templates目录下;定义元模型,这个元 模型要与模板相匹配,模板的argument一般就是元模型最外层的container;构造一个用户界面,让用户可以构造元模型的实例;提供一个 Action比如按钮,用户按下即开始生成代码。

代码的生成是由JETEmitter完成的,它会在workbench里先生成一个.JETEmitter项目,把模板转换为java类(这个过程类似JSP转换为Servlet),然后调用这些类的generate()方法得到结果。

我做了一个生成.txt文件的生成器插件,点此下载。安装后在Eclipse主菜单里选File->New->Others,在New对话框里选择Sample Wizards下的Echo filename text file,新建的文本文件内容里会包含文件名。

因为用途有限,JET的资料不是很多,这里有两篇:链接1链接2,其中后者在EMF的帮助里也找得到。

最后补充一个Tip,在plugin里访问一个相对路径文件的方法如下:

String base = Platform.getBundle(pluginId).getEntry("/").toString();
String relativeUri = "templates/echo.txtjet";
JETEmitter emitter = new JETEmitter(base + relativeUri, getClass().getClassLoader());

更正(08/23),上面说的方法只对JETEmitter有效,得到的路径是Platform内部路径而非本地路径,这里提供另一个方式可以得到本地路径:

Platform.asLocalURL(Platform.getBundle(pluginId).getEntry("/log4j.properties"))

 

感受Ruby on Rails

最近看到几篇介绍Ruby on Rails(RoR)的文章,忍不住自己也下载了一份来体验一下,简单说说感受。

参考文档建立了一个很简单的系统,没花多少时间,这主要归功于scaffold函数提供了缺省的web界面。要想修改这个界面却不那么简单,配置上只要修改一两处,但必须手写一个.rhtml模板文件,这里面存在不少代码,而且ruby代码和html代码交叉得很厉害,和asp差不太多。当然,我想这一步是使用任何框架都无法避免的,看到有文章说RoR的开发效率是struts的十倍,我保留意见。

RoR另一个提高开发效率的途径是做了很多假设来代替配置文件,例如控制文件都放在controller目录里,模型文件都放model目录,url映射就是控制文件名的前半部分,数据库表名与model的对应,等等。我很赞同这种方式,一是节约了写一堆xml配置文件的时间,二是任何熟悉RoR的人都能很快找到需要的类。

由于对Ruby并不熟悉,所以我看.rb文件里的代码会比较吃力。Ruby是解释型的语言,它在语法上有一些方便之处,例如变量的表示;而且它是比较彻底的面向对象语言,连数字123都是对象;三是省略了编译这个步骤,源代码修改后可以立即生效(Eclipse的增量编译基本上也可以达到这个效果);缺点应该是主要是性能方面,我想很可能比不上jsp。

长时间使用一种语言后,总想偶尔换换口味。Java是我最喜欢的无疑,同时也很羡慕掌握多门语言的高手,碰到问题先考虑用哪种语言实现,毕竟每门语言都有自己擅长。继续研究研究Ruby,也许它会成为我的另一杆枪。

另外,RDT是一个Ruby开发的Eclipse插件,但对RoR似乎没有特别的支持。除了.rb文件的编辑器外,它还专门集成了一个正则表达式验证工具,看来Ruby在这方面也比较在行。

一个用OWL-S组装Web服务的例子

OWL-S可以用来描述Web服务,这个帖子将介绍一个非常简单的例子,也许对理解Web服务的组装有些作用。这个服务是对已有Web服务进行组装和执行,所以你并不需要发布自己的Web服务。你需要安装ProtegeOWL-S Editor插件,我用的版本前者是3.1 beta build 191,后者是build 15,它们在一起运行得还不错。

所用的Web服务在http://www.bs-byg.dk/hashclass.wsdl,它包含两个方法:HashString和CheckHash,前者用指定编码方式(MD5、SHA1等等)对指定字符串编码,后者根据指定编码方式检查一个字符串(HashString)是否是另一个字符串(OriginString)的编码结果。我们将把这两个方法组装成一个服务,对输入的编码方式和待编码字符串先进行编码,然后检查编码的结果是否正确,如果正确返回true,否则返回false。下面是组装步骤,完整的工程在这里下载

1、确认你的OWL-S Editor已经安装到Protege里,启动Protege,新建一个owl文件类型的工程,在菜单project->config里勾选上owls选项,按确定后Protege的主界面会多出一个OWL-S Editor页。

2、转到OWL-S Editor页,按左上角的WSDL按钮,在弹出的对话框里输入Web服务的地址http://www.bs-byg.dk/hashclass.wsdl,然后按回车,过一会儿在对话框里会显示出这个Web服务的信息,左边是Operations列表。

图1 用来导入WSDL的对话框

3、因为每次只能import一个Operation,所以先选择HashString,然后按右下方的Import按钮,这时系统会提示要把生成的owls文件(扩展名为.owl)保存在一个位置,你可以选择任何位置。

4、使用同样的方法把CheckHash方法也导入进来,这样我们就有了两个可用于组装的Web服务了。如果你愿意的话,可以单独执行看看,方法是选择一个Service,然后按绿色的执行按钮。

图2 导入的两个服务

5、现在开始组装它们。为此我们新建一个Service实例(按Create Service按钮)、一个Profile实例、一个CompositeProcess实例和一个WSDLGrounding实例,分别命名为myservice、myprofile、myprocess和mygrounding好了。

6、接下来把它们连接起来,首先选中myservice,把它的describedBy属性置为myprocess,presents属性置为myprofile,supports属性置为mygrounding。

7、现在对myprocess进行编辑,OWL-S Editor提供了一个可视化的编辑界面(Visual Editor),利用它可以很方便的定义CompositeProcess的各个步骤。选中myprocess,右边切换到Visual Editor,这里有一些粉红色的按钮用来定制流程。我们首先创建一个Sequence(表示按顺序执行),然后选中这个Sequence,创建两个Perform和一个Produce,每个Perform代表调用一个Web服务,而Produce的作用是在最后得到返回值。这时右边的图形应该像下面这样,因为我们尚未对Perform和Produce进行定制。

图3 包含三个有用节点的process图

8、在图形的Perform/Produce节点上点一下就可以修改它的属性,先来修改第一个。点一下第一个矩形节点(第一个Perform),在对话框里把process属性设置为wi1:HashStringProcess(注意:如果导入WSDL时改变了前缀,这里就不是wi1),为了方便阅读,把Name属性改为hashPerform。类似的,第二个矩形节点的process属性应该是wi2:CheckHashProcess,Name则改为checkPerform;对于唯一的Produce节点,改名为produce。现在右边的图如下所示。

图4 改名后的process图

9、现在从Visual Editor切换到Properties页,在这里为myprocess定义输入和输出参数。它的输入应该是wi1:HashType和wi1:Str,而输出应该是wi2:CheckHashResult,也就是说,对于我们组装出来的Web服务来说,输入是编码类型和待编码字符串,而输出是验证结果。

10、上面我们定义了myprocess拥有的参数,现在就要用到它们了。切换回Visual Editor,在树型列表里选则第一个Perform(hashPerform),把右边切换到Properties页,现在ToParameter属性里还是空白,我们要把myprocess的输入映射到这个Perform,所以按添加按钮把两个输入参数(wi1:HashType和wi1:  Str)加到ToParameter里。选中它们中的一个,可以看到右边有BindingType选项,缺省为valueSource这一项,就用它即可,在下面的FromPerform下拉框里只有一个选项TheParentPerform,选中它。在最下面的FromParameter里选择和你选择的ToParameter项一样的那个选项(wi1:HashType->wi1:HashType,wi1:Str->wi1:Str)。

图5 通过参数传递产生“数据流”

11、对于checkPerform,它有三个输入参数,我们希望HashType和hashPerform具有同样的值,所以它的设置和上一步里对HashType的设置一样;OriginalString的设置和上一步的Str一样;HashString属性是上一步得到的结果,所以FromPerform属性应该是hashPerform,FromParameter属性则是wi1:HashStringResult。

12、对produce的设置很简单,在ToParameter属性里加入我们要的结果wi2:CheckHashResult,FromPerform选checkPerform,FromParameter选wi2:CheckHashResult。现在,myprocess对应的process图如下所示。

图6 可以从图中看到服务的结构

13、对myprocess的设置到此就结束了,最困难的部分完成了,剩下的工作都很简单和显而易见。选中mygrounding,在它的hasAtomicProcessGrounding属性里加上wi1:HashStringAtomicProcessGrounding和wi2:CheckHashAtomicProcessGrounding。

14、现在myservice已经可以执行了(myprofile里还可以增加一些信息用来描述这个服务)。现在选中myservice,按下执行按钮,在弹出的对话框里HashType框填MD5,Str框填test(任意字符串都可以),然后按Execute按钮就会看到结果,当然,这个服务不论你输入什么字符串都会得到true值,原因不用我说了吧。

图7 执行组装后的服务

Encoded vs Literal, RPC vs Document

我一直没有分清题目里所写的概念,看过JAX-RPC规范后还是模糊,原因主要是对XML本身就没有特别深入的理解。不过现在感觉好象明白了一些。

Encoded和Literal是两种Typing system,前者对应http://schemas.xmlsoap.org/soap/encoding/,后者利用XML Schema定义数据类型。

“Literal对应于types中的element只有一层,Encoded对应于types中element多层的。

用Literal描述的参数客户必须提供明确的参数名,用Encoded描述的参数客户可以用param1,param2,param3等代替。”

在Axis中,使用org.apache.axis.description.OperationDesc类指定自己要使用的方式,方法如下:

“oper.setStyle(org.apache.axis.enum.Style.DOCUMENT); oper.setUse(org.apache.axis.enum.Use.LITERAL);,这个是对于document方式的,如果是rpc方式应该设置为:oper.setStyle(org.apache.axis.enum.Style.RPC); oper.setUse(org.apache.axis.enum.Use.ENCODE);”,然后还要用call.setOperation(oper);把OperationDesc对象实例和Call对象联系起来。

----赵晓冬

关于RPC和Document的比较,这里有两篇不错的文章:Operation Style (Document/RPC) and Message format(literal/encoded) ,The Difference Between RPC and Document Style WSDL

Struts分页的一个实现

在Web应用程序里,分页总让我们开发人员感到很头疼,倒不是因为技术上有多么困难,只是本来和业务没有太多关系的这么一个问题,你却得花不少功夫来处理。要是稍不留神,时不时出点问题就更郁闷了。我现在做的一个项目也到了该处理分页的时候了,感觉以前处理得都不好,所以这次有所改变,基本目标是在现有(未分页)的代码基础上,尽量少做修改,并且同样的代码可以应用于不同模块的分页。以下就是我用的方法:

首先,考虑分页绝大多数发生在列表时,组合查询时也需要用到。在我的项目里,列表的Action一般名字为ListXXXActioin,例如客户列表是ListClientsAction等等。在未分页前,ListXXXAction里会把所有的对象取出,通过request.setAttribute()放在request里,然后将请求转向到列表的jsp(例如listClients.jsp)显示出来(你可能会说不要在Action里放业务逻辑,但现在这不是我们考虑的重点)。而分页后,我们只取用户请求页对应的那些对象。为了最大限度的达到代码重用,我做了以下工作:

1、新建一个Pager类,该类有beginPage、endPage、currentPage、pageSize和total等int类型的属性,分别代表开始页、结束页、当前页、每页记录数和总记录数,它主要是让jsp页面显示页导航使用的。请注意currentPage属性是从0开始的。

2、新建一个AbstractListActioin类,并让所有ListXXXAction都继承它。在这个类里覆盖execute()方法,可以在这里判断权限等等,并在判断权限通过后执行一个abstract的act()方法,这个act()由ListXXXAction来实现。

3、在AbstractListAction里增加getPage()方法,用来从request得到用户请求的页码(若未请求则认为是第0页):

protected int getPage(HttpServletRequest request) { 
    String p = request.getParameter("p"); 
    if (p == null) 
        return 0; 
    else 
        try { 
            return Integer.parseInt(p); 
        } catch (NumberFormatException e) { 
            return 0; 
        } 
}

4、在AbstractListAction里增加makePager()方法,用来向request里增加一个Pager类的实例,供jsp页面显示页导航:

protected Pager makePager(HttpServletRequest request, int total) { 
    Pager pager=new Pager(); 
    pager.setTotal(total); 
    pager.setPageSize(Config.getInstance().getPageSize()); 
    pager.setBeginPage(0); 
    pager.setEndPage(((pager.getTotal()) - 1) / pager.getPageSize() + 1); 
    pager.setCurrentPage(getPage(request)); 
    return pager; 
}

注意在我的项目里,每页记录数是写在配置文件里的,如果你没有配置文件,上面第4行setPageSize()的参数直接填数字即可,例如pager.setPageSize(10);

5、这样,所有的ListXXXAction都可以使用getPage()得到请求的页码,并且能够方便的通过makePager()构造需要放在request里的pager对象了。现在要在从数据库取数据的代码上再做一些修改,即只取所需要的那一部分数据。由于我的项目中使用了Hibernate,所以这个修改也不是很困难。未分页前,在我的ListClientsAction里是通过构造一个Query来得到全部Client的,现在,只要在构造这个Query后再加两句(setMaxResults和setFirstResult)即可:

Query query =  ;//构造query的语句  
int total =  ;//得到总记录数  
Pager pager = makePager(request, total);//调用父类中的方法构造一个Pager实例 
query.setMaxResults(pager.getPageSize());//设置每页记录数 
query.setFirstResult(pager.getCurrentPage() * pager.getPageSize()); //设置开始位置 
request.setAttribute(Pager.class.getName(), pager);//把pager放在request里 
request.setAttribute(Client.class.getName(), query.list());

目前存在一个问题,就是在上面代码的第二句中,应该是获得总记录数,但我暂时没有特别好的办法不得到全部对象而直接得到记录数,只能很恐怖的用“int total = query.list().size();”,汗……

6、最后,我写了一个页导航的jsp页面pager.jsp,供各个显示列表的jsp来include,代码如下:

<%Pager pager=(Pager)request.getAttribute(Pager.class.getName());%>
<table width="90%" border="0" align="center" cellpadding="2" cellspacing="1" bgcolor="#CCCCCC">
<tr>
    <td bgcolor="#EEEEEE" align="right">
    <bean:message key="prompt.pager" arg0="<%=String.valueOf(pager.getTotal())%>"/>
        [
<%
for(int i=pager.getBeginPage();i<=pager.getEndPage();i++){
    if(i==pager.getCurrentPage()){
    %>
        <%=(i+1)%>
    <%}else{
        String qs=request.getQueryString()==null?"":request.getQueryString();
        String op = "p="+pager.getCurrentPage();//Original page parameter expression
        String np = "p="+i;//New expression
        if(qs.indexOf(op)==-1)
            qs=np+"&"+qs;
        qs=qs.replaceAll(op,np);
        %>
        <a href="<%="?"+qs%>"><%=(i+1)%></a>
    <%}%>
    <%if(i<pager.getEndPage()-1){%>
    &nbsp;
    <%}%>
<%}%>
]
</td></tr>
</table>

我觉得有必要解释一下,在上面的代码中,关于每一页对应的url是这样处理。request.getQueryString()中可能包含“q=2”这样的页码请求,也可能不包含即缺省请求第0页,所以统一用replaceAll()方法将其去掉,然后将对应的页码请求串(如“q=3”)加在qs的前面。这样做的好处是,每个模块都可以使用这个页导航,并且不会丢失url中的其他参数(例如今后加入排序功能后,url中可能包含“direction=desc”这样的参数)。

05-4-14 Update:我发现在Tomcat4.1和Websphere5.0里,request.getRequestURL()方法得到的地址是不一样的,所以考虑到兼容性,每个页码的链接都使用相对本页的链接。

在列表jsp(listClients.jsp)中,很简单的这样include它(之所以要放在<logic:notEmpty>里,是希望在没有记录可显示的时候就不显示页导航了):

<logic:notEmpty name="<%=Client.class.getName()%>"> 
    <%@include file="/pager.jsp"%> 
</logic:notEmpty>

经过上面几步的处理,我的客户列表已经可以实现分页了,效果见下图。如果在另外一个模块中也需要分页,比如部门列表时,只需要1、修改ListDeptsAction继承AbstractListAction,2、在ListDeptsAction里增加setMaxResults()和setFirstResults()方法,3、在listDepts.jsp中适当的位置include页导航,就可以了,改动是相当小的。

最后,如果希望组合查询的结果也能够分页,必须指定组合查询表单的method属性为“GET”,这样查询要求会被记录在url中,分页导航从而能够正常的工作(每次换页都将查询要求和请求的页码提交)。

Struts在服务端验证的问题和暂时解决方法

我们知道,如果ActionForm继承了ValidatorForm,就可以在validate()方法里进行数据验证,其返回是一个ActionErrors对象。但我发现,在验证出无效的数据输入后,由于Struts在返回inputForward的时候只会保留原先的ActionForm对象在request里,所以如果我在Action里曾手动向request里setAttribute()过其他对象时,就会提示找不到那个对象。

目前的解决方法比较无奈,就是把原来放在request里的对象改为放在session里,但我担心除非用完后马上手动清除这个对象,否则会带来很多不必要的麻烦。我自己是很不喜欢使用session对象的,特别是在Action到页面的数据传递,request应该是最合适的选择。

多模块Struts应用程序的几个问题(及部分解决方法)

Struts从1.1版本开始支持把应用程序分为多个模块,每个模块可以看作独立的应用程序,在带来方便的同时,我也发现了一些问题。比如有一个struts应用程序分了大约十个模块,现在有以下问题不知道大家一般是怎么解决的:

1、因为要进行验证,所以在每个模块对应的资源文件里都要有“errors.required={0} is required.”等资源,有没有只用在一个文件里定义的方法?

2、用tiles的时候,要在每个模块对应的tiles-defs.xml里定义几乎相同的definition,有没有只用在一个文件里定义的方法?(我试过在缺省模块里定义一个definition,然后在模块里extends它,但不行,extends似乎只找当前模块)

3、使用ExceptionHandler的时候,为什么在exception标签里指定了bundle属性还是只在当前模块里找资源?我希望把一些重复使用的异常处理声明在一个文件里,例如NotLoginException、NoSuchObjectException等等,并且它们对应的key也指向同一个资源文件里的资源(利用bundle属性),怎么实现?

经过一段时间的摸索,第一个和第三个问题基本上解决了,其实它们可以看作同一类问题,就是资源的问题。在struts-config-xxx.xml里定义资源文件时,可以指定一个factory属性,不指定时使用缺省的“org.apache.struts.util.PropertyMessageResourcesFactory”类。我的解决方法是自定义一个CustomMessageResourcesFactory类,将多个资源文件以逗号分隔的形式作为参数(即message-resources的parameter属性)传给它,在需要资源的地方会遍历它们进行查找。同时还要自定义一个CustomMessageResources类,它的getMessage()方法里是查找资源的关键代码,而factory只是解析逗号分隔的参数构造并返回CustomMessageResources实例。

CustomMessageResourcesFactory的代码比较简单,如下所示:

package eg;

import java.util.Arrays;

import org.apache.struts.util.MessageResources;
import org.apache.struts.util.MessageResourcesFactory;

public class CustomMessageResourcesFactory extends MessageResourcesFactory{

    public MessageResources createResources(String config) {
        
        return new CustomMessageResources(Arrays.asList(config.split(",")));
    }

}

CustomMessageResources就稍微复杂一些,不过很幸运,我在网上找到了一个完全符合自己要求的类,下载地址在这里,如果链接已失效请联系我。

这样,在每个模块的struts-config-xxx.xml里,只要像下面这样定义资源文件就可以实现共享资源的功能了,其中ErrorResources中是所有模块都需要的错误信息资源:

<message-resources factory="eg.CustomMessageResourcesFactory" 
    parameter="eg.ApplicationResources,eg.ErrorResources" />

上面参考了这篇文章http://javaboutique.internet.com/tutorials/Dynaform/index-7.html,它是通过修改ActionServlet使用CustomMessageResources的,我觉得还是自定义factory的方式更自然些。

第二个问题暂时还没有解决,也许要修改handler实现。