GWT如何减小RPC调用的代码序列化器大小

8
我发现GWT生成的JavaScript代码中超过60%是为RPC序列化器生成的。 此外,我发现序列化器在服务接口之间不共享。也就是说,如果我有两个RPC服务接口引用了AccountDTO类型,那么我将得到两个序列化器类而不是同一类型的1个序列化器类。 为了减少编译代码的大小,我考虑使用延迟绑定来替换所有的服务接口,只使用一个大接口。如果可能的话,这样GWT编译器将只生成一个AccountDTO序列化器而不是2个。 我不确定这是否是一个好主意,或者是否有更好的解决方案来解决我的问题。 我尝试实现的是像这样的东西:
// Define new interface that extends all service interfaces
public interface GenericService extends RemoteService,
                    AccountingService,
                    FinancialService,..., { }

public interface GenericServiceAsync extends AccountingServiceAsync,
                         FinancialServiceAsync, ..., { }

// At Application.gwt.xml do:

<module>
...
...
    <replace-with class="com.arballon.gwt.core.client.GenericService">
        <when-this-is class="com.arballon.gwt.core.client.AccountingService>
    </replace-with>
    <replace-with class="com.arballon.gwt.core.client.GenericService">
        <when-this-is class="com.arballon.gwt.core.client.FinancialService>
    </replace-with>
    ...
    ...

但是我收到了以下错误信息:

[ERROR] 'file:/C:/Users/Daniel/EclipseWorkspace/ADK/src/com/arballon/gwt/core/client/FinancialService.java'中的错误: [ERROR] 第31行:无法找到重新绑定结果 'com.arballon.gwt.core.client.GenericService'

如果您对此问题有任何想法,将不胜感激。 谢谢。

Daniel

5个回答

4
GWT的RPC生成代码构建了几个类来完成其工作,如您所述:每个要在网络上传输的类型都有一个*_FieldSerializer,以及用于RemoteService异步类型的*_Proxy类。该代理类型需要*_TypeSerializer,这是您问题的根源——由于某种原因,GWT在字符串->js函数映射中链接了所有序列化/反序列化方法,可能是为了便于快速查找,但这些设置代码带来的成本是需要在最终构建中保留的大量代码行数。更优化的方法可以让每个FieldSerializer具有注册方法,其中它将其方法添加到Proxy拥有的静态映射中——然而,由于GWT试图优化不引用instantiate()、deserialize()和serialize()方法(如果看起来它们不会被调用),这导致了一些问题。
您的问题源于有多个可序列化的类型,并且您尝试构建描述特定功能单元并重用许多模型类型的RemoteService类型。这是一个值得称赞的目标,尤其是它可能使您的服务器端代码看起来更好,但很明显,GWT会为此付出代价。
我在freenode上尝试向您提供的解决方案(作为niloc132)是构建一个单独的大型RemoteService类型,您命名为GeneralService,以及匹配的GeneralServiceAsync,每个都扩展了所有现有的rpc服务类型。我的第一个想法是使用replace-with告诉生成器系统,当您需要每个RemoteService类型时,将其替换为GeneralService,但正如Tahir所指出的那样,这没有意义——GWT不会将rebind结果传回自身以继续查找。相反,我建议在需要服务异步类型时执行以下操作:
AccountingServiceAsync service = (AccountingServiceAsync) GWT.create(GeneralService.class)
GeneralService的重新绑定结果将实现GeneralServiceAsync,它本身可分配给AccountingServiceAsync。如果我没记错的话,您说过您有提供这些服务的静态方法/字段-请更改这些位置以始终创建一个GeneralServiceAsync实例。只要您不在任何RemoteService子类型上调用GWT.create,您就会将TypeSerializers的数量限制为一个。
顺便说一句,RemoteServiceProxy子类型是无状态的,因此确保仅创建一个实例可能更容易构建,但不会节省运行时内存或时间,因为它们几乎肯定被编译为静态方法。然而,*_TypeSerializer类具有状态,但每个类只有一个实例,因此合并所有RemoteService可能会节省非常少量的工作内存。

3

经过多次交流后,我们终于找到了一个解决方案,并且我想分享给大家以便帮助其他人。 首先,我必须提到Colin Alworth的帮助,没有他的支持,这个解决方案是不可能的。 同时,我也要提到我对最终解决方案并不是很自豪,但它对我们有效,并且目前是我们最好的选择。

最终我们所做的是,如Colin在上一篇文章中所述,将每个服务接口的GWT.create替换为创建GenericBigService接口。

所以我们的第一个补丁如下:

1)创建GenericBigService接口,它继承了所有服务接口(目前有52个接口),并创建它的Async兄弟。我们通过一个phytom脚本完成了这个操作。

因此,我们的GenericBigInterface看起来像这样:

package com.arballon.gwt.core.client;

import com.google.gwt.user.client.rpc.RemoteService;

public interface GenericBigService extends RemoteService,
                                       AccountingService,
                                       ActionClassifierService,
                                       AFIPWebService,
                                       AnalyticalService,
                                       AuthorizationService,
                                       BudgetService,
                                       BusinessUnitService,
                                       CatalogPartService,
                                       CategoryService,
                                       ClientDepositService,
                                       .....
                                       .....
{ }

2) 每个服务接口中都有一个Util内部静态类,用于实例化异步实例,在其中我们将GWT.create替换为创建GenericBigInterface。

因此,我们的某个服务接口如下所示:

public interface FinancialPeriodBalanceCategoryService extends RemoteService {
    /**
 * Utility class for simplifying access to the instance of async service.
 */
public static class Util {
    private static FinancialPeriodBalanceCategoryServiceAsync instance;
    public static FinancialPeriodBalanceCategoryServiceAsync getInstance() {
        if (instance == null) {
            instance = GWT.create(GenericBigService.class);
((ServiceDefTarget)instance).setServiceEntryPoint(GWT.getModuleBaseURL()+"FinancialPeriodBalanceCategoryService");
        }
        return instance;
    }
}

我们需要进行serServiceEntyPoint调用,以保持我们的web.xml不被修改。
当我们第一次编译时,它能够成功编译,但在运行时,服务器调用会抛出异常。
IncompatibleRemoteServiceException Blocked attempt to access interface GenericBigService 

”是由FinancialPeriodBalanceCategoryService未实现的。

没错,我们正在使用一个没有实现的接口调用服务,这就是问题出现的地方。目前我们还没有找到更好的解决方案,所以我们决定实现以下方法:

我们将RPC.java替换为自己的副本,并进行以下代码更改:

在decodeRequest方法中,我们做了以下更改:

  if (type != null) {
    /*if (!implementsInterface(type, serviceIntfName)) {
      // The service does not implement the requested interface
      throw new IncompatibleRemoteServiceException(
          "Blocked attempt to access interface '" + serviceIntfName
              + "', which is not implemented by '" + printTypeName(type)
              + "'; this is either misconfiguration or a hack attempt");
    }*/
    if (!implementsInterface(type, serviceIntfName)) {
          if(!serviceIntfName.contains("GenericBigService")){
              throw new IncompatibleRemoteServiceException(
                      "Blocked attempt to access interface '" + serviceIntfName
                          + "', which is not implemented by '" + printTypeName(type)
                          + "'; this is either misconfiguration or a hack attempt");
          }
    }

这样做的好处有:
1)我们花了1小时20分钟来完成6个排列,现在只需要20分钟。
2)在devMode中,所有操作都开始变得更快了。启动保持不变,但是一旦开始执行,速度就非常快。
3)编译大小减少是另一个有趣的结果,我们将剩余的片段从6MB减少到1.2MB,整个JS编译的大小减少了约50%至60%。
我们对GWT-RPC感到非常满意,不想放弃它,但是typeSerializers确实是一个问题,主要是由于生成的JS文件过大。
这种解决方案不太优雅,但它可以很好地工作。
再次感谢Colin的帮助!
祝好,
Daniel

如果您感兴趣的话,我刚刚在GWT-RPC生成器上制作了一个补丁,以便为所有应用程序服务构建一个大型类型映射。 对于我的客户来说,这将生成的js文件大小从42Mo减少到19Mo(他们使用许多具有通用Serializable参数的小RPC服务接口)。 我正在设置一个标志,以便您可以根据您的应用程序选择是否启用该补丁。 如果您感兴趣,请告诉我,我可以在官方gwt git repo上生成一个补丁。 - Arnaud Tournier

1
对于任何GWT-RPC服务,GWt将生成一个代理(Proxy),一个类型序列化器(TypeSerializer)。对于每个可能通过GWT传递的对象,您都将拥有一个FieldSerializer类。每个类只能有一个FieldSerializer,因此您无法为一个AccountDTO拥有两个FieldSerializers。
您尝试使用的延迟绑定规则将不起作用。例如,您有以下内容:
MyServiceAsync sync = GWT.create(MyService.class);
延迟绑定规则将将其更改为:
MyServiceAsync sync = new MyServiceAsync_Proxy();
您的规则实际上会执行以下操作:
MyServiceAsync sync = new MyGenericService();//无效,因为MyGenericService是一个接口
因此,您的解决方案将无法工作。
由于您说60%的应用程序生成的代码与RPC相关,我怀疑您存在RPC类型爆炸问题。
检查GWT在编译期间是否会抛出任何警告,或者是否会为RPC TypeSerializers生成存根,最有可能的情况是您的某些服务中存在非常常见的接口。

不幸的是,我没有RPC类型爆炸问题,我有的是一个巨大的应用程序,客户端超过250000行代码并且还在增长,现在大部分生成的代码(13 Mb JS)来自于Service TypeSerializers,根据编译报告。但我理解你的观点,延迟绑定也无法解决这个问题,所以我需要其他的方法,也许离开RPC并开始使用JSON可以解决这个问题。我需要做更多的研究。谢谢你的回答!Daniel - Daniel Ardison
我认为jusio说得有道理。客户端Java LOC可能会对JavaScript大小产生影响,但这是另一个问题。至于TypeSerializers,你能否向我们展示编译报告中出现的一些重复内容? - Tahir Akhtar
@Daniel Adrison,13 MB 真的是一个很大的数字。尝试使用 -gen 选项运行编译器,并检查 TypeSerializer 包含了什么。你的 DTO 层有多大?例如,你通过 GWT-RPC 发送了多少个类?另外,可能你正在使用某个 DTO 类作为另外 1000 个对象的超类。基本上这将是相同的 RPC 类型爆炸,但没有警告 =) 无论如何,我强烈建议查看生成的 TypeSerializers 的源代码。 - jusio

1
如果你想要更好的解决方案,为什么不使用命令模式呢?这样只需要一个GWT服务来接受Command子类型并返回Result子类型(你可以通过使用泛型使其类型安全)。
好处在于你只需在一个gwt servlet中声明一个方法,然后就可以将请求分发到任何其他服务器端服务。
命令模式还能给你带来很多附加的好处,因为你有一个集中控制点来进行安全检查或者允许你透明地批处理请求。
这样你在服务器端对GWT的曝光度就会小得多。

0
据我所知,GWT代码生成应该提供接口的具体实现。然后将此实现转换为特定排列的JavaScript。
另一方面,您的示例则是用一个接口替换另一个接口。如果您从GWT编译器的角度来看,可能会看到这种配置存在的问题。
假设您是GWT编译器,并且您在将客户端代码转换为JavaScript时看到以下行:
AccountingServiceAsync accountingServiceAsync = (AccountingServiceAsync) GWT.create(AccountingService.class);
accountingServiceAsync.recordTransaction(transaction,callback);

所以你需要找出在第2行应该发生什么。具体来说,你需要知道在哪里可以找到accountingServiceAsync.recordTransaction()的实现。于是你开始查看所有的配置文件,看是否有规则指定了用于AccountingService(非异步)的实现类应该使用哪个。但不幸的是,你没有找到任何信息。但是你注意到AccountingService也是一个RemoteService。所以你再次深入研究你的配置文件。然后,啊哈,你发现了一条规则,指定你可以使用ServiceInterfaceProxyGenerator 生成远程服务实现。你很高兴地将提供AccountingService实现的任务交给了ServiceInterfaceProxyGenerator。

但是假设你的配置告诉你可以用GenericService替换AccountingService,你会说“太棒了,来吧”。但就在这时,你发现GenericService也是一个接口。显然,你会感到失望,说:“现在,我要拿另一个接口怎么办,我只需要一个AccountingService的实现。”此时,你会想通过向程序员抛出一个神秘的错误来报复他。

因此,这解释了为什么你的解决方案(理论上)不起作用。至于你实际关心的JavaScript臃肿问题,我很惊讶这个问题居然存在,考虑到GWT团队在优化编译后的JavaScript方面所付出的努力。你如何测试你的编译输出是否有重复?


是的,Tahir,我曾经误解了延迟绑定的用法,现在我明白了,但我仍然有一个问题,就是这个生成的大量TypeSerializers代码。我需要做更多的研究。感谢你的回答。 - Daniel Ardison

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接