3

I am trying to implement custom annotation and aspect which will insert path variable into request body before validation. For now it looks like this...

@Aspect
@Component
public class AddParameterToFormAspect {

@Before("@annotation(addParameterToForm)")
public void addParameterToForm(JoinPoint joinPoint, AddParameterToForm addParameterToForm) {
    String form = addParameterToForm.form();
    String pathVariable = addParameterToForm.pathVariable();
    CodeSignature methodSignature = (CodeSignature) joinPoint.getSignature();
    List<String> methodParamNames = Arrays.asList(methodSignature.getParameterNames());
    int formIndex = 0;
    int pathVariableIndex = 0;

    for(String s : methodSignature.getParameterNames()) {
        if(s.equals(form)) {
            formIndex = methodParamNames.indexOf(s);
        }
        if(s.equals(pathVariable)) {
            pathVariableIndex = methodParamNames.indexOf(s);
        }
    }

    Object[] methodArgs = joinPoint.getArgs();
    Object formObject = methodArgs[formIndex];
    Field pathVariableObject;

    try {
        pathVariableObject = formObject.getClass().getDeclaredField(pathVariable);
        pathVariableObject.setAccessible(true);
        pathVariableObject.set(formObject, methodArgs[pathVariableIndex]);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }

}

Controller example of working annotation...

@PostMapping("/test/{username}")
@AddParameterToForm(pathVariable = "username", form = "user")
public String test(@PathVariable String username, @RequestBody User user) {
    return user.getUsername();
}

Controller example of validation not working...

@PostMapping("/{domainCode}")
@AddParameterToForm(pathVariable = "domainCode", form = "userAddForm")
public ResponseEntity<UserDto> saveUserForDomain(@PathVariable(name="domainCode") String domainCode, @RequestBody @Valid  final UserAddForm userAddForm, BindingResult results) {...}

Adding path variable to form works but it seems @Valid no longer works, problem is probably in join point expression... How can I make it to do advice before validation and then validate?

Mido
  • 33
  • 5
  • OMG, I have just taken a look at your code. Are you seriously matching method parameter names encoded in annotations via reflection? This is slow, not type-safe and definitely not safely refactorable without breaking the application and only noticing during runtime. Apart from the initial AOP problem, this design is broken. Don't do it! Please explain what exactly you are trying to achieve. I am 100% sure there must be a better solution. This code is so contrived, nobody will understand it anymore in 3 months, not even you. – kriegaex Feb 08 '18 at 01:09
  • It gets even worse: You match annotation parameters also to field names in classes such as `User` and `AddParameterToForm`. This is a maintenance nightmare. Stay away from relying on parameter or field names and from reflection. – kriegaex Feb 08 '18 at 01:26
  • I have another question: If you are already doing the reflection thing anyway, would it not be better to match on parameter annotations such as `@RequestBody` and `@PathVariable` instead of using your own custom annotation redundantly encoding the same information? – kriegaex Feb 08 '18 at 02:11
  • Yeah, now when I think about it, it's really bad but at least it worked. Anyways I'm gona try solving the problem without custom annotation, using Filter or Interceptor. Thank you for all the help :) – Mido Feb 08 '18 at 13:39

1 Answers1

1

Changing method parameters in a @Before advice is not meant to work. You should use an @Around advice in order to change parameters before calling thisJoinPoint.proceed(). This is because when calling thisJoinPoint.getArgs() you get copies of primitive type parameters, you cannot manipulate the originals in a before-advice. You are lucky that you want to manipulate object types in this case, so that is the reason it works. Using an around-advice would enable you to pass completely new arguments to a method or just manipulate the original objects, you are free to choose.

Furthermore, you should - whenever possible - use args() in order to bind your method arguments of interest to advice parameters in order to be able to interact with them in a non-cryptic and type-safe manner. Creating a local variable and assigning some value to it will not influence the method parameter of the same type at all. Why should it?

Feel free to ask follow-up questions if this explanation is not comprehensive enough for you. Then I could add some sample code for you, too.


Update after question edit:

After having inspected you code a bit more closely and in addition to my remarks earlier today in my comments under your question, disregarding the content of your aspect code, your actual problem is that the validation check cause by @Valid annotations is performed before the method is executed. I.e. what is validated is not the state after the aspect has done its job (populate member fields in your target objects) but the state before the aspect runs. It is actually the same problem discussed in this question, see also M. Deinum's and my suggestions how to solve it:

  • Maybe you want to try full AspectJ via LTW (load-time weaving) and see if a call() pointcut instead of the implicit execution() pointcut used by Spring AOP solves the problem. You would weave into the calling code (method calls) instead of the callee (method execution) itself. Chances are, that this happens before validation is performed.

  • A more Spring-like way to solve it is to use a Spring interceptor (M. Deinum mentions HandlerInterceptor) instead of an aspect. There is also a link to an example by someone else.

Having said that, I still recommend to refactor your code so as not to use reflection and matching strings on method parameter names or class member names. I think you could also get rid of your custom annotation by matching your pointcut on methods parameters with @RequestBody and @PathVariable annotations.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • I updated the aspect (see original post) so now it works for any method... I already tried with Around but then my annotation doesn't even work, unlike when using Before. Could you add sample to my code so I can see how Around should be implemented? – Mido Feb 07 '18 at 14:00
  • Also I tried most of solutions for getting Around to work like adding EnableAspectJAutoProxy(proxyTargetClass = true), changing to public Object addParamterToForm... and then returning proceedingJoinPoint.proceed() but none of it seem to work... – Mido Feb 07 '18 at 15:07
  • I have updated my answer with info about the `@Valid` problem and possible solutions. – kriegaex Feb 08 '18 at 10:46