Olivier
Olivier

Reputation: 3495

Using Spring IoC to set up enum values

Is there a way to set up such enum values via Spring IoC at construction time?

What I would like to do is to inject, at class load time, values that are hard-coded in the code snippet below:

public enum Car
{
        NANO ("Very Cheap", "India"),
        MERCEDES ("Expensive", "Germany"),
        FERRARI ("Very Expensive", "Italy");

        public final String cost;
        public final String madeIn;

        Car(String cost, String madeIn)
        {
                this.cost= cost;
                this.madeIn= madeIn;
        }

}

Let's say that the application must be deployed in Germany, where Nanos are "Nearly free", or in India where Ferraris are "Unaffordable". In both countries, there are only three cars (deterministic set), no more no less, hence an enum, but their "inner" values may differ. So, this is a case of contextual initialization of immutables.

Upvotes: 33

Views: 57377

Answers (13)

Mukesh Kumar Gupta
Mukesh Kumar Gupta

Reputation: 1647

I have faced the same issue when I was working to localize my enum label in different locales.

Enum Code:

public enum Type {
   SINGLE("type.single_entry"),
   MULTIPLE("type.multiple_entry"),
   String label;

   Type(String label) {
     this.label = label;
   }

   public String getLabel() {
     String translatedString = I18NTranslator.getI18NValue(getLocale(), label);
     return StringUtils.isEmpty(translatedString) ? label : translatedString;
   }
}

My I18NTranslator class which basically load the message source to get localized content. I18Ntransalator class depends on springContext if you don't write you might face a peculiar bug. Some time might face a dependency related which causes null pointer exception. I had put a lot of effort to resolve this issue.

@Component
@DependsOn({"springContext"})
public class I18NTranslator {

    private static MessageSource i18nMessageSource;

    public static String getI18NValue(Locale locale, String key) {
        if (i18nMessageSource != null)
            return i18nMessageSource.getMessage(key, null, locale);
        return key;
    }

    @PostConstruct
    public void initialize() {
        i18nMessageSource = SpringContext.getBean("i18nMessageSource", MessageSource.class);
    }
}

We have to set the spring context

@Component
@Slf4j
public class SpringContext implements ApplicationContextAware {

    private static ApplicationContext context;

    public static <T extends Object> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }

    public static <T extends Object> T getBean(String beanClassName, Class<T> beanClass) {
        return context.getBean(beanClassName, beanClass);
    }

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        SpringContext.context = context;
    }
}

Now it is time to define the bean for I18NMessageSource.

@Configuration
public class LocaleConfiguration implements WebMvcConfigurer {

    @Bean(name = "i18nMessageSource")
    public MessageSource getMessageResource() {
        ReloadableResourceBundleMessageSource messageResource = new ReloadableResourceBundleMessageSource();
        messageResource.setBasename("classpath:i18n/messages");
        messageResource.setCacheSeconds(3600);
        messageResource.setDefaultEncoding("UTF-8");
        return messageResource;
    }

    @Bean(name = "localeResolver")
    public LocaleResolver getLocaleResolver() {
        return new UrlLocaleResolver();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //UrlLocalInterceptor is custom locale resolver based on header paramter.
        UrlLocaleInterceptor localeInterceptor = new UrlLocaleInterceptor();
        registry.addInterceptor(localeInterceptor);
    }
}

PS: if you need the custom interceptor code I can share in the comment. Defines all local properties files inside resources/i18n folder with messages prefix like messages_en.properties for english and messages_fr.properties fro french.

Upvotes: 2

Guito
Guito

Reputation: 1191

I have done it in the following way:

@Component
public class MessageSourceHelper {
    @Inject
    public MessageSource injectedMessageSource;

    public static MessageSource messageSource;

    public static String getMessage(String messageKey, Object[] arguments, Locale locale) {
        return messageSource.getMessage(messageKey, arguments, locale);
    }

    @PostConstruct
    public void postConstruct() {
        messageSource = injectedMessageSource;
    }

}

That way you can easily use it in the enum to get messages in the following way:

MessageSourceHelper.getMessage(key, arguments, locale);

Upvotes: 4

Tom McIntyre
Tom McIntyre

Reputation: 3699

OK, it's quite fiddly, but it CAN be done.

It's true that Spring cannot instantiate enums. But that's not a problem - Spring can also use factory methods.

This is the key component:

public class EnumAutowiringBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    private final List<Class<? extends Enum>> enumClasses = new ArrayList<>();

    public EnumAutowiringBeanFactoryPostProcessor(Class<? extends Enum>... enumClasses) {
        Collections.addAll(this.enumClasses, enumClasses);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        for (Class<? extends Enum> enumClass : enumClasses) {
            for (Enum enumVal : enumClass.getEnumConstants()) {
                BeanDefinition def = new AnnotatedGenericBeanDefinition(enumClass);
                def.setBeanClassName(enumClass.getName());
                def.setFactoryMethodName("valueOf");
                def.getConstructorArgumentValues().addGenericArgumentValue(enumVal.name());
                ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition(enumClass.getName() + "." + enumVal.name(), def);
            }
        }
    }
}

Then the following test class shows that it works:

@Test
public class AutowiringEnumTest {

    public void shouldAutowireEnum() {
        new AnnotationConfigApplicationContext(MyConig.class);

        assertEquals(AutowiredEnum.ONE.myClass.field, "fooBar");
        assertEquals(AutowiredEnum.TWO.myClass.field, "fooBar");
        assertEquals(AutowiredEnum.THREE.myClass.field, "fooBar");
    }

    @Configuration
    public static class MyConig {

        @Bean
        public MyClass myObject() {
            return new MyClass("fooBar");
        }

        @Bean
        public BeanFactoryPostProcessor postProcessor() {
            return new EnumAutowiringBeanFactoryPostProcessor(AutowiredEnum.class);
        }
    }

    public enum AutowiredEnum {
        ONE,
        TWO,
        THREE;

        @Resource
        private MyClass myClass;

    }

    public static class MyClass {

        private final String field;

        public MyClass(String field) {
            this.field = field;
        }
   }

}

Upvotes: 6

Cjxcz Odjcayrwl
Cjxcz Odjcayrwl

Reputation: 22867

You can use Enum class as factory bean. Example: setting serializationInclusion field with enum value:

            <property name="serializationInclusion">
                <bean class="org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion" factory-method="valueOf">
                    <constructor-arg>   
                        <value>NON_NULL</value>
                    </constructor-arg>
                </bean>
            </property>

But actually (Spring 3.1) a simpler solution works: you just write the enum value and Spring recognizes what to do:

<property name="serializationInclusion" value="NON_NULL"/>

Upvotes: 0

Jason
Jason

Reputation: 71

You can't create new enum values via Spring, they must be declared in the class. However, since the enum values will be singletons anyway (created by the JVM), any configurations that should be set, or services to be injected, can be done via invoking static methods in the enum class:

http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/beans/factory/config/MethodInvokingFactoryBean.html

Upvotes: 4

mP.
mP.

Reputation: 18266

Attempting to mutate an Enum is well silly and goes completely against their design objectives. An enum by definition represents a distinct value within a group. If you ever need more / less values you will need to update the source. While you can change an enums state by adding setters (after all they are just objects) your hacking the system.

Upvotes: 1

Olivier
Olivier

Reputation: 3495

Here is the solution I came to (thanks to Javashlook whose answer put me on track). It works, but it's most probably not a production-grade way of doing it.

But better than a thousand words, here is the code, I'll let you judge by yourself.

Let's take a look at the revised Car enum :

public enum Car {
    NANO(CarEnumerationInitializer.getNANO()), MERCEDES(
            CarEnumerationInitializer.getMERCEDES()), FERRARI(
            CarEnumerationInitializer.getFERRARI());

    public final String cost;
    public final String madeIn;

    Car(ICarProperties properties) {
        this.cost = properties.getCost();
        this.madeIn = properties.getMadeIn();
    }
}

And here are the "plumbling" classes :

//Car's properties placeholder interface ...
public interface ICarProperties {
    public String getMadeIn();
    public String getCost();
}
//... and its implementation
public class CarProperties implements ICarProperties {
    public final String cost;
    public final String madeIn;

    public CarProperties(String cost, String madeIn) {
        this.cost = cost;
        this.madeIn = madeIn;
    }
    @Override
    public String getCost() {
        return this.cost;
    }
    @Override
    public String getMadeIn() {
        return this.madeIn;
    }
}

//Singleton that will be provide Car's properties, that will be defined at applicationContext loading.
public final class CarEnumerationInitializer {
    private static CarEnumerationInitializer INSTANCE;
    private static ICarProperties NANO;
    private static ICarProperties MERCEDES;
    private static ICarProperties FERRARI;

    private CarEnumerationInitializer(ICarProperties nano,
            ICarProperties mercedes, ICarProperties ferrari) {
        CarEnumerationInitializer.NANO = nano;
        CarEnumerationInitializer.MERCEDES = mercedes;
        CarEnumerationInitializer.FERRARI = ferrari;
    }

    public static void forbidInvocationOnUnsetInitializer() {
        if (CarEnumerationInitializer.INSTANCE == null) {
            throw new IllegalStateException(CarEnumerationInitializer.class
                    .getName()
                    + " unset.");
        }
    }

    public static CarEnumerationInitializer build(CarProperties nano,
            CarProperties mercedes, CarProperties ferrari) {
        if (CarEnumerationInitializer.INSTANCE == null) {
            CarEnumerationInitializer.INSTANCE = new CarEnumerationInitializer(
                    nano, mercedes, ferrari);
        }
        return CarEnumerationInitializer.INSTANCE;
    }

    public static ICarProperties getNANO() {
            forbidInvocationOnUnsetInitializer();
        return NANO;
    }

    public static ICarProperties getMERCEDES() {
            forbidInvocationOnUnsetInitializer();
        return MERCEDES;
    }

    public static ICarProperties getFERRARI() {
            forbidInvocationOnUnsetInitializer();
        return FERRARI;
    }
}

Finally, the applicationContext definition :

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <bean id="nano" class="be.vinkolat.poc.core.car.CarProperties">
        <constructor-arg type="java.lang.String" value="Cheap"></constructor-arg>
        <constructor-arg type="java.lang.String" value="India"></constructor-arg>
    </bean>
    <bean id="mercedes"
        class="be.vinkolat.poc.core.car.CarProperties">
        <constructor-arg type="java.lang.String" value="Expensive"></constructor-arg>
        <constructor-arg type="java.lang.String" value="Germany"></constructor-arg>
    </bean>
    <bean id="ferrari" class="be.vinkolat.poc.core.car.CarProperties">
        <constructor-arg type="java.lang.String"
            value="Very Expensive">
        </constructor-arg>
        <constructor-arg type="java.lang.String" value="Italy"></constructor-arg>
    </bean>
    <bean id="carInitializer"
        class="be.vinkolat.poc.core.car.CarEnumerationInitializer"
        factory-method="build" lazy-init="false">
        <constructor-arg type="be.vinkolat.poc.core.car.CarProperties"
            ref="nano" />
        <constructor-arg type="be.vinkolat.poc.core.car.CarProperties"
            ref="mercedes" />
        <constructor-arg type="be.vinkolat.poc.core.car.CarProperties"
            ref="ferrari" />
    </bean>
</beans>

It works, but there is one major weakness : CarEnumerationInitializer MUST be instantiated BEFORE any reference is made to Car enumeration, otherwise CarProperties are null, meaning that Car's properties can't be set when Car is loaded (hence the IllegalStateException thrown, to at least make it crashes in a predictable and documentated way). carInitializer bean's property lazy-init set to an explicit false, to put emphasis on the need to load it as soon as possible. I would say it may be useful in a simple application, one where you can easely guess where a first call to Car will be made. For a larger one, it will probably be such a clutter that I didn't encourage you to use it.

Hope this help, comments and vote (up and down) very welcome :) I'll wait for a few days to make this one the accepted answer, to let you react.

Upvotes: 0

javashlook
javashlook

Reputation: 10471

I don't think it can be done from Spring's ApplicationContext configuration. But, do you really need it done by Spring, or can you settle for simple externalization using ResourceBundle; like this:

public enum Car
{
    NANO,
    MERCEDES,
    FERRARI;

    public final String cost;
    public final String madeIn;

    Car()
    {
            this.cost = BUNDLE.getString("Car." + name() + ".cost");
            this.madeIn = BUNDLE.getString("Car." + name() + ".madeIn");
    }

    private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(...);

}

In the properties file, one for each specific locale, enter the keys describing the possible internal enum values:

Car.NANO.cost=Very cheap
Car.NANO.madeIn=India
Car.MERCEDES.cost=Expensive
...

The only drawback of this approach is having to repeat the name of enum fields (cost, madeIn) in Java code as strings. Edit: And on the plus side, you can stack all properties of all enums into one properties file per language/locale.

Upvotes: 14

Greg Noe
Greg Noe

Reputation: 1156

All right, this is a bit complex but you may find a way to integrate it. Enums are not meant to change at runtime, so this is a reflection hack. Sorry I don't have the Spring implementation part, but you could just build a bean to take in the enum class or object, and another field that would be the new value or values.

Constructor con = MyEnum.class.getDeclaredConstructors()[0];
Method[] methods = con.getClass().getDeclaredMethods();
for (Method m : methods) {
  if (m.getName().equals("acquireConstructorAccessor")) {
    m.setAccessible(true);
    m.invoke(con, new Object[0]);
  }
}
Field[] fields = con.getClass().getDeclaredFields();
Object ca = null;
for (Field f : fields) {
  if (f.getName().equals("constructorAccessor")) {
    f.setAccessible(true);
    ca = f.get(con);
  }
}
Method m = ca.getClass().getMethod(
  "newInstance", new Class[] { Object[].class });
m.setAccessible(true);
MyEnum v = (MyEnum) m.invoke(ca, new Object[] { 
  new Object[] { "MY_NEW_ENUM_VALUE", Integer.MAX_VALUE } });
  System.out.println(v.getClass() + ":" + v.name() + ":" + v.ordinal());

This is taken from this site.

Upvotes: 0

Greg Noe
Greg Noe

Reputation: 1156

<bean id="car" class="Foo">
    <property name="carString" value="NANO" />
</bean>

And then in your class Foo, you would have this setter:

public void setCar(String carString) {
    this.carString = Car.valueOf(carString);
}

Upvotes: 1

Brian Agnew
Brian Agnew

Reputation: 272337

Why not provide a setter (or constructor argument) that takes a String, and simply call Enum.valueOf(String s) to convert from a String to an enum. Note an exception will get thrown if this fails, and your Spring initialisation will bail out.

Upvotes: 4

bruno conde
bruno conde

Reputation: 48265

Do you mean setting up the enum itself?

I don't think that's possible. You cannot instantiate enums because they have a static nature. So I think that Spring IoC can't create enums as well.

On the other hand, if you need to set initialize something with a enum please check out the Spring IoC chapter. (search for enum) There's a simple example that you can use.

Upvotes: 16

tpdi
tpdi

Reputation: 35171

What do you need to set up? The values are created when the class loads, and as it's an enum, no other values can be created (unless you add them to the source and recompile).

That's the point of an enum, to be able to give limit a type to an explicit range of constant, immutable values. Now, anywhere in your code, you can refer to a type Car, or its values, Car.NANO, Car.MERCEDES, etc.

If, on the other hand, you have a set of values that isn't an explicit range, and you want to be able to create arbitrary objects of this type, you'd use the same ctor as in your post, but as a regular, not enum class. Then Spring provides various helper clases to read values from some source (XML file, config file, whatever) and create Lists of that type.

Upvotes: 1

Related Questions