advise controller method *before* @Valid annotation is handled
Asked Answered
T

3

3

I am adding rate-limiting to a restful webservice using Spring MVC 4.1.

I created a @RateLimited annotation that I can apply to controller methods. A Spring AOP aspect intercepts calls to these methods and throws an exception if there have been too many requests:

@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RateLimitingAspect {

    @Autowired
    private RateLimitService rateLimitService;

    @Before("execution(* com.example..*.*(.., javax.servlet.ServletRequest+, ..)) " +
            "&& @annotation(com.example.RateLimited)")
    public void wait(JoinPoint jp) throws Throwable {

        ServletRequest request =
            Arrays
                .stream(jp.getArgs())
                .filter(Objects::nonNull)
                .filter(arg -> ServletRequest.class.isAssignableFrom(arg.getClass()))
                .map(ServletRequest.class::cast)
                .findFirst()
                .get();
        String ip = request.getRemoteAddr();
        int secondsToWait = rateLimitService.secondsUntilNextAllowedAttempt(ip);
        if (secondsToWait > 0) {
          throw new TooManyRequestsException(secondsToWait);
        }
    }

This all works perfectly, except when the @RateLimited controller method has parameters marked as @Valid, e.g.:

@RateLimited
@RequestMapping(method = RequestMethod.POST)
public HttpEntity<?> createAccount(
                           HttpServletRequest request,
                           @Valid @RequestBody CreateAccountRequestDto dto) {

...
}

The problem: if validation fails, the validator throws MethodArgumentNotValidException, which is handled by an @ExceptionHandler, which returns an error response to the client, never triggering my @Before and therefore bypassing the rate-limiting.

How can I intercept a web request like this in a way that takes precedence over parameter validation?

I've thought of using Spring Interceptors or plain servlet Filters, but they are mapped by simple url-patterns and I need to differentiate by GET/POST/PUT/etc.

Tadzhik answered 10/3, 2015 at 22:9 Comment(1)
It is a good problem. Wondering, what is @RateLimited doing. Default field validation using @Valid is painful in some cases. Is it possible for you to create your own Validator. I can think of adding Header to differentiate this specific request. But that would add conditional check for each request. I would try to solve it at method level and if that does not work then work my way up to interceptorSikata
T
7

I eventually gave up on trying to find an AOP solution and created a Spring Interceptor instead. The interceptor preHandles all requests and watches for requests whose handler is @RateLimited.

@Component
public class RateLimitingInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private final RateLimitService rateLimitService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (HandlerMethod.class.isAssignableFrom(handler.getClass())) {
            rateLimit(request, (HandlerMethod)handler);
        }
        return super.preHandle(request, response, handler);
    }

    private void rateLimit(HttpServletRequest request, HandlerMethod handlerMethod) throws TooManyRequestsException {

        if (handlerMethod.getMethodAnnotation(RateLimited.class) != null) {
            String ip = request.getRemoteAddr();
            int secondsToWait = rateLimitService.secondsUntilNextAllowedInvocation(ip);
            if (secondsToWait > 0) {
                throw new TooManyRequestsException(secondsToWait);
            } else {
                rateLimitService.recordInvocation(ip);
            }
        }
    }
}
Tadzhik answered 1/4, 2015 at 17:51 Comment(0)
T
1

Add the following controller advice in your application.

@ControllerAdvice  
public class ApplicationControllerAdvice {

@InitBinder
@RateLimited
protected void activateBeanPropertyAccess(DataBinder dataBinder) {
    dataBinder.initBeanPropertyAccess();
}
}

The @RateLimited should call the class RateLimitingAspect. So, after this all the constraints validator will be called.

Tyndale answered 24/5, 2022 at 10:2 Comment(0)
A
0

See if it's feasible for you to implement similar logic for @@AfterThrowing advice as well which will have similar pointcut.

Argosy answered 11/3, 2015 at 7:41 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.