Search code examples
javaspringspring-mvcspring-aop

Spring MVC: How to read and change a @PathVariable value


This question is very similar to this one, but I dont know where to start.

Suppose I have an action like this:

@GetMapping("/foo/{id}")
public Collection<Foo> listById(@PathVariable("id") string id) {
    return null;
}

How could one intercept the listById method and change the value of id (Eg.: concat a string, pad with zeros etc)?

My scenario is that mostly of the IDs are left-padded with zeros (lengths differ) and I dont want to leave this to my ajax calls.

Expected solution:

@GetMapping("/foo/{id}")
public Collection<Foo> listById(@PathVariablePad("id", 4) string id) {
    // id would be "0004" on "/foo/4" calls
    return null;
}

Solution

  • Ok, here is how I've done it.

    Since we can't inherit annotations and thus @PathVariable's target are only parameters, we have to create a new annotation, as follows:

    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface PathVariablePad {
    
        int zeros() default 0;
    
        @AliasFor("name")
        String value() default "";
    
        @AliasFor("value")
        String name() default "";
    
        boolean required() default true;
    
    }
    

    Now we need to create a HandlerMethodArgumentResolver. In this case, since all I want is to left-pad a @PathVariable with zeros, we're going to inherit PathVariableMethodArgumentResolver, like this:

    public class PathVariablePadderMethodArgumentResolver extends PathVariableMethodArgumentResolver {
    
        private String leftPadWithZeros(Object target, int zeros) {
            return String.format("%1$" + zeros + "s", target.toString()).replace(' ', '0'); // Eeeewwwwwwwwwwww!
        }
    
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return parameter.hasParameterAnnotation(PathVariablePad.class);
        }
    
        @Override
        protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
            PathVariablePad pvp = parameter.getParameterAnnotation(PathVariablePad.class);
    
            return new NamedValueInfo(pvp.name(), pvp.required(), leftPadWithZeros("", pvp.zeros()));
        }
    
        @Override
        protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
            PathVariablePad pvp = parameter.getParameterAnnotation(PathVariablePad.class);
    
            return leftPadWithZeros(super.resolveName(name, parameter, request), pvp.zeros());
        }
    
    }
    

    Finally, let's register our method argument resolver (xml):

    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean class="my.package.PathVariablePadderMethodArgumentResolver" />
        </mvc:argument-resolvers>
    </mvc:annotation-driven>
    

    The usage is pretty simple and heres how to do this:

    @GetMapping("/ten/{id}")
    public void ten(@PathVariablePad(zeros = 10) String id) {
        // id would be "0000000001" on "/ten/1" calls
    }
    
    @GetMapping("/five/{id}")
    public void five(@PathVariablePad(zeros = 5) String id) {
        // id would be "00001" on "/five/1" calls
    }