web-dev-qa-db-fra.com

Comment personnaliser les noms de paramètre lors de la liaison des objets de commande spring mvc

J'ai un objet de commande:

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

Qui est lié par spring-mvc:

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

Ce qui fonctionne bien pour http://example.com/foo?jobType=permanent&location=Stockholm. Mais maintenant, je dois le faire fonctionner pour l’url suivante:
http://example.com/foo?jt=permanent&loc=Stockholm

Évidemment, je ne veux pas changer d'objet de commande, car les noms de champs doivent rester longs (comme ils sont utilisés dans le code). Comment puis-je personnaliser cela? Y at-il une option pour faire quelque chose comme ça:

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

Cela ne fonctionne pas (@RequestParam ne peut pas être appliqué aux champs).

La chose à laquelle je pense est un convertisseur de message personnalisé similaire à FormHttpMessageConverter et qui lit une annotation personnalisée sur l'objet cible.

58
Bozho

Voici ce que j'ai travaillé:

Tout d'abord, un paramètre de résolution:

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

Et ensuite en enregistrant le résolveur de paramètres en utilisant un post-processeur. Il devrait être enregistré en tant que <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;
    }

}
13
Bozho

Cette solution est plus concise mais nécessite l'utilisation de RequestMappingHandlerAdapter, que Spring utilise lorsque <mvc:annotation-driven /> est activé . J'espère que cela aidera quelqu'un . L'idée est d'étendre ServletRequestDataBinder comme suit:

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

Processeur approprié:

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

Annotation: 

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

}

Configuration de printemps:

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

Et enfin, utilisation (comme la solution Bozho):

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

Dans Spring 3.1, ServletRequestDataBinder fournit un raccordement pour des valeurs de liaison supplémentaires:

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

La sous-classe ExtendedServletRequestDataBinder l'utilise pour ajouter des variables de modèle d'URI en tant que valeurs de liaison. Vous pouvez l'étendre davantage pour permettre d'ajouter des alias de champs spécifiques à une commande.

Vous pouvez remplacer RequestMappingHandlerAdapter.createDataBinderFactory (..) pour fournir une instance WebDataBinder personnalisée. Du point de vue du contrôleur, cela pourrait ressembler à ceci:

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

il n’existe pas de méthode Nice pour le faire, vous pouvez uniquement choisir la solution de contournement à appliquer. La différence entre la manipulation

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

et

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

est ce travail est un haricot et stringjob n'est pas (pas de surprise jusqu'à présent). La vraie différence est que les beans sont résolus avec le mécanisme standard de résolution de beans Spring, tandis que les paramètres de chaîne sont résolus par spring MVC, qui connaît le concept de l'annotation @RequestParam. Pour résumer la longue histoire, il n’ya aucun moyen dans la résolution standard du bean Spring (qui utilise des classes telles que PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor) de résoudre "jt" en une propriété appelée "jobType" ou du moins que je ne sache pas.

Les solutions de contournement peuvent être, comme d'autres l'ont suggéré, pour ajouter un PropertyEditor ou un filtre personnalisé, mais je pense que cela gâche le code. À mon avis, la solution la plus propre serait de déclarer une classe comme celle-ci:

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

    public void setJt(String jt) {
         super.job = jt;
    }

}

puis utilisez-le dans votre contrôleur

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

METTRE À JOUR :  

Une option légèrement plus simple consiste à ne pas étendre, simplement à ajouter les accesseurs supplémentaires, les ajusteurs à la classe d'origine

public class Job {

    private String jobType;
    private String location;

    public String getJt() {
         return jobType;
    }

    public void setJt(String jt) {
         jobType = jt;
    }

}
3
Peter Szanto

Je voudrais vous diriger dans une autre direction. Mais je ne sais pas si cela fonctionne .

Je voudrais essayer de manipuler la liaison elle-même.

Ceci est fait par WebDataBinder et sera invoqué à partir de HandlerMethodInvoker, méthode Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception

Je n’ai pas de regard approfondi dans Spring 3.1, mais ce que j’ai vu, c’est que cette partie du printemps a beaucoup changé. Il est donc possible d'échanger le WebDataBinder. Au printemps 3.0, il semble impossible de remplacer HandlerMethodInvoker.

2
Ralph

Vous pouvez utiliser Jackson com.fasterxml.jackson.databind.ObjectMapper pour convertir toute carte en votre classe DTO/POJO avec des accessoires imbriqués. Vous devez annoter vos POJO avec @JsonUnwrapped sur un objet imbriqué. Comme ça:

public class MyRequest {

    @JsonUnwrapped
    private NestedObject nested;

    public NestedObject getNested() {
        return nested;
    }
}

Et que l'utiliser comme ça:

@RequestMapping(method = RequestMethod.GET, value = "/myMethod")
@ResponseBody
public Object myMethod(@RequestParam Map<String, Object> allRequestParams) {

    MyRequest request = new ObjectMapper().convertValue(allRequestParams, MyRequest.class);
    ...
}

C'est tout. Un peu de codage. En outre, vous pouvez donner des noms à vos accessoires usign @JsonProperty.

2
Demel

Il y a une petite amélioration à la réponse de Jkee. 

Afin de prendre en charge l'héritage, vous devez également analyser les classes parent.

/**
 * ServletRequestDataBinder which supports fields renaming using {@link ParamName}
 *
 * @author jkee
 * @author Yauhen Parmon
 */
public class ParamRenamingProcessor extends ServletModelAttributeMethodProcessor {

    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    //Rename cache
    private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>();

    public ParamRenamingProcessor(boolean annotationNotRequired) {
       super(annotationNotRequired);
    }

    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
        Object target = binder.getTarget();
        Class<?> targetClass = Objects.requireNonNull(target).getClass();
        if (!replaceMap.containsKey(targetClass)) {
            replaceMap.put(targetClass, analyzeClass(targetClass));
        }
        Map<String, String> mapping = replaceMap.get(targetClass);
        ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), mapping);
        Objects.requireNonNull(requestMappingHandlerAdapter.getWebBindingInitializer())
                .initBinder(paramNameDataBinder);    
        super.bindRequestParameters(paramNameDataBinder, nativeWebRequest);
    }

    private Map<String, String> analyzeClass(Class<?> targetClass) {
        Map<String, String> renameMap = new HashMap<>();
        for (Field field : targetClass.getDeclaredFields()) {
            ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
            if (paramNameAnnotation != null && !paramNameAnnotation.value().isEmpty()) {
               renameMap.put(paramNameAnnotation.value(), field.getName());
            }
        }
        if (targetClass.getSuperclass() != Object.class) {
            renameMap.putAll(analyzeClass(targetClass.getSuperclass()));
        }
        return renameMap;
    }
}

Ce processeur analysera les champs des superclasses annotées avec @ParamName. De plus, il n’utilise pas la méthode initBinder avec 2 paramètres, qui est obsolète à partir de Spring 5.0. Tout le reste dans la réponse de jkee est OK. 

1
Yauhen Parmon

Il existe un moyen simple, vous pouvez simplement ajouter une méthode de définition supplémentaire, comme "setLoc, setJt".

1
Jack

Merci la réponse de @jkee.
Voici ma solution.
Premièrement, une annotation personnalisée:

@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamName {

  /**
   * The name of the request parameter to bind to.
   */
  String value();

}

Un client 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 paramètre résolveur:

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

}

Enfin, une configuration de bean permettant d’ajouter ParamNameProcessor au premier des résolveurs d’arguments:

@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éthode du contrôleur:

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

C'est tout.

1
Allen Kerr

Essayez d'intercepter la demande en utilisant InterceptorAdaptor, puis en utilisant un mécanisme de vérification simple pour décider de transmettre la demande au gestionnaire du contrôleur. Enroulez également HttpServletRequestWrapper autour de la demande pour vous permettre de remplacer les demandes getParameter().

De cette façon, vous pouvez repasser le nom du paramètre actuel et sa valeur dans la requête à consulter par le contrôleur.

Exemple d'option:

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

Enfin, enroulez la HandlerInterceptorAdaptor autour de votre gestionnaire de contrôleur, comme indiqué ci-dessous. La SelectedAnnotationHandlerMapping vous permet de spécifier quel gestionnaire sera intercepté.

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

&EACUTE;DIT&EACUTE;.

1
Bitmap