programing

Spring MVC 명령어개체를 바인드할 때 파라미터 이름을 커스터마이즈하려면 어떻게 해야 합니까?

cafebook 2023. 2. 27. 22:21
반응형

Spring MVC 명령어개체를 바인드할 때 파라미터 이름을 커스터마이즈하려면 어떻게 해야 합니까?

명령 개체가 있습니다.

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

spring-mvc에 의해 결합됩니다.

@RequestMapping("/foo")
public String 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대상 객체에 대한 사용자 지정 주석을 읽습니다.

이 솔루션에서는 보다 간결하지만 Request Mapping Handler Adapter를 사용해야 합니다.이러한 어댑터는 스프링이 다음 경우에 사용합니다.<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();

}

스프링 구성:

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

마지막으로 사용방법(Bozho 솔루션 등)은 다음과 같습니다.

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

}

Spring 3.1에서는 ServletRequestDataBinder는 추가 바인드 값에 대한 후크를 제공합니다.

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

Extended ServletRequestDataBinder 서브클래스는 이 서브클래스를 사용하여 URI 템플릿 변수를 바인딩 값으로 추가합니다.명령별 필드 별칭을 추가할 수 있도록 이 값을 더 확장할 수 있습니다.

RequestMappingHandlerAdapter.createDataBinderFactory()를 덮어쓰고 커스텀 WebDataBinder 인스턴스를 제공할 수 있습니다.컨트롤러의 관점에서 보면 다음과 같습니다.

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

@jkee의 답변 감사합니다.
제 해결책은 이렇습니다.
먼저 사용자 지정 주석:

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

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

}

고객의 Data Binder:

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

}

마지막으로 첫 번째 인수 리졸바에 ParamNameProcessor를 추가하기 위한 bean 설정입니다.

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

그게 다예요.

간단한 방법은 "setLoc, setJt"와 같은 setter 메서드를 하나 더 추가하는 것입니다.

적절한 방법은 없습니다.적용하는 회피책만 선택할 수 있습니다.취급의 차이

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

그리고.

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

그 일은 식은 죽 먹기지만, 현악기 일은 그렇지 않다(아직까지는 놀랍지 않다).실제 차이점은 콩은 표준 Spring bean resolver 메커니즘으로 해결되지만 문자열 파라미터는 @RequestParam 주석의 개념을 알고 있는 봄 MVC로 해결된다는 것입니다.요약하자면 표준 스프링빈 해상도(PropertyValues, PropertyValue, GenericTypeAwarePropertyDescriptor 등의 클래스 사용)에서는 "jt"를 "jobType"이라는 속성에 해결할 방법이 없습니다.

회피책은 커스텀 Property Editor 또는 필터를 추가하는 다른 제안과 같습니다만, 코드를 혼란스럽게 하는 것 같습니다.제 생각에 가장 깨끗한 해결책은 다음과 같은 클래스를 선언하는 것입니다.

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) {
   ...
}

갱신:

조금 더 간단한 옵션은 확장하지 않고 원래 클래스에 추가 getter와 setter를 추가하는 것입니다.

public class Job {

    private String jobType;
    private String location;

    public String getJt() {
         return jobType;
    }

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

}

Jackson com.fasterxml.jackson.databind를 사용할 수 있습니다.ObjectMapper: 중첩된 소품을 사용하여 지도를 DTO/POJO 클래스로 변환합니다.중첩된 개체에 @JsonUnwraped를 사용하여 POJO에 주석을 달아야 합니다.다음과 같이 합니다.

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

그게 다예요.약간의 코드화.또한 소품 @JsonProperty에 임의의 이름을 붙일있습니다.

다른 방향으로 안내하고 싶습니다.하지만 나는 그것이 효과가 있는지 모르겠다.

나는 바인딩 자체를 조작하려고 노력할 것이다.

은 것은에 it it it it it it it에 의해 .WebDataBinder 에에호호 and and and and에서 호출됩니다.HandlerMethodInvoker 법 methodObject[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception

3.이 되었습니다.Web Data Binder 따환수 。3.에서는 Spring 3.0을 않으면 합니다.HandlerMethodInvoker.

를 사용하여 요구를 대행 수신한 후 간단한 체크메커니즘을 사용하여 요구를 컨트롤러 핸들러에 전송할지 여부를 결정합니다.또한 랩HttpServletRequestWrapper에서는, 「」를 수 합니다.getParameter().

이렇게 하면 실제 파라미터 이름과 그 값을 컨트롤러에 의해 표시되는 요구에 다시 할당할 수 있습니다.

옵션 예:

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를 사용하면 감청할 핸들러를 지정할 수 있습니다.

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

편집.

jkee의 답변이 약간 개선되었습니다.

상속을 지원하려면 부모 클래스도 분석해야 합니다.

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

이 프로세서는 @ParamName으로 주석을 단 슈퍼클래스의 필드를 분석합니다. 이 문구는 하지 않습니다.initBinderSpring 5.0에서 권장되지 않는 2개의 매개 변수를 사용하는 방법입니다.OK를 하다

언급URL : https://stackoverflow.com/questions/8986593/how-to-customize-parameter-names-when-binding-spring-mvc-command-objects

반응형