Как настроить имена параметров при привязке объектов команды spring mvc
у меня есть объект команды:
public class Job {
private String jobType;
private String location;
}
который связан с spring-mvc:
@RequestMapping("/foo")
public Strnig doSomethingWithJob(Job job) {
...
}
работает на http://example.com/foo?jobType=permanent&location=Stockholm
. Но теперь мне нужно заставить его работать для следующего url вместо этого:http://example.com/foo?jt=permanent&loc=Stockholm
очевидно, что я не хочу менять свой командный объект, потому что имена полей должны оставаться длинными (поскольку они используются в коде). Как я могу настроить это? Есть ли возможность сделать что-то вроде этого:
public class Job {
@RequestParam("jt")
private String jobType;
@RequestParam("loc")
private String location;
}
это не сработает (@RequestParam
не может быть применен к полям).
то, о чем я думаю, это пользовательский конвертер сообщений, похожий на FormHttpMessageConverter
и прочитать пользовательскую аннотацию на целевой объект
9 ответов:
это решение более лаконично, но требует использования RequestMappingHandlerAdapter, который весной использовать, когда
<mvc:annotation-driven />
включено. Надеюсь, это кому-то поможет. Идея состоит в том, чтобы расширить ServletRequestDataBinder следующим образом:/** * ServletRequestDataBinder which supports fields renaming using {@link ParamName} * * @author jkee */ public class ParamNameDataBinder extends ExtendedServletRequestDataBinder { private final Map<String, String> renameMapping; public ParamNameDataBinder(Object target, String objectName, Map<String, String> renameMapping) { super(target, objectName); this.renameMapping = renameMapping; } @Override protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { super.addBindValues(mpvs, request); for (Map.Entry<String, String> entry : renameMapping.entrySet()) { String from = entry.getKey(); String to = entry.getValue(); if (mpvs.contains(from)) { mpvs.add(to, mpvs.getPropertyValue(from).getValue()); } } } }
соответствующий обработчик:
/** * Method processor supports {@link ParamName} parameters renaming * * @author jkee */ public class RenamingProcessor extends ServletModelAttributeMethodProcessor { @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; //Rename cache private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>(); public RenamingProcessor(boolean annotationNotRequired) { super(annotationNotRequired); } @Override protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) { Object target = binder.getTarget(); Class<?> targetClass = target.getClass(); if (!replaceMap.containsKey(targetClass)) { Map<String, String> mapping = analyzeClass(targetClass); replaceMap.put(targetClass, mapping); } Map<String, String> mapping = replaceMap.get(targetClass); ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping); requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest); super.bindRequestParameters(paramNameDataBinder, nativeWebRequest); } private static Map<String, String> analyzeClass(Class<?> targetClass) { Field[] fields = targetClass.getDeclaredFields(); Map<String, String> renameMap = new HashMap<String, String>(); for (Field field : fields) { ParamName paramNameAnnotation = field.getAnnotation(ParamName.class); if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) { renameMap.put(paramNameAnnotation.value(), field.getName()); } } if (renameMap.isEmpty()) return Collections.emptyMap(); return renameMap; } }
Аннотация:
/** * Overrides parameter name * @author jkee */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ParamName { /** * The name of the request parameter to bind to. */ String value(); }
Spring config:
<mvc:annotation-driven> <mvc:argument-resolvers> <bean class="ru.yandex.metrika.util.params.RenamingProcessor"> <constructor-arg name="annotationNotRequired" value="true"/> </bean> </mvc:argument-resolvers> </mvc:annotation-driven>
и, наконец, использование (например, решение Божо):
public class Job { @ParamName("job-type") private String jobType; @ParamName("loc") private String location; }
вот что я получил работу:
во-первых, параметр, распознаватель:
/** * This resolver handles command objects annotated with @SupportsAnnotationParameterResolution * that are passed as parameters to controller methods. * * It parses @CommandPerameter annotations on command objects to * populate the Binder with the appropriate values (that is, the filed names * corresponding to the GET parameters) * * In order to achieve this, small pieces of code are copied from spring-mvc * classes (indicated in-place). The alternative to the copied lines would be to * have a decorator around the Binder, but that would be more tedious, and still * some methods would need to be copied. * * @author bozho * */ public class AnnotationServletModelAttributeResolver extends ServletModelAttributeMethodProcessor { /** * A map caching annotation definitions of command objects (@CommandParameter-to-fieldname mappings) */ private ConcurrentMap<Class<?>, Map<String, String>> definitionsCache = Maps.newConcurrentMap(); public AnnotationServletModelAttributeResolver(boolean annotationNotRequired) { super(annotationNotRequired); } @Override public boolean supportsParameter(MethodParameter parameter) { if (parameter.getParameterType().isAnnotationPresent(SupportsAnnotationParameterResolution.class)) { return true; } return false; } @Override protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; bind(servletRequest, servletBinder); } @SuppressWarnings("unchecked") public void bind(ServletRequest request, ServletRequestDataBinder binder) { Map<String, ?> propertyValues = parsePropertyValues(request, binder); MutablePropertyValues mpvs = new MutablePropertyValues(propertyValues); MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class); if (multipartRequest != null) { bindMultipart(multipartRequest.getMultiFileMap(), mpvs); } // two lines copied from ExtendedServletRequestDataBinder String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; mpvs.addPropertyValues((Map<String, String>) request.getAttribute(attr)); binder.bind(mpvs); } private Map<String, ?> parsePropertyValues(ServletRequest request, ServletRequestDataBinder binder) { // similar to WebUtils.getParametersStartingWith(..) (prefixes not supported) Map<String, Object> params = Maps.newTreeMap(); Assert.notNull(request, "Request must not be null"); Enumeration<?> paramNames = request.getParameterNames(); Map<String, String> parameterMappings = getParameterMappings(binder); while (paramNames != null && paramNames.hasMoreElements()) { String paramName = (String) paramNames.nextElement(); String[] values = request.getParameterValues(paramName); String fieldName = parameterMappings.get(paramName); // no annotation exists, use the default - the param name=field name if (fieldName == null) { fieldName = paramName; } if (values == null || values.length == 0) { // Do nothing, no values found at all. } else if (values.length > 1) { params.put(fieldName, values); } else { params.put(fieldName, values[0]); } } return params; } /** * Gets a mapping between request parameter names and field names. * If no annotation is specified, no entry is added * @return */ private Map<String, String> getParameterMappings(ServletRequestDataBinder binder) { Class<?> targetClass = binder.getTarget().getClass(); Map<String, String> map = definitionsCache.get(targetClass); if (map == null) { Field[] fields = targetClass.getDeclaredFields(); map = Maps.newHashMapWithExpectedSize(fields.length); for (Field field : fields) { CommandParameter annotation = field.getAnnotation(CommandParameter.class); if (annotation != null && !annotation.value().isEmpty()) { map.put(annotation.value(), field.getName()); } } definitionsCache.putIfAbsent(targetClass, map); return map; } else { return map; } } /** * Copied from WebDataBinder. * * @param multipartFiles * @param mpvs */ protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) { for (Map.Entry<String, List<MultipartFile>> entry : multipartFiles.entrySet()) { String key = entry.getKey(); List<MultipartFile> values = entry.getValue(); if (values.size() == 1) { MultipartFile value = values.get(0); if (!value.isEmpty()) { mpvs.add(key, value); } } else { mpvs.add(key, values); } } } }
и затем регистрация преобразователя параметров с помощью постпроцессора. Он должен быть зарегистрирован как
<bean>
:/** * Post-processor to be used if any modifications to the handler adapter need to be made * * @author bozho * */ public class AnnotationHandlerMappingPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String arg1) throws BeansException { return bean; } @Override public Object postProcessBeforeInitialization(Object bean, String arg1) throws BeansException { if (bean instanceof RequestMappingHandlerAdapter) { RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean; List<HandlerMethodArgumentResolver> resolvers = adapter.getCustomArgumentResolvers(); if (resolvers == null) { resolvers = Lists.newArrayList(); } resolvers.add(new AnnotationServletModelAttributeResolver(false)); adapter.setCustomArgumentResolvers(resolvers); } return bean; } }
весной 3.1, ServletRequestDataBinder предоставляет крюк для дополнительных значений привязки:
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { }
подкласс ExtendedServletRequestDataBinder использует его для добавления переменных шаблона URI в качестве значений привязки. Вы можете расширить его еще больше, чтобы можно было добавлять псевдонимы полей для конкретных команд.
Вы можете переопределить RequestMappingHandlerAdapter.createDataBinderFactory(..) чтобы предоставить пользовательский экземпляр WebDataBinder. С точки зрения контроллера это может выглядеть так это:
@InitBinder public void initBinder(MyWebDataBinder binder) { binder.addFieldAlias("jobType", "jt"); // ... }
нет хороший встроенный способ сделать это, вы можете только выбрать, какой способ применить. Разница между обработкой
@RequestMapping("/foo") public String doSomethingWithJob(Job job)
и
@RequestMapping("/foo") public String doSomethingWithJob(String stringjob)
является ли эта работа бобом, а stringjob-нет (неудивительно до сих пор). Реальная разница заключается в том, что бобы разрешаются со стандартным механизмом Spring bean resolver, в то время как строковые параметры разрешаются spring MVC, который знает концепцию аннотации @RequestParam. Чтобы сделать длинную историю короткой дороги нет в стандартном разрешении Spring bean (то есть с использованием классов, таких как PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor) для разрешения "jt" в свойство под названием "jobType" или, по крайней мере, я не знаю об этом.
обходные пути могут быть такими, как другие предложили добавить пользовательский редактор свойств или фильтр, но я думаю, что это просто испортит код. На мой взгляд наилучшим решением было бы объявить класс следующим образом :
public class JobParam extends Job { public String getJt() { return super.job; } public void setJt(String jt) { super.job = jt; } }
тогда используйте это в вашем контроллер
@RequestMapping("/foo") public String doSomethingWithJob(JobParam job) { ... }
обновление :
немного более простой вариант-не расширять, просто добавьте дополнительные геттеры, сеттеры в исходный класс
public class Job { private String jobType; private String location; public String getJt() { return jobType; } public void setJt(String jt) { jobType = jt; } }
Я хотел бы указать вам на другую сторону. но я не знаю, если он работает.
Я бы попытался манипулировать самой привязкой.
это делают
WebDataBinder
и будет вызван изHandlerMethodInvoker
методObject[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception
у меня нет глубокого взгляда весной 3.1, но то, что я видел, заключается в том, что эта часть весны была сильно изменена. Таким образом, возможно, можно обменять WebDataBinder. Весной 3.0 это швы не возможно без переопределения
HandlerMethodInvoker
.
попробуйте перехватить запрос с помощью
InterceptorAdaptor
, а затем с помощью простого механизма проверки решить, следует ли foward запрос к обработчику контроллера. Также обернитеHttpServletRequestWrapper
вокруг запроса, чтобы вы могли переопределить запросыgetParameter()
.таким образом, вы можете repass реальное имя параметра и его значение в запрос на контроллер.
примера:
public class JobInterceptor extends HandlerInterceptorAdapter { private static final String requestLocations[]={"rt", "jobType"}; private boolean isEmpty(String arg) { return (arg !=null && arg.length() > 0); } public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //Maybe something like this if(!isEmpty(request.getParameter(requestLocations[0]))|| !isEmpty(request.getParameter(requestLocations[1])) { final String value = !isEmpty(request.getParameter(requestLocations[0])) ? request.getParameter(requestLocations[0]) : !isEmpty(request .getParameter(requestLocations[1])) ? request.getParameter(requestLocations[1]) : null; HttpServletRequest wrapper = new HttpServletRequestWrapper(request) { public String getParameter(String name) { super.getParameterMap().put("JobType", value); return super.getParameter(name); } }; //Accepted request - Handler should carry on. return super.preHandle(request, response, handler); } //Ignore request if above condition was false return false; } }
наконец-то обернуть
HandlerInterceptorAdaptor
вокруг обработчик контроллера, как показано ниже. ЭлементSelectedAnnotationHandlerMapping
позволяет указать, какой обработчик будет interecepted.<bean id="jobInterceptor" class="mypackage.JobInterceptor"/> <bean id="publicMapper" class="org.springplugins.web.SelectedAnnotationHandlerMapping"> <property name="urls"> <list> <value>/foo</value> </list> </property> <property name="interceptors"> <list> <ref bean="jobInterceptor"/> </list> </property> </bean>
редактировать.
Вы можете использовать Jackson com.быстрее!Джексон.databind.ObjectMapper для преобразования любой карты в ваш класс DTO / POJO с вложенными реквизитами. Вам нужно аннотировать свои POJOs с помощью @JsonUnwrapped на вложенном объекте. Вот так:
public class MyRequest { @JsonUnwrapped private NestedObject nested; public NestedObject getNested() { return nested; } }
и чем использовать его так:
@RequestMapping(method = RequestMethod.GET, value = "/myMethod") @ResponseBody public Object myMethod(@RequestParam Map<String, Object> allRequestParams) { MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class); ... }
вот и все. Немного кодирования. Кроме того, вы может дать любые имена для вашего реквизита usign @JsonProperty.
спасибо ответ @jkee .
Вот мое решение.
Во-первых, пользовательская аннотация:@Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ParamName { /** * The name of the request parameter to bind to. */ String value(); }
клиент DataBinder:
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder { private final Map<String, String> paramMappings; public ParamNameDataBinder(Object target, String objectName, Map<String, String> paramMappings) { super(target, objectName); this.paramMappings = paramMappings; } @Override protected void addBindValues(MutablePropertyValues mutablePropertyValues, ServletRequest request) { super.addBindValues(mutablePropertyValues, request); for (Map.Entry<String, String> entry : paramMappings.entrySet()) { String paramName = entry.getKey(); String fieldName = entry.getValue(); if (mutablePropertyValues.contains(paramName)) { mutablePropertyValues.add(fieldName, mutablePropertyValues.getPropertyValue(paramName).getValue()); } } } }
решатель параметров:
public class ParamNameProcessor extends ServletModelAttributeMethodProcessor { @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; private static final Map<Class<?>, Map<String, String>> PARAM_MAPPINGS_CACHE = new ConcurrentHashMap<>(256); public ParamNameProcessor() { super(false); } @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestParam.class) && !BeanUtils.isSimpleProperty(parameter.getParameterType()) && Arrays.stream(parameter.getParameterType().getDeclaredFields()) .anyMatch(field -> field.getAnnotation(ParamName.class) != null); } @Override protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) { Object target = binder.getTarget(); Map<String, String> paramMappings = this.getParamMappings(target.getClass()); ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), paramMappings); requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest); super.bindRequestParameters(paramNameDataBinder, nativeWebRequest); } /** * Get param mappings. * Cache param mappings in memory. * * @param targetClass * @return {@link Map<String, String>} */ private Map<String, String> getParamMappings(Class<?> targetClass) { if (PARAM_MAPPINGS_CACHE.containsKey(targetClass)) { return PARAM_MAPPINGS_CACHE.get(targetClass); } Field[] fields = targetClass.getDeclaredFields(); Map<String, String> paramMappings = new HashMap<>(32); for (Field field : fields) { ParamName paramName = field.getAnnotation(ParamName.class); if (paramName != null && !paramName.value().isEmpty()) { paramMappings.put(paramName.value(), field.getName()); } } PARAM_MAPPINGS_CACHE.put(targetClass, paramMappings); return paramMappings; } }
наконец, конфигурация bean для добавления ParamNameProcessor в первый из преобразователей аргументов:
@Configuration public class WebConfig { /** * Processor for annotation {@link ParamName}. * * @return ParamNameProcessor */ @Bean protected ParamNameProcessor paramNameProcessor() { return new ParamNameProcessor(); } /** * Custom {@link BeanPostProcessor} for adding {@link ParamNameProcessor} into the first of * {@link RequestMappingHandlerAdapter#argumentResolvers}. * * @return BeanPostProcessor */ @Bean public BeanPostProcessor beanPostProcessor() { return new BeanPostProcessor() { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof RequestMappingHandlerAdapter) { RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean; List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(adapter.getArgumentResolvers()); argumentResolvers.add(0, paramNameProcessor()); adapter.setArgumentResolvers(argumentResolvers); } return bean; } }; } }
Param pojo:
@Data public class Foo { private Integer id; @ParamName("first_name") private String firstName; @ParamName("last_name") private String lastName; @ParamName("created_at") @DateTimeFormat(pattern = "yyyy-MM-dd") private Date createdAt; }
метод контроллера:
@GetMapping("/foos") public ResponseEntity<List<Foo>> listFoos(@RequestParam Foo foo, @PageableDefault(sort = "id") Pageable pageable) { List<Foo> foos = fooService.listFoos(foo, pageable); return ResponseEntity.ok(foos); }
вот и все.