分享
 
 
 

[引用] 为 Eclipse 插件添加日志框架

王朝java/jsp·作者佚名  2006-01-09
窄屏简体版  字體: |||超大  

为什么要采用日志?

良好的开发人员都知道精心设计、测试和调试的重要性。虽然 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 配置顺序图

对于这个 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 中实现的。为了显示它是如何工作的,我创建了两个插件:

HelloPlugin 是从一个项目模板中构建出来的,它显示一个消息对话框,其中显示 "Hello, Eclipse world"。

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 插件目录中包含该文件。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有