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实现。

[WS]一个简单的WSDL文档(下)

虽然发布的服务很简单,Axis帮我们生成的WSDL文档看起来却是比较复杂的,之所以这样的主要原因是WSDL要考虑到兼容各种实现和具有可扩展性,这就像我们使用一些框架做开发会使代码总量增加,而好处是使逻辑更加清晰。这篇帖子的上半部分介绍了WSDL里常用到的名称空间,现在就来说说WSDL里各元素的含义。

一个WSDL文档里一般包含<types>、<message>、<portType>、<binding>和<service>这几个元素,其中<types>、<message>和<portType>可以看作抽象的接口定义,而<binding>和<service>是具体的实现,注:有些时候也把<binding>看作接口的一部分。你也许看过一些WSDL把这两部分分开写在两个xml文件里,并在其中一个文件里引入(import)另一个的情况,这也是为什么要区分接口和实现的原因之一。在现实世界里,接口部分很可能是由某个组织(例如某行业协会)制定好的,该组织的成员在发布自己的Web服务时都要引入它,从而达到统一标准的目的。

<types>标签用来定义Web服务里用到的,XML Schema定义的数据类型以外的自定义数据类型,对于我们自定义的类(Book),会对应到一个<complexType>,其中用<element>元素指定每个参数的类型。JAX-RPC规范中规定了Java语言的数据类型到XML Schema数据类型的映射,例如int<->xsd:int、java.lang.String<->xsd:string等等,还有数组的映射方式。

<message>标签定义Web服务里的消息,最常见的就是请求和响应消息。<message>中可以有<part>元素,它对应Java类中各个方法的参数或返回值,例如addBook()方法有一个Book类型的参数,则在WSDL中会有<part name=”book” type=”tns1:Book”/>的描述。

<portType>标签表示一个服务的类型,就是接口的意思了。WSDL里有些概念很容易混淆,比如port和service的区别,我把service理解为有一个具体URL的服务,而port代表某一地址,portType是service的抽象,不知道对不对。我们看一个WSDL文档,一般就该先找<portType>元素,看看这个WSDL代表的Web服务里都有哪些方法,它们的参数和返回值是什么。这些方法是在<portType>里用<operation>元素表示的,<operation>可以有<input>和<output>子元素,表示方法的输入和输出。注意,方法可以是只有输入或只有输出的。

<binding>元素将portType与具体的传输协议绑定。现在,绝大多数都是与SOAP绑定的,对每一个方法的输入和输出,都要指定SOAP的表示方法。JAX-RPC规范规定,SOAP绑定可以有rpc和document两种类型,分别表示远程过程调用和基于消息的方式。use属性可以是encoded或literal,对于前者要支持rpc的方式,对于后者要支持rpc和document的方式,它们使得SOAP消息的格式有所区别,但我还没有仔细研究,你可以参考一下JAX-RPC 1.1版本的6.3-6.4节。又想起另外一个问题,SOAP和HTTP的关系是怎样的,绑定到SOAP就等于绑定到HTTP了吗,应该不是,那么在哪里指定Web服务绑定的应用层协议(HTTP、SMTP等等)呢?(Update: 由transport属性指定应用层协议)

最后,<service>元素通过<port>子元素把服务联系到一个具体的URL,更确切点,应该是把一个已绑定的portType联系到某个URL,这样就知道该把SOAP消息发给哪个服务器了。

我觉得之所以应该花比较多的时间理解WSDL,因为WSDL在整个Web服务中扮演了十分核心的角色,它是对Web服务的一个比较完整的语法上的描述,同时,它还与XML、SOAP以及UDDI都有着非常密切的联系,因此对于我们更好的认识Web服务体系结构是很重要的。虽然现在的Web服务开发工具都能自动进行Java<->WSDL的转换,但理解WSDL对于Web服务的不论是设计、开发还是修改调试都是必要的。

参考资料: