Extending the NetBeans Platform: Creating Custom Modules and Services
Introduction
The NetBeans Platform is a modular, extensible framework for building rich desktop applications on the Java platform. Extending it with custom modules and services lets you add features, decouple concerns, and reuse functionality across projects. This article shows a clear, practical path to create modules and services, register them, and integrate them into the platform using NetBeans APIs and best practices.
Prerequisites
- Java 11+ (or the Java version supported by your NetBeans release)
- NetBeans IDE with NetBeans Platform development features
- Basic knowledge of Maven or Ant (Maven is used in examples)
- Familiarity with OOP and Java Service APIs
Core concepts
- Module: A deployable unit that contributes code, UI, and metadata to a NetBeans application.
- Service: A component (often defined by an interface) providing reusable functionality; other modules obtain services rather than concrete implementations.
- Lookup: The platform’s dependency injection mechanism for locating services at runtime.
- Layer XML: Module metadata (actions, menus, windows) declared in XML and merged into the application’s system filesystem.
- ModuleInfo/Annotations: Annotations such as @ServiceProvider simplify service registration.
Step 1 — Create a NetBeans Module project
- In NetBeans IDE: File → New Project → NetBeans Modules → Module (Maven).
- Set groupId/artifactId and module codenamebase (e.g., com.example.myservice).
- Choose dependencies: Add APIs you need (Utilities, Window System, Actions, etc.).
- Build to generate the module skeleton.
Step 2 — Define a service interface
Create a simple interface to represent the service contract. Keep contracts small and focused.
Example:
package com.example.myservice.api;
public interface GreetingService {
String greet(String name);
}
Step 3 — Implement the service
Create an implementation in another module (the provider). This separation allows swapping implementations.
Example:
package com.example.myservice.impl;
import com.example.myservice.api.GreetingService;
public class DefaultGreetingService implements GreetingService {
@Override
public String greet(String name) {
return “Hello, “ + (name == null ? “world” : name) + ”!”;
}
}
Step 4 — Register the service with Lookup
Use the @ServiceProvider annotation so the platform can discover the implementation at runtime.
Example:
package com.example.myservice.impl;
import com.example.myservice.api.GreetingService;
import org.openide.util.lookup.ServiceProvider;
@ServiceProvider(service = GreetingService.class)
public class DefaultGreetingService implements GreetingService {
@Override
public String greet(String name) {
return “Hello, “ + (name == null ? “world” : name) + ”!”;
}
}
When compiled, the annotation processor generates registration entries so Lookup.findAll(GreetingService.class) and Lookup.getDefault().lookup(GreetingService.class) will locate the implementation.
Step 5 — Consume the service
From a consumer module (UI or logic), obtain the service via Lookup.
Example:
import com.example.myservice.api.GreetingService;
import org.openide.util.Lookup;
public class GreetingAction {
private final GreetingService service = Lookup.getDefault().lookup(GreetingService.class);
public void perform() {
String message = service.greet(“Alice”);
System.out.println(message);
}
}
For multiple implementations, use Lookup.lookupAll(GreetingService.class) and iterate or select by annotation/priority.
Step 6 — Contribute UI: Actions, Menus, and Windows
Use layer.xml or annotations to add actions and menus.
Action via annotation:
import org.openide.awt.ActionID;
import org.openide.awt.ActionRegistration;
import org.openide.awt.ActionReference;
import org.openide.util.Lookup;
@ActionID(category = “File”, id = “com.example.myservice.GreetAction”)
@ActionRegistration(displayName = “Greet”)
@ActionReference(path = “Menu/File”, position = 1300)
public final class GreetAction implements ActionListener {
public void actionPerformed(ActionEvent e) {
GreetingService service = Lookup.getDefault().lookup(GreetingService.class);
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message(service.greet(“User”)));
}
}
Register windows by creating TopComponents and declaring them in the layer or using @TopComponent.Description/@TopComponent.Registration annotations.
Step 7 — Use Lookup for loose coupling and testing
- Lookup allows runtime replacement (mock implementations in tests).
- For testability, use Lookup.getDefault().lookup(…) or injection via constructors in non-platform-managed classes.
- Register mock services in tests using Lookup.getDefault().lookupResult(…) and ServiceProvider for test modules.
Step 8 — Best practices
- Design small, focused service interfaces.
- Prefer interfaces in an API module with no UI dependencies.
- Keep implementations in separate modules so they can be swapped.
- Use Lookup and @ServiceProvider for discovery; avoid static singletons.
- Declare module dependencies explicitly in module.xml or pom.xml.
- Use layer.xml for declarative contributions (actions, menus, icons).
- Document module contracts and versioning for compatibility.
Example module layout (Maven)
- my-app/ (application)
- api/ (defines GreetingService)
- impl/ (implements and registers service)
- ui/ (actions, windows consuming service)
Troubleshooting
- Service not found: ensure provider module is enabled and @ServiceProvider generated file is present under META-INF/services.
- ClassCastException: confirm consumer depends on same API artifact version.
- Action/menu not visible: verify layer entries or action annotations compiled and module included in application.
Conclusion
Creating custom modules and services on the NetBeans Platform is straightforward when you follow clear separation of API and implementation, use Lookup for discovery, and declare UI contributions via layer metadata or annotations. This architecture yields modular, testable, and maintainable desktop applications that leverage the platform’s powerful infrastructure.