Spring create component manually and register as singleton

Viewed 1984

Suppose I have a component such as:

@Component
public class MyComponent {
    ...
}

Now, I want to instantiate this component manually and make it a singleton, so in my main function, I create a new context and then create a new MyComponent and register it as a singleton:

var context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBeanFactory().registerSingleton(MyComponent.class.getCanonicalName() + ".ORIGINAL", new MyComponent());
context.refresh();

The problem I am finding is that MyComponent is still being created by Spring, so I don't end up with a single instance of the class, but two.

How can I make this work?

Full example:

Main class:

package my.spring;

import static org.springframework.beans.factory.config.AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        context.getBeanFactory()
               .registerSingleton(MyComponent.class.getCanonicalName() + ORIGINAL_INSTANCE_SUFFIX,
                                  new MyComponent());

        MyComponent singleton = context.getBean(MyComponent.class); // This fails because there are now two such types

        primaryStage.setScene(new Scene(new VBox(20,
                                                 new TextArea("Hello Spring"),
                                                 new TextArea(singleton.myName))));
        primaryStage.setOnCloseRequest(w -> context.close());
        primaryStage.show();
    }
}

AppConfig.java

package my.spring;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class AppConfig {

}

MyComponent.java

package my.spring;

import org.springframework.stereotype.Component;

@Component
public class MyComponent {
    public final String myName = getClass().getCanonicalName();
}

The solution I am looking for should not involve filtering out MyComponent in the AppConfig component scan. I am looking for a dynamic solution that will allow me to tell spring not to create certain singletons when they already exist in the dependency graph.

Is this possible or not?

2 Answers

Here are different solutions for this:

1) Remove annotation @Component form your class ``MyComponent`.

2) Define interface and inject it where you need as interface. In your code create and implementation of the interface like '... registerSingleton(..., new MyComponentImpl())'.

Following approaches do not fit all your requirements, but I'd suggest that you consider them, too.

3) If your goal is just to register a component with a particular name, consider using @Component("MyName").

4) Use configuration class (annotated with @Configuration). In the method that provides the bean of type MyComponent manually create your bean. Here you don't register the bean manually, but still you have a control over creation of the bean. For instance you can initialize it differently depending on some configuration parameters, or you can even create instances of different subclasses.

5) Implement a factory that will create this bean. Again, you will not inject it directly. But you will have full control over creation of your bean.

If you tell us what you are actually trying to achieve, there can be also other solutions.

There's proven scheme how to create such sort of application (still trying to remember original GitHub publication). It goes quite different way comparing with yours. But it solves a problem with singleton usage. Main idea: You run regular Spring application, but it quickly forwards to JavaFX launch scheme; that, in turn, continues standard Spring app init/run code sequence at predefined point. Here's excerpt from working code (Java 17 code base).

@SpringBootApplication
public class SpringApp
{
    public static void main(String[] args) 
    {
        Application.launch(JavaFXApp.class, args);
    }
    //.. other referenced stuff here
}

public final class JavaFXApp extends Application {

    private ConfigurableApplicationContext applicationContext;
    private PrimaryController primaryController; // JavaFX bindings use WeakListeners, so keep top level controller here, for example

    @Override
    public void init() throws Exception {
        String[] args = getParameters().getUnnamed().toArray(new String[0]);
        applicationContext = new SpringApplicationBuilder(SpringApp.class).run(args);
        applicationContext.getBean(SpringApp.class).init(args); // not sure it looks nice
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryController = applicationContext.getBean(PrimaryController.class);
        applicationContext.publishEvent(new StageReadyEvent(primaryStage));
    }

    @Override
    public void stop() throws Exception {
        applicationContext.getBean(SpringApp.class).terminate(); // not sure it looks nice
        applicationContext.close();
    }

}

@Component
public class PrimaryStageBuildListener implements ApplicationListener<StageReadyEvent> {

    private static final String FXML = "/primary.fxml";

    @Autowired
    private FxmlBuilder builder;
    @Value("${jfx.appl.title}")
    private String title;
    @Value("${jfx.appl.width}")
    private int width;
    @Value("${jfx.appl.height}")
    private int height;

    @Override
    public void onApplicationEvent(StageReadyEvent event) {
        Stage stage = event.getStage();
        stage.setScene(new Scene(builder.build(FXML), width, height));
        stage.setTitle(title);
        stage.show();
    }
}

@Component
public class FxmlBuilder {

    @Autowired
    private ApplicationContext applicationContext;

    public <T> T build(String fxml) {
        T gui;
        try {
            FXMLLoader fxmlLoader = new FXMLLoader(FxmlBuilder.class.getResource(fxml));
            fxmlLoader.setControllerFactory(applicationContext::getBean);
            gui = fxmlLoader.load();
        } catch (IOException e) {
            throw new RuntimeException("Failed to build GUI from FXML " + fxml, e);
        }
        return gui;
    }
}
Related