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