در Spring توسط مکانیزم Data Binding میتوان داده ها را به نوع های ساده مثل int, String, boolean تبدیل کرد و انها را در قالب Object دریافت کرد.
ولی در عمل ما با دیتا های پیچیده تری روبرو خواهیم بود که مکانیزم تبدیل آنها را باید خودمان تعیین کنیم
ابتدا با یک مثال ساده شروع میکنیم
در Spring اینترفیس < Converter< S , T برای اینکار وجود دارد که S دیتایی که دریافت میکند و T دیتایی است که تبدیل کرده و برمی گرداند مانند :
@Component public class StringToLocalDateTimeConverter implements Converter<String, LocalDateTime> { @Override public LocalDateTime convert(String source) { return LocalDateTime.parse( source, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } }
حالا میتوانیم این Component را در Controller استفاده کنیم :
@GetMapping("/findbydate/{date}") public GenericEntity findByDate(@PathVariable("date") LocalDateTime date) { return ...; }
آیا میتوان در RequestParam@ مقادیر را به Enum هم تبدیل کنیم ؟ مثلا :
public enum Modes { ALPHA, BETA; }
ابتدا برای آن یک تبدیل کننده مینویسیم :
public class StringToEnumConverter implements Converter<String, Modes> { @Override public Modes convert(String from) { return Modes.valueOf(from); } }
حالا آنرا ثبت میکنیم :
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToEnumConverter()); } }
و در Controller توسط RequestParam@ میتوان آنرا دریافت و تبدیل به نوع Enum ثبت شده کرد :
@GetMapping public ResponseEntity<Object> getStringToMode(@RequestParam("mode") Modes mode) { // ... }
و یا حتی بصورت PathVariable دریافت کرد :
@GetMapping("/entity/findbymode/{mode}") public GenericEntity findByEnum(@PathVariable("mode") Modes mode) { // ... }
حال کمی دیتا را پیچیده تر میکنیم و یک کلاس Abstract پدر را فرض کنید که تعدادی فرزند دارد و میخواهیم با استفاده همان پدر مشترک مابقی فرزندان توسط یک مبدل مشترک بصورت ابجکت تبدیل کنیم
کلاس پدر :
public abstract class AbstractEntity { long id; public AbstractEntity(long id){ this.id = id; } }
که دو فرزند دارد :
public class Foo extends AbstractEntity { private String name; // standard constructors, getters, setters }
public class Bar extends AbstractEntity { private int value; // standard constructors, getters, setters }
برای اینکه تبدیل کننده آنر بنویسیم نیاز به <ConverterFactory<S, R داریم :
public class StringToAbstractEntityConverterFactory implements ConverterFactory<String, AbstractEntity>{ @Override public <T extends AbstractEntity> Converter<String, T> getConverter(Class<T> targetClass) { return new StringToAbstractEntityConverter<>(targetClass); } private static class StringToAbstractEntityConverter<T extends AbstractEntity> implements Converter<String, T> { private Class<T> targetClass; public StringToAbstractEntityConverter(Class<T> targetClass) { this.targetClass = targetClass; } @Override public T convert(String source) { long id = Long.parseLong(source); if(this.targetClass == Foo.class) { return (T) new Foo(id); } else if(this.targetClass == Bar.class) { return (T) new Bar(id); } else { return null; } } } }
کلاس Converter ساخته شده را ثبت میکنیم :
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverterFactory(new StringToAbstractEntityConverterFactory()); } }
و در نهایت در Controller از آن استفاده میکنیم :
@RestController @RequestMapping("/string-to-abstract") public class AbstractEntityController { @GetMapping("/foo/{foo}") public ResponseEntity<Object> getStringToFoo(@PathVariable Foo foo) { return ResponseEntity.ok(foo); } @GetMapping("/bar/{bar}") public ResponseEntity<Object> getStringToBar(@PathVariable Bar bar) { return ResponseEntity.ok(bar); } }
Bind کردن Domain Object :
گاهی دیتای ما که قرار است Bind شود بصورت مستقیم دریافت نمیشود مثلا از header, session, cookie قرار است آنرا دریافت کنیم و یا در یک data source ای ذخیره شده است در این دو حالت ما نیاز داریم روش دیگری را در پیش بگیریم
برای همچین پارامتر هایی اول ما نیاز داریم یک annotation تعریف کنیم :
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface Version { }
حالا نیاز داریم که اینترفیس HandlerMethodArgumentResolver را پیاده سازی کنیم این اینترفیس یک استراتژی برای پیدا کردن مقادیر آرگومان های متد از دیتای ارسالی است
public class HeaderVersionArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter methodParameter) { return methodParameter.getParameterAnnotation(Version.class) != null; } @Override public Object resolveArgument( MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest(); return request.getHeader("Version"); } }
و در انتهای کار به Spring اجازه میدهیم نحوه پیدا کردن دیتا را بشناسد :
@Configuration public class WebConfig implements WebMvcConfigurer { //... @Override public void addArgumentResolvers( List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(new HeaderVersionArgumentResolver()); } }
حالا میتوانیم آنرا در Controller استفاده کنیم :
@GetMapping("/entity/{id}") public ResponseEntity findByVersion( @PathVariable Long id, @Version String version) { return ...; }
همانگونه که میبینیم مقدار برگشتی متد resolveArgument در کلاس پیاده سازی شده HandlerMethodArgumentResolver یک Object است و نه String