Pergunta

Suppose I've a class Fruit and it's two subclasses - Apple and Grape:

class Fruit {
    public void grind() { }
}

class Apple extends Fruit { }
class Grape extends Fruit { }

In spring properties file, I've a property that decides which bean to register at startup. At a time, I'll only have either Apple or Grape instance registered as a bean. The property is:

# This can be either apple or grape
app.fruit = apple

In the Java configuration file, I'm binding a String attribute using @Value with this property, and based on that, I'll create appropriate instance. I'm trying to use factory pattern here. So, I've a FruitFactory like this:

class FruitFactory {
    private Map<String, Fruit> map = new HashMap<String, Fruit>();

    public FruitFactory() {
        map.put("apple", new Apple());
        map.put("grape", new Grape());
    }

    public Fruit getFruit(String fruit) {
        return map.get(fruit);
    }
}

And here's my spring configuration class:

class SpringConfig {
    @Value("${app.fruit}")
    private String fruitType;

    @Bean
    public FruitFactory fruitFactory() {
        return new FruitFactory();
    }

    @Bean
    public Fruit getFruit() {
          return fruitFactory().getFruit(fruitType);
    }
}

So, here're my few questions:

  • Will the instances stored in the map inside the factory be spring managed bean? Is there any issue with the implementation? I've tried it, and it is working fine, and I'm confused whether the instances are really spring managed.

  • I was trying to implement it in a better way, so that when a new fruit comes, I don't have to modify my factory. On way is to provide a register() method in factory and let all the Fruit subclasses invoke it. But the issue is when and how the subclasses will be loaded? I'll not be using the classes, not before putting their instances into the map. Can anyone suggest a better way?


Edit:

As suggested in comment and answer, I've tried using @Profile instead of factory pattern. But I'm facing some issues in that. Here's what I've:

@Configuration
@Profile("apple")
class AppleProfile {
    @Bean
    public Fruit getApple() {
        return new Apple();
    }
}

@Configuration
@Profile("grape")
class GrapeProfile {
    @Bean
    public Fruit getGrape() {
        return new Grape();
    }
}

And in a ServletListener, I've set the active profile:

class MyServletListener implements ServletContextListener {
    @Value("${app.fruit}")
    private String fruitType;

    public void contextInitialized(ServletContextEvent contextEvent) {
        // Get Spring Context
        WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(contextEvent
            .getServletContext());
        context.getAutowireCapableBeanFactory().autowireBean(this);

        ConfigurableEnvironment configEnvironment = (ConfigurableEnvironment) context.getEnvironment();
        logger.debug("0;Setting Active Profile: " + cacheRetrievalMode);

        configEnvironment.setActiveProfiles(cacheRetrievalMode);
    }
}

This is properly setting the active profile, which I can see. The only issue is, the listener is declared before the ContextLoaderListener, and by the time this is executed, the beans are already been created. Is there any alternative?

Foi útil?

Solução

Will the instances stored in the map inside the factory be spring managed bean?

Making the FruitFactory a managed bean

@Bean
public FruitFactory fruitFactory() {
    return new FruitFactory();
}

doesn't make any of the objects it's referring to managed beans. However, this

@Bean
public Fruit getFruit() {
      return fruitFactory().getFruit(fruitType);
}

does make that one returned Fruit a managed bean. @Bean marks a method as a bean definition and bean factory (it creates the bean). The object you return will be managed by Spring's bean life cycle.

Is there any issue with the implementation?

It seems weird that you're creating a FruitFactory bean but also a Fruit from that same FruitFactory. Are you even going to inject the FruitFactory elsewhere in the application?

I was trying to implement it in a better way, so that when a new fruit comes, I don't have to modify my factory

Seriously, your factory is messing everything up. Spring already does its job, and more! Annotations make your life easier. You can give an identifier to the @Bean. You can qualify the bean with @Qualifier (and then also qualify the injection target with @Qualifier). You can set a @Profile for when and under which conditions the bean should be initialized.

But the issue is when and how the subclasses will be loaded? I'll not be using the classes, not before putting their instances into the map. Can anyone suggest a better way?

You can use bean initMethods, which you specify as a @Bean annotation attribute, or a @PostConstruct annotated method to do post-initialization logic. You can use these to register the beans with the factory, which you'll have injected (but that design doesn't sound right to me, you'd have to show us more.)

You should also look into InitializingBean and FactoryBean.


For setting the active profile, one possibility is to do the following. Create an ApplicationContextInitializer which sets the active profile by reading from a .properties file. You won't be able to use @PropertySources here because this isn't a bean.

Something like

public class ProfileContextInitializer implements
        ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        PropertySource<Map<String, Object>> source = null;
        try {
            source = new ResourcePropertySource("spring.properties");
            String profile = (String) source.getProperty("active.profile");
            System.out.println(profile);
            ConfigurableEnvironment env = applicationContext.getEnvironment();
            env.setActiveProfiles(profile);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

You can register this in your deployment descriptor

<context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>com.yourapp.ProfileContextInitializer</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

When the ContextLoaderListener is created, it will pick up and instantiate your class and call its initialize method. This is done before the WebApplicationContext is refreshed.

You should probably just set a VM argument for the active profile and avoid all of this.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top