Projekt, nad którym obecnie pracuję, to system aplikacji mikroserwisowych komunikujących się za pomocą protokołu HTTP. Ze względu na ograniczone konteksty używanej domeny poszczególne aplikacje nie mają pełnej wiedzy o encjach – po prostu nie ma takiej potrzeby. Niestety „odchudzenie” encji w pewnych częściach systemu powoduje konieczność ich konwertowania i uzupełniania o dodatkowe dane podczas komunikacji. W celu enkapsulacji logiki konwersji tworzyłem kolejne konwertery, które z czasem zaczęły być uciążliwe i niepraktyczne w zastosowaniu.
Pierwsze rozwiązanie
Pierwszym zastosowanym rozwiązaniem były klasy specjalizujące się w konwersji dwóch typów w każdym kierunku. Przykładem jest klasa FolderConverter, która transformowała obiekty Folder na FolderDTO i odwrotnie.
1 2 3 4 5 6 7 8 9 10 11 |
@Component class FolderConverter implements AbstractConverter<Folder, FolderDTO> { public Folder convert(FolderDTO folderDTO) { //conversion logic here } public FolderDTO convert(Folder folder) { //conversion logic here } } |
Jej użycie było całkiem wygodne:
1 2 3 4 |
@Autowired private FolderConverter folderConverter; ... FolderDTO dto = folderConverter.convert(folder); |
Niestety pojawił się problem przy konwertowaniu list. Nie mogłem znaleźć generycznego rozwiązania zarówno dla pojedynczych encji jak i całych kolekcji.
Interfejs Converter
Kilka zapytań do wyszukiwarki i nieco więcej postów na stackoverflow podpowiedziało zastosowanie interfejsu Converter<> dostarczanego przez framework Spring.
Po przeczytaniu dokumentacji i kilku przykładów postanowiłem spróbować tego podejścia. W kodzie zmieniły się tylko dwie konwencje:
- dla każdego kierunku transformacji stworzony został osobny konwerter
- każda klasa implementowała interfejs Converter<>
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Component class FolderConverter implements Converter<Folder, FolderDTO> { public FolderDTO convert(Folder folder) { //conversion logic here } } @Component class FolderDTOConverter implements Converter<FolderDTO, Folder> { public Folder convert(FolderDTO folder) { //conversion logic here } } |
Ta zmiana, jak wynikało z dokumentacji oraz przykładów, miała zapewnić bardziej generyczne rozwiązanie i ratunek przy konwertowaniu kolekcji. I tak rzeczywiście było. Niestety używanie konwerterów w takim zapisie stało się nieco uciążliwe:
1 2 3 4 5 6 7 8 |
@Autowired private FolderConverter folderConverter; @Autowired private FolderDTOConverter folderDTOConverter; ... Folder folder = folderDTOConverter.convert(folderDTO); folderDTO = folderConverter.convert(folder); |
ConversionService
Kolejne akapity dokumentacji naprowadziły mnie na użycie domyślnego serwisu konwersji. Jedyne czego wymagał to rejestracja własnych konwerterów.
Rozwiązaniem było stworzenie nowego pliku konfiguracyjnego, który byłby odpowiedzialny za uzupełnienie paczki konwerterów w serwisie.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@Configuration public class ConverterConfig extends WebMvcConfigurerAdapter { @Bean public FolderConverter getFolderConverter() { return new FolderConverter(); } @Bean public FolderDTOConverter getFolderDTOConverter() { return new FolderDTOConverter(); } @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(getFolderConverter()); registry.addConverter(getFolderDTOConverter()); super.addFormatters(registry); } } |
Pierwsze próby zamiany obiektów zakończyły się powodzeniem. Niestety problem pojawił się przy konwerterach, które potrzebowały innych zależności. Nie były one poprawnie wstrzykiwane przez co w logach aplikacji można było znaleźć dużo informacji o NullPointerException. Debug aplikacji potwierdził ten problem.
ConverterRegistryPostProcessor
Kolejne godziny szukania rozwiązania doprowadziły mnie do artykułu na stronie apprenticeshipnotes.org.
Po drobnym przerobieniu zaprezentowanego kodu konfiguracja ConversionService wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class ConverterRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanPostProcessor, ApplicationContextAware { private final String CONVERSION_SERVICE_NAME = "mvcConversionService"; private static final Logger logger = Logger.getLogger(ConverterRegistryPostProcessor.class); public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { //registry.registerBeanDefinition(CONVERSION_SERVICE_NAME, BeanDefinitionBuilder.rootBeanDefinition(ConversionServiceFactoryBean.class).getBeanDefinition()); } public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (CONVERSION_SERVICE_NAME.equals(beanName)) { logger.info("ConversionService bean detected"); Map<String, Converter> beansOfType = appCtx.getBeansOfType(Converter.class); DefaultFormattingConversionService conversionFactoryBean = (DefaultFormattingConversionService) bean; Set converters = new HashSet(beansOfType.values()); converters.forEach(c -> conversionFactoryBean.addConverter((Converter)c)); } return bean; } public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } ApplicationContext appCtx; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { appCtx = applicationContext; } } |
Do kodu z powyższego artykułu wprowadziłem następujące zmiany:
- nie tworzę nowej instancji ConversionService, a uzupełniam tę istniejącą (mvcConversionService)
- dodaję wszystkie implementacje interfejsu Converter zamiast klas oznaczonych adnotacją TypeConverter
Użycie tak skonfigurowanego serwisu jest bardzo proste
1 2 3 4 |
@Autowired private ConversionService conversionService; FolderDTO dto = conversionService.convert(folder, FolderDTO.class); |
Konwersja kolekcji
Konwersja kolekcji stała się nieco prostsza:
1 2 3 4 |
TypeDescriptor sourceType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(FolderDTO.class)); TypeDescriptor targetType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Folder.class)); List<Folder> target = (List<Folder>) conversionService.convert(folderList, sourceType, targetType); |
Rozwiązanie to nie jest najwygodniejsze i najładniejsze, ale niestety obecnie nie znalazłem lepszego.
Testowanie jednostkowe
Każdy z konwerterów jest traktowany jak normalna klasa, więc nie pojawia się tutaj nic niezwykłego podczas testów.
Nieco ciekawiej jest przy testowaniu klas, które używają ConversionService. W takim przypadku musimy użyć mechanizmu mockowania:
1 2 3 4 |
@Mock private ConversionService conversionService; ... when(conversionService.convert(any(FolderDTO.class), same(Folder.class))).thenReturn(folder); |
Comments