Using Spring IoC to set up enum values

2020-01-27 03:47发布

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.

13条回答
你好瞎i
2楼-- · 2020-01-27 04:30

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.

查看更多
啃猪蹄的小仙女
3楼-- · 2020-01-27 04:34

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

查看更多
戒情不戒烟
4楼-- · 2020-01-27 04:35

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;
        }
   }

}
查看更多
淡お忘
5楼-- · 2020-01-27 04:36

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.

查看更多
做自己的国王
6楼-- · 2020-01-27 04:37

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.

查看更多
女痞
7楼-- · 2020-01-27 04:39

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.

查看更多
登录 后发表回答