Issue
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.
Solution
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) {
...
}
Answered By - jccampanero
Answer Checked By - Terry (JavaFixing Volunteer)