2

I have controller like

@MessageMapping("/room.register")
@SendTo("#{sendTo}")
public Message addUser(@Payload Message message,
                       SimpMessageHeaderAccessor headerAccessor) {
    headerAccessor.getSessionAttributes().put("username", 
    message.getSender());
    return message;

}

And i want to change value of SendTo annotation in runtime.

I tried to do it as follows:

@Aspect
public class SendToAspect {
@Autowired
private WebSocketConfigurationProperties webSocketConfigurationProperties;


@Around("execution (public * *(..)) && @annotation(ann)")
public Object execute(final ProceedingJoinPoint point, final SendTo ann) 
throws Throwable {

    MethodSignature signature = (MethodSignature) point.getSignature();
    Method method = signature.getMethod();
    method.setAccessible(true);

    Annotation[] annotations = method.getDeclaredAnnotations();
    for (int i = 0; i < annotations.length; i++) {
        if (annotations[i].annotationType().equals(SendTo.class)) {
            annotations[i] = new SendTo() {

                @Override
                public Class<? extends Annotation> annotationType() {
                    return SendTo.class;
                }

                @Override
                public String[] value() {
                    return new String[] 
                            {webSocketConfigurationProperties.getTopic()};
                }
            };
        }
    }
    return point.proceed();
}

}

but this only changes in the annotation array (Annotation[] annotations) and in the method annotations (method.getDeclaredAnnotations()) does not change.

Please tell me how to do this and is it possible at all?

resclub
  • 31
  • 5
  • Annotations are determined at compile time, which is why they can only contain constants. As for your question, you stepped into a trap called the [XY problem](https://meta.stackexchange.com/a/66378/309898). Instead of trying to explain **how** you want to solve your problem, tell us **what** you want to achieve. Why would anyone in their right mind want to change annotations? – kriegaex Aug 31 '19 at 10:45
  • @kriegaex Hi! I just want to read the annotation value (SendTo (value = "..")) from the properties value - apllication.yaml. I asked this question before but did not find a solution https://stackoverflow.com/questions/57677433/how-can-i-pass-properties-placeholder-to-annotations-from-yaml-config-file – resclub Aug 31 '19 at 18:04
  • You might want to look into [destination variable placeholders](https://stackoverflow.com/a/27055764/1082681) and also in my answer about [how to evaluate SpEL (Spring Expression Language)](https://stackoverflow.com/a/53825701/1082681). Maybe one of these two approaches is viable for you. I am not a Spring user, though, just an AOP expert. There might be better on-board means to achieve what you want. – kriegaex Sep 01 '19 at 01:37

1 Answers1

2

Quoting my own (slightly modified) comment first because I still think these two approaches are what you should try first:

You might want to look into

Maybe one of these two approaches is viable for you.

Having said that, you can also turn to the dark side of the force and really try to manipulate annotation values. Here is a little proof of concept in AspectJ (not Spring AOP, but the pointcut syntax would be identical):

Sample driver application:

package de.scrum_master.app;

import org.springframework.messaging.handler.annotation.SendTo;

public class Application {
  @SendTo("original")
  public void doSomething() throws NoSuchMethodException, SecurityException {
    SendTo sendTo = Application.class
      .getDeclaredMethod("doSomething")
      .getAnnotationsByType(SendTo.class)[0];
    System.out.println(sendTo.value()[0]);
  }

  public static void main(String[] args) throws NoSuchMethodException, SecurityException {
    new Application().doSomething();
    new Application().doSomething();
    new Application().doSomething();
  }
}

Without an aspect this would print:

original
original
original

No surprises here. Now use this aspect (in Spring AOP you should also add a @Component annotation):

package de.scrum_master.aspect;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Map;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.messaging.handler.annotation.SendTo;

@Aspect
public class SendToAspect {

  @Before("execution(* *(..)) && @annotation(sendTo)")
  public void changeAnnotation(JoinPoint thisJoinPoint, SendTo sendTo) {
    System.out.println(thisJoinPoint + "\n  [BEFORE] " + sendTo);
    changeAnnotationValue(sendTo, "value", new String[] { "changed" });
    System.out.println("  [AFTER]  " + sendTo);
  }

  @SuppressWarnings("unchecked")
  public static Object changeAnnotationValue(Annotation annotation, String key, Object newValue) {
    Object handler = Proxy.getInvocationHandler(annotation);
    Field f;
    try {
      f = handler.getClass().getDeclaredField("memberValues");
    } catch (NoSuchFieldException | SecurityException e) {
      throw new IllegalStateException(e);
    }
    f.setAccessible(true);
    Map<String, Object> memberValues;
    try {
      memberValues = (Map<String, Object>) f.get(handler);
    } catch (IllegalArgumentException | IllegalAccessException e) {
      throw new IllegalStateException(e);
    }
    Object oldValue = memberValues.get(key);
    if (oldValue == null || oldValue.getClass() != newValue.getClass()) {
      throw new IllegalArgumentException();
    }
    memberValues.put(key, newValue);
    return oldValue;
  }

}

Now the console log is:

execution(void de.scrum_master.app.Application.doSomething())
  [BEFORE] @org.springframework.messaging.handler.annotation.SendTo(value=[original])
  [AFTER]  @org.springframework.messaging.handler.annotation.SendTo(value=[changed])
changed
execution(void de.scrum_master.app.Application.doSomething())
  [BEFORE] @org.springframework.messaging.handler.annotation.SendTo(value=[changed])
  [AFTER]  @org.springframework.messaging.handler.annotation.SendTo(value=[changed])
changed
execution(void de.scrum_master.app.Application.doSomething())
  [BEFORE] @org.springframework.messaging.handler.annotation.SendTo(value=[changed])
  [AFTER]  @org.springframework.messaging.handler.annotation.SendTo(value=[changed])
changed

As you can see in the log above, executing the aspect advice every time the method is called is inefficient if you only need to change the value once and its value is not dynamic. Instead, you could also manipulate the annotation from another place outside of an aspect or add a static boolean member to the aspect in order to manipulate the annotation only once:

  static boolean done = false;

  @Before("execution(* *(..)) && @annotation(sendTo)")
  public void changeAnnotation(JoinPoint thisJoinPoint, SendTo sendTo) {
    if (done)
      return;
    System.out.println(thisJoinPoint + "\n  [BEFORE] " + sendTo);
    changeAnnotationValue(sendTo, "value", new String[] { "changed" });
    System.out.println("  [AFTER]  " + sendTo);
    done = true;
  }

Then the output would be:

execution(void de.scrum_master.app.Application.doSomething())
  [BEFORE] @org.springframework.messaging.handler.annotation.SendTo(value=[original])
  [AFTER]  @org.springframework.messaging.handler.annotation.SendTo(value=[changed])
changed
changed
changed

See also:

  • Java-EX project, helper method in my aspect above taken from there
  • Java-EX was also inspired by a SO question, by the way.
kriegaex
  • 63,017
  • 15
  • 111
  • 202