内容


为 Eclipse 插件添加日志框架

两种增强 Eclipse 日志功能的方法

为什么要采用日志?

良好的开发人员都知道精心设计、测试和调试的重要性。虽然 Eclipse 可以帮助开发人员实现这些任务,但是它怎样处理日志呢?很多开发人员相信对于良好的软件开发实践来说,日志是不可或缺的一部分。如果您曾经修正过他人部署过的程序,您无疑也会同意这一点。幸运的是,日志对于性能的影响很小,大部分情况下甚至根本不会对性能产生任何影响,而且由于日志工具非常简单易用,因此学习曲线也非常平滑。因此,对于现有的优秀工具,我们没有理由不在应用程序中添加日志功能。

可以使用的工具

如果您正在编写一个 Eclipse 插件,那么您可以使用 org.eclipse.core.runtime.ILog 所提供的服务,它可以通过 Plug 类的 getLog() 方法进行访问。只需要使用正确的信息创建一个 org.eclipse.core.runtime.Status 的实例,并调用 ILog 的 log() 方法即可。

这个日志对象可以接收多个日志监听器实例。Eclipse 添加了两个监听器:

  • 一个监听器向 "Error Log(错误日志)" 视图中写入日志。
  • 一个监听器向位于 “${workspace}/.metadata/.log" 的日志文件中写入日志。

您也可以创建自己的日志监听器,只需实现 org.eclipse.core.runtime.ILogListener 接口并使用 addLogListener() 方法将其添加到日志对象中即可。这样,每个日志事件都可以调用这个类的 logging() 方法。

虽然所有的内容都非常简单,但是这种方法存在一些问题。如果您希望修改一个已部署好的插件目标,那么应该如何处理?或者说要如何控制记录下来的日志信息的数量?还有,这种实现可能会对性能造成影响,因为它总是要向所有的监听器发送日志事件。这就是为什么我们通常只在极端的情况(例如错误条件)中才会看到要记录日志的原因。

另一方面,还有两个专门用于日志的杰出的工具。一个来自 Java 2 SDK 1.4 的 java.util.logging 包;另外一个来自 Apache,名为 Log4j。

这两个工具都采用了日志对象的层次结构的概念,都可以将日志事件发送到任意数目的处理程序(Handler,在 Log4j 中称为 Appender)中,它代表了发送给格式化程序(Formatter,在 Log4j 中称为 Layout)进行格式化的消息。这两个工具都可以通过属性文件进行配置。 Log4j 还可以使用 xml 文件进行配置。

记录器可以有一个名称并与某一级别相关联。记录器可以继承父母的设置(级别,处理程序)。名为“org”的记录器会自动成为另外一个名为“org.eclipse” 的记录器的父母;因此不管您在配置文件中怎样对“org”进行设置,这些设置都可以被“org.eclipse”记录器继承。

我更喜欢哪一个工具?这两个工具我都曾经用过,不过我比较喜欢 Log4j。只有在非常简单的程序中我才使用 java.util.logging,我并不想在这样的程序中添加 log4j.jar。关于这两个工具的详细介绍,请参阅 Java 文档和 Apache 的站点(请参阅 参考资料中的链接)。

一种改进的日志

如果存在改进 Eclipse 日志体验的方法,那不是很棒吗?但这样做有两个问题:

  • 缺少外部配置文件。
  • 性能问题,同时还有缺乏对日志行为进行细粒度控制。

给出这个难题之后,我开始考虑将日志工具集成到 Eclipse 中的方法。我可以使用的第一个选择是 java.util.logging,原因非常简单:在 JSDK1.4 发行版中已经包含了这个包。

我想采用一个编辑器,通过配置文件对日志行为进行定制,从而允许将日志事件发送到任何可用的处理程序中。我计划另外创建两个处理程序:一个负责将日志事件发送到“Error Log”视图中,另外一个将日志写入插件所在的位置:“${workspace}/.metadata/.plugins/${plugin.name}"。

所有的内容都将包含在一个日志管理器插件(Plug-in Log Manager)中。您只能将其加入插件从属关系中,并从中获得日志对象。

然而,根据我的经验,我不推荐使用 java.util.logging 来实现这项功能。因为实现的代码将很长,而且只能保留一个 LogManager 实例;它使用系统类装载程序来达到这个目的。这样,所有的用户只有一个层次结构,您会失去隔离性。因此,如果很多应用程序都在使用这个记录器,那么它们将共享设置,一个应用程序的记录器实例可以继承其他应用程序记录器的设置。

既然如此,为什么我们不对 LogManager 进行扩充,并自己实现一个记录器呢?这种方法的问题是 LogManager 实例使用了系统类的装载程序从配置文件中对类进行实例化。这种插件的优点之一是通过使用不同的类装载程序提供隔离性。如果您的日志管理程序需要隔离性,那么由于架构的限制, java.util.logging 可能不适合您的要求。

另一方面,Log4j 已经证明是非常有用的。不管您相信与否,Log4j 的记录器的层次结构保留在一个称为 Hierarchy 的对象中。因此,您可以为每个插件都创建一个层次结构,这样问题就解决了。您还可以创建一个定制的 appender (处理程序)将事件发送给 "Error Log" 视图,再创建一个将事件发送到插件所在的位置。这样生活就变得美好起来了。

现在让我们回顾一下整个过程是如何实现的,我们从插件编辑器的角度入手,创建插件,并将 com.tools.logging 添加到从属类型列表中,然后创建一个 Log4j 配置文件。对 PluginLogManager 进行实例化,并使用配置文件对其进行配置。由于这个过程只需要做一次,因此您只需要在启动插件时执行这项操作即可。对于日志语句,只需像在 Log4j 中那样使用它即可。 清单 1 给出了一个例子:

清单 1. TestPlugin 插件类中 PluginLogManager 的配置
private static final String LOG_PROPERTIES_FILE = "logger.properties";
public void start(BundleContext context) throws Exception {
   super.start(context);
   configure();
}
private void configure() {
   try {
      URL url = getBundle().getEntry("/" + LOG_PROPERTIES_FILE);
      InputStream propertiesInputStream = url.openStream();
      if (propertiesInputStream != null) {
         Properties props = new Properties();
         props.load(propertiesInputStream);
         propertiesInputStream.close();
         this.logManager = new PluginLogManager(this, props);
         this.logManager.hookPlugin(
          TestPlugin.getDefault().getBundle().getSymbolicName(),
          TestPlugin.getDefault().getLog()); 
      }	
   } 
   catch (Exception e) {
      String message = "Error while initializing log properties." +
                       e.getMessage();
      IStatus status = new Status(IStatus.ERROR,
      getDefault().getBundle().getSymbolicName(),
      IStatus.ERROR, message, e);
      getLog().log(status);
      throw new RuntimeException(
           "Error while initializing log properties.",e);
   }         
}

无论在何时部署插件,都只需要修改日志配置文件和日志过滤条件,或者修改其输出,而不需要修改任何代码。更好的一点是,如果日志被禁用,那么所有的语句都不会影响性能,因为性能是 Log4j 设计的主要考虑因素之一。因此您可以在任何必要的地方采用这种记录器的方法。

如何实现

对于 com.tools.logging 的使用,我们就谈这么多;现在让我们来看一下其实现。

首先来看一下类 PluginLogManager。每个插件都有一个日志管理器。该管理器包含一个 hierarchy 对象,以及定制 appenders 所需的数据,如清单 2 所示。该对象并非直接源自于 Hierarchy 对象,因此不便将它暴露给最终用户。它在实现方面提供了更多的自由。构造函数使用默认的 DEBUG 级别创建一个 hierarchy 对象,然后使用提供的属性对其进行配置。它还可以简单地使用 xml 属性;只有对于对 Xerces 插件添加从属性并使用 DOMConfigurator 而不是 PropertyConfigurator 才是必要的。这部分内容留给读者作为练习。

清单 2. PluginLogManager 构造函数
public PluginLogManager(Plugin plugin,Properties properties) {
   this.log = plugin.getLog();  
   this.stateLocation = plugin.getStateLocation(); 
   this.hierarchy = new Hierarchy(new RootCategory(Level.DEBUG));
   this.hierarchy.addHierarchyEventListener(new PluginEventListener());
   new PropertyConfigurator().doConfigure(properties,this.hierarchy);	
   LoggingPlugin.getDefault().addLogManager(this); 
}

注意 PluginLogManager 内部类是如何实现 org.apache.log4j.spi.HierarchyEventListener 的。这是向定制的 appender 传递必要信息的一种解决方案。在已经对 appender 进行实例化和完整配置并准备添加它时,会调用 addAppenderEvent() 方法,如清单 3 所示:

清单 3. PluginEventListener 类
private class PluginEventListener implements HierarchyEventListener {
		
   public void addAppenderEvent(Category cat, Appender appender) {
      if (appender instanceof PluginLogAppender) {
         ((PluginLogAppender)appender).setLog(log);
      }			
      if (appender instanceof PluginFileAppender) {
         ((PluginFileAppender)appender).setStateLocation(stateLocation);
      }
   }
	
   public void removeAppenderEvent(Category cat, Appender appender) {
   }
}

为了更好地理解 appender 的生命周期以及一些决定,可以使用 UML 顺序图(UML Sequence Diagram)。图 1 显示了创建和配置 PluginFileAppender 实例的事件顺序。

Figure 1. PluginFileAppender 配置顺序图
PluginFileAppender 配置顺序图
PluginFileAppender 配置顺序图

对于这个 appender 来说,我们对 org.apache.log4j.RollingFileAppender 进行了扩展。这不但允许您自由对文件进行操作,而且还提供了很多有用特性,例如文件大小的上限;当达到文件上限时,日志自动重叠写入另一个文件。

通过选择对 RollingFileAppender 进行扩展,您还需要对其行为进行正确处理。当 Log4j 创建 appender 之后,就会调用“setter”方法从配置文件中对其属性进行初始化,然后调用 activateOptions() 方法让附加程序完成未完成的任何初始化操作。在进行这项操作时, RollingFileAppender 实例会调用 setFile() ,它将打开日志文件并准备好写入日志。只有此时 Log4j 才会通知 PluginEventListener 实例。

显然,在有机会设置插件位置前,您不能打开文件。因此当调用 activateOptions() 时,如果还没有位置信息,就会被标记为未决的;当最后设置位置信息时,会再次调用该方法,此时 appender 就准备好,可以使用了。

另外一个 appender PluginLogAppender 的生命周期相同,不过由于它并没有对现有的 appender 进行扩展,因此您不必担心初始化的问题。appender 在 addAppenderEvent 方法被调用之前不会启动。Log4j 文档对如何编写定制 appender 进行了详细的讨论。清单 4 给出了 append 方法。

清单 4. PluginLogAppender 的 append 方法
public void append(LoggingEvent event) {
		
   if (this.layout == null) {
      this.errorHandler.error("Missing layout for appender " +
             this.name,null,ErrorCode.MISSING_LAYOUT); 
      return;
   }
   String text = this.layout.format(event);
   Throwable thrown = null;
   if (this.layout.ignoresThrowable()) {
      ThrowableInformation info = event.getThrowableInformation();
      if (info != null)
         thrown = info.getThrowable(); 
   }
		
   Level level = event.getLevel();
   int severity = Status.OK;
   if (level.toInt() >= Level.ERROR_INT) 
      severity = Status.ERROR;
   else
   if (level.toInt() >= Level.WARN_INT)
      severity = Status.WARNING;
   else
   if (level.toInt() >= Level.DEBUG_INT) 
      severity = Status.INFO;
	
   this.pluginLog.log(new Status(severity,
             this.pluginLog.getBundle().getSymbolicName(),
             level.toInt(),text,thrown));
}

LoggingPlugin 类维护了 PluginLogManagers 的一个列表。这是必需的,这样,在插件停止时,就可以关闭该插件的所有层次结构,并正确删除 appender 和记录器,如清单 5 所示。

清单 5. LoggingPlugin 类处理日志管理器
private ArrayList logManagers = new ArrayList(); 
public void stop(BundleContext context) throws Exception {
   synchronized (this.logManagers) {
      Iterator it = this.logManagers.iterator();
      while (it.hasNext()) {
         PluginLogManager logManager = (PluginLogManager) it.next();
         logManager.internalShutdown(); 
      }
     this.logManagers.clear(); 
   }
   super.stop(context);
}
void addLogManager(PluginLogManager logManager) {
   synchronized (this.logManagers) {
      if (logManager != null)
         this.logManagers.add(logManager); 
   }
}
	
void removeLogManager(PluginLogManager logManager) {
   synchronized (this.logManagers) {
      if (logManager != null)
         this.logManagers.remove(logManager); 
   }
}

插入 PluginLogManager 类的内容有很多。有时您所从属的插件,特别是那些从属于 workbench 的插件,可能引发异常。这些异常通常都会被 Eclipse 记录到日志中。允许将从属插件(dependent plug-in)插入日志框架中,这非常有用。在触发异常时,Eclipse 要记录的所有日志都会被放入日志框架,它与其他记录器共享配置文件。这种方法非常有用,因为这样可以将所有的内容都集中在一个位置上,并可以保留一个事实的历史样本,从而有助于修正应用程序的问题。

这可以通过实现 org.eclipse.core.runtime.ILogListener 并将其添加到从属插件的 ILog 实例中实现。基本上,您只需要将其与 Eclipse 的日志相关联。然后,这种实现就可以将所有的请求都重定向到一个使用您选择的名字(通常是一个插件标识符)创建的记录器中。然后您可以通过相同的配置文件对输出结果进行配置;只需指定记录器的名字、设置过滤条件、添加 appender 即可。该类如清单 6 所示:

清单 6. PluginLogListener 类
class PluginLogListener implements ILogListener {
   private ILog log;
   private Logger logger;
   PluginLogListener(ILog log,Logger logger) {
      this.log = log;
      this.logger = logger;
      log.addLogListener(this);
   }
   void dispose() {
      if (this.log != null) {
         this.log.removeLogListener(this);
         this.log = null;
         this.logger = null;
      } 
   }
   public void logging(IStatus status, String plugin) {
      if (null == this.logger || null == status) 
         return;
	
      int severity = status.getSeverity();
      Level level = Level.DEBUG;  
      if (severity == Status.ERROR)
         level = Level.ERROR;
      else
      if (severity == Status.WARNING)
         level = Level.WARN;
      else
      if (severity == Status.INFO)
         level = Level.INFO;
      else
      if (severity == Status.CANCEL)
         level = Level.FATAL;
      plugin = formatText(plugin);
      String statusPlugin = formatText(status.getPlugin());
      String statusMessage = formatText(status.getMessage());
      StringBuffer message = new StringBuffer();
      if (plugin != null) {
         message.append(plugin);
         message.append(" - ");
      }    
      if (statusPlugin != null && 
           (plugin == null || !statusPlugin.equals(plugin))) {
         message.append(statusPlugin);
         message.append(" - ");
      }	
      message.append(status.getCode());
      if (statusMessage != null) {
         message.append(" - ");
         message.append(statusMessage);
      } 		
      this.logger.log(level,message.toString(),status.getException());	
   }
   
   static private String formatText(String text) {
      if (text != null) {
         text = text.trim();
         if (text.length() == 0) return null;
      } 
      return text;
   }
}

整个框架是在一个插件项目 com.tools.logging 中实现的。为了显示它是如何工作的,我创建了两个插件:

  1. HelloPlugin是从一个项目模板中构建出来的,它显示一个消息对话框,其中显示 "Hello, Eclipse world"。
  2. TestPluginLog 作为一个与 HelloPlugin 的一个从属插件添加的,因此它可以被勾挂在相同的日志级别中。它有一个方法 dummyCall() ,可以使用 Eclipse API 添加一条假消息,然后它会被重定向到 HelloPlugin 的日志中。

其他插件的从属类型都已经设置好了,例如 org.eclipse.ui 或 org.eclipse.core.runtime。

为了显示 logger.properties 配置文件的强大功能,在创建该文件时我非常小心。正如您在清单 7 中看到的一样,我们定义了两个 appender: appender A1 是一个 PluginFileAppender 类,它被分配给根记录器。其他记录器都是从这个根记录器继承而来,都将使用这个 appender。因此,所有的日志,包括来自 TestPluginLog 插件的日志,都被写入一个位于插件所在位置的文件中。

清单 7. HelloPlugin 项目中的 Logger.properties 文件
log4j.rootCategory=, A1
# A1 is set to be a PluginFileAppender
log4j.appender.A1=com.tools.logging.PluginFileAppender
log4j.appender.A1.File=helloplugin.log
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%p %t %c - %m%n
# A2 is set to be a PluginLogAppender
log4j.appender.A2=com.tools.logging.PluginLogAppender
log4j.appender.A2.layout=org.apache.log4j.PatternLayout
log4j.appender.A2.layout.ConversionPattern=%p %t %c - %m%n
# add appender A2 to helloplugin level only
log4j.logger.helloplugin=, A2

另外一个 appender 是 A2,它是一个 PluginLogAppender 类,只能将它添加到记录器 "helloplugin" 中,因此 TestPluginLog 没有使用它。否则,在 "Error View" 窗口中 "TestPluginLog" 就会有两项:一个来自于 Eclipse,另外一个来自于 com.tools.logging。您可以自己做个实验,然后就会明白我的意思了。只需将 A2 添加到 log4j.rootCategory 中并删除 log4j.logger.helloplugin 所在的那个行即可。

清单 8 显示了在点击 "sample menu" 并显示消息框之后 ${workspace}/.metadata/.plugins/HelloPlugin/helloplugin.log 的内容。注意 TestPluginLog Eclipse 日志是如何写入最后一行中的。通过将您自己的日志和 Eclipse 插件日志写入一个输出文件中,可以保留日志事件的序列。

清单 8. helloplugin.log
INFO main helloplugin.actions.SampleAction - starting constructor.
INFO main helloplugin.actions.SampleAction - ending constructor.
WARN main helloplugin.actions.SampleAction - init
WARN main helloplugin.actions.SampleAction - run method
WARN main TestPluginLog - TestPluginLog - 0 - Logging using the Eclipse API.

结束语

本文介绍了两种改进 Eclipse 日志功能的方法。一种方法是在插件中使用 com.tools.logging,这样就可以使用 Log4j 中所有有用的特性;如果您愿意的话,它依将是 Eclipse 日志框架的一部分。另外一种方法与一个插件相关,该插件并不了解 Log4j,但即时只使用 Eclipse 日志 API,也可以对其日志输出进行配置。

实际上,您并不需要使用 com.tools.logging。现在,您可以展开示例代码,并将其作为一个单独的 jar 文件加入您自己的插件中。当然,不要忘记了 Log4j 的 jar 文件。

插件是使用新的 OSGI 创建的。所有的代码都是使用 Eclipse 3.0 Release Candidate 1、Sun Java 2 SDK 1.4.2 和 Log4j 1.2.8 进行开发的,并在这些环境中进行了测试。在可以下载的代码中,不包括 log4j-1.2.8.jar 文件。如果您要下载这些代码,应该从 Apache 的 Log4j 中获得这个 jar 文件,并在 com.tools.logging 项目和 com.tools.logging_1.0.0 插件目录中包含该文件。


下载资源


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=23240
ArticleTitle=为 Eclipse 插件添加日志框架
publish-date=10182004