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 i
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.
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"/>
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
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);
<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);
}
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.