添加(自定义)解码器到WebMVC端点

huangapple go评论69阅读模式
英文:

Add (custom) decoder to WebMVC endpoint

问题

我有一个WebMVC端点:

@RequestMapping(path = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> execute(@PathVariable String id) {
   ...
}

在这里,首先应该对提供的id进行解码。是否有可能定义一个注解,在调用端点之前在“后台”执行此操作?类似于以下内容:

@RequestMapping(path = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> execute(@PathVariable @DecodedIdentifier String id) {
   ...
}

请注意@DecodedIdentifier注解。我知道它目前并不存在,但这个例子希望能解释我的意图。我知道Jersey的JAX-RS实现可以做到这一点,但是Spring的WebMVC呢?

在这里,我正在使用base64解码,但我想知道是否可以注入一个自定义解码器。

英文:

I have a WebMVC endpoint:

@RequestMapping(path = &quot;/execution/{id}&quot;, method = RequestMethod.POST)
public ResponseEntity&lt;...&gt; execute(@PathVariable String id) {
   ...
}

Here, the provided id should be decoded first. Is it possible to define an annotation which does this "in the background"; that is, prior to calling the endpoint? Something in the lines of:

@RequestMapping(path = &quot;/execution/{id}&quot;, method = RequestMethod.POST)
public ResponseEntity&lt;...&gt; execute(@PathVariable @DecodedIdentifier String id) {
   ...
}

Note the @DecodedIdentifier annotation. I know it does not exists, but it hopefully explains my intent. I know this is possible with Jersey's JAX-RS implementation, but what about Spring's WebMVC?

Here, I am using base64 decoding, but I wondering if I could inject a custom decoder as well.

答案1

得分: 4

虽然你可以使用注解,但我建议你为此目的使用自定义Converter

根据你的示例,你可以像这样做。

首先,你需要定义一个适合转换的自定义类。例如:

public class DecodedIdentifier {
  private final String id;

  public DecodedIdentifier(String id) {
    this.id = id;
  }

  public String getId() {
    return this.id;
  }
}

然后,为你的自定义类定义一个 Converter。它可以执行 Base64 解码:

public class DecodedIdentifierConverter implements Converter<String, DecodedIdentifier> {

  @Override
  public DecodedIdentifier convert(String source) {
    return new DecodedIdentifier(Base64.getDecoder().decode(source));
  }
}

为了告诉 Spring 关于这个转换器,你有几个选项。

如果你在运行 Spring Boot,你只需将该类标注为 @Component自动配置逻辑会处理 Converter 的注册。

@Component
public class DecodedIdentifierConverter implements Converter<String, DecodedIdentifier> {

  @Override
  public DecodedIdentifier convert(String source) {
    return new DecodedIdentifier(Base64.getDecoder().decode(source));
  }
}

确保配置你的组件扫描,以便 Spring 可以检测类中的 @Component 注解。

如果你在不使用 Spring Boot 的 Spring MVC 中,你需要手动注册这个 Converter

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new DecodedIdentifierConverter());
    }
}

在注册了 Converter 之后,你可以在你的 Controller 中使用它:

@RequestMapping(path = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> execute(@PathVariable DecodedIdentifier id) {
   ...
}

还有其他可以遵循的选项。请考虑阅读这篇文章,它将为你提供有关这个问题的进一步信息。

作为一个附带说明,上面提到的文章指出,你可以直接在类中定义一个 valueOf 方法,该方法将存储转换服务的结果,例如在你的例子中是 DecodedIdentifier,这将允许你摆脱 Converter 类:老实说,我从未尝试过这种方法,也不知道在什么条件下它可以工作。话虽如此,如果有效的话,它可以简化你的代码。如果你认为合适的话,请尝试一下。

更新

感谢 @Aman 的评论,我仔细审查了 Spring 文档。在那之后,我发现,虽然我认为前面提到的转换方法更适合这个用例 - 你实际上在执行转换 - 另一个可能的解决方案是使用自定义Formatter

我已经知道 Spring 使用这种机制来执行多次转换,但我之前不知道可以基于注解注册自定义格式化程序,这是答案中提出的原始想法。想象一下像DateTimeFormat这样的注解,这是完全合理的。实际上,在Stackoverflow上之前已经描述过这种方法(请参阅这个问题中的被接受的答案)。

对于你的情况(基本上是上述答案的转录),可以首先定义你的 DecodedIdentifier 注解:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface DecodedIdentifier {
}

实际上,你可以考虑通过包括信息应该在其中进行处理的编码方式来丰富这个注解。

然后,创建相应的 AnnotationFormatterFactory

import java.text.ParseException;
import java.util.Base64;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;

import org.springframework.context.support.EmbeddedValueResolutionSupport;
import org.springframework.format.AnnotationFormatterFactory;
import org.springframework.format.Formatter;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.stereotype.Component;

@Component
public class DecodedIdentifierFormatterFactory extends EmbeddedValueResolutionSupport
  implements AnnotationFormatterFactory<DecodedIdentifier> {

  @Override
  public Set<Class<?>> getFieldTypes() {
    return Collections.singleton(String.class);
  }

  @Override
  public Printer<?> getPrinter(DecodedIdentifier annotation, Class<?> fieldType) {
    return this.getFormatter(annotation);
  }

  @Override
  public Parser<?> getParser(DecodedIdentifier annotation, Class<?> fieldType) {
    return this.getFormatter(annotation);
  }

  private Formatter getFormatter(DecodedIdentifier annotation) {
    return new Formatter<String>() {
      @Override
      public String parse(String text, Locale locale) throws ParseException {
        // 如果注解可以提供有关要使用的编码的一些信息,这个逻辑将会非常可重用
        return new String(Base64.getDecoder().decode(text));
      }

      @Override
      public String print(String object, Locale locale) {
        return object;
      }
    };
  }
}

在你的 Spring MVC 配置中注册这个工厂:

@Configuration
@EnableWebMvc

<details>
<summary>英文:</summary>

Although you can use annotations, I recommend you to use a custom [```Converter```](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/convert/converter/Converter.html) for this purpose.

Following your example, you can do something like this.

First, you need to define a custom class suitable to be converted. For instance:

```java
public class DecodedIdentifier {
  private final String id;

  public DecodedIdentifier(String id) {
    this.id = id;
  }

  public String getId() {
    return this.id;
  }
}

Then, define a Converter for your custom class. It can perform the Base64 decoding:

public class DecodedIdentifierConverter implements Converter&lt;String, DecodedIdentifier&gt; {

  @Override
  public DecodedIdentifier convert(String source) {
    return new DecodedIdentifier(Base64.getDecoder().decode(source));
  }
}

In order to tell Spring about this converter you have several options.

If you are running Spring Boot, all you have to do is annotate the class as a @Component and the auto configuration logic will take care of Converter registration.

@Component
public class DecodedIdentifierConverter implements Converter&lt;String, DecodedIdentifier&gt; {

  @Override
  public DecodedIdentifier convert(String source) {
    return new DecodedIdentifier(Base64.getDecoder().decode(source));
  }
}

Be sure to configure your component scan so Spring can detect the @Component annotation in the class.

If you are using Spring MVC without Spring Boot, you need to register the Converter 'manually':

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new DecodedIdentifierConverter());
    }
}

After Converter registration, you can use it in your Controller:

@RequestMapping(path = &quot;/execution/{id}&quot;, method = RequestMethod.POST)
public ResponseEntity&lt;...&gt; execute(@PathVariable DecodedIdentifier id) {
   ...
}

There are also other options you can follow. Please, consider read this article, it will provide you further information about the problem.

As a side note, the above mentioned article indicates that you can directly define a valueOf method in the class which will store the result of the conversion service, DecodedIdentifier in your example, and it will allow you to get rid of the Converter class: to be honest, I have never tried that approach, and I do not know under which conditions it could work. Having said that, if it works, it can simplify your code. Please, if you consider it appropriate, try it.

UPDATE

Thanks to @Aman comment I carefully reviewed the Spring documentation. After that, I found that, although I think that the conversion approach aforementioned is better suited for the use case - you are actually performing a conversion - another possible solution could be the use of a custom Formatter.

I already knew that Spring uses this mechanism to perform multiple conversion but I were not aware that it is possible to register a custom formatter based on an annotation, the original idea proposed in the answer. Thinking about annotations like DateTimeFormat, it makes perfect sense. In fact, this approach were previously described here, in Stackoverflow (see the accepted answer in this question).

In your case (basically a transcription of the answer above mentioned for your case):

First, define your DecodedIdentifier annotation:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface DecodedIdentifier {
}

In fact, you can think of enriching the annotation by including, for example, the encoding in which the information should be processed.

Then, create the corresponding AnnotationFormatterFactory:

import java.text.ParseException;
import java.util.Base64;
import java.util.Collections;
import java.util.Locale;
import java.util.Set;

import org.springframework.context.support.EmbeddedValueResolutionSupport;
import org.springframework.format.AnnotationFormatterFactory;
import org.springframework.format.Formatter;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import org.springframework.stereotype.Component;

@Component
public class DecodedIdentifierFormatterFactory extends EmbeddedValueResolutionSupport
  implements AnnotationFormatterFactory&lt;DecodedIdentifier&gt; {

  @Override
  public Set&lt;Class&lt;?&gt;&gt; getFieldTypes() {
    return Collections.singleton(String.class);
  }

  @Override
  public Printer&lt;?&gt; getPrinter(DecodedIdentifier annotation, Class&lt;?&gt; fieldType) {
    return this.getFormatter(annotation);
  }

  @Override
  public Parser&lt;?&gt; getParser(DecodedIdentifier annotation, Class&lt;?&gt; fieldType) {
    return this.getFormatter(annotation);
  }

  private Formatter getFormatter(DecodedIdentifier annotation) {
    return new Formatter&lt;String&gt;() {
      @Override
      public String parse(String text, Locale locale) throws ParseException {
        // If the annotation could provide some information about the
        // encoding to be used, this logic will be highly reusable
        return new String(Base64.getDecoder().decode(text));
      }

      @Override
      public String print(String object, Locale locale) {
        return object;
      }
    };
  }
}

Register the factory in your Spring MVC configuration:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addFormatterForFieldAnnotation(new DecodedIdentifierFormatterFactory());
    }
}

Finally, use the annotation in your Controllers, exactly as you indicated in your question:

@RequestMapping(path = &quot;/execution/{id}&quot;, method = RequestMethod.POST)
public ResponseEntity&lt;...&gt; execute(@PathVariable @DecodedIdentifier String id) {
   ...
}

答案2

得分: 1

你可以通过实现HandlerMethodArgumentResolver来实现这一点:

public class DecodedIdentifierArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(DecodedIdentifier.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String value = webRequest.getParameterValues(parameter.getParameterName())[0];
        return Base64.getDecoder().decode(value);
    }
}
英文:

You can achieve this implementing a HandlerMethodArgumentResolver:

public class DecodedIdentifierArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(DecodedIdentifier.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String value = webRequest.getParameterValues(parameter.getParameterName())[0];
return Base64.getDecoder().decode(value);
}
}

答案3

得分: 0

自定义的 HandlerMethodArgumentResolver@PathVariable 或者 @RequestParam 一起使用的问题在于,它永远不会被执行,因为 @PathVariable@RequestParam 分别拥有它们自己的解析器,这些解析器会在任何自定义解析器之前被执行。如果我想要使用 Hashids 来混淆 Long 类型的 id 参数怎么办?那么参数必须以经过哈希处理的 String 形式传递,然后解码为原始的 Long 类型 id 值。我该如何进行转换类型的更改?

英文:

The problem with a custom HandlerMethodArgumentResolver and @PathVariable or @RequestParam is that it will never get executed, as @PathVariable and @RequestParam have their own resolvers each, which get executed prior to any custom resolvers. What if I want to obfuscate Long id param with Hashids? Then, the parameter has to be passed as a hashed String, get decoded to original Long id value. How do I provide conversion type change?

huangapple
  • 本文由 发表于 2020年10月15日 19:06:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/64370226.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定