Spring AOP: How to read path variable value from U

2019-02-28 02:31发布

问题:

I want to create Spring aspect which would set method parameter, annotated by custom annotation, to an instance of a particular class identified by an id from URI template. Path variable name is parameter of the annotation. Very similar to what Spring @PathVariable does.

So that controller method would look like:

@RestController
@RequestMapping("/testController")
public class TestController {

    @RequestMapping(value = "/order/{orderId}/delete", method = RequestMethod.GET)
    public ResponseEntity<?> doSomething(
            @GetOrder("orderId") Order order) {

        // do something with order
    }

}

Instead of classic:

@RestController
@RequestMapping("/testController")
public class TestController {

    @RequestMapping(value = "/order/{orderId}/delete", method = RequestMethod.GET)
    public ResponseEntity<?> doSomething(
            @PathVariable("orderId") Long orderId) {

        Order order = orderRepository.findById(orderId);
        // do something with order
    }
}

Annotation source:

// Annotation
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface GetOrder{

    String value() default "";
}

Aspect source:

// Aspect controlled by the annotation
@Aspect
@Component
public class GetOrderAspect {

    @Around( // Assume the setOrder method is called around controller method )
    public Object setOrder(ProceedingJoinPoint jp) throws Throwable{

        MethodSignature signature = (MethodSignature) jp.getSignature();
        @SuppressWarnings("rawtypes")
        Class[] types = signature.getParameterTypes();
        Method method = signature.getMethod();
        Annotation[][] annotations = method.getParameterAnnotations();
        Object[] values = jp.getArgs();

        for (int parameter = 0; parameter < types.length; parameter++) {
            Annotation[] parameterAnnotations = annotations[parameter];
            if (parameterAnnotations == null) continue;

            for (Annotation annotation: parameterAnnotations) {
                // Annotation is instance of @GetOrder
                if (annotation instanceof GetOrder) {
                    String pathVariable = (GetOrder)annotation.value();                        

                    // How to read actual path variable value from URI template?
                    // In this example case {orderId} from /testController/order/{orderId}/delete

                    HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder
                            .currentRequestAttributes()).getRequest();
                    ????? // Now what?

                }
           } // for each annotation
        } // for each parameter
        return jp.proceed();
    }
}

UPDATE 04/Apr/2017:

Answer given by Mike Wojtyna answers the question -> thus it is accepted.

Answer given by OrangeDog solves the problem form different perspective with existing Spring tools without risking implementation issue with new aspect. If I knew it before, this question would not be asked.

Thank you!

回答1:

If you already have access to HttpServletRequest, you can use HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE spring template to select map of all attributes in the request. You can use it like that:

request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE)

The result is a Map instance (unfortunately you need to cast to it), so you can iterate over it and get all the parameters you need.



回答2:

The easiest way to do this sort of thing is with @ModelAttribute, which can go in a @ControllerAdvice to be shared between multiple controllers.

@ModelAttribute("order")
public Order getOrder(@PathVariable("orderId") String orderId) {
    return orderRepository.findById(orderId);
}

@DeleteMapping("/order/{orderId}")
public ResponseEntity<?> doSomething(@ModelAttribute("order") Order order) {
    // do something with order
}

Another way is to implement your own PathVariableMethodArgumentResolver that supports Order, or register a Converter<String, Order>, that the existing @PathVariable system can use.



回答3:

Assuming that it is always the first parameter bearing the annotation, maybe you want to do it like this:

package de.scrum_master.aspect;

import java.lang.annotation.Annotation;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import de.scrum_master.app.GetOrder;

@Aspect
@Component
public class GetOrderAspect {
  @Around("execution(* *(@de.scrum_master.app.GetOrder (*), ..))")
  public Object setOrder(ProceedingJoinPoint thisJoinPoint) throws Throwable {
    MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
    Annotation[][] annotationMatrix = methodSignature.getMethod().getParameterAnnotations();
    for (Annotation[] annotations : annotationMatrix) {
      for (Annotation annotation : annotations) {
        if (annotation instanceof GetOrder) {
          System.out.println(thisJoinPoint);
          System.out.println("  annotation = " + annotation);
          System.out.println("  annotation value = " + ((GetOrder) annotation).value());
        }
      }
    }
    return thisJoinPoint.proceed();
  }
}

The console log would look like this:

execution(ResponseEntity de.scrum_master.app.TestController.doSomething(Order))
  annotation = @de.scrum_master.app.GetOrder(value=orderId)
  annotation value = orderId

If parameter annotations can appear in arbitrary positions you can also use the pointcut execution(* *(..)) but this would not be very efficient because it would capture all method executions for each component in your application. So you should at least limit it to REST controlers and/or methods with request mappings like this:

@Around("execution(@org.springframework.web.bind.annotation.RequestMapping * (@org.springframework.web.bind.annotation.RestController *).*(..))")

A variant of this would be

@Around(
  "execution(* (@org.springframework.web.bind.annotation.RestController *).*(..)) &&" +
  "@annotation(org.springframework.web.bind.annotation.RequestMapping)"
)