About the Author
null
Spotlight Features

The Rich Engineering Heritage Behind Dependency Injection

Andrew McVeigh takes us on a tour of the rich heritage behind dependency injection, what it represents, and tells us why its here to stay.

Java, the OLPC, and community responsibility

The "One Laptop Per Child" project has a great device ready to ship, but there's no Java on there. Let's think about working together to put Java on OLPC!

Universal Logger Plug-ins for RCP Applications

When building an RCP application, you may need to include a third party code library that uses a common logger API to generate log messages. It may not be practical to modify the library to use the Eclipse ILog API. Therefore, an implementation of the logger library must be incorporated into your application.

One approach is to embed a logger library into the plug-ins that use it. This approach creates some problems especially when plug-ins from multiple contributors are brought together into a single RCP application:

First, memory and disk usage is unnecessarily consumed. Memory is used due to a copy of the library in each plug-in's classloader. The library's static data, such as configuration, is not shared. Multiple copies overuse disk space, and application download time is affected, too.

Second, there is opportunity for confusion at runtime. For example, a LOG4J Appender instantiated by one plug-in will not be usable by another plug-in's LOG4J embbeded library. This occurrence will generate a VM exception. Also, to resolve import directives, Eclipse will non-deterministically select any plug-in that exports the logger packages.

Third, plug-ins that embed a logger library pre-empt an RCP application developer's logger decision. Requirements and design goals of the application, not the plug-in, should drive the decision as to which logger framework should be used. If contributing plug-ins include their own copy of a framework, they have configured logging too early. RCP application deployers should be free to decide which framework to use at packaging time, without a dependency on the plug-in developer's decisions. However, without available logger plug-ins, contributing plug-in developers are forced to make this decision. If logger facade plug-ins were available, plug-in developers would assume that the deployers can get the logging framework they choose.

Fourth, in order to create an extensible LOG4J plug-in, Eclipse Classloading buddy policy must be utilized. If not, then Appenders provided by later plug-ins will not be discovered. In order to extend JDK1.4 logging, the Handler implementation must be instantiated outside of LogManager and bound to Loggers. Otherwise, they will not be found. These issues are easy to resolve, but let's solve it once in a single plug-in, not for every plug-in that carries a logger implementation.

This article aims to help RCP application and plug-in developers avoid the above problems. A group of logger plug-ins are presented and constructed. These plug-ins can become part of every plug-in developers toolbox. The article considers Apache's Logging LOG4J, Sun's JDK 1.4 Logging, Eclipse logging framework using org.eclipse.core.runtime.ILog, Apache's Jakarta Commons Logging, and the Simple Logging Framework for Java logger libraries, and shows how these can be built into modular plug-ins and be combined in any manner as may be required.

In the first section, creating plug-ins from common logger frameworks and facade libraries will be described. A logger facade plug-in upon a custom logging framework, such as PDE ILog API is developed. Then for LOG4J and the JDK 1.4 Logger frameworks a strategy for controlling logger configuration in a single file is presented. Finally, in a demonstration of the flexibility of these plug-ins, instructions for swapping one plug-in for another is given.

This article is not a tutorial about Eclipse plug-in development. If you are new to Eclipse, you can find better introductory articles elsewhere. However, if you have read those introductory articles and are looking for examples of developing plug-ins, this article can be useful. It contains some examples of plug-in development features, including:

  • creating a code library plugin
  • class loading and setting buddy policy
  • importing vs. requiring
  • extension points

This article assumes that the reader has configured and used a logger framework, either LOG4J or JDK 1.4 Logging, so little explanation of how these work will be provided. The article also assumes the reader has knowledge of logger facade libraries like JCL.

Eclipse 3.1 was used to implement these plug-ins.

Logger Code Library Plug-ins

In this section, we will create the plug-ins for logger frameworks and logger facade API.

Converting a code library into a plug-in is non-destructive. The only difference between a regular code library and a code library plug-in is the plug-in's jar's MANIFEST.MF file contains OSGi directives. You will not have to modify or compile any of the provider's source code. Eclipse does all the work which is to unwrap the provider's distribution jar file and to re-jar the contents with an edited MANIFEST.MF file.

Before you do this for any code library, be sure to read the library's license. Make sure you understand and adhere to the terms of the license.

I did not upload these framework or facade library plug-ins. For the most part, they are exactly identical to their binary distributions. I have provided only the MANIFEST.MF files which you can copy and paste into a plug-in editor as needed. My hope is that the logger framework providers will incorporate these manifest directives into their archive, and so distribute them as ready to use Eclipse plug-ins, and so rendering these next steps an academic exercise.

Logger Frameworks

In this section we will create code library plug-ins for LOG4J and NLOG4J logger frameworks. JDK 1.4 Logging is also a logger framework, but it is not possible to create a plug-in for it because it is already built into the J2SE runtime.

LOG4J Framework Code Library Plug-in

LOG4J is an Apache project and is a popular logger framework.

Here are the steps to create the LOG4J plug-in:

  1. Download and install the LOG4J distribution to your system. Get version 1.2. [You can create a plug-in for LOG4J 1.3 alpha by repeating these instructions.]
  2. In Eclipse, start a new plug-in project: select New->Project->Plug-in Development->Plug-in from existing JAR archive.
  3. In the JAR selection wizard, select Add External and select the LOG4J jar from the install directory: dist/lib/log4j-1.2.12.jar
  4. Enter plugin-properties and click Finish:

A few comments about the value of the properties selected for this plugin: In order to communicate to a plug-in developer that the binary code in this plug-in is completely untouched and identical to the Apache distribution, we do what we can to let it be known that the producer of the library is Apache, not us (all we have done here is provide some META-INF/MANIFEST.MF data fields). We use Apache's version number, attribute Apache's LOG4J team, and even name the plug-in from Apache's java package name.

Eclipse will churn away for a few moments as it explodes the archive into your filesystem. Then, the plug-in editor appears. The Apache log4j license requires us to include the license text when redistributing, so before doing anything else, import the license file from the LOG4J's root distribution folder into the expanded META-INF folder of the plug-in.

Eclipse Classloading and Appenders

Eclipse classloader architecture will normally prevent this LOG4J plug-in from finding logger Handlers in dependent plug-ins. Eclipse's class finding algorithm looks for classes in required and imported plug-ins, and the java runtime, and at the last, it looks for classes in its Buddy plug-ins. In order for LOG4J to find Handlers within its dependents, the LOG4J plug-in must have a Classloading Buddy Policy, and if the policy is 'registered', which we will use here, dependents must declare themselves to be a Buddy of this plug-in. This is the purpose of this line in the MANIFEST.MF file:

Eclipse-BuddyPolicy: registered

Buddy classloading is not without performance cost and not without risk of finding the wrong class with the right name. In the case of the LOG4J plug-in, there will likely be only a handful of Appenders to search for, and only at the framework's configuration time.

Here is the final MANIFEST.MF file for LOG4J.

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Log4j Plug-in
Bundle-SymbolicName: org.apache.log4j
Bundle-Version: 1.2.12
Bundle-ClassPath: .
Bundle-Vendor: Apache Software Foundation
Bundle-Localization: plugin
Export-Package: org.apache.log4j,
 org.apache.log4j.spi
Eclipse-BuddyPolicy: registered

LOG4J contains many more exportable packages. For our purpose here, we don't need any of them so they are not exported.

NLOG4J Code Library Plug-in

NLOG4J is a direct replacement for LOG4J. You can create a plug-in for this library by following almost exactly the same steps as for LOG4J (use different values in the wizard's properties).

Here is the MANIFEST.MF file for NLOG4J.

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: NLOG4J Code Library Plug-in
Bundle-SymbolicName: org.nlog4j
Bundle-Version: 1.2.18
Bundle-ClassPath: .
Bundle-Vendor: slf4j.org
Bundle-Localization: plugin
Export-Package: org.apache.log4j,
 org.apache.log4j.spi,
Eclipse-BuddyPolicy: registered

The NLOG4J archive also contains an implementation of SLF4J, which we cover next. Greater flexibility is gained if we put these in separate plug-ins.

Logger Facade API Code Library

In this section we will create Logging Facade API plug-ins each of which will export both the Jakarta Commons Logging (JCL) and SLF4J logger APIs. If a developer uses either of these API for log messages, his product would be isolated from the logger framework.

We use SLF4J's implementation of JCL in order to avoid confrontation with Apache's JCL's dynamic discovery mechanism which is based on class discovery. I don't know whether Eclipse classloading implementation and Apache's discovery are operable together, but I don't have to. SLF4J provides a simple implementation of the same interface.

These facade plug-ins we create are interchangeable. As a result, a JCL/SLF4J facade plug-in for LOG4J can replace a facade plug-in for JDK1.4 (and vice versa) for example. The logger framework used in an application is determined by which variant of the SLF4J plug-in is installed and enabled.

SLF4J Code Library Plug-in

After you download and install the SLF4J distribution, take a look at the libraries in the install directory.

/
    jcl104-over-slf4j.jar
    slf4j-jdk14.jar
    slf4j-log4j12.jar
    slf4j-log4j13.jar
    slf4j-nop.jar
    slf4j-simple.jar

Create a plug-in for each of the SLF4J variants that you expect to be useful. If you would like to use the NLOG4J variant, you have to extract the code library from the NLOG4J distribution. Do Not bundle more than one slf4j-*.jar file in any plug-in. Create each plug-in in the same steps used in creating the LOG4J plug-in. Remember to include the JCL compatibility library in every one of these plug-ins.

  1. In Eclipse, start a new plug-in project: select New->Project->Plug-in Development->Plug-in from existing JAR archive.
  2. In the JAR selection wizard, select Add External and select the slf4j-* and jcl104-over-slf4j jars.
  3. Click Finish
  4. Copy and paste the corresponding MANIFEST.MF from the text below into the new plug-in's editor, tab MANIFEST.MF.

The plug-in naming convention - naming by the java package name - is broken in this case because slf4j.org produces multiple libraries having the org.slf4j package name. Use the framework name as a discriminator as in the following naming pattern:

  • Use org.slf4j.jdk14 as the name of the slf4j-jdk14 plug-in.
  • Use org.slf4j.log4j12 as the name of the slf4j-log4j12 plug-in.
  • etc...

Note that according to the slf4j.org documentation, LOG4J 1.2 and LOG4J 1.3 alpha have runtime (not buildtime) differences and so separate SLF4J implementations are necessary. As a result, declare a 'required' dependency in these SLF4J plug-ins. 'Required' dependencies can only be satisfied when the named plug-in is installed and enabled at runtime. Using the LOG4J version number in the dependency list will prevent accidental use of the wrong version of LOG4J.

Here are the MANIFEST.MF files for SLF4J plug-ins which were used for prototyping the ideas of this article. Please observe, as in the case for Apache's LOG4J plug-in, names and versions reflect the releases from the provider, as is right and proper.

For LOG4J 1.2:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: SLF4j Log4j 1.2 Code Library Plug-in
Bundle-SymbolicName: org.slf4j.log4j12
Bundle-Version: 1.0.0.rc1
Bundle-ClassPath: .
Bundle-Localization: plugin
Export-Package: org.apache.commons.logging,
 org.apache.commons.logging.impl,
 org.slf4j,
 org.slf4j.impl,
 org.slf4j.spi
Bundle-Vendor: slf4j.org
Require-Bundle: org.apache.log4j;bundle-version="1.2.0"

For JDK1.4 Logging:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: SLF4J Jdk14 Code Library Plug-in
Bundle-SymbolicName: org.slf4j.jdk14
Bundle-Version: 1.0.0.rc1
Bundle-ClassPath: .
Bundle-Localization: plugin
Export-Package: org.apache.commons.logging,
 org.apache.commons.logging.impl,
 org.slf4j,
 org.slf4j.impl,
 org.slf4j.spi
Bundle-Vendor: slf4j.org

For SimpleLog (distributed with SLF4J):

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: SLF4j Simple Code Library Plug-in
Bundle-SymbolicName: org.slf4j.simple
Bundle-Version: 1.0.0.rc1
Bundle-ClassPath: .
Bundle-Vendor: slf4j.org

Bundle-Localization: plugin
Export-Package: org.apache.commons.logging,
 org.apache.commons.logging.impl,
 org.slf4j,
 org.slf4j.impl,
 org.slf4j.spi

For NOP logger (a null logger implementation provided by SLF4J):

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: SLF4j NOP Library Plug-in
Bundle-SymbolicName: org.slf4j.nlog4j
Bundle-Version: 1.0.0.rc1
Bundle-ClassPath: .
Bundle-Vendor: slf4j.org
Bundle-Localization: plugin
Export-Package: org.apache.commons.logging,
 org.apache.commons.logging.impl,
 org.slf4j,
 org.slf4j.impl,
 org.slf4j.spi

For NLOG4J:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: SLF4j NLOG4J Library Plug-in
Bundle-SymbolicName: org.slf4j.nlog4j
Bundle-Version: 1.0.0.rc1
Bundle-ClassPath: .
Bundle-Vendor: slf4j.org
Bundle-Localization: plugin
Export-Package: org.apache.commons.logging,
 org.apache.commons.logging.impl,
 org.slf4j,
 org.slf4j.impl,
 org.slf4j.spi


Custom Logging

Custom logger frameworks can also be wrapped with the SLF4J library. This is done easily. The plug-in discussed here wraps PDE's logger framework. It is available here: slf4j-pde plug-in jar.

To create a new SLF4J Logger, the SLF4J web page directs us to copy SLF4J source code, and implement the following interfaces: org.slf4j.spi.Logger, org.slf4j.spi.LoggerFactory and to provide implementations of org.slf4j.impl.StaticLoggerBinder and org.slf4j.impl.StaticMarkerBinder.

In the custom Logger implementation, each of the log methods will create an IStatus and log it to a plug-in's ILog using the ILog.log(IStatus s) method. They map the Logger level to appropriate IStatus severity.

One problem to resolve: Each plug-in has its own ILog implementation and the right one has to be chosen. For example, the Logger "org.openproject.ui" should send log messages to the org.openproject plug-in's ILog not the SLF4J plug-in's ILog. To resolve, an extension point is defined in the SLF4J plug-in. The org.openproject plug-in will define the extension and SLF4J will use the extension to map to the plug-in's ILog. Here is the extension point definition:

   <extension point="org.slf4j.pde.loggers">
    <loggerMapEntry    regex="^org.openproject.ui\..*" symbolicName="org.openproject.ui"/>
   </extension>

The above extension declaration will map any Logger whose name matches the regular expression "regex" to the ILog of the plug-in whose symbolic name is "symbolicName" field.

There are additional features of this plug-in which can be implemented:

  • The Logger can be sensitive to the plug-ins trace options. Each Logger can map to a debug option and will check its value before sending the IStatus to an ILog. This can make the application code cleaner compared to an inline option check.
  • The Logger can implement SLF4J Marker to provide a code value. Marker is a parameter on the Logger methods and is described as a decoration. Code is a property in IStatus which is conveyed by ILog. Presumably, Code can be used to more precisely map the warning or error message to a course of action for the user.
  • The implementation should register as a listener to plug-in registration so that it can handle dynamic plug-in installation of the Eclipse platform.

The logger library plug-ins are now ready. In the next section, we'll show how a client plug-in would use them.

Using the logger plugins

In this section we'll demonstrate how to use the logger plug-ins by providing a short JUnit test plug-in. The plug-in and source is available for download.

Here is the JUnit test case we intend to make succeed.

public class LoggerCase1 extends LoggerTestCase {
    
    public void testSLF4JLog() {
        org.slf4j.Logger log = 
            org.slf4j.LoggerFactory.getLogger(LoggerCase1.class);
        String msg = "log from slf4j";
        log.info(msg);
        assertTrue(isLogged(msg));
    }
    
    
    public void testApacheJCLLog() {
        org.apache.commons.logging.Log log = LogFactory.getLog(LoggerCase1.class.getName());
        String msg = "log from Apache Commons log";
        log.info(msg);
        assertTrue(isLogged(msg));
    }
}

The LoggerTestCase class implements JUnit's TestCase.setUp() and TestCase.tearDown() which register/deregister as a listener to the test plug-in's ILog. LoggerTestCase.isLogged(String msg) will return true if the message was received while it was listening.

This client plug-in imports, not requires, the SLF4J plug-ins. Import means that any plug-in that exports these packages are suitable for the test. If 'requires' is used, then the flexibility to select the SLF4J plug-in at configuration will be forfeited.

Logging on LOG4J Framework

This section shows aspects of using the LOG4J framework, including specifying the path of the configuration file, and a sample implementation of a LOG4J Appender which writes to PDE's ILog API.

log4j.properties

In the logTest plug-in's source directory, find a file named log4j.properties with the following content:

# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=DEBUG, A1

# A1 is set to be my plugin log appender.
log4j.appender.A1=logTest.PluginLogAppender
log4j.appender.A1.SymbolicName=logTest

# A1 uses PatternLayout.
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

This file configures log4j to send messages to extension class PluginLogAppender.

For the LOG4J plug-in to find the file, its location is specified on the test plug-ins startup command line.

  • Select Run->Run.. and the last run's configuration will appear.
  • Select Main tab.
  • In the VM Arguments field, find the following string: -Dlog4j.configuration=/bin/log4j.properties. This vm option defines the LOG4J configuration over all LOG4J implementations in the application: the one in the LOG4J plug-in and in any plug-ins that bundle the LOG4J implementation.

LOG4J Appender

The logTest.PluginLogAppender extends from LOG4J's org.apache.log4j.AppenderSkeleton. The Appender has a configurable property, symbolicName, which provides the symbolic name of the plug-in whose ILog is used to output the log message. The Platform class has methods used to resolve the Bundle and to obtain its ILog:

    private ILog getBundleILog() {
        // get the bundle for a plug-in
        Bundle b = Platform.getBundle(getSymbolicName());
        if(b == null) {
            this.errorHandler.error(String.format("Plugin: %s not found in %s.", getSymbolicName(), this.name));
            return null;
        }
        return Platform.getLog(b);
    }
    

This appender also converts LOG4J Log Level to an IStatus severity:

    private int getSeverity(LoggingEvent ev) {    
        Level level = ev.getLevel();
        if(level == Level.FATAL || level == Level.ERROR)
            return IStatus.ERROR;
        else if(level == Level.WARN)
            return IStatus.WARNING;
        else if(level == Level.INFO) 
            return IStatus.INFO;
        else // debug, trace and custom levels
            return IStatus.OK;
    }
    

In the Appender's append method, the above methods are used to build an org.eclipse.core.runtime.Status and pass it to the Bundle's ILog.

Classloading the Appender

LOG4J will read the configuration file and load the named Appender class. However, the Appender class will not be found by the plug-in's classloader until it consults its buddy policy which is 'registered'. The classloader iterates through the list of registered buddies for the first buddy that can return the named class. Once found, the class is loaded, an object is instantiated and logging begins.

Therefore, the plug-in that contains the Appender has the following line in its MANIFEST.MF file:

Eclipse-RegisterBuddy: org.apache.log4j, org.nlog4j

Both log4j and nlog4j plug-ins are named here because either may be looking for this Appender.

When you run the JUnit test case, the following will appear in the ILog file:

Logging on JDK 1.4 Logging Framework

This section shows aspects of using the JDK 1.4 Logging Framework, including specifying the location of the configuration file, and classloading a sample implementation of a Handler which writes to PDE's ILog.

logging.properties

In JDK Logging, the configuration file is named logging.properties. Here are the contents of the logging.properties file in the test case plug-in:

.level= FINE
logTest.PluginLogHandler.level=FINE
logTest.PluginLogHandler.formatter = java.util.logging.SimpleFormatter

logTest.Jdk14LogTest.level=FINE

Specifying the file's location is also done using a Java VM property, as in the LOG4J case.

  • Select Main tab.
  • In the VM Arguments field, enter the following string: -Djava.util.logging.config.file=/bin/logging.properties. This vm option defines the configuration file used for all plugins which use JDK Logging.

Classloading the Handler

After having walked through the interesting parts of a LOG4J Appender, the same for the JDK Handler are simply not as interesting, so I won't review them here. What is interesting is how the our custom Handler class is found and instantiated.

The class that reads the logging configuration and instantiates Handlers is java.utils.logging.LogManager. It is loaded by the System Classloader. This handler class is inside the logTest plug-in and is not visible to the System Classloader. Hence, configuration fails. You may have noticed that the logging.properties above does not have a 'handlers' property set. You might reasonably expect this to be set to logTest.PluginLogHandler. This would be ineffective in the Eclipse runtime. Even Buddy policy can't help in this case.

As a workaround, the handler class can be instantiated in the plug-in's activation method. This works because when the plug-in looks for a class, Eclipse find classes that are local to the caller's the plug-in. Here is an implementation of a start method that loads the Handler and binds it to a Logger.

    public void start(BundleContext context) throws Exception {
        super.start(context);
        
        // handler instantiated in my plugin will see Platform
        PluginLogHandler h = new PluginLogHandler(context.getBundle().getSymbolicName());
        Logger log = Logger.getLogger(context.getBundle().getSymbolicName());
        log.addHandler(h);
    }

When the JUnit test case is run, the following will appear in the log:

Swapping SLF4J plug-ins

Any of the SLF4J plug-ins are usable by the logTest plug-in. This section shows how you can swap the plug-ins in your workspace allowing you to run the unit tests under any and all. Hopefully, this will prove the flexibility of using logger facade plug-ins, specifically SLF4J's.

If you use the LOG4J variant SLF4J plug-in, be sure to configure the LOG4J framework. Likewise if you use the JDK Logging variant. No additional configuration is necessary if you use the SimpleLog, NOP, or custom PDE SLF4J variants.

Swapping works because these SLF4J plug-ins all export the same packages, and because the plug-ins that depend on them 'import' the packages (instead of 'requiring' them).

To swap these plug-ins in your workspace, simply close one plug-in project and open another. If you use a target environment during development, remove the old plug-in from the target directory, copy in the new plug-in to the target directory, revisit the target preference page and reload the directory.

Don't forget this rule: Be sure to have only one active at a time.

There is an unexpected behavior while swapping plug-ins in the developer's workspace. I have submitted it to the eclipse team. See Bugzilla: 115961 for more information and workarounds.

Conclusion

As you can see, creating plug-ins for logger libraries is not hard. As a plug-in or RCP developer, you can build them yourself for your needs. It would be more convenient and encouraging if these were available from logger library providers.

Hopefully, if you have to wrap a code library with different functionality, this article has been of some help in highlighting the relevant Eclipse plug-in development features.

Consider these last thoughts as you decide which plug-ins to use.

  1. If all plug-ins contributing to an RCP application use a logger facade API, then any of the logger facade plug-ins can be used in your application. Don't use the LOG4J or JDK14 Logger variants if you need only to log to the PDE ILog. The custom PDE variant provided above is a smaller and simpler solution. On the other hand, if you have complex logging output requirements (e.g., multiple files, formats, etc...), the LOG4J or JDK14 Logger variants are useful.
  2. If one or more contributing plug-ins directly use a logger framework API, your RCP application must include each logger framework plug-in. This is the least flexible case. To support the other plug-ins in the application, you can still use any variant of SLF4J, but it may be best if you use the variant for the logger framework that you are forced to use.

References

  • Eclipse Rich Client Platform: Designing, Coding, and Packaging Java Applications, by Jeff McAffer, Jean-Michel Lemieux, Addison Wesley Professional, October 2005, ISBN: 0-321-33461-2