基于 Equinox 的 OSGi Console 的研究和探索

自定制 OSGi Console 进行组建和服务生命周期管理

李 雪锋, 硕士研究生, 北京交通大学
朱 志辉, 软件工程师, EMC

 

简介: 模块化编程的好处已经很好地被理解了约 40 年,但在 OSGi 之前,开发人员不得不自己发明模块化设计和系统。随着软件应用体系结构的不断发展,OSGi 已经成为了一种很好的软件体系模型。基于 OSGi 框架的应用程序因为其模块化、动态性和面向服务的特性而受到广泛欢迎,然而传统的 OSGi 框架的控制台在适用性和功能扩展性等诸多方面都有很多限制。为此,本文探讨一种自定制 OSGi 控制台来进行服务和组件管理的方法。

 

概述

OSGi 的现状和发展趋势

模块化编程的好处已经很好地被理解了约 40 年,但在 OSGi 之前,开发人员不得不自己发明模块化设计和系统。OSGi 标准为软件提供了模块化、动态性、面向组件和服务的特性,因此越来越受到软件开发商以及开源社区的关注。在网络计算和云计算体系高速发展的带动下,单一节点的传统应用程序正逐渐被分布式多节点应用程序所取代。OSGi 标准正向着分布式、跨虚拟机的方向发展,跨虚拟机的组件和服务访问成为了构建分布式 OSGi 应用程序的重要方式。

OSGi 当前控制台的限制和不足

OSGi 控制台对于 OSGi 组件和服务生命周期管理以及模块的调试和诊断的作用是至关重要的。组件和服务的管理者可以通过控制台向 OSGi 框架提交控制命令,由控制台传递到命令解释器,然后解释器再去命令提供者那里查找执行命令,并返回执行结果到控制台,从而完成管理过程的交互。

然而,在 OSGi 应用中使用控制台并不是十分方便的,在有些情况下,OSGi 实现的默认控制台是无法使用的,这一点在文中会有详细分析。因此,我们需要研究探索 OSGi 的控制台的内部机制,寻找更加具有适用性和扩展性的控制台解决方案。

本文在 OSGi 控制台方面的研究和探索方向

OSGi 标准仅仅规定了命令定制和解释的接口,命令列表的具体提供和解释,以及控制台的实现,并不在 OSGi 标准的规范范围内。每个遵循 OSGi 标准的实现都可以提供自己的命令和控制台。这就为我们自定制 OSGi 控制台提供了方便。

本文使用 Equinox 作为 OSGi 标准的实现来研究和探索控制台。Equinox 是 eclipse 采用的一种 OSGi 标准的实现框架。借助 eclipse 的知名度,Equinox 也得到了很大程度的推广普及,目前很多公司的大型软件产品都是构建在这一框架之上。

Equinox 框架提供了一个默认控制台实现,这个 FrameworkConsole 以通过 System.in 接受用户的命令请求,查询并执行命令,通过 System.out 将结果反馈给用户,完成交互过程。本文在分析在具体应用场景中现有控制台实现的不足的基础上,研究并挖掘了 Equinox 控制台的扩展功能,并对自己实现控制台从而满足更多自定制需求进行了探索,希望能够抛砖引玉,为更多 OSGi 的使用者提供一个参考。

 

OSGi 控制台的研究和探索

Equinox 控制台实现在具体应用环境中的局限性分析

在通常情况下,Equinox 提供的默认控制台可以提供对组件和服务的生命周期控制和管理的功能。但在有些情况下,如果不对 Equinox 系统做任何修改,是不能直接使用控制台进行管理的。

这里先对 Equinox 控制台在具体应用环境中可能遇到的限制和不足做出分析,然后在接下来的部分,会对这些限制和不足提供尝试性解决方案。

场景一,在不做任何修改时 Equinox 控制台无法使用的情况。这种情况在 OSGi 应用程序的日常开发中很常见的。举例来说:如果使用 equinox servletbridge 通过外部的 servlet 容器来启动 OSGi 应用,若不添加任何启动参数则无法使用 OSGi 控制台;通过 windows 服务来启动的 Websphere Application Server 同样也无法使用默认的控制台;如果用户希望从远程访问控制台,对系统的组件和服务进行调试和控制,这种需求也是无法满足的。

场景二,Equinox 控制台的默认行为无法满足用户需求的情况。举例来说:有些情况下,需要对控制台的添加访问控制机制,那么传统的控制台就无法提供这样的附加功能,因为他的行为已经固定了。

场景三,需要控制台服务完全和应用程序解耦合的情况。在某些情况下,需要对原有系统软件不做任何改变,而同时提供可嵌入式的控制台。控制台和应用程序完全解耦,具有高度的可复用性。

下面就针对这三种场景,给出经过尝试性解决方案。

对 Equinox 默认控制台服务内部机制的研究

Equinox 其实是一个相对完善的 OSGi 实现框架,在充分了解它的内部实现机制之后,场景一所提到的限制就可以很容易的解决。

首先来分析一下 Equinox 框架启动过程中是如何加载控制台的。

由于 eclipse3.4(3.0 以上版本)是完全构建在 Equinox 框架之上的,所以可以从 eclipse3.4 的启动过程入手。org.eclipse.osgi_3.4.3.R34x_v20081215-1030.jar 是 eclipse3.4 启动的插件,也是 Equinox 框架的核心所在。这个包里面既包含 Equinox 对于 OSGi 标准的实现,也包含了 eclipse 启动所需要的类。org.eclipse.core.runtime.adaptor.EclipseStarter 这个类是负责启动 eclipse 的,里面有很多和配置相关的参数。其中:

public static final String PROP_CONSOLE = "osgi.console";

public static final String PROP_CONSOLE_CLASS = "osgi.consoleClass";

这两个属性是控制台相关的,PROP_CONSOLE_CLASS用来指定控制台的实现类,PROP_CONSOLE用来指定控制台所使用的 socket 通信端口。原来 Equinox 默认实现的控制台也是有通过 socket 进行远程通信的能力的,只不过要受到很多限制,这一点后面会详细分析。而通过设定控制台实现类,Equinox 为开发者提供了一个自定制控制台的切入点。

先从最简单的情况入手。Equinox 可以通过添加系统属性激活基于 socket 通信的远程控制台。控制台类是在org.eclipse.osgi.framework.launcher 中被加载的。在 org.eclipse.core.runtime.adaptor.EclipseStart 类中也有类似的startConsole() 方法,同样用来加载控制台,如 清单 1 所示。


清单 1. 加载控制台

				
 private void doConsole(OSGi osgi, String[] consoleArgs) { 
 Constructor consoleConstructor;   
    Object osgiconsole; 
    Class[] parameterTypes; 
    Object[] parameters; 
    try { 
        Class osgiconsoleClass = Class.forName(osgiConsoleClazz); 
        if (consolePort.length() == 0) { 
            parameterTypes = new Class[] {OSGi.class, String[].class}; 
            parameters = new Object[] {osgi, consoleArgs}; 
        } else { 
            parameterTypes = new Class[] {OSGi.class, int.class, String[].class}; 
            parameters = new Object[] {osgi, new Integer(consolePort), consoleArgs}; 
        } 
        consoleConstructor = osgiconsoleClass.getConstructor(parameterTypes); 
        osgiconsole = consoleConstructor.newInstance(parameters); 
        Thread t = new Thread(((Runnable) osgiconsole), OSGI_CONSOLE_COMPONENT_NAME); 
        t.start(); 
    } catch (NumberFormatException nfe) { 
        System.err.println(NLS.bind(Msg.LAUNCHER_INVALID_PORT, consolePort)); 
    } catch (Exception ex) { 
        informAboutMissingComponent(OSGI_CONSOLE_COMPONENT_NAME, OSGI_CONSOLE_COMPONENT); 
    } 
 } 
     

 

在 launcher 类的 doConsole()方法中,先检查启动参数中是否有指定了控制台使用的端口号,构造相应的参数列表,通过反射方式调用控制台类的构造方法来创建控制台对象。如果指定了特定端口号,则构造控制台对象时使用的是 socket 获取的输入输出流;如果没有指定端口号,则使用默认的标准输入输出流来构造控制台。

通过指定控制台使用 socket 方式建立通信,就可以完全解决场景一提到的问题。指定控制台端口号的方法并不复杂,针对不同的情况,可以有下面三种种方法:

  • 在开发环境(以 eclipse 为例)中,需要在运行 OSGi 应用的配置中添加启动参数。这种情况有两种方法:其一,eclipse 中选择 Run -> Run Configurations,选择你将要运行的应用点击 Arguments 选项卡,在上半部分的 Program arguments 中输入 -console 8090,这样就可以打开使用 8090 socket 通信的控制台,如果不填写端口号,则会默认为使用标准输入输出的控制台。其二,也可以通过添加 VM arguments 来设定端口,在 VM arguments 中添加 -Dosgi.console=8090,注意如果添加这一项,需要把上面 Program arguments 中的 -console 参数删去。
  • 如果在外部通过自定义 bat 文件来启动 OSGi 框架,则 bat 文件中可以这样写:java -Dorg.osgi.service.http.port=8080 -jar equinox.jar -console 8090 这与上面提到的情况类似。
  • 如果使用 equinox servletbridge 通过外部的 servlet 容器来启动 OSGi 应用,由于这种方式下,OSGi 应用以普通的 web 应用程序的形式存在。启动控制台需要在 web.xml 文件中的 org.eclipse.equinox.servletbridge.BridgeServlet添加启动参数,如 清单 2 所示。


清单 2. 添加启动参数

				
	  <init-param> 
		 <param-name>commandline</param-name> 
		 <param-value>-console 8090</param-value> 
	  </init-param> 
	 

 

这种方式不但可以让 OSGi 应用的部署本地畅通无阻的访问 OSGi 控制台,还可以让远端计算机通过 socket 客户端访问 OSGi 控制台。比如使用 windows 系统的可以使用 telnet 命令,例如:telnet 192.168.1.1 8090。

通过上面的尝试,我们已经可以通过 socket 建立与控制台的通信,使得本地和远程访问控制台执行 OSGi 命令来管理组件和服务的生命周期变得可行,解决了场景一提出的问题。但是这种方式下,控制台的访问者使用的还是 equinox 提供的默认控制台的功能,而无法实现必要的自定制功能。因此在实际应用中的作用还是有限的。这就如同场景二提到的情况。

自定义 OSGi 标准的控制台实现类

如果希望自己实现自定制功能的控制台,解决场景二遇到的问题,需要自己实现一个控制台类,并且添加另外一个启动参数。

equinox 提供一个默认的控制台实现,org.eclipse.osgi.framework.internal.core.FrameworkConsole,可以参照这个类来实现自己的控制台。比如你的控制台通常也需要实现 socket 通信,就可以借用 getSocketStream()方法,但如果你想使用的不是 socket 流,那么也可以使其他流或者其他通信方式。建立通信之后,控制台的执行过程都在 console() 这个方法中,执行过程也可以自定制。例如 清单 3 就是自定制的控制台过程,在用户接入之后要求用户输入用户名和密码进行认证。通过认证的用户将会看到 OSGi 提示符,并可以执行 OSGi 的相关操作。


清单 3. 自定制的控制台过程

				
	  protected void console() {    
 // wait to receive commands from console and handle them 
    BufferedReader br = in; 
    //cache the console prompt String 
    String consolePrompt = "\r\n" + ConsoleMsg.CONSOLE_PROMPT; //$NON-NLS-1$ 
    for(int i=0;i<3;i++) 
    { 
        try { 
            out.print("Input your username:"); 
            out.flush(); 
            String user=br.readLine(); 
            out.print("Input your password:"); 
            out.flush(); 
            String pass=br.readLine(); 
            if(!user.contentEquals("admin") || !pass.contentEquals("admin")) 
            { 
                out.print("Your username or password is invalid.Please input again"); 
                out.flush(); 
            } 
            else 
            { 
                break; 
            } 
        } catch (IOException e) { 
        // TODO Auto-generated catch block 
        e.printStackTrace(); 
        } 
 } 
    while (!disconnected()) { 
        out.print(consolePrompt); 
        out.flush(); 

        String cmdline = null; 
        try { 
            if (blockOnready && !useSocketStream) { 
    // bug 40066: avoid waiting on input stream - apparently generates contention with 
    //other native calls 
                try { 
                    while (!br.ready()) 
                    Thread.sleep(300); 
                    cmdline = br.readLine(); 
                } catch (InterruptedException e) { 
        // do nothing; probably got disconnected 
                } 
            } else 
                cmdline = br.readLine(); 
        } catch (IOException ioe) { 
            if (!shutdown) 
                ioe.printStackTrace(out); 
        } 
        if (cmdline == null) { 
            if (!useSocketStream) 
        // better shutdown; the standard console has failed us.  Don't want to get in an 
        //endless loop (bug 212313) (bug 226053) 
                shutdown(); 
            break; 
        } 
        if (!shutdown) 
            docommand(cmdline); 
    } 
 } 
			

 

控制台类编写好之后,如何告诉 equinox 加载这个类作为控制台的实现呢?可以通过设置 osgi.consoleClass 这个参数来指定控制台类。这个参数的内容就是你自定义的控制台类的路径。这个启动参数的添加方法和前面提到过的 osgi.console 的三种设定方式基本相同,除了不能在 web.xml 文件的 servlet 启动参数中添加外,其他的两种方式都可以用来添加这个参数。

例如: osgi.consoleClass=org.myConsole.myFrameworkConsole

添加这个参数之后,还有最后一个问题。控制台的实现类是需要和 OSGi 的核心实现 bundle 即org.eclipse.osgi_3.4.0.v20080605-1900.jar启动时一起加载到系统中的,当核心 bundle 启动的时候,其他所有 bundle 都没有加载,只有等核心 bundle 启动之后才会去加载其他 bundle。这样一来,如果自己的控制台实现类打包在一个普通的 bundle 中,那核心 bundle 启动的时候是加载不到这个类的。解决方法就是创建一个 fragment 的 bundle,此 bundle 的 Fragment-Host设置为 org.eclipse.osgi;bundle-version="3.4.2",即作为核心 bundle 的附属,这样此 bundle 将和核心 bundle 一起启动,这样自定义的类就可以被核心 bundle 加载到了。注意:需要把包含控制台实现类的包导出以便其他 bundle 来引用。

fragment bundle 的 manifest.mf 文件中重要的几个属性:

Fragment-Host: org.eclipse.osgi;bundle-version="3.4.2"

Export-Package: myconsole

注意,自定义的控制台实现类所在的包需要添加到 Export-Package中,才能在启动过程中被加载。

对于 fragment bundle 的开发就不再给出详细步骤了,可以参考本文提供的范例程序,或者联系作者进行讨论。

自定义 OSGi 标准的命令执行器

至此我们已经讨论了场景一和场景二中提到的限制的解决方案。但这些方式使用的控制台也存在很明显的不利。首先,远程管理和控制中至关重要的一个环节是安全方面的访问控制,通过 socket 通信来进行访问控制将引入附加的安全机制,这种安全机制和系统本身固有的安全机制无法联系,导致系统冗余;其次,无论使用上面描述的任何一种方式,都需要添加 OSGi 启动参数,需要对原有配置做修改,这对于在既有项目中嵌入控制台服务来说,无疑会造成不便,就如同场景三所描述的情况。

我们可以换一种角度来看控制台。控制台的本质,就是接受用户的输入,解析输入,执行命令,然后返回操作结果,完成于用户的一次交互。因此我们需要的仅仅是一个通信方式和一个命令解析器。

为了避免使用 socket 通信带来的安全问题以及添加启动参数的不便,需要采用其他的远程通信方式。而 http 协议时目前 web 应用程序中最常用的通信协议,基于通信 http 协议的 servlet 技术也比较成熟,因此我们这里使用基于 HTTP 协议的 servlet 请求方式来传递用户请求。用户请求到达之后,servlet 要做的就是解释并执行请求。OSGi 标准提供了两个接口,org.eclipse.osgi.framework.console.CommandInterpreter和 org.eclipse.osgi.framework.console.CommandProvider。实现 CommandProvider 接口的类负责提供 OSGi 可执行的命令,实现这个接口必须实现的方法是 public String getHelp(),此方法用来提供命令帮助,此外还可以自定义一系列以下划线“_”开头的方法,用来提供具体的命令和命令的具体操作,下划线后面即为命令名称。命令解释器 CommandInterpreter 就是把用户输入和这里的名称匹配从而找到命令相对应的方法并执行。

equinox 默认提供了这两个接口的实现类,分别是 org.eclipse.osgi.framework.internal.core.FrameworkCommandProvider以及 org.eclipse.osgi.framework.internal.core.FrameworkCommandInterpreter。 为了实现自定制的功能,用户也可以自己编写这两个实现类。 FrameworkCommandProvider 提供了大部分常用的操作,所以一般情况下可以直接使用这个类,如果需要添加自定义操作,可以通过继承这个类来扩展;但由于 FrameworkCommandInterpreter 中命令执行结果输出的流已经绑定为 FrameworkConsole 中的输出流了,所以要自己实现一个 CommandInterpreter。

首先,编写用于和用户交互的 ConsoleServlet,负责接收用户输入和将响应结果返回给用户。在这个应用场景中,用户通过 web 应用程序经行组件和服务的生命周期管理,所提交的一系列命令都会被提交到这个 servlet 中进行处理,如清单 4 所示。


清单 4. 提交命令

				
	 public class ConsoleServlet extends HttpServlet { 
     BundleContext bc; 
     Bundle[] b; 

     public ConsoleServlet() 
     {        
         bc=InternalPlatform.getDefault().getBundleContext(); 
     } 
     public void doGet(HttpServletRequest request, HttpServletResponse response)   
        throws ServletException, IOException 
     {   
         this.doPost(request, response);   
     }   
     public void doPost(HttpServletRequest request, HttpServletResponse response) 
	 throws ServletException, IOException   
     { 
          BufferedReader br=request.getReader(); 
          String command=br.readLine(); 
          System.out.println(command); 
          response.setContentType("text/html");   
          PrintWriter out = response.getWriter();   
          HttpServletConsole.setOutStream(out);// 设定 HttpServletConsole 使用的输出流
          HttpServletConsole.docmd(command); 
          for(EventObject eo:Activator.eventInfoMap.keySet()) 
          { 
             out.println(Activator.eventInfoMap.get(eo)+"\n"); 
             System.out.println(Activator.eventInfoMap.get(eo)+"\n"); 
          } 
          Activator.eventInfoMap.clear(); 
     } 
 } 
			

 

然后,编写控制台类 HttpServletConsole。这个类中最关键的方法是如何将用户的输入交给命令解释器去解释,这个方法就是 docmd(),如清单 5 所示。


清单 5. docmd() 方法

				
	  /** The OSGi Command Provider */ 
 protected final CommandProvider osgicp = new FrameworkCommandProvider(osgi).intialize(); 
 /** A tracker containing the service object of all registered command providers */ 
 protected static final ServiceTracker cptracker 
           = new ServiceTracker(context, CommandProvider.class.getName(), null); 
 /** 
 *  Process the args on the command line. 
 *  This method invokes a CommandInterpreter to do the actual work. 
 * 
 *  @param cmdline a string containing the command line arguments 
 */ 
 public static void docmd(String cmdline) { 
     cptracker.open();    
     if (cmdline != null && cmdline.length() > 0) { 
          CommandInterpreter intcp 
	             = new ConsoleCommandInterpreter(cmdline, myGetServices(), out); 
          String command = intcp.nextArgument(); 
         if (command != null) { 
              intcp.execute(command); 
          } 
     } 
 } 

 /** 
 * Return an array of service objects for all services 
 * being tracked by this ServiceTracker object. 
 * 
 * The array is sorted primarily by descending Service Ranking and 
 * secondarily by ascending Service ID. 
 * 
 * @return Array of service objects; if no service 
 * are being tracked then an empty array is returned 
 */ 
 public static Object[] myGetServices() { 
     ServiceReference[] serviceRefs = cptracker.getServiceReferences(); 
     if (serviceRefs == null) 
         return new Object[0]; 
     Util.dsort(serviceRefs, 0, serviceRefs.length); 

     Object[] serviceObjects = new Object[serviceRefs.length]; 
     for (int i = 0; i < serviceRefs.length; i++) 
         serviceObjects[i] = context.getService(serviceRefs[i]); 
     return serviceObjects; 
 } 

 // 
 public ConsoleCommandInterpreter(String cmdline,
                                 Object[] commandProviders,PrintWriter out) 
 { 
     tok = new StringTokenizer(cmdline); 
     this.commandProviders = commandProviders; 
     this.out = out; 
 } 
			

 

ConsoleCommandInterpreter 是自定义的 CommandInterpreter 实现,在这个类的构造方法中,需要将输出流作为构造方法的参数传入,这样命令的执行结果才能够返回到用户那里。同时,通过 osgi 的 API 获取到了提供命令的服务的类,这些类都是 CommandProvider 的实现,都可以提供执行特定操作的命令,这些类的对象也作为构造方法的参数传入命令解释器。自定义命令解释器的其他部分与 ConsoleCommandInterpreter 类似,可以参考本文的范例代码。 整个过程的流程可以概括成:

  • 用户提交命令请求
  • ConsoleServlet 接受请求,解析命令文本
  • 把命令文本和提供命令的类交给 ConsoleCommandInterpreter
  • 执行命令并且把结果返回指定输出流

至此,所有由用户发起的请求都可以得到响应了。但是由于 http 协议的“请求 - 应答”模型的限制,一般来说服务器端无法主动的吧信息“推送”到客户端呈现给用户。为了保证服务器端的状态可以尽量及时的反映到客户端上,通常采用 http 请求“轮询”的方法,即每隔固定的时间段就发送一次请求,更新一次状态,造成客户端状态和服务器端实时同步的用户体验。那么如果服务器端系统内部发生了某些事件,这些信息又如何让用户获知呢?我们可以通过 OSGi 的事件监听机制来获取系统内部事件信息,再通过轮询方式让这些信息尽量及时的呈现给用户。

OSGi 提供了三种基本 listener 接口,分别是 BundleListener,,FrameworkListener,ServiceListener。当这几种 listener 监听的事件发生时,它们会执行相应的方法来处理事件信息。我们可以自己编写一个类实现这三个接口,如 清单 6 所示:


清单 6. 实现接口

				
 public class Activator implements BundleActivator, 
		 BundleListener, FrameworkListener, ServiceListener 
 { 
 public static BundleContext context; 
 static Vector vec; 
 public static boolean isBundleChanged=true; 
 public static Hashtable<EventObject,String> eventInfoMap 
	                       = new Hashtable<EventObject,String>(); 

 public void start(BundleContext context) throws Exception 
 { 
     Activator.context=context; 
     Bundle[] bundles=context.getBundles(); 
     context.addBundleListener(this); 
     context.addFrameworkListener(this); 
     context.addServiceListener(this); 

     public void bundleChanged(BundleEvent event) 
     { 
         isBundleChanged=true; 
         int causedBy=event.getType(); 
         String CausedBy=""; 
         switch(causedBy) 
         { 
              case 1: 
                  CausedBy="has been installed."; 
                  break; 
              case 2: 
                  CausedBy="has been started."; 
                  break; 
              case 4: 
                  CausedBy="has been stopped."; 
                  break; 
              case 8: 
                  CausedBy="has been updated."; 
                  break; 
              case 16: 
                  CausedBy="has been uninstalled."; 
                  break; 
              case 32: 
                  CausedBy="has been resolved."; 
                  break; 
              case 64: 
                  CausedBy="has been unresolved."; 
                  break; 
              case 128: 
                  CausedBy="is about to be activated."; 
                  break; 
              case 256: 
                  CausedBy="is about to deactivated."; 
                  break; 
              case 512: 
                  CausedBy="will be lazily activated."; 
                  break; 
         } 
         Bundle eventBundle=event.getBundle(); 
         String bundleEventInfo=String.valueOf(eventBundle.getBundleId())+": "+ 
         eventBundle.getSymbolicName()+" "+CausedBy; 
         eventInfoMap.put(event , bundleEventInfo); 
     } 

     public void frameworkEvent(FrameworkEvent event) 
     { 
         int causedBy=event.getType(); 
          String CausedBy=""; 
          switch(causedBy) 
          { 
              case 1: 
                  CausedBy="The Framework has started. "; 
                  break; 
              case 2: 
                  CausedBy="An error has occurred. "; 
                  break; 
              case 4: 
                  CausedBy="A PackageAdmin.refreshPackage operation has completed. "; 
                  break; 
              case 8: 
                  CausedBy="A StartLevel.setStartLevel operation has completed. "; 
                  break; 
              case 16: 
                  CausedBy="A warning has occurred."; 
                  break; 
              case 32: 
                  CausedBy="An informational event has occurred. "; 
                  break; 
          } 
          Bundle eventBundle=event.getBundle(); 
          String bundleEventInfo=CausedBy+" Caused by bundle "+ 
     String.valueOf(eventBundle.getBundleId())+": "+ 
     eventBundle.getSymbolicName(); 
     eventInfoMap.put(event, event.toString()); 
     } 
     public void serviceChanged(ServiceEvent event) 
     { 
         int causedBy=event.getType(); 
         String CausedBy=""; 
         switch(causedBy) 
         { 
             case 1: 
                  CausedBy=" has been registered. "; 
                  break; 
              case 2: 
                  CausedBy="'s properties have been modified. "; 
                  break; 
              case 4: 
                  CausedBy=" is in the process of being unregistered. "; 
                  break; 
         } 
          String bundleEventInfo=event.toString()+CausedBy; 
          eventInfoMap.put(event, bundleEventInfo); 
     } 
 /* 
 * (non-Javadoc) 
 * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) 
 */ 
     public void stop(BundleContext context) throws Exception 
     { 
         System.out .print(eventInfoMap); 
     } 
 } 
	 

 

这样,所有的时间的信息都会被缓存在 eventInfoMap 这个 map 中,客户端可以设定间隔固定的时间段发起一次请求,获取此 map 中的信息呈现给客户。

至此,控制台的基本功能就都实现了。

这种方式实现的控制台有很多优良特性:首先,基于 servlet 的通信机制,有很成熟的安全访问控制机制,比如 spring security 等;其次,servlet 基于 http 协议,可以方便的集成到 web 应用程序中;第三,此控制台本身也是插件,可以不添加任何启动参数,无缝的集成到系统中提供远程控制台访问,有效的管理 OSGi 系统中的组件和服务。这些特性可以很好的解决场景三中提到的限制和不足。

然而,这种方式的控制台也存在明显的不足。受到 HTTP 通信协议请求应答模型的限制,这种插件式控制台只是一个命令执行器,虽然能够通过各种事件来反应系统信息,但无法把系统的日志信息完整的呈现给用户。这也是这种方式的最大不足,是和原生控制台的最大差距所在。

 

总结

探索自定制 OSGi 控制台的意义

通过上面的探索,我们针对三种应用场景由浅入深的提出了三种解决方案。三种实现方式中,与控制台原有系统的耦合度是依次递减的:第一种方式直接通过添加启动参数激活系统默认控制台的 socket 通信功能;第二种方式是通过启动参数添加自定义的控制台实现类;第三种则是干脆抛开系统控制台规则,自己接收、解释和执行命令,并反馈执行结果给用户。

这三种方式中各自有针对的应用场景,都是为了满足某一应用场景的需求,绝非原生控制台的代替品,也不是 OSGi 控制台的最佳实践。因此在选择这三种控制台的实现方式之前需要认真分析系统的应用场景,选择出最合适的解决方案

 

参考资料

作者简介

李雪锋,北京交通大学在读硕士研究生,在 IBM CDL Optim 开发团队实习。对 java 技术、web 开发、OSGi 以及云计算技术有浓厚的兴趣。

朱志辉,拥有清华大学硕士学位,目前供职于 IBM CDL Optim 开发团队,从事软件开发多年,精通 J2EE 和 OSGI 开发。

posted @ 2012-03-04 00:31  weep  阅读(220)  评论(0编辑  收藏  举报