DRW详解

来源:互联网 发布:徐水互动网络平台环保 编辑:程序博客网 时间:2024/05/29 12:14

                                                                                                                              DWR框架应


组件分析:DWR框架解实现了java方法与js脚本之间的透明调用。DWR给程序员的界面由三部分组成:

   

   
  

1.js脚本库:

engine.js是dwr的核心脚本库,发送ajax请还求,设定回调函数都必须用到这个库,所以每个使用dwr的页面,都必须引入这个js。Util.js是dwr工具类库,它不是必须引入的,但一般情况下你都会用到。

 

2.DwrServlet:

   这个servlet是服务器端处理所有dwr请求的入口,我们只需将其配置到web.xml中,注意的是,这个servlet在web.xml中配置的url-pattern必须对应与要引入的dwr相关js库的路径。

   页面上发起的所有请求首先会由DwrServlet接收,它会根据dwr.xml中的相关配置,输出转化后的对应java对象的js脚本;在整个体系中,这个Servlet是核心组件。

3.java业务逻辑实现:
 这部分不属与dwr框架,它是我们自己的业务实现javaBean;javaBean被配置到dwr.xml中,即可在客户端通过js调用其中的方法,或在这个javaBean中控制页面组件的实现---就需要到dwr.jar中的类库。

业务逻辑实现的javaBean可以是直接编码在包中的java类,也可以是来自与hibernate、spring等框架组装的对象;这些都可能过dwr.xml中的配置实现。

2.DWR中的数据类型转换

我们可以将DWR框架框架看做一个Convert,在java与js之间转换其类数据型:对象、方法、参数/返回值类型转换。

        

 

Java与js之间默认转换的数据类型转换对应如下表:

JavaScript

Java

Array数组

List、Collection、数组

Boolean

Boolean

Object

Map、Java Bean

String

String

Date

Date

Numbers

Int、double、float等数值

XML Dom

Dom

undefined

null

   符合getter/setter规范的pojo要转换为js中的对象,需要在dwr.xml中另外配置,详见后面说明。特别注意的是:在javabean到js Object的转换中,

1.后台的javaBean必须遵循Java Bean规范即实现Getter和Setter方法访问其对应属性。

2.要输出到js中的javaBean必须在dwr.xml中配置其转换规则,即Allow部分的Convert元素

<allow>

   . . .

   <!-- 用户管理 cn.netjava.dwr.UserManager.java让js调用 -->

    <createcreator="new"javascript="UserManger"scope="script">

      <paramname="class"value="cn.netjava.dwr.UserManger"/>

    </create>

    <convertmatch="cn.netjava.dwr.UserInfo"converter="bean"/>

   . . .

  </allow>

3.在js中接收到对应的javaBean对象时,js对象的属性名与javaBean的属性名相同。

4.可以利用dwr.util.js中的工具函数直接将后台反回的Array、Collection、Map对象赋与页面上的select或列表组件。

3.dwr Servlet配置:

     Web应用中要使用dwr的第一件事是在web.xml中配置dwr的servlet,一个较为全面的配置如下:

<servlet>

  <servlet-name>dwr-invoker</servlet-name>

  <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class>

  <init-param>

     <param-name>debug</param-name>

     <param-value>true</param-value>

  </init-param>

  <init-param>

      <param-name>activeReverseAjaxEnabled</param-name>

      <param-value>true</param-value>

    </init-param>

    <init-param>

      <param-name>initApplicationScopeCreatorsAtStartup</param-name>

      <param-value>true</param-value>

    </init-param>

    <init-param>

      <param-name>maxWaitAfterWrite</param-name>

      <param-value>100</param-value>

    </init-param>

</servlet>

一些关键初始化参数说明为:

name

desc

debug

设置成true 使DWR 能够debug 和进入测试页面,默认为false。

maxWaitingThreads

DWR调用最大等待线程数,默认为100。

welcomeFiles

类似于web.xml 的<welcome-file-list>标签,默认值为index.html, index.htm, index.jsp。

logLevel

日志级别,可以是值可以是:FATAL,ERROR,WARN(默认),INFO,DEBUG

crossDomainSessionSecurity

默认值为true,如设成false,就可以从其他域(webApp)进行请求dwr,这样不利与安全保护。

  详细全面说明,请看dwr文档:http://getahead.org/dwr/server/servlet

4.dwr中的session管理和页面转发

   Dwr可以通过js调用后台java方法返回某一个url结果,如下代码示例:

//dwr将要在前台调用的返回某个页面数据的方法 destURL:目标页面

    public String getInclude(StringdestURL) throws ServletException, IOException{

        //取得webapp上下文对象

        org.directwebremoting.WebContext web= WebContextFactory.get();

        //返回页面结果

        return web.forwardToString("/"+destURL);

    }    

   前台调用返回结果后,就可以直接将返回的内容填充到某个div中,如:

/**前台计用java方法返回某个url数据*/

function getDestURL(destURL){

    //调用服务器上的对象方法,并设定回调函数

    JavaClass.sayHello(destURL,callBack);

}

//定义响应调用结果的回调函数:将返回的页面内容展示到destDiv层中

var callBack=function(data){

     document.getElementById("destDiv").innerHtml=data;

  };

WebContext对象是dwr在后台管理web交互时的关键类,它通过dwr中的WebContextFactory工厂类得到;是扩展了javax.servlet.ServletContext的对象;通过WebContext对象,我们可以得到前台请求这个方法时的servlet中的Request、Response、Session等对象,如下代码示例:

//dwr将要在前台调用的返回某个页面数据的方法 destURL:目标页面

    public String getInclude(String destURL)throws ServletException, IOException{

        //取得webapp上下文对象

        org.directwebremoting.WebContext web= WebContextFactory.get();

        //得到servlet中的request/response对象

        javax.servlet.http.HttpServletRequest request=web.getHttpServletRequest();

        javax.servlet.http.HttpServletResponse response=web.getHttpServletResponse();

        //在此可提取request中相关请求参数...

        //取得session对象

        javax.servlet.http.HttpSession session=request.getSession();

        //在此通过session做用户登陆等验证....

 

        //返回页面结果

        return web.forwardToString("/"+destURL);

    }

   可以看出,dwr中要控制会话管理,也是相关的简单,关键就是一个WebContext对象。但这种编程模型,似乎离我们前面所讲的MVC己相去甚远了。

5.dwr.xml配置

dwr.xml中配置了所有要转换的java对象的规则,这个文件默认放置在web-inf目录下;在dwr.xml中配置的对象,可直接在页面进行测试。以下配置为例:

<allow>

   . . .

<!-- 聊天消息调用接口 -->

    <createcreator="new"javascript="ChatWithSamePage"scope="application">

      <paramname="class"value="cn.netjava.dwr.ChatWithSamePage"/>

<include method="sendWebMessage"/>

    </create>                     

    <convertmatch="cn.netjava.dwr.WebMessage"converter="bean"/>

  

  </allow>

关键元素配置说明;

1.<allow>元素:其中的每一个create子元素定义了DWR 能够创建和转换的类。

2. <create>元素:定义一个让DWR转换的java类及转换规则

        creator属性:默认为new,意思是通过new关键字创建对象,被转换的javaObj必须有一个无参构造器;如取值为none,则不创建对象:一种情况是类中所有方法都是静态时使用,再就是对象在前面的create中己声明,则scope大于在此使用的,即使用己存在webContext中的对象。

        Javascript:对象被转换后,在前台引入的js脚本名字,这个脚本由dwr动态生成。

        Scope元素:转换后对象存在的范围,与Servlet中的数据做用范围同理,取值可以是application, session, request和page;默认为page范围。

        Include元素:默认情况下,class中所有方法都会被convter,include限定了只需要转换可以前台调用class的方法,如上配置中,就只可调用WebMessage中的sendWebMessage方法。

3.convert元素: 一般用于指定需要转换到前台的javaBean对象。其它常用数据类型,己由dwr默认转换。

  全面详尽的说明,请见http://getahead.org/dwr/server/dwrxml。

6.使用annotation代替dwr.xml配置

Annotation是从java5开始在语言级别提供的一项新特性,Annotation提供了一条与程序元素关联任何信息或者任何元数据(metadata)的途径。从某些方面看,annotation就像修饰符一样被使用,并应用于包、类型、构造方法、方法、成员变量、参数、本地变量的声明中。这些信息被存储在annotation的“name=value”结构对中。annotation类型是一种接口,能够通过java反射API的方式提供对其信息的访问。

 简单的说,Annotation可以通过对类编写时的注解声明控制类对象运行时的规则(虽然我认为这会带来一定的混乱,即你看到的类源代码与运行时特征并不相符)。

在dwr中使用Annotation可以直接在web.xml中配置远程访问类和bean,如下代码示:

<servlet>

  <description>DWR controller servlet</description>

  <servlet-name>DWR controller servlet</servlet-name>

  <servlet-class>

    org.directwebremoting.servlet.DwrServlet

  </servlet-class>

  <init-param>

  <!-- 以下为可远程访问的java类和bean -->

    <param-name>classes</param-name>

    <param-value>

      com.example.RemoteFunctions, com.example.RemoteBean

    </param-value>

  </init-param>

</servlet>

在编写RemoteFunctions和RemoteBean时,同样在其中要使用dwr Annotation:

@Create(name = "Functions")

publicclass RemoteFunctions {

    //其中的方法即可被dwr导出前台访问

}

要转换一个bean 类可以被远程访问, 使用@Convert@RemoteProperty 标注:

@Convert

publicclass Foo {

     

     @RemoteProperty

     privateintfoo;

     publicint getFoo() {

         returnfoo;

     }

     @RemoteProperty

     publicint getBar() {

         returnfoo * 42;

     }

 }

使用dwr Annotation标注远程访问类要注意的是,编译级别的jre必须是1.5以上。

7.engine.js说明:

如其名所示,它是dwr中的核心js库,在每个使用到dwr的页面,都必须引入!

<script   src="dwr/engine.js"/></script>

  <script   src="dwr/util.js"/></script>

DWR脚本一些全局属性名可以通过engine.setXXX()设定,例如:

 

//设置调用超时

DWREngine.setTimeout(1000);

//将dwr设置为同步调用

DWREngine.setAsync(true);

关与这个脚本的详细说明,请见:http://getahead.org/dwr/browser/engine

8.util.js说明:

Util.js是dwr的前台工具类库,一个好处是,我们或以脱离dwr环境单独的使用它。它有

4 个基本的操作页面的函数:getValue[s]()和setValue[s]()可以操作大部分HTML 元素了

table,list 和image。getText()可以操作selectlist。 如果要修改table 可以用addRows()

和removeAllRows()。要修改列表(select 列表和ul,ol 列表)可以用addOptions()和

removeAllOptions()。

   其中常用的函数说明列表如下:

函数

说明

addOptions(id,valueArray)

removeAllOptions(id)

添加,移除下接列表中的值。如:

DWRUtil.addOptions(selectid, map)

会将map中的key(为int)和value(为Seting)对创建到select中option组

件的value和text。

addRows(id)

removeAllRows(id, array, cellfuncs, [options]);

操作table的函数,这两个函数的第一个参数都是table、tbody、thead、tfoot 的id。一般来说最好使用tbody,如removeAllRows(tbodyID)将移除表体中所有行。

onReturn

当按下return 键时要调用的函数,例如:

<input type="text"

onkeypress="DWRUtil.onReturn(event,submitFunction)"/>

<input type="button" onclick="submitFunction()"/>

selectRange(eleID,start,end)

选中输入框中文字的某部分。

getValue(id)

setValue(id,value)

读取/设定页面中指定ID组件的值。可以操作select、input、textArea、div和span。

useLoadingMessage

等待消息,这个前面己经详细讨论过。

  Util.js的详细说明请看dwr在线文档:http://getahead.org/dwr/browser/util;同时建议你

阅读dwr针对某个对象生成的js脚本源码,这样理解会更深入一些。

9.DWR进阶: loading界面创建

    常见的一个需求是,当页面请求在服务器端需要较长处理时间时,在页面上最好给用户一个提示消息框,这称为loading页面,如果你还没看到过这个东东,请参看一下gMail的界面。

   DWR中要实现这个功能非常简单!简单得只需要一句话:

dwr.util.useLoadingMessage("正在处理,请秒候. . .");

以上节为例,只需要在updateResult()函数体中第一行加上这句话就是:

/**响应界面事件,调用java对象的方法*/

function updateResult(){

      //显示loading界面:

dwr.util.useLoadingMessage("正在处理,请秒候. . .")

      //使用dwr工具类得到userName组件内的值

    var name = dwr.util.getValue("userName");

    //调用服务器上的对象方法,并设定回调函数

    FirstDWR.sayHello(name,callBack);

}

 

OK,重测试,还看不到?那是因为太快了,它一闪而过---修改FirstDWR中的sayHello方法如下:

public String sayHello(String userName){

        //为了前台显示loading界面,模拟任务占用较长时间

        try{

            Thread.sleep(3000);

        }catch(Exception ef){}

        System.out.println("客户端发送的请求是: "+userName);

        count++;

        returncount+" "+userName+"您好!服务器时间是:"+System.currentTimeMillis();

    }

让它执行时多用点时间而己,这下你看到了吧?


  但这个loading可能还不会让你满意?其实它总是出现在屏的右上角;上图之所以好像是到了中间,其实是因为我放小了浏览器窗口而己。另外,它的颜色总是红的,如果能将一幅gif动画显示到loading中多好?这好办,我们查看dwr.util.useLoadingMessage函数代码如下:

/**

 * Setup a GMail style loading message.

 * @see http://getahead.org/dwr/browser/util/useloadingmessage

 */

dwr.util.useLoadingMessage = function(message){

  var loadingMessage;

  if (message) loadingMessage = message;

  else loadingMessage ="Loading";

  dwr.engine.setPreHook(function(){

    var disabledZone = dwr.util.byId('disabledZone');

    if (!disabledZone){

      disabledZone = document.createElement('div');

      disabledZone.setAttribute('id','disabledZone');

      disabledZone.style.position = "absolute";

      disabledZone.style.zIndex = "1000";

      disabledZone.style.left = "0px";

      disabledZone.style.top = "0px";

      disabledZone.style.width = "100%";

      disabledZone.style.height = "100%";

      document.body.appendChild(disabledZone);

      var messageZone = document.createElement('div');

      messageZone.setAttribute('id','messageZone');

      messageZone.style.position = "absolute";

      messageZone.style.top = "0px";

      messageZone.style.right = "0px";

      messageZone.style.background ="red";

      messageZone.style.color = "white";

      messageZone.style.fontFamily = "Arial,Helvetica,sans-serif";

      messageZone.style.padding = "4px";

      disabledZone.appendChild(messageZone);

      var text = document.createTextNode(loadingMessage);

      messageZone.appendChild(text);

      dwr.util._disabledZoneUseCount = 1;

    }

    else {

      dwr.util.byId('messageZone').innerHTML = loadingMessage;

      disabledZone.style.visibility = 'visible';

      dwr.util._disabledZoneUseCount++;

    }

  });

  dwr.engine.setPostHook(function(){

    dwr.util._disabledZoneUseCount--;

    if (dwr.util._disabledZoneUseCount == 0){

      dwr.util.byId('disabledZone').style.visibility ='hidden';

    }

  });

};

可以在dwr主站http://getahead.org/dwr/browser/util/useloadingmessage上查看这段代码的说明,老外真的诚实,说了这段是抄gmail的,呵呵。如果你只是要修改显示文字的大小,颜色,位置,那么将这个函数复制出来改变其中style的值即可。这段代码好像很复杂,但我们如只要在这段代码中将它改为在loading时传入一个图片地址做为参数,并让这个图片显示在窗体中心,就容易多了,修改后的函数接收一个图片地址,将图片显示出来即可,代码如下:

/**载入loading动画,imageSrc:动画图片地址*/

function useLoadingImage(imageSrc){

  var loadingImage;

  if (imageSrc) loadingImage = imageSrc;

  else loadingImage ="ajax-loader.gif";

  dwr.engine.setPreHook(function(){

    var disabledImageZone = $('disabledImageZone');

    if (!disabledImageZone){

      disabledImageZone = document.createElement('div');

      disabledImageZone.setAttribute('id','disabledImageZone');

      disabledImageZone.style.position = "absolute";

      disabledImageZone.style.zIndex = "1000";

      disabledImageZone.style.left = "0px";

      disabledImageZone.style.top = "0px";

      disabledImageZone.style.width = "100%";

      disabledImageZone.style.height = "100%";

      var imageZone = document.createElement('img');

      imageZone.setAttribute('id','imageZone');

      imageZone.setAttribute('src',imageSrc);

      imageZone.style.position = "absolute";

      imageZone.style.top = "0px";

      imageZone.style.right = "0px";

      disabledImageZone.appendChild(imageZone);

      document.body.appendChild(disabledImageZone);

     

    }

    else {

      $('imageZone').src = imageSrc;

      disabledImageZone.style.visibility = 'visible';

    }

  });

  dwr.engine.setPostHook(function(){

    $('disabledImageZone').style.visibility ='hidden';

  });

}

嗯,这段代也其实也是从dwr站上抄来的,现在将这个函数放到页面中来试一下,事件处理代码做如下更改:

/**响应界面事件,调用java对象的方法*/

function updateResult(){

      //显示loading界面:

        //window.showModalDialog("index.jsp");

useLoadingImage("image/javaDancing.gif")     

//不用这个了。

//dwr.util.useLoadingMessage("正在处理,请秒候. . .")

      //使用dwr工具类得到userName组件内的值

    var name = dwr.util.getValue("userName");

    //调用服务器上的对象方法,并设定回调函数

    FirstDWR.sayHello(name,callBack);

}

 

看到的界面效果如下:


在实际的应用中,可不能用这个danceJava的小人,需要美工做一幅合适的gif动画。

2.DWR+sp+hb整合:AJAX版《员工日志管理》系统

1.DWR+SP+HB结构说明:

在实际的项目中,我们可以利用各种现成的框架实现快速开发,例如使用hibernate映射数据库到java对象,在spring中提供业务实现的javaBean装载,这些配置好的jabaBean和pojo可以通过dwr直接导出到前台调用,是不是很完美J ,dwr中提供了这样的整合配置功能。整合后的系统结构如下图:

 

 

  

  通过如上整合,让sp负责加载业务处理的javaBean,hb负责pojo到数据库的映射,我们的工作就可用来只做最重要的事:动态页面的js编码和后业务逻辑的实现。

2.dwr与spring整合说明:

   Dwr与sp整合只需要指示dwr使用sp配置中的bean即可。

<allow>

 . . .

<!—说明使用spring创建bean-->

<create creator="spring"javascript="IUserinfoDAO">

<!—根据spring中配置的bean的id属性查找bean-->

<param name="beanName"value="IUserinfoDAO"/>

<!—设定spring配置文件的位置名字-->

<param name="location"     value="applicationContext.xml"/>

 </create>

</allow>

 

如果spring配置文件己在web.xml中配置,则此处不需再配置。

直接在web.xml中配置spring:

<context-param>

  <param-name>contextConfigLocation</param-name>

  <param-value>/WEB-INF/applicationContext.xml</param-value>

</context-param>

<listener>

  <listener-class>

    org.springframework.web.context.ContextLoaderListener

  </listener-class>

</listener>

3.dwr与hibernate整合说明:

   我的理解是,dwr不存在与hb整合的概念---只是把pojo通过如下配置暴露出来而己;当然,如有必要,你也可以将所有的dao暴露给前台调用。好像不关hb的什么事。

<convert match="cn.netjava.pojo.*"converter="hibernate3"/>

  这句配置中的converter使用了hibernate3,如是hb2,则改成对应版本号;这里其实也可以将改为bean---就像转换非hb生成的pojo那样,区别只是,hb的converter不会去读pojo没有初始化的值。

  注意,上面这句配置转化了所有数据库表的pojo对象到前台js中,如果你要排除某些属性的暴露,可以create元素中加上排除参数,如下所示:

      <paramname="exclude"value="propertyToExclude1, propertyToExclude2"/>

4.dwr+hibernate时lazyload问题

       在hibernate 的one-to-many,many-to-one,many-to-many中,为了效率的提高,我们一般都采用lazy机制, 对于数据量较大的系统建设全部设置为lazy=”true”;需要注意的是,hb.hbm.xml中有两种可以配置lazy机制,一种是对表级(对象关联);当lazy=”true”时,子对象可以引用到父对象的ID,如下所示:

<class name="cn.netjava.pojo.Userinfo"table="USERINFO"lazy="false">

        <idname="id"type="java.lang.Long">

            <columnname="ID"/>

             <generatorclass="sequence">

                 <paramname="sequence">SEQ_USERINFO_ID</param>

            </generator>

        </id>

 一种是对于行级配置,即一对多或多对一引用时,是否lazyload:

<set name="worktasks"inverse="true"lazy="true">

            <key>

                <columnname="USERINFOID"precision="1"scale="0"not-null="true"/>

            </key>

            <one-to-manyclass="cn.netjava.pojo.Worktask"/>

        </set>

  一般情况下,行级对象不需要lazy,即配置为true。

   但从大数据量使用的经验来看,lazy机制较难控制,对与数据量在500W级别以上的系统;建议最好是自己手写jdbc或仅使用hb导出pojo和对应的dao,而不要让hb负责一对多或多对一的关联关系!

5.dwr整合总结:

现在流行整合框架风,所以只是要工具,拿起来第一要问的是怎整合?dwr可以与任何框架整合,这取决与你的需求,它同样可以与Struts整合---我实在看不出这样做的什么好处:

  <allow>

  ...

  <createcreator="struts"javascript="ScriptName">

    <paramname="formBean"value="formBeanName"/>

  </create>

  ...

</allow>

  如果你非要在dwr前台调用struts的action,那只能重构你的Action了。

  说实在的,我看不出这种整合有什么必要。

你最好仅仅将dwr当做一个转换器使用;它在安全性上有所不足,这是由与它的灵活性所致,一般情况下,我们只能通过create中的include 和exclude 元素来控制远程调用Bean 中可以被调用的方法。当然,高级的解决方案可以通过整合Acegi实现;另外,dwr它仅仅是使用异步方式在js和java对象间传送数据,漂亮的页面效果它也做不出----这就是我们最后的总结。

5.任务与总结:

1.一个类似的开源项目json也是通过提供rpc机制实现页面调用java代码的, 利用它内置的一个轻级量JSON-RPC JavaScripIt客户端,可以让你透明地在JavaScript中调用Java代码。JSON-RPC-Java可运行在Servlet容器中如Tomcat也可以运行在JBoss与其它J2EE应用服务器中因此可以在一个基于JavaScript与DHTML的Web应用程序中利用它来直接调用普通Java方法与EJB方法。项目主页:http://oss.metaparadigm.com/jsonrpc/。

2.将我们前面完成的《员工日志管理系统》的某一个模块用dwr+sp+hb重构。

3.选择一款则重与界面展示的AJAX框架,构建一个简单demo,写总结分析其与dwr的适用方向及各自优劣点。

3.关于AJAX的总结

1.ajax框架分类

做为一名软件开发者,在使用ajax技术开发应用时,大多的痛处是:漂亮的界面怎么搞定;大多数的专业java工程师开发的界面都是恐怖的;特别是在引入了ajax技术后,页面表示己不像mvc中那样清晰;与美工良好的配合需要程序员对css,html,javaScript技术更深入的掌握---而不仅仅是看懂为止,因此,更多时间,我们会在应用中使用像dwr这样成熟的ajax框架。

     根以俺之见,ajax框架可以分为如下四类型:

第一种是基与传输的解决方案,如我们前面所说到的dwr,当然还有大名鼎鼎的json;这种解决方案通过其框架的转换,使得页面的js可以透明调用服务器端语言方法并自动转换其间交换的数据类型;但其在页面的表现手段较差,如果你要展示一个漂亮互动的tree或grid,这样样的框架并没有提供支持;另外就是它仅支持java语言的服务器端。

第二种可以理解为java语言到js的翻译器:即服务器端以编写事件调用机制通讯的java代码,通过其框架导出成前台的js脚本调用;对于开发者而言,只需要编写后端java代码即可;这种类型框架的噱头就是:“不写一行js代码也能应用ajax技术!”。典型的代码如GWT、ZK等,当然其缺点也是显示而易见的。

     第三种则是以独立与服务器端语言的js脚本库形式发布,如yui、ext、jQuery等;这种基本上是一个独立的js类库,带有众多的工具函组、tree、form、grid等现成的组件,且有统一的设计风格;所以它的优势就是页面组件非常完善;当然,学习起来需要对js语法相当熟悉。

     第四种则是全栈式设计的RIA开发,可选的技术(平台)有javaFX、Flex、及WPF等。这种方式可以实现功能最为强大的ajax开发----可以理解为它将传统的本机应用开搬到了web上。但其对网络带宽要求较高,且需要客户端浏览器插件支持。(http://www.riachina.com/ )上有相关较多的讨论。