Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring AOP with RequestDispatcher causes recursive calls

Spring-servlet.xml:

<aop:config>
    <aop:advisor advice-ref="interceptor" pointcut="@annotation(Validator)"/>
</aop:config>

<bean id="interceptor" class="org.aopalliance.intercept.MethodInterceptor" />

MethodInterceptor invoke():

if (!valid){
   RequestDispatcher rd = request.getRequestDispatcher(errorView);
   rd.forward(request, response);
}

Working flow of control:

My interceptor is called before any Spring controller method that is annotated with the Validator annotation. The intention is to validate the request, if validation fails, forward the request to a different view. This is usually working. If there is an error (!valid), the RequestDispatcher.forward is called. This causes another Spring controller method to be called which ultimately shows the error view. This normally works.

Issue:

For some Spring controllers, my RequestDispatcher's errorView causes the request to be forwarded back to the same method causing an infinite loop (invoke()gets called over and over). I think this is because of how the Spring controller's request mappings (see below) are set up.

Error view: @RequestMapping(value = URL, params="error")

Normal view: @RequestMapping(value = URL, params="proceed")

So when the first request is routed it's got 'proceed' in the request params. Then when there's an error and the RequestDispatcher forwards to the view with the 'error' param in the query string, it should forward to the "Error view" method above, but it doesn't. It always forwards to the 'proceed' method causing an infinite loop on the MethodInterceptor invoke(). This seems to be because the 'proceed' parameter is still in the HttpServletRequest. However this isn't easy to fix because the whole point of the interceptor is that it has no knowledge of the Spring controller itself - it only knows if an error occurred, and that it should forward to the error view if an error occurred.

Workaround:

Using the request mappings below, it fixes the issue. This is probably because the HttpServletRequest parameter is overwritten when using the key=value notation.

Error view: @RequestMapping(value = URL, params="view=error")

Normal view: @RequestMapping(value = URL, params="view=proceed")

Question

How can I "properly" fix the issue without resorting to the workaround shown above? Is there a more standard way to forward to the correct spring controller?

like image 265
KyleM Avatar asked Nov 08 '22 16:11

KyleM


1 Answers

Solution#1:

Having configured as following:

Error view: @RequestMapping(value = URL, params="error")

Normal view: @RequestMapping(value = URL, params="proceed")

You could try for redirect as follows:

MethodInterceptor invoke():

if (!valid){

 //  RequestDispatcher rd = request.getRequestDispatcher(errorView);
 //  rd.forward(request, response);
     response.sendRedirect(errorView);
}

Drawback: the browser would make a second request, therefore the old method parameters are no longer in the httpservletrequest.

WorkArround: To Avoid drawback, You could use Spring MVC Flash Attribute. You could follow this tutorial to know how Flash Attribute works.

Refs:FlashAttributesExample

Solution#2:

How can I "properly" fix the issue without resorting to the workaround shown above? Is there a more standard way to forward to the correct spring controller?

You could incorporate by implementing you own RequestMappingHandlerAdapter.

Solution#3:

Here is the code for the aspect:

public class RequestBodyValidatorAspect {
  private Validator validator;

  @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
  private void controllerInvocation() {
  }

  @Around("controllerInvocation()")
  public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {

    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method method = methodSignature.getMethod();
    Annotation[][] argAnnotations = method.getParameterAnnotations();
    String[] argNames = methodSignature.getParameterNames();
    Object[] args = joinPoint.getArgs();

    for (int i = 0; i < args.length; i++) {
      if (hasRequestBodyAndValidAnnotations(argAnnotations[i])) {
        validateArg(args[i], argNames[i]);
      }
    }

    return joinPoint.proceed(args);
  }

  private boolean hasRequestBodyAndValidAnnotations(Annotation[] annotations) {
    if (annotations.length < 2)
      return false;

    boolean hasValid = false;
    boolean hasRequestBody = false;

    for (Annotation annotation : annotations) {
      if (Valid.class.isInstance(annotation))
        hasValid = true;
      else if (RequestBody.class.isInstance(annotation))
        hasRequestBody = true;

      if (hasValid &amp;&amp; hasRequestBody)
        return true;
    }
    return false;
  }

  @SuppressWarnings({"ThrowableInstanceNeverThrown"})
  private void validateArg(Object arg, String argName) {
    BindingResult result = getBindingResult(arg, argName);
    validator.validate(arg, result);
    if (result.hasErrors()) {
      throw new HttpMessageConversionException("Validation of controller input parameter failed",
              new BindException(result));
    }
  }

  private BindingResult getBindingResult(Object target, String targetName) {
    return new BeanPropertyBindingResult(target, targetName);
  }

  @Required
  public void setValidator(Validator validator) {
    this.validator = validator;
  }
}

One limitation with this work-around is that it can only apply a single validator to all controllers. You can also avoid it.

public class TypeMatchingValidator implements Validator, InitializingBean, ApplicationContextAware {
  private ApplicationContext context;
  private Collection<Validator> validators;

  public void afterPropertiesSet() throws Exception {
    findAllValidatorBeans();
  }

  public boolean supports(Class clazz) {
    for (Validator validator : validators) {
      if (validator.supports(clazz)) {
        return true;
      }
    }

    return false;
  }

  public void validate(Object target, Errors errors) {
    for (Validator validator : validators) {
      if (validator.supports(target.getClass())) {
        validator.validate(target, errors);
      }
    }
  }

  private void findAllValidatorBeans() {
    Map<String, Validator> validatorBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, Validator.class, true, false);
    validators = validatorBeans.values();
    validators.remove(this);
  }

  public void setApplicationContext(ApplicationContext context) throws BeansException {
    this.context = context;
  }
}

Spring XML configuration file using the validator aspect and the meta-validator together:

 <!-- enable Spring AOP support -->
  <aop:aspectj-autoproxy proxy-target-class="true"/>

  <!-- declare the validator aspect and inject the validator into it -->
  <bean id="validatorAspect" class="com.something.RequestBodyValidatorAspect">
    <property name="validator" ref="validator"/>
  </bean>

  <!-- inject the validator into the DataBinder framework -->
  <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="webBindingInitializer">
      <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer" p:validator-ref="validator"/>
    </property>
  </bean>

  <!-- declare the meta-validator bean -->
  <bean id="validator" class="com.something.TypeMatchingValidator"/>

  <!-- declare all Validator beans, these will be discovered by TypeMatchingValidator -->
  <bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
  <bean class="com.something.PersonValidator"/>
  <bean class="com.something.AccountValidator"/>

Resources Refs:scottfrederick:pring-3-Validation-Aspect

Solution#4:

Yet another solution for form validation using aop , you can check the blog: form-validation-using-aspect-oriented-programming-aop-in-spring-framework

like image 52
CrawlingKid Avatar answered Nov 11 '22 04:11

CrawlingKid