问题
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?
回答1:
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
- destination variable placeholders for
@SendTo
or@SubscribeMapping
and also- my answer about how to evaluate SpEL (Spring Expression Language).
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.
来源:https://stackoverflow.com/questions/57733914/how-i-can-change-method-annotations-value-in-runtime