`
barryzhong
  • 浏览: 21256 次
  • 性别: Icon_minigender_1
社区版块
存档分类

多双慧眼看代码漏洞 BlazeDS FlexClient.java

 
阅读更多

这篇博文旨在提高编码的健壮性。良好的生命周期设计是代码健壮的前提之一,任何一个独立的类都应该维护好自己的生命周期,即使在客户类调用过程中没有产生错误。若生命周期设计存在漏洞或者编码与生命周期设计不一致,一定能从代码中找到微妙的逻辑混乱之处。这些混乱之处倒也未必能够引起运行时的defect,因为这涉及到代码运行时覆盖率的问题,不过作为一个独立的单元来看,它的逻辑稍有一点不严密。

 

BlazeDS 是 Adobe公司的一个开源项目,相对应的有一个商业质量的软件LiveCycleDS. 这两个项目都是为了使能(Enable) Flex RIA的Client端与JavaEE服务器互操作而创建的项目。进而Adobe希望Flash的企业版本Flex可以在丰富的互联网应用中与JavaEE共同成为占有一席之地的编程框架。

 

BlazeDS功能也非常强大,除了支持传统的HTTP以及基于HTTP的SOAP通信协议以外,还支持AMF(Adobe Message Format)协议,Adobe声称该协议的传输效率是传统协议的10倍(笔者没有进行过验证)。除此以外,还支持一些传统的HTTP协议不能完成的服务器端推消息的操作。当然几年前就有了Comet以及现在的Websocket,但是Adobe的BlazeDS在更早就提供了非常完善的多种可以支持错误恢复的消息通信机制。比如:实时的流媒体单向通道,准实时的long polling,以及piggybacking等等。它考虑到当时浏览器的限制,比如IE在不进行破戒的情况下仅支持2个并发连接,Firefox仅支持8个并发连接等等,如果没有办法建立实时的流媒体通道,那么框架将自动fallback到准实时的long polling模式等等。还可以支持多人共同参与互动的Dashboard等等。线上有很多相关样例资源以及教程,总而言之,为了满足丰富的互联网的用户体验,BlazeDS在通信机制上确实下了不少功夫。然而Flex还是没有被广泛认可,不少人都觉得它实在是比较慢。再加上苹果和安卓系统都去掉了对Flash的原生支持。这条技术路线的前景可谓惨淡。不过本文不是为了宣传推销BlazeDS,而是站在生命周期的视角来探讨BlazeDS这个开源项目中FlexClient.java中的一些潜在问题。

 

在大概了解了项目背景之后,那么我们更容易理解FlexClient这个类的职责了。在Server Side, 每一个FlexClient对象代表了一个与服务器通信的Flex的Client,可能是在AIR运行时当中,也可能是在浏览器的AVM运行时当中。FlexClient相对MessageBroker来讲,他是完成消息推送到其他MessageClient的负责方;相对Endpoint来讲,FlexClient是完成从该Endpoint对应的消息队列Poll消息下发给客户端的负责方。

 

源代码连接: FlexClient.java

 

它的生命周期如图1所示:


图1.FlexClient的生命周期示意图

 


FlexClient关于生命周期的控制由3个属性完成:

 

    /**
     * Flag indicating whether the instance is valid; once invalidated this flag is
     * set to false.
     */
    boolean valid;

  

    /**
     * Flag used to break cycles during invalidation.
     */
    /* package visibility for FlexClientManager */ volatile boolean invalidating;

    /**
     * Instance level lock to sync for state changes.
     */
    final Object lock = new Object();

 

 

 

        其中lock代表了状态变更的同步范围,以及一些相关的临界区操作;valid用来表示当前FlexClient所处在是否可用的状态;而invalidating是表示一个中间状态。valid在构造器内被初始化为true,在invalidate最后被设定成false。invalidating的三次操作都在invalidate方法中,一次读,两次写。

 

 

    /**
     * @exclude
     * Constructs a new FlexClient instance having the specified Id.
     *
     * @param manager The FlexClientManager managing this instance.
     * @param id The Id for this instance.
     */
    public FlexClient(FlexClientManager manager, String id)
    {
        this.id = id;
        flexClientManager = manager;
        updateLastUse();
        valid = true;

        if (Log.isDebug())
            Log.getLogger(FLEX_CLIENT_LOG_CATEGORY).debug("FlexClient created with id '" + this.id + "'.");
    }

 

 

 

    /**
     * Invalidates the FlexClient.
     */
    public void invalidate()
    {
        synchronized (lock)
        {
            if (!valid || invalidating)
                return; // Already shutting down.

            invalidating = true; // This thread gets to shut the FlexClient down.
            flexClientManager.removeFlexClient(this);
            cancelTimeout();
        }

        // Unregister from all FlexSessions.
        if (!sessions.isEmpty())
        {
            for (FlexSession session : sessions)
                unregisterFlexSession(session);
        }

        // Invalidate associated MessageClient subscriptions.
        if (messageClients != null && !messageClients.isEmpty())
        {
            for (MessageClient messageClient : messageClients)
            {
                messageClient.removeMessageClientDestroyedListener(this);
                messageClient.invalidate();
            }
            messageClients.clear();
        }

        // Notify destroy listeners that we're shutting the FlexClient down.
        if (destroyedListeners != null && !destroyedListeners.isEmpty())
        {
            for (FlexClientListener destroyListener : destroyedListeners)
            {
                destroyListener.clientDestroyed(this);
            }
            destroyedListeners.clear();
        }

        // Unbind all attributes.
        if (attributes != null && !attributes.isEmpty())
        {
            Set<String> keySet = attributes.keySet();
            String[] keys = keySet.toArray(new String[keySet.size()]);
            for (String key : keys)
                removeAttribute(key);
        }

        // Close any registered push handlers.
        if (endpointPushHandlers != null && !endpointPushHandlers.isEmpty())
        {
            for (EndpointPushHandler handler : endpointPushHandlers.values())
            {
                handler.close(true /* notify Channel of disconnect */);
            }
            endpointPushHandlers = null;
        }

        synchronized (lock)
        {
            valid = false;
            invalidating = false;
        }

        if (Log.isDebug())
            Log.getLogger(FLEX_CLIENT_LOG_CATEGORY).debug("FlexClient with id '" + this.id + "' has been invalidated.");
    }

 

 

 以上两段代码包含了全部对于两个状态标记属性的赋值操作。除此以外,一些依赖于Valid状态的方法,进行checkValid的判断,个别方法单独加了lock用来同步,其他方法也通过volatile来保证了有序性(由于编译器会对源代码进行优化而重新调整代码顺序,详见Java Memory Model或者Java Language Specification):

 

 

    /**
     * Utility method that tests validity and throws an exception if the instance
     * has been invalidated.
     */
    protected void checkValid()
    {
        synchronized (lock)
        {
            if (!valid)
            {
                MessageException e = new MessageException();
                e.setMessage(FLEX_CLIENT_INVALIDATED);
                throw e;
            }
        }
    }

 

 

 

    /**
     * @exclude
     * Poll for outbound messages for the FlexClient.
     * This method is only invoked by internal code while processing a client poll request; it
     * is not intended for general public use.
     * Poll requests that trigger this method come from client-side polling channels and the request
     * is not specific to a single Consumer/MessageClient instance so process any queued messages for
     * the specified endpoint across all subscriptions.
     *
     * @param endpointId The Id of the endpoint that received the poll request.
     * @return The flush result including messages to return in the poll response and
     *         an optional wait time for the next poll/flush.
     */
    public FlushResult poll(String endpointId)
    {
        synchronized (lock)
        {
            checkValid();

            EndpointQueue queue = outboundQueues.get(endpointId);

            if (queue != null)
                return internalPoll(queue);

            // Otherwise, the client is not subscribed.
            throwNotSubscribedException(endpointId);
        }
        return null;
    }

 

/**

     * List of registered FlexClient attribute listeners.

     */

    private volatile CopyOnWriteArrayList<FlexClientAttributeListener> attributeListeners;

...
   /**
     * Adds a FlexClient attribute listener that will be notified when an
     * attribute is added, removed or changed. If the attribute implements
     * FlexClientBindingListener, it will be notified before any
     * FlexClientAttributeListeners are notified.
     *
     * @param listener The listener to add.
     */
    public void addClientAttributeListener(FlexClientAttributeListener listener)
    {
        if (listener != null)
        {
            checkValid();

            if (attributeListeners == null)
            {
                synchronized (lock)
                {
                    if (attributeListeners == null)
                        attributeListeners = new CopyOnWriteArrayList<FlexClientAttributeListener>();
                }
            }

            attributeListeners.addIfAbsent(listener);
        }
    }

 

类似的还有图2的一些方法:


图2 Valid状态可以执行的方法


 然而,也有一些使用了lock,但是并没有进行当前生命周期状态判定的方法,这并不违背什么,只是原则上,这些方法应该是与状态无关的,任何状态下都应该可以执行而且有意义的方法。如以下方法



 

    /**
     * Removes a FlexClient attribute listener.
     *
     * @param listener The listener to remove.
     */
    public void removeClientAttributeListener(FlexClientAttributeListener listener)
    {
        // No need to check validity; removing a listener is always ok.
        if (listener != null && attributeListeners != null)
            attributeListeners.remove(listener);
    }

    /**
     * Removes a FlexClient destroyed listener.
     *
     * @see flex.messaging.client.FlexClientListener
     *
     * @param listener The listener to remove.
     */
    public void removeClientDestroyedListener(FlexClientListener listener)
    {
        // No need to check validity; removing a listener is always ok.
        if (listener != null && destroyedListeners != null)
            destroyedListeners.remove(listener);
    }

 

 

 

不过下面这个方法至少在表面上不是那么有意义,虽然不会引起什么错误,但是从逻辑上讲,还是不够严密。

 

    /**
     * @exclude
     * Registers an <tt>EndpointPushHandler</tt> for the specified endpoint to handle pushing messages
     * to remote clients.
     *
     * @param handler The <tt>EndpointPushHandler</tt> to register.
     * @param endpointId The endpoint to register for.
     */
    public void registerEndpointPushHandler(EndpointPushHandler handler, String endpointId)
    {
        synchronized (lock)
        {
            if (endpointPushHandlers == null)
                endpointPushHandlers = new HashMap<String, EndpointPushHandler>(1);

            if (endpointPushHandlers.containsKey(endpointId))
            {
                MessageException me = new MessageException();
                me.setMessage(ENDPOINT_PUSH_HANDLER_ALREADY_REGISTERED, new Object[] {getId(), endpointId});
                throw me;
            }

            endpointPushHandlers.put(endpointId, handler);
        }
    }

 

 

显而易见,对于任何一个Invalid的FlexClient来说,向其中注册EndpointPushHandler,找不到一个非常合理的理由。类似的还有一些,如图3当中有一些在图2中没有出现的方法(其中有个别方法直接访问了valid属性的除外,比如directFlush)

 


图3 使用lock进行同步的方法

 

图3中一共包含了32个引用,图2中一共包含了14个引用。除去个别的直接访问valid进行校验的方法外,还有不少方法没有进行状态检查。虽然在运行时也没有错误,但是如果是在Invalid状态执行,肯定也是没有意义的。因为在Invalidating状态切换到Invalid状态过程中,那些被注册进去的MessageClient就已经被删掉了。而在Valid状态和Invalidating状态时,这个方法被其他类调用是有意义的,并且也应该是线程同步的。

 

 

    /**
     * @exclude
     * Used internally to disassociate a MessageClient (subscription) from a FlexClient.
     *
     * @param messageClient The MessageClient to disassociate from the FlexClient.
     */
    public void unregisterMessageClient(MessageClient messageClient)
    {
        if (messageClients != null && messageClients.remove(messageClient))
        {
            messageClient.removeMessageClientDestroyedListener(this);
            String endpointId = messageClient.getEndpointId();
            // Manage the outbound queue that this subscription uses.
            synchronized (lock)
            {
                EndpointQueue queue = outboundQueues.get(endpointId);
                if (queue != null)
                {
                    // Decrement the ref count of MessageClients using this queue.
                    queue.messageClientRefCount--;

                    // Unregister the message client from the outbound throttle
                    // manager (if one exists).
                    OutboundQueueThrottleManager tm = queue.processor.getOutboundQueueThrottleManager();
                    if (tm != null)
                        tm.unregisterAllSubscriptions(messageClient.getDestinationId());

                    // If we're not attempting to notify the remote client that this MessageClient has
                    // been invalidated, remove any associated messages from the queue.
                    if (!messageClient.isAttemptingInvalidationClientNotification())
                    {
                        Object messageClientId = messageClient.getClientId();
                        for (Iterator<Message> iter = queue.messages.iterator(); iter.hasNext(); )
                        {
                            Message message = iter.next();
                            if (message.getClientId().equals(messageClientId))
                                iter.remove();
                        }
                    }

                    // If no active subscriptions require the queue, clean it up if possible.
                    if (queue.messageClientRefCount == 0)
                    {
                        if (queue.messages.isEmpty() || messageClient.isClientChannelDisconnected())
                        {
                            if (queue.asyncPoll != null) // Close out async long-poll if one is registered.
                            {
                                FlushResult flushResult = internalFlush(queue);
                                // If the MessageClient isn't attempting client notification, override
                                // and do so in this case to suppress the next poll request from the remote client
                                // which will fail triggering an unnecessary channel disconnect on the client.
                                if (!messageClient.isAttemptingInvalidationClientNotification())
                                {
                                    CommandMessage msg = new CommandMessage();
                                    msg.setClientId(messageClient.getClientId());
                                    msg.setOperation(CommandMessage.SUBSCRIPTION_INVALIDATE_OPERATION);
                                    List<Message> messages = flushResult.getMessages();
                                    if (messages == null)
                                        messages = new ArrayList<Message>(1);
                                    messages.add(msg);
                                }
                                completeAsyncPoll(queue.asyncPoll, flushResult);
                            }

                            // Remove the empty, unused queue.
                            outboundQueues.remove(endpointId);
                        }
                        // Otherwise, the queue is being used by a polling client or contains messages
                        // that will be written by a delayed flush.
                        // Leave it in place. Once the next poll request or delayed flush occurs the
                        // queue will be cleaned up at that point. See internalFlush() and shutdownQueue().
                    }

                    // Make sure to notify any threads waiting on this queue that may be associated
                    // with the subscription that's gone away.
                    synchronized (queue)
                    {
                        queue.notifyAll();
                    }
                }
                // And if this subscription was associated with an endpoint push handler, unregister it.
                if (endpointPushHandlers != null)
                {
                    EndpointPushHandler handler = endpointPushHandlers.get(endpointId);
                    if (handler != null)
                        handler.unregisterMessageClient(messageClient);
                }
            }
        }
    }

 

 

可见,写出逻辑严密的代码实属不易。只要在鸡蛋里挑骨头总是能找到的。在实践过程中,使用几个boolean的标记用来表示对象的关键状态的事情到时经常发生。这样的写法确实容易把人搞迷糊,比如在进行状态校验时,到底是程序员忽略了其他状态标记属性,还是其他属性均与当前使用状态标记属性无关,这可能需要读者花一番心血才能搞清楚,甚至对于写代码的人来讲,过一段时间,自己也糊涂了。况且,这种写法确实容易产生逻辑漏洞。

 

如果使用生命周期框架的编程模型,可以避免这样的令人困惑的表达而使得对生命周期的描述十分清晰的同时,还可以减少代码中的判断逻辑,像FlexClient这个类中的所有checkValid方法的调用以及checkValid方法自己都可以不写。除此以外,对那些确实改变了FlexClient状态的方法添加@Transition标记,不但更清晰的显示的指出了方法在当前对象中的作用之外,还完成了状态检查的作用,而无需关心到底应该对哪几个状态标记进行检查。

 

如果对提高代码健壮性,可读性和减少代码量感兴趣,请参考Lifecycle框架。相关博文:

 

 

  • 大小: 49.5 KB
  • 大小: 97.9 KB
  • 大小: 198.8 KB
分享到:
评论

相关推荐

    MyEclipse7.5+flex4+spring3.0.5+struts2.2.1+hibernate3.6.0+blazeds4.0.0.14931完美整合方案

    本方案提供了一种集成化的开发环境,即"MyEclipse7.5+flex4+spring3.0.5+struts2.2.1+hibernate3.6.0+blazeds4.0.0.14931完美整合方案",它将多个流行的技术框架整合在一起,为Web应用程序开发提供了一个强大的平台...

    最简单的BlazeDS实现flex与java通信.rtf

    最简单的BlazeDS实现flex与java通信..无积分下载...最简单的BlazeDS实现flex与java通信..无积分下载...最简单的BlazeDS实现flex与java通信..无积分下载...最简单的BlazeDS实现flex与java通信..无积分下载...最简单的...

    blazeds-bin-4.0.1.17657.zip

    4. **Blazeds.war**: 这是BlazDS的核心Web应用程序,包含了运行时库和其他必要的组件。将这个WAR文件部署到Servlet容器(如Tomcat)上,即可启用BlazDS服务。开发者可以通过它创建数据服务、发布Java方法供Flex...

    MyEclipse_8.5+flex_4+Blazeds配置.docx

    3. JDK:Java Development Kit,用于编译和运行Java代码。 4. Blazeds:Adobe的服务器端技术,用于Flex和Java之间的通信。 5. Adobe Flash Builder 4 Plugin:这是一个插件,用于在MyEclipse中集成Flex开发功能。 *...

    blazeds-turnkey-4.0.0.14931.zip

    Blazeds Turnkey 4.0.0.14931是一个重要的软件包,主要用于在Flex应用程序和Java服务器之间建立双向通信。Flex是Adobe开发的一款用于构建富互联网应用程序(RIA)的开源框架,而Blazeds是Flex与后端数据服务交互的...

    FLEX——blazeDS原理.pdf

    - **BlazeDS**:Adobe BlazeDS是一个开源远程服务和消息传递框架,主要用于实现Flex或AIR客户端与Java后端之间的通信。它可以提供低延迟的数据推送和远程对象调用能力。 #### 二、BlazeDS工作原理详解 1. **通道...

    blazeds-bin-3.3.0.22497.zip

    在BlazeDS中,blazeds.war包含了服务器端的组件和服务,如Remoting服务、MessageBrokering服务,以及Flex客户端与Java服务器之间的通信所需的类和配置。你可以将此WAR文件部署到支持Servlet 2.4或更高版本的任何Java...

    blazeds完整压缩包blazeds.war,ds-console.war,samples.war

    现在我们来深入探讨这三个核心组件:blazeds.war、ds-console.war和samples.war。 1. **blazeds.war**: 这是BlazDS的主要部署单元,包含了BlazeDS服务的全部功能。当你将blazeds.war部署到像Apache Tomcat这样的...

    WEB项目-集成Flex3+BlazeDS3.2.

    而BlazeDS是Adobe官方提供的一个轻量级中间件,用于实现在Flex和Java后端之间的数据通信。本教程将深入讲解如何在已有的Flex3基础上集成BlazeDS3.2,实现基于RPC模式的消息传递,让Flex应用程序能够调用后端服务方法...

    blazeds下载,java和flex的通信工具

    Blazeds是Adobe官方推出的一款开源工具,全称为BlazeDS Project,它是Java和Flex之间进行数据通信的重要桥梁。Blazeds使得开发人员能够利用Java后端服务与Flex前端应用程序进行实时的双向通信,实现富互联网应用...

    blazeds.zip文件

    3. **源代码**:对于开源项目,`blazeds.zip`可能包含BlazeDS的源代码,这有助于开发者深入理解其工作原理,并可以根据需要进行定制或扩展。 4. **示例和教程**:压缩包中可能还包括演示应用或教程文档,帮助初学者...

    blazeds.war

    "使用说明.txt"可能包含了部署和配置BlazeDS的详细步骤,而"BlazeDS.war.url"则可能是一个链接,指向有关BlazeDS的更多信息或者下载页面。如果你计划开发基于Flex的RIA应用并与Java后端紧密集成,那么理解并掌握...

    BlazeDS.war

    在本案例中,我们讨论的核心是"BlazeDS.war"文件,这是一个Web应用归档(Web Application Archive),它是Java EE应用程序部署的标准格式。 1. **BlazeDS.war**:这个文件是BlazeDS服务器组件的打包形式,遵循WAR...

    flex+Cairngorm+blazeds整合.doc

    这种整合方式使得前端Flex应用能够高效地与Java后端进行数据交互,同时利用Cairngorm的MVC架构,提高了代码的可维护性和可扩展性。通过合理的组织和配置,开发者可以构建出功能强大、性能优良的RIA应用。

    blazeDS samples.war

    blazeDS应用的官方示例,对于学习很有用的。

    flex使用BlazeDS远程调用java例子.

    在本例子中,我们将探讨如何使用Flex与Java后端进行远程调用,借助Adobe的BlazeDS服务。BlazeDS是Flex与Java服务器之间通信的一个中间件,支持AMF(Action Message Format)协议,提供数据推送、拉取和消息代理等...

    Blazeds与java通信

    Blazeds是一个强大的Java服务器端技术,主要用于实现Flex(一种基于Adobe Flash的用户界面开发工具)与后端Java应用程序之间的数据交互。它提供了一个全面的解决方案,使开发人员能够构建富互联网应用程序(RIA),...

Global site tag (gtag.js) - Google Analytics