javareflectionannotationsmodularmodular-design

Using ServiceLoader to get All Instances of an Annotation


All!

I know that long questions are frowned upon, but this is the only way I will be able to get my problem adequately explained. So, I apologize up front for the length of this question.

I am working on a modular project that will be extensible via add-on modules. To that end, I am wanting to allow add-on modules to be able to provide their own menus, menu items and toolbar buttons. To accomplish this, I have created an API with some annotations in it. This API is located in a module called "Menu.API", and has the following classes defined:

@MenuProvider:

package com.my.menu.api;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CLASS)
@Repeatable(RepeatableMenus.class)
public @interface MenuProvider {
    String name();
    String text();
    int position();
}

@RepeatableMenus:

package com.my.menu.api;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RepeatableMenus {
    MenuProvider[] value();
}

For the sake of brevity, we will only concentrate on this annotation, because I am sure the rest of them will work with whatever solution I am able to get for this one.

With this module completed, I created the module-info.java file with the following contents:

module Menu.API {
    requires java.base;   // Just for the sake of completeness.

    exports com.my.menu.api;
}

So, with the API in place, I created a second module called "Menu.Platform", with the following class defined:

package com.my.platform;

// Imports removed for brevity.

@MenuProvider(name = "fileMenu", text = "File", position = Integer.MIN_VALUE)
@MenuProvider(name = "editMenu", text = "Edit", position = Integer.MIN_VALUE + 1)
@MenuProvider(name = "toolsMenu", text = "Tools", position = Integer.MAX_VALUE - 1000)
@MenuProvider(name = "helpMenu", text = "Help", position = Integer.MAX_VALUE)
public class App {

    private final MainFrame mainFrame;
    private final JMenuBar menuBar;
    private final JToolBar toolBar;
    private final JPanel statusPanel;
    private final JLabel statusLabel;
    
    public static MenuProvider provider() {
        // I am not sure how to send back my @MenuProvider annotations.
    }

    public App() {
        configureWindowParts();
    }

    private void configureWindowParts() {
        // No problems here...
    }

    private void initialize(String[] args) {
        // I do not need the args[] variable yet.
        
        createMenuBar();
    
        // ... the rest of the initialization.
    }

    private void createMenuBar() {
        ServiceLoader<MenuProvider> menusLoader = ServiceLoader.load(MenuProvider.class);

        // PostionableMenu is a subclass of JMenu that implements Comparable and provides
        //+ the `position` parameter.
        List<PositionableMenu> menus = new ArrayList<>();

        for (MenuProvider p : menusLoader) {
            PositionableMenu menu = new PositionableMenu(p.name(), p.text(), p.position());
            menus.add(menu);
        }

        Collections.sort(menus);

        menus.foreach(m -> {
            menuBar.add(m);
        });
    }
}

module-info.java:

module Menu.Platform {
    requires java.desktop;
    requires Menu.API;

    uses com.my.menu.api.MenuProvider;
    uses com.my.menu.api.RepeatableMenus;

    export com.my.platform;

    provides com.my.menu.api.MenuProvider with com.my.platform.App;
}

So my problem is that I receive the error:

the service implementation type must be a subtype of the service interface type,
or have a public static no-args method named "provider" returning the service implementation

...when I do not have the public static MenuProvider provider() method in my App class. But then when I put that method in my App class, I have no clue how to return those four (4) @MenuProviders from the method.

Any assistance that can be given to point me in the right direction is greatly appreciated. Thank you all in advance!

-SC

[EDIT] Well, I spent so long thinking of just how to ask this question that the answer came to me shortly after posting it...

What needs to be returned from the provider method is obtained by reflecting on the class:

    public static MenuProvider provider(){
        MenuProvider[] provider = App.class.getAnnotationsByType(MenuProvider.class);
        
        // How to return an array? If I change the return type to an
        //+ array, I get errors again because the provider method only
        //+ wants to return a single instance.
    }

This was my conundrum now. So I was thinking about how to edit this question when the answer hit me like a ton of bricks! Instead of returning the MenuProvider, I needed to return the RepeatableMenus instance. The value property of RepeatableMenus returns an array of MenuProviders. DUH!

So, I updated the provider method as follows:

    public static RepeatableMenus provider(){
        RepeatableMenus provider = App.class.getAnnotation(RepeatableMenus.class);
        
        return provider;
    }

And, I changed my module-info.java file to this:

module Menu.Platform {
    requires java.desktop;
    requires Menu.API;

    uses com.my.menu.api.MenuProvider;
    uses com.my.menu.api.RepeatableMenus;

    export com.my.platform;

    provides com.my.menu.api.RepeatableMenus with com.my.platform.App;
}

Now, I am receiving all of the @MenuProvider instances from my class. I cannot wait to try it from an additional module...

Thank you all anyway for the possible help you would have offered.

-SC


Solution

  • The documentation of ServiceLoader explains quite well how it works with a good example.

    First of all, it isn't designed to work with annotations. The service loader is supposed to return all registered classes implementing a given interface, not all classes marked with a given annotation. Technically you can create classes that implements your annotation, but it's totally counter intuitive, and won't help you to find all marked classes.

    Secondly, if your registered service implementation doesn't have a no-args constructor, or if it doesn't implement the service interface, it must have a static provider() method which returns an instance of the service interface. It means that you can only return a single instance and not a list. If you want to have multiple services, you must have multiple classes.

    If you want to find all classes marked with a given annotation as you have currently designed it, you will need to have a so called classpath scanner. For example, ClassGraph

    OF course if you are using a framework like Spring, you should use their discovery facility with their annotations like @Service or @Component, instead of reinventing the wheel. It is called context scan in that case. Given your question, I assume you aren't using such a framework.