问题
I have a WebMVC endpoint:
@RequestMapping(path = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> 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 = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> 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:
Although you can use annotations, I recommend you to use a custom Converter 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:
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<String, DecodedIdentifier> {
@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<String, DecodedIdentifier> {
@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 = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> 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<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 {
// 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 Controller
s, exactly as you indicated in your question:
@RequestMapping(path = "/execution/{id}", method = RequestMethod.POST)
public ResponseEntity<...> execute(@PathVariable @DecodedIdentifier String id) {
...
}
回答2:
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);
}
}
来源:https://stackoverflow.com/questions/64370226/add-custom-decoder-to-webmvc-endpoint