Let's dig in to how the SciJava Common library works under the hood!
First and foremost, SciJava Common is a plugin framework—a base for developing highly modular and extensible Java applications. All plugins available on Java's classpath are automatically discovered and made available. This is accomplished by scanning classpath resources for the file path META-INF/json/org.scijava.plugin.Plugin
. Such files are generated at compile time by a Java annotation processor that writes them in response to @Plugin
annotations on Java classes.
In ImageJ2 , we invented a plugin mechanism to replace ImageJ 1.x plugins, now called "commands" in ImageJ2 to distinguish from other plugins. We factor out plugin discovery from ImageJ2 into a base library: SciJava Common.
#@ImageJ ij
// Behind a firewall? Configure your proxy settings here.
//System.setProperty("http.proxyHost","myproxy.domain")
//System.setProperty("http.proxyPort","8080")
"ImageJ is ready to go."
ImageJ is ready to go.
The job of the Context
is to keep track of the currently available plugins. The SciJava context is a container of named Service
plugins that hold local state information.
References to all the @Plugin
-annotated classes that are discovered are contained in a single, master Context
. Each application is responsible for creating its own Context
to manage plugins and contextual state.
In ImageJ, a Context
is automatically created when the application starts up, so plugin developers do not need to create their own. In fact, creating your own Context
typically causes problems, as it will be a different container than what the rest of ImageJ is using. Instead, plugin instances within a common Context
are provided automatically by the framework—you just have to ask for it.
Typically, ImageJ plugin developers will be writing Service
and/or Command
plugins. If you need to use another plugin—for example, the LogService
—you should not manually create it as this effectively disconnects you from your Context
. Instead, you should ask your Context
for an instance by adding a field of the desired type and annotating it with the @Parameter
annotation. For example:
import org.scijava.command.Command
import org.scijava.log.LogService
import org.scijava.plugin.Parameter
import org.scijava.plugin.Plugin
@Plugin(type = Command.class)
public class MyPlugin implements Command {
// This @Parameter notation is 'asking' the Context
// for an instance of LogService.
@Parameter
private LogService log
@Parameter
private String message
@Override
public void run() {
// Just use the LogService!
// There is no need to construct it, since the Context
// has already provided an appropriate instance.
log.info(message)
}
}
// Save a reference to the class for later.
myPluginClass = MyPlugin.class
// TODO: Figure out why log message in this command are swallowed.
// Execute our sample command.
ij.command().run(MyPlugin.class, true, "message", "Success!").get()
groovy.lang.MissingMethodException: No signature of method: org.scijava.plugin.DefaultPluginService.addPlugin() is applicable for argument types: (java.lang.Class) values: [class MyPlugin] Possible solutions: addPlugin(org.scijava.plugin.PluginInfo), addPlugins(java.util.Collection), getPlugin(java.lang.Class), getPlugin(java.lang.String), getPlugin(java.lang.Class, java.lang.Class), getPlugins() at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:58) at org.codehaus.groovy.runtime.callsite.PojoMetaClassSite.call(PojoMetaClassSite.java:49) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125) at Script15.run(Script15.groovy:29) at org.scijava.plugins.scripting.groovy.GroovyScriptEngine.eval(GroovyScriptEngine.java:303) at org.scijava.plugins.scripting.groovy.GroovyScriptEngine.eval(GroovyScriptEngine.java:122) at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264) at org.scijava.script.ScriptModule.run(ScriptModule.java:159) at org.scijava.module.ModuleRunner.run(ModuleRunner.java:167) at org.scijava.jupyter.kernel.evaluator.Worker.run(Worker.java:109) at org.scijava.thread.DefaultThreadService$2.run(DefaultThreadService.java:220) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)
No Outputs
This will allow the Context
to provide you with the appropriate instance of your requested service.
info = ij.plugin().getPlugin(myPluginClass)
No Outputs
plugin = ij.plugin().createInstance(info)
plugin.run() // but message is still null
//TODO: Consider using a different plugin type
[ERROR] Cannot create plugin: null java.lang.NullPointerException at org.scijava.plugin.DefaultPluginService.createInstance(DefaultPluginService.java:236) at org.scijava.plugin.PluginService$createInstance$0.call(Unknown Source) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125) at Script11.run(Script11.groovy:2) at org.scijava.plugins.scripting.groovy.GroovyScriptEngine.eval(GroovyScriptEngine.java:303) at org.scijava.plugins.scripting.groovy.GroovyScriptEngine.eval(GroovyScriptEngine.java:122) at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264) at org.scijava.script.ScriptModule.run(ScriptModule.java:159) at org.scijava.module.ModuleRunner.run(ModuleRunner.java:167) at org.scijava.jupyter.kernel.evaluator.Worker.run(Worker.java:109) at org.scijava.thread.DefaultThreadService$2.run(DefaultThreadService.java:220) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) java.lang.NullPointerException: Cannot invoke method run() on null object at org.codehaus.groovy.runtime.NullObject.invokeMethod(NullObject.java:91) at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:48) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) at org.codehaus.groovy.runtime.callsite.NullCallSite.call(NullCallSite.java:35) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113) at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:117) at Script11.run(Script11.groovy:3) at org.scijava.plugins.scripting.groovy.GroovyScriptEngine.eval(GroovyScriptEngine.java:303) at org.scijava.plugins.scripting.groovy.GroovyScriptEngine.eval(GroovyScriptEngine.java:122) at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264) at org.scijava.script.ScriptModule.run(ScriptModule.java:159) at org.scijava.module.ModuleRunner.run(ModuleRunner.java:167) at org.scijava.jupyter.kernel.evaluator.Worker.run(Worker.java:109) at org.scijava.thread.DefaultThreadService$2.run(DefaultThreadService.java:220) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)
In some rare cases, manual plugin construction is unavoidable. Understand that if the MyPlugin
class above is manually constructed—i.e. via new MyPlugin()
—the LogService
parameter will be null
. Automatic population only occurs if the plugin instance itself is retrieved via the framework. When you must manually construct a plugin instance, you can still re-connect it to an existing Context
via its injection mechanism:
import org.scijava.plugin.Plugin
import org.scijava.Context;
import org.scijava.plugin.Parameter
import org.scijava.command.Command
@Plugin(type = Command.class)
public class MyService implements Command {
// This service will manually create plugin instances.
// So, we need a reference to our containing Context,
// then we can use it to inject our plugins.
@Parameter
private Context context
@Override
public void run() {
// Manually create a plugin instance.
// It is not connected to a Context yet
MyPlugin plugin = new MyPlugin()
// Inject the plugin instance with our Context.
context.inject(plugin)
// Now that our plugin is injected, we can use
// it with the knowledge that its parameters
// have been populated.
plugin.run() // but message is still null
}
}
// executing our sample command
ij.command().run(MyService.class, true).get()
MyService
Services are—surprise!—SciJava Plugins. Just like plugins, there are Service interfaces and implementing classes. This allows a proper separation between the Service's public contract and the details of its implementation.
Services are defined as interfaces, with concrete implementations as plugins. This design provides seams in the right places so that behavior at every level can be customized and overridden.
Services provide two important functions to the SciJava framework: utility methods and persistent state. If you want to add reusable Java methods that can be used throughout the SciJava framework, then you should create a Service
to provide this functionality. If you need to track Context-wide variables or configuration, a Service
should be used to encapsulate that state.
Conceptually, a Service
satisfies the role of static utility classes on a per-Context basis. In this way, only one instance of each Service
class can be associated with a given Context
instance; an association that occurs automatically during Context
creation. Furthermore, when a Context
is asked for an implementation of a given Service
, only the highest priority instance will be returned.
Services often build on or reuse functionality defined in each other. For example, the PluginService
sees ubiquitous use in retrieving and working with plugin instances. For such reuse, @Parameter
annotation can be used to declare inter-service requirements. During Context
startup, these relationships will be resolved automatically.
import org.scijava.service.SciJavaService
import org.scijava.service.Service
import org.scijava.service.AbstractService
import org.scijava.app.StatusService
import org.scijava.plugin.Plugin
import org.scijava.plugin.Parameter
// Example Service Interface:
public interface HelloService extends SciJavaService {
public void sayHello()
}
// Example implementation:
@Plugin(type = Service.class)
public class DefaultHelloService extends AbstractService implements HelloService {
@Parameter
private StatusService status
@Override
public void initialize() {
// initialize as little as possible here
}
@Override
public void sayHello() {
status.showStatus("Howdy!")
}
}
"HelloService ready"
HelloService ready
Whereas Service
s provide internal functionality, Command
s are plugins designed to be executed as one-offs, typically interacting with users to achieve some desired outcome. When opening the ImageJ GUI, Commands are what populate your menu structure: exposing functionality and algorithms in a way that can be consumed by non-developers.
When writing Command
s you will often declare @Parameter
s on fields that cannot be resolved automatically by the Context
—for example, numeric values or file paths. Instead of being instantiated at Context
startup as a Service
would be, Command
s are created and executed on demand.
When a Command
is executed, it goes through a series of pre-processing steps to populate its @Parameter
s using its associated Context
. If any parameters are left unresolved and a UI is available, the framework will automatically build and display an appropriate dialog to get user input. In this way, input harvesting is decoupled from functional operation—allowing developers to focus on what's really important without repetition of code. This also means that Command
s can typically run headlessly without any extra development effort.
A common pattern in Command
development is to wrap Service
functionality. For example, opening an image from a path is a fundamental operation in ImageJ. To this end, developers can directly use the DatasetIOService
. Users then get this same functionality from the menus via the OpenDataset
command—which itself simply calls into the DatasetIOService
.
A Gateway
is a plugin intended to make life easier for developers. It wraps a Context
, offering type-safe access to core services. Everything you can do with a gateway you can also do without it—but the gateway object makes the API much more succinct and convenient.
// Create a SciJava gateway.
import org.scijava.SciJava
sj = new SciJava()
// Now bask in the convenience!
import org.scijava.service.Service
[
"Plugin count" : sj.plugin().getPlugins().size(),
"Module count" : sj.module().getModules().size(),
"Service count" : sj.plugin().getPluginsOfType(Service.class).size(),
"SciJava version" : sj.getVersion(),
"Where is SciJava?" : sj.getApp().getLocation()
]
Key | Value |
---|---|
Plugin count | 1506 |
Module count | 1093 |
Service count | 92 |
SciJava version | 2.64.0 |
Where is SciJava? | file:/Users/curtis/anaconda3/envs/java_env/opt/scijava-jupyter-kernel/scijava-common-2.64.0.jar |
Because virtually everything is a plugin in ImageJ, there are too many to explicitly enumerate, let alone cover in a tutorial. To get ideas for functionality that can be added, a good starting point is to look for services in the javadoc, or the ImageJ search portal. Many service types have supplemental plugins for easy functional extension. In particular, the imagej-common and scijava-common repositories will contain plugin definitions for many essential operations.
A brief list of some of the more useful plugin types to extend:
Ops
provide a reusable set of image processing algorithms.Image formats
allow new types of images to be opened in ImageJ.Converters
allow the framework to interchange types, outside of normal Java class hierarchy restrictions.Input Preprocessors
give you control over the population of @Parameters
.Displays
control how UI elements are presented to users.If you know the function you want to modify but can't determine its location in the code, please ask other developers. You're part of the community now!