sirve responsebody requestmapping que puede para método mvc modelo escritura encontrar ejemplo commandname atributo arquitectura java spring spring-mvc

java - responsebody - Cómo personalizar los nombres de los parámetros al vincular los objetos de comando Spring mvc



para que sirve spring mvc (9)

En Spring 3.1, ServletRequestDataBinder proporciona un enlace para valores de vinculación adicionales:

protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { }

La subclase ExtendedServletRequestDataBinder lo usa para agregar variables de plantilla URI como valores de enlace. Puede extenderlo aún más para que sea posible agregar alias de campo específicos del comando.

Puede anular RequestMappingHandlerAdapter.createDataBinderFactory (..) para proporcionar una instancia personalizada de WebDataBinder. Desde la perspectiva de un controlador, podría verse así:

@InitBinder public void initBinder(MyWebDataBinder binder) { binder.addFieldAlias("jobType", "jt"); // ... }

Tengo un objeto de comando:

public class Job { private String jobType; private String location; }

Que está obligado por spring-mvc:

@RequestMapping("/foo") public Strnig doSomethingWithJob(Job job) { ... }

Lo cual funciona bien para http://example.com/foo?jobType=permanent&location=Stockholm . Pero ahora necesito hacerlo funcionar para la siguiente url en su lugar:
http://example.com/foo?jt=permanent&loc=Stockholm

Obviamente, no quiero cambiar mi objeto de comando, porque los nombres de campo deben permanecer largos (como se usan en el código). ¿Cómo puedo personalizar eso? ¿Hay una opción para hacer algo como esto?

public class Job { @RequestParam("jt") private String jobType; @RequestParam("loc") private String location; }

Esto no funciona ( @RequestParam no se puede aplicar a los campos).

Lo que estoy pensando es un convertidor de mensajes personalizado similar a FormHttpMessageConverter y leer una anotación personalizada en el objeto de destino


Esta solución es más concisa pero requiere el uso de RequestMappingHandlerAdapter, que Spring usa cuando <mvc:annotation-driven /> habilitado. Espero que ayude a alguien. La idea es extender ServletRequestDataBinder de esta manera:

/** * 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()); } } } }

Procesador apropiado:

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

Anotación:

/** * 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(); }

Configuración de primavera:

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

Y finalmente, el uso (como la solución Bozho):

public class Job { @ParamName("job-type") private String jobType; @ParamName("loc") private String location; }


Esto es lo que obtuve trabajando:

Primero, un resolutor de parámetros:

/** * 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); } } } }

Y luego registrar el resolvedor de parámetros usando un postprocesador. Debe registrarse como un <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; } }


Gracias la respuesta de @jkee.
Aquí está mi solución.
Primero, una anotación personalizada:

@Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ParamName { /** * The name of the request parameter to bind to. */ String value(); }

Un cliente 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()); } } } }

Un resolutor de parámetros:

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

Finalmente, una configuración de bean para agregar ParamNameProcessor en el primero de los resolutores de argumento:

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

Método del controlador:

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

Eso es todo.


Hay una manera simple, puedes agregar un método setter más, como "setLoc, setJt".


Intente interceptar la solicitud utilizando InterceptorAdaptor y luego, mediante un mecanismo de verificación simple, decida si desea enviar la solicitud al controlador del controlador. También envuelva HttpServletRequestWrapper alrededor de la solicitud para que pueda anular las solicitudes getParameter() .

De esta forma, puede volver a pasar el nombre del parámetro real y su valor a la solicitud para que el controlador lo vea.

Opción de ejemplo:

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

Finalmente, HandlerInterceptorAdaptor el HandlerInterceptorAdaptor alrededor del controlador controlador como se muestra a continuación. La función SelectedAnnotationHandlerMapping permite especificar qué controlador será intereceptado.

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

EDITADO .


Me gustaría señalarte en otra dirección. Pero no sé si funciona

Intentaré manipular el enlace en sí mismo.

Se hace por WebDataBinder y se invocará desde el método HandlerMethodInvoker Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception

No tengo una mirada profunda en Spring 3.1, pero lo que he visto es que esta parte de Spring ha cambiado mucho. Por lo tanto, es posible intercambiar el WebDataBinder. En Spring 3.0 no es posible sin anular el HandlerMethodInvoker .


Puede usar Jackson com.fasterxml.jackson.databind.ObjectMapper para convertir cualquier mapa a su clase DTO / POJO con accesorios anidados. Debe anotar sus POJO con @JsonUnwrapped en un objeto anidado. Me gusta esto:

public class MyRequest { @JsonUnwrapped private NestedObject nested; public NestedObject getNested() { return nested; } }

Y que usarlo así:

@RequestMapping(method = RequestMethod.GET, value = "/myMethod") @ResponseBody public Object myMethod(@RequestParam Map<String, Object> allRequestParams) { MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class); ... }

Eso es todo. Un poco de codificación. Además, puede dar cualquier nombre a sus utilería con la propiedad @JsonProperty.


no hay una buena forma de hacerlo, solo puede elegir qué solución aplicará. La diferencia entre manejo

@RequestMapping("/foo") public String doSomethingWithJob(Job job)

y

@RequestMapping("/foo") public String doSomethingWithJob(String stringjob)

es que el trabajo es un frijol y el trabajo de cuerdas no lo es (ninguna sorpresa hasta el momento). La diferencia real es que los beans se resuelven con el mecanismo de resolución Spring bean estándar, mientras que los parámetros de cadena se resuelven mediante MVC de primavera que conoce el concepto de la anotación @RequestParam. Para acortar la historia, no hay forma en la resolución estándar de bean de primavera (es decir, usar clases como PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor) para resolver "jt" a una propiedad llamada "jobType" o al menos no lo sé.

Las soluciones alternativas podrían ser como otras sugeridas para agregar un PropertyEditor personalizado o un filtro, pero creo que simplemente daña el código. En mi opinión, la solución más limpia sería declarar una clase como esta:

public class JobParam extends Job { public String getJt() { return super.job; } public void setJt(String jt) { super.job = jt; } }

luego usa eso en tu controlador

@RequestMapping("/foo") public String doSomethingWithJob(JobParam job) { ... }

ACTUALIZAR:

Una opción un poco más simple es no extenderse, solo agregue los getters extra, setters a la clase original

public class Job { private String jobType; private String location; public String getJt() { return jobType; } public void setJt(String jt) { jobType = jt; } }