web-dev-qa-db-fra.com

Utilisation de Spring IoC pour configurer les valeurs enum

Existe-t-il un moyen de définir de telles valeurs enum via Spring IoC au moment de la construction?

Ce que je voudrais faire est d’injecter, au moment du chargement de la classe, des valeurs codées en dur dans l’extrait de code ci-dessous:

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

}

Supposons que l'application doit être déployée en Allemagne, où les Nanos sont "presque gratuits", ou en Inde, où les Ferrari sont "inabordables". Dans les deux pays, il n'y a que trois voitures (ensemble déterministe), ni plus ni moins, d'où un enum, mais leurs valeurs "intérieures" peuvent différer. Donc, c’est un cas de initialisation contextuelle d’immutables.

27
Olivier

Voulez-vous dire configurer la enum elle-même?

Je ne pense pas que ce soit possible. Vous ne pouvez pas instancier des enums car ils ont une nature static. Donc, je pense que Spring IoC ne peut pas créerenums également.

D'autre part, si vous avez besoin de définir quelque chose d'initialisation avec enum, veuillez consulter le chapitre Spring IoC . (rechercher enum) Il existe un exemple simple que vous pouvez utiliser.

14
bruno conde

Je ne pense pas que cela puisse être fait à partir de la configuration ApplicationContext de Spring. Mais avez-vous vraiment besoin que cela soit fait d'ici Spring ou pouvez-vous vous contenter d'une externalisation simple en utilisant ResourceBundle ; comme ça:

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(...);

}

Dans le fichier de propriétés, un pour chaque paramètre régional spécifique, entrez les clés décrivant les valeurs d'enum internes possibles:

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

Le seul inconvénient de cette approche est de devoir répéter le nom des champs enum (cost, madeIn) dans le code Java sous forme de chaînes. Edit: Et du côté positif, vous pouvez empiler toutes les propriétés de toutes les énumérations dans un seul fichier de propriétés par langue/paramètres régionaux.

10
javashlook

OK, c'est assez délicat, mais cela PEUT être fait.

Il est vrai que le printemps ne peut instancier des enums. Mais ce n'est pas un problème - Spring peut également utiliser des méthodes d'usine. 

C'est le composant clé:

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

Ensuite, la classe de test suivante montre que cela fonctionne:

@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
Tom McIntyre

Vous ne pouvez pas créer de nouvelles valeurs enum via Spring, elles doivent être déclarées dans la classe. Cependant, étant donné que les valeurs enum seront toujours des singletons (créées par la JVM), toutes les configurations à définir, ou les services à injecter, peuvent être effectuées via l’appel de méthodes statiques dans la classe enum:

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

4
Jason

Pourquoi ne pas fournir un setter (ou argument de constructeur) qui prend une chaîne, et simplement appeler Enum.valueOf (String s) pour convertir une chaîne en une énumération. Notez qu'une exception sera levée si cela échoue et que votre initialisation Spring sera annulée.

3
Brian Agnew

Je l'ai fait de la manière suivante:

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

}

De cette façon, vous pouvez facilement l'utiliser dans l'énumération pour obtenir des messages de la manière suivante:

MessageSourceHelper.getMessage(key, arguments, locale);
3
Guito
<bean id="car" class="Foo">
    <property name="carString" value="NANO" />
</bean>

Et puis dans votre classe Foo, vous auriez ce passeur:

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

De quoi avez-vous besoin pour vous installer? Les valeurs sont créées lors du chargement de la classe. Comme il s’agit d’une énumération, aucune autre valeur ne peut être créée (à moins que vous ne les ajoutiez à la source et que vous les recompiliez).

C’est l’intérêt d’une énumération de pouvoir limiter un type à une plage explicite de valeurs constantes et immuables. Désormais, n'importe où dans votre code, vous pouvez faire référence à un type Car ou à ses valeurs, Car.NANO, Car.MERCEDES, etc.

Si, en revanche, vous avez un ensemble de valeurs qui n'est pas une plage explicite et que vous voulez pouvoir créer des objets arbitraires de ce type, vous utiliserez le même ctor que dans votre message, mais classe régulière, pas enum. Ensuite, Spring fournit différentes classes d'assistance pour lire les valeurs d'une source (fichier XML, fichier de configuration, peu importe) et créer des listes de ce type.

1
tpdi

Tenter de muter un Enum est bien ridicule et va totalement à l’encontre de ses objectifs de conception. Une énumération, par définition, représente une valeur distincte au sein d'un groupe. Si vous avez besoin de plus/moins de valeurs, vous devrez mettre à jour la source. Bien que vous puissiez changer un état enums en ajoutant des setters (après tout ce ne sont que des objets), vous piratez le système.

1
mP.

Voici la solution à laquelle je suis arrivé (grâce à Javashlook dont la réponse m'a mis sur la bonne voie). Cela fonctionne, mais ce n'est probablement pas une façon de le faire en production.

Mais mieux que mille mots, voici le code, je vous laisse juger par vous-même.

Jetons un coup d'œil à l'énumération Car révisée:

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

Et voici les cours "en chute libre":

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

Enfin, la définition de applicationContext:

<?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>

Cela fonctionne, mais il y a une faiblesse majeure: CarEnumerationInitializer DOIT être instanciée AVANT que toute référence soit faite à l'énumération Car, sinon CarProperties est null, ce qui signifie que les propriétés de Car ne peuvent pas être définies lorsque Car est chargé (par conséquent, IllegalStateException est levé, à au moins qu’il se bloque de manière prévisible et documentée). La propriété carInitializer de bean lazy-init est définie sur une false explicite, pour souligner le besoin de la charger le plus tôt possible ..__ Je dirais que cela peut être utile dans une application simple, une où vous pouvez facilement deviner où un premier appel à Car sera faite. Pour un plus grand, ce sera probablement un tel fouillis que je ne vous ai pas encouragé à l'utiliser.

J'espère que cette aide, vos commentaires et votre vote (de haut en bas) seront les bienvenus :) J'attendrai quelques jours pour que celui-ci soit la réponse acceptée, pour vous permettre de réagir.

0
Olivier

Bien, c’est un peu complexe, mais vous pouvez trouver un moyen de l’intégrer. Les énumérations ne sont pas censées changer à l'exécution, il s'agit donc d'un piratage de réflexion. Désolé, je n'ai pas la partie implémentation de Spring, mais vous pouvez simplement créer un bean pour intégrer la classe ou l'objet enum, ainsi qu'un autre champ qui serait la ou les nouvelles valeurs.

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());

Ceci est pris de ce site .

0
Greg Noe

Vous pouvez utiliser Enum class en tant que bean factory . Exemple: définition du champ serializationInclusion avec la valeur enum:

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

Mais en réalité (Spring 3.1) une solution plus simple fonctionne: vous écrivez simplement la valeur enum et Spring reconnaît ce qu'il faut faire:

<property name="serializationInclusion" value="NON_NULL"/>
0
Danubian Sailor