4

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!

Community
  • 1
  • 1
Michal Foksa
  • 11,225
  • 9
  • 50
  • 68
  • Did you have a look at the source for how `@PathVariable` is used? – OrangeDog Apr 03 '17 at 12:39
  • No I did not, honestly I do not know where to start, it is quite obscure. – Michal Foksa Apr 03 '17 at 12:45
  • As an aside, you should be using an @DeleteMapping with a delete verb against a url of `/order/{orderId}`, not a 'get' with a `/order/{orderId}/delete` url. Also, IDs are usually better as uuids, rather than numbers. Further, isn't loading a resource before you delete it a waste of computing resources, given that in most backing stores you can issue a `delete by key` type command. – Software Engineer Apr 03 '17 at 12:59
  • Oh yeah, didn't spot that. If you use GETs for state-changing operations you're going to have a bad time. – OrangeDog Apr 03 '17 at 13:02
  • It is just an example, simplified real problem, nothing more.Thank you for advice. – Michal Foksa Apr 03 '17 at 13:19

3 Answers3

7

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.

Mike Wojtyna
  • 751
  • 5
  • 10
4

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.

OrangeDog
  • 36,653
  • 12
  • 122
  • 207
  • Your approach solves the problem form different, more elegant perspective. Thank you! – Michal Foksa Apr 04 '17 at 07:56
  • How can get pathVariable above of function in custom annotation? I ask in this [link](https://stackoverflow.com/questions/60297081) – mgh Feb 23 '20 at 11:19
1

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)"
)
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • I do not want to get value of `@GetOrder` annotation, but value of path variable identified by the `@GetOrder`. – Michal Foksa Apr 03 '17 at 13:23
  • But you need the annotation value in order to take the next step. I solved the corresponding Spring AOP problem for you (your code did not even compile in my IDE). I am an AOP expert and found the question because it is tagged with `spring-aop`, but not a Spring user. So this is the part I could do for you. The Spring part you have to solve by yourself. Maybe next time you ask two separate questions to find an expert for each. – kriegaex Apr 03 '17 at 14:19
  • Actually getting annotation value is solved the in the example `String pathVariable = (GetOrder)annotation.value();` – Michal Foksa Apr 03 '17 at 14:27
  • No, this snippet is exactly what does not compile, it has to be `((GetOrder) annotation).value()` ;-) Furthermore, it should be `if (annotation instanceof GetOrder)` instead of `if (annotation instanceof GetProfile)`. – kriegaex Apr 03 '17 at 15:50
  • Right, it was a copy/paste problem. I am sorry if it misled you. – Michal Foksa Apr 03 '17 at 16:46