Conversion service

Convertisseurs globaux dans Spring

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Dans l'article consacré à des formulaires dans Spring, on a abordé la question de conversion des types de données (property editors). Cependant, ce n'est pas la seule fonctionnalité du Spring qui peut se charger de convertir un objet d'un type à un autre. Le concept qui aide à supporter cela s'appelle conversion service.

Qu'est-ce que c'est un conversion service dans Spring ?

La version 3 du Spring a amené une nouveauté cachée dans le package org.springframework.core.convert.converter. Il s'agit d'un Service Provider Interface (SPI), c'est-à-dire d'une spécification visant à définir les règles de son implémentation.

Conversion service est basée sur l'implémentation de l'interface Converter <S, T> où S signifie la classe d'origine et T la classe en laquelle l'objet devra être converti. La méthode convert() de cette interface prend comme objet le fichier source et le transforme en celui souhaité. Regardons cela sur l'exemple d'un convertisseur qui traduit l'instance org.springframework.security.core.userdetails.User en Subscriber :

public class UserToSubscriberConverter implements Converter<User, Subscriber> {
    @Autowired
    private SubscriberService subscriberService;

    public Subscriber convert(User user) {
        return subscriberService.loadByUsername(user.getUsername());
    }
}

L'exemple est banal mais il illustre bien la logique d'implémentation de l'interface Converter. Dans ce cas, la méthode convert() retourne l'instance Subscriber à partir de l'instance User. La conversion s'effectue via la recherche de l'utilisateur dans une des méthodes du subscriberService.

Quand utiliser un conversion service ?

L'illustration de conversion présentée dans le paragraphe précédent rend bien l'utilité des convertisseurs. On stocke dans la session l'instance de la classe User tandis que souvent dans le cas, par exemple pour modifier les paramètres d'un compte, on veut utiliser l'instance Subscriber. Grâce à cette dernière on peut faire appel à des méthodes des repositories (subscriber.save()) pour mettre à jour les informations nécessaires. Le code ci-dessous affiche cette utilité :

// SubscriberController.java
@Controller
public class SubscriberController extends FrontendController {
    public String modifyAvatarHandle(@ModelAttribute("subscriber") @Validated({SubscriberAvatarCheck.class}) 
    Subscriber subscriber, BindingResult binRes, @LoggedUser AuthenticationFrontendUserDetails user, 
    Model layout, RedirectAttributes redAtt) {
        logger.info("Received POST request " + subscriber);
        if (binRes.hasErrors()) {
            redAtt.addFlashAttribute("error", true);
            redAtt.addFlashAttribute("subscriber", subscriber);
            redAtt.addFlashAttribute("errors", binRes);
        } else {
            try {
                Subscriber subFromUser = conversionService.convert(user, Subscriber.class);
                subFromUser.setAvatarFile(subscriber.getAvatarFile());
                subscriberService.addAvatar(subFromUser);
                redAtt.addFlashAttribute("success", true);
            } catch (Exception e) {
                binRes.addError(getExceptionError("subscriber"));
                redAtt.addFlashAttribute("error", true);
                redAtt.addFlashAttribute("subscriber", subscriber);
                redAtt.addFlashAttribute("errors", binRes);
            }
        }
        return "redirect:/account/avatar";
    }
    // ...
}

// SubscriberServiceImpl.java
@Service("subscriberService")
public class SubscriberServiceImpl implements SubscriberService {
    @Override
    public Subscriber addAvatar(Subscriber subscriber) throws Exception {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            Map uploadResult = imageTool.uploadFile("avatar", subscriber.getAvatarFile(), subscriber.getLogin());
            if (((Boolean)uploadResult.get("uploadResult")) == true) {
                subscriber.setAvatar((String)uploadResult.get("fileBasename"));
                subscriber = subscriberRepository.save(subscriber);
            
                transactionManager.commit(status);
            } else {
                subscriber = null;
            }
        } catch(Exception e) {
            logger.error("An error occured on adding subscriber avatar", e);
            transactionManager.rollback(status);
            subscriber = null;
            throw new Exception(e);
        }
        return subscriber;
    }
    // ...
}

Comment implémenter conversion service sous Spring ?

Conversion service est un objet sans propriétaire, initialisé au moment de démarrage de l'application. Ensuite il est partagé par tous les threads. Pour l'enregistrer dans le fichier de contexte, il faut le déterminer avec une liste des convertisseurs qu'il supporte :

    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean id="userToSubscriberConv" class="library.converter.UserToSubscriberConverter"/>
                <bean id="userToAdminConv" class="library.converter.UserToAdminConverter"/>
                <bean id="bookToStringConv" class="library.converter.BookToStringConverter"/>
                <bean id="stringToLangConv" class="library.converter.StringToLangConverter"/>
                <bean id="stringToQuestionConv" class="library.converter.StringToQuestionConverter"/>
                <bean id="longToLangConv" class="library.converter.LongToLangConverter"/>
                <bean id="writerLangFormToListConv" class="library.converter.WriterLangFormToListConverter"/>
                <bean id="stringToPaymentMethodConv" class="library.converter.StringToPaymentMethodConverter"/>
                <bean id="stringToPenaltyConv" class="library.converter.StringToPenaltyConverter"/>
                <bean id="stringToSubscriberConv" class="library.converter.StringToSubscriberConverter"/>
                <bean id="stringToSubscriptionConv" class="library.converter.StringToSubscriptionConverter"/>
                <bean id="setToBeanPropertyBindingResultConv" class="library.converter.SetToBeanPropertyBindingResultConverter"/>
                <bean id="authenticationFrontendUserDetailsToUsernamePasswordAuthenticationTokenConv" class="library.converter.AuthenticationFrontendUserDetailsToUsernamePasswordAuthenticationTokenConverter"/>
            </set>
        </property>
    </bean>

La définition est donc simple. On précise un bean< contenant la classe FormattingConversionServiceFactoryBean. Elle permet de spécifier non seulement un Set des convertisseurs mais aussi un Set des formateurs.

En plus de cela, on rajoute <mvc:annotation-driven conversion-service="conversionService"> dans le fichier de contexte. Grâce à cela, toutes les conversions seront gérées automatiquement dans le contrôleur pendant la transmission des données.

L'utilisation d'un conversion service se fait alors via l'injection du bean dans un contrôleur, comme ici :

@Controller
public class SubscriberController extends FrontendController {
    @Autowired
    private ConversionService conversionService;
    // ...
}

Une question ? Une remarque ?

*

*

Un conseil JavaScript

Comment récupérer la hauteur de l'élément avec jQuery ?

On peut le faire avec deux méthodes. La première fonctionne correctement pour tous les navigateurs sauf Internet Explorer. Pour le cas des navigateurs Microsoft, elle marche uniquement quand la hauteur est spécifiée en dur :

$('#element').css('height')
L'alternative est la méthode height() qui est gérée par tous les navigateurs, même si l'élément a la hauteur "auto" :
$('#element').height();