Spring Converter SPI

A Spring Framework talán egyik legnagyobb előnye, hogy bizonyos gyakran használt funkcionalitásra egy frappáns megoldást biztosít, és ezt az egész keretrendszeren belül konzekvensen alkalmazza. Az egyik ilyen funkcionalitás a Spring Converter SPI.

Nagyon gyakran van szükség arra, hogy szöveges értékből egy objektumot gyártsunk. Szöveg szerepelhet sok helyen. Szerepelhet a Spring xml konfigurációjában, Spring Expression Language-ben (SpEL), valamint a HTTP protokoll is alapvetően szöveges. Azonban láthatjuk, hogy akár az xml konfigurációban alkalmazhatunk más, long, double, stb. típusú értékeket, valamint a Spring controllerekben is definiálhatunk ilyen típussal url paramétereket, path változókat, header bejegyzéseket, stb. A Spring a konverziót automatikusan elvégzi. De mi van akkor, ha mi nem ilyen gyakori típusokká akarjuk konvertálni a szövegeinket, hanem pl. egy saját osztály egy példányává.

A Spring nagyon könnyen bővíthető, és ezen konverziós mechanizmus mögött a Converter SPI áll, mely megengedi, hogy saját konvertereket implementáljunk. Sőt, ezeket a konvertereket igazán sok helyen használhatjuk is.

Ezen használati helyeket tekinti át ez a poszt, melyhez példaprogram is készült, és elérhető a GitHubon.

Példának vegyünk egy gázóra (GasHour) osztályt. Ennek a különlegessége, hogy 6:00 az első órája, és a nyári és téli időszámítás miatt létezik egy 23, és egy 25 órás gáznap is. Ennek szöveges reprezentációja pl. 2015-01-01 9., ami a 2015. január 1-ei gáznap 9. gázóráját jelenti.

Ez az osztály legyen valami hasonló:

public class GasHour {

    private LocalDate date;

    private int hour;

    public static GasHour parse(String s) {
        // Szövegből GasHour példánnyá alakítás
    }
}

Az ehhez tartozó konverter nagyon egyszerű:

public class GasHourConverter implements Converter<String, GasHour> {

    @Override
    public GasHour convert(String s) {
        return GasHour.parse(s);
    }
}

A konverter használatához definiáljunk egy ConversionService objektumot, és regisztráljuk a konvertert.

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="jtechlog.springconverter.GasHourConverter"/>
        </set>
    </property>
</bean>

Vagy akár Java kódból:

@Bean
public ConversionService conversionService() {
    ConversionServiceFactoryBean factoryBean = 
        new ConversionServiceFactoryBean();
    factoryBean.setConverters(
        Collections.singleton(new GasHourConverter()));
    factoryBean.afterPropertiesSet();
    ConversionService conversionService = factoryBean.getObject();
    return conversionService;
}

Ezután az xml konfigurációban szereplő értékeket is fel tudja oldani.

<bean id="fooService"
        class="jtechlog.springconverter.FooService">
    <property name="startGasHour" value="2015-11-11 5." />
</bean>

Ha a beanünk implementációja a következő:

public class FooService {

    private GasHour startGasHour;

    private void setStartGasHour(GasHour startGasHour) {
	    this.startGasGour = startGasHour;
    }
}

A konvertereket egyszerűen használhatjuk a ConversionService példányon keresztül is, nem kell a konkrét konverterre hivatkoznunk.

@Autowired
public FooService(ConversionService conversionService) {
    this.conversionService = conversionService;
}

public void execute() {
    GasHour gasHour = 
        conversionService.convert("2011-11-11 5.", GasHour.class);
}

Listákra nem kell külön konvertert írnunk, ugyanis képes a Spring kezelni, ha a listák elemeire van konverter. Azonban a Java furcsa generikus kezelése miatt ez nem triviális.

List<GasHour> gasHours = (List<GasHour>) conversionService.convert(
        Arrays.asList("2011-11-11 5.", "2011-11-11 6.", "2011-11-11 7."),
        TypeDescriptor.collection(
                List.class, TypeDescriptor.valueOf(String.class)),
        TypeDescriptor.collection(
                List.class, TypeDescriptor.valueOf(GasHour.class)));

Nagyon szépen használható SpEL-ben is, az előbb említett FooService osztály esetén:

<bean id="fooService"
        class="jtechlog.springconverter.FooService">
    <property name="startGasHour" value="#{'2015-11-11 5.'}" />
</bean>

De természetesen programozottan is:

StandardEvaluationContext evaluationContext = 
    new StandardEvaluationContext();
StandardTypeConverter converter = 
    new StandardTypeConverter(conversionService);
evaluationContext.setTypeConverter(converter);
ExpressionParser expressionParser = new SpelExpressionParser();
GasHour gasHour = expressionParser.parseExpression("2011-11-11 5.")
    .getValue(evaluationContext, GasHour.class);
assertThat(gasHour, is(GasHour.parse("2011-11-11 5.")));

Amennyiben Spring MVC-ben is használni szeretnénk, a konvertereket regisztrálhatjuk a WebMvcConfigurerAdapter-ben is.

@Configuration
@EnableWebMvc
public class WebAppConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new GasHourConverter());
    }
}

Ekkor controllerben is működik az automatikus konverzió:

@Controller
public class GasHourController {

    @RequestMapping("/gashour")
    @ResponseBody
    public String getGasHour(@RequestParam GasHour gasHour) {
        return gasHour.toString();
    }
}

Még egy érdekesség, a Spring Data JPA is tudja használni, amikor egy lekérdezés eredményét, ami entitások listája, dto listákká akarjuk konvertálni. Ez akkor működik, ha a lapozást használjuk, és ehhez a visszatérési érték Page típus. Ebben a map() metódust kell hívni, a következőképpen.

public Page<LocationDto> listLocations(Pageable pageable) {
    return locationDao.findAllOrderById(pageable)
        .map(new LocationConverter());
}

Az ehhez tartozó konverter:

public class LocationConverter implements Converter<Location, LocationVO> {

    @Override
    public LocationVO convert(Location location) {
        LocationDto locationDto = new LocationDto();
        locationDto.id = location.getId();
        locationDto.lat = location.getLat();
        locationDto.lon = location.getLon();
        return locationDto;
    }
}