Spring Boot新版本中自定义@ChannelHandler注解不被识别如何解决?
提问内容
我正在学习Packt出版的一份开发教程,其中示例用到了多个自定义注解,但该教程的代码是2018年产出的,Spring版本迭代后大概率出现了兼容问题。
目前在创建Bean时Spring无法识别我自定义的@ChannelHandler注解,本地运行时该注解完全不生效。
关键代码片段
自定义@ChannelHandler注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface ChannelHandler { /** * Channel patter, alias of value() */ String pattern() default ""; /** * The channel pattern that the handler will be mapped to by {@link WebSocketRequestDispatcher} * using Spring's {@link org.springframework.util.AntPathMatcher} */ String value() default ""; }
注解使用示例
@ChannelHandler("/board/*") public class BoardChannelHandler { private static final Logger log = LoggerFactory.getLogger(BoardChannelHandler.class); @Action("subscribe") public void subscribe(RealTimeSession session, @ChannelValue String channel) { log.debug("RealTimeSession[{}] Subscribe to channel `{}`", session.id(), channel); SubscriptionHub.subscribe(session, channel); } @Action("unsubscribe") public void unsubscribe(RealTimeSession session, @ChannelValue String channel) { log.debug("RealTimeSession[{}] Unsubscribe from channel `{}`", session.id(), channel); SubscriptionHub.unsubscribe(session, channel); } }
自定义@Action注解
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Action { /** * The action pattern. It needs to be an exact match. * <p>For example, "subscribe" */ String value() default ""; }
请问该问题的故障点是什么?新版Spring是否需要额外添加其他注解才能让这类自定义注解正常工作?
补充更新
关联业务代码
ChannelHandlerInvoker实现
public class ChannelHandlerInvoker { private static final Logger log = LoggerFactory.getLogger(ChannelHandlerInvoker.class); private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); private String channelPattern; private Object handler; // Key is the action, value is the method to handle that action private final Map<String, Method> actionMethods = new HashMap<>(); public ChannelHandlerInvoker(Object handler) { Assert.notNull(handler, "Parameter `handler` must not be null"); Class<?> handlerClass = handler.getClass(); ChannelHandler handlerAnnotation = handlerClass.getAnnotation(ChannelHandler.class); Assert.notNull(handlerAnnotation, "Parameter `handler` must have annotation @ChannelHandler"); Method[] methods = handlerClass.getMethods(); for (Method method : methods) { Action actionAnnotation = method.getAnnotation(Action.class); if (actionAnnotation == null) { continue; } String action = actionAnnotation.value(); actionMethods.put(action, method); log.debug("Mapped action `{}` in channel handler `{}#{}`", action, handlerClass.getName(), method); } this.channelPattern = ChannelHandlers.getPattern(handlerAnnotation); this.handler = handler; } public boolean supports(String action) { return actionMethods.containsKey(action); } public void handle(IncomingMessage incomingMessage, RealTimeSession session) { Assert.isTrue(antPathMatcher.match(channelPattern, incomingMessage.getChannel()), "Channel of the handler must match"); Method actionMethod = actionMethods.get(incomingMessage.getAction()); Assert.notNull(actionMethod, "Action method for `" + incomingMessage.getAction() + "` must exist"); // Find all required parameters Class<?>[] parameterTypes = actionMethod.getParameterTypes(); // All the annotations for each parameter Annotation[][] allParameterAnnotations = actionMethod.getParameterAnnotations(); // The arguments that will be passed to the action method Object[] args = new Object[parameterTypes.length]; try { // Populate arguments for (int i = 0; i < parameterTypes.length; i++) { Class<?> parameterType = parameterTypes[i]; Annotation[] parameterAnnotations = allParameterAnnotations[i]; // No annotation applied on this parameter if (parameterAnnotations.length == 0) { if (parameterType.isInstance(session)) { args[i] = session; } else { args[i] = null; } continue; } // Only use the first annotation applied on the parameter Annotation parameterAnnotation = parameterAnnotations[0]; if (parameterAnnotation instanceof Payload) { Object arg = JsonUtils.toObject(incomingMessage.getPayload(), parameterType); if (arg == null) { throw new IllegalArgumentException("Unable to instantiate parameter of type `" + parameterType.getName() + "`."); } args[i] = arg; } else if (parameterAnnotation instanceof ChannelValue) { args[i] = incomingMessage.getChannel(); } } actionMethod.invoke(handler, args); } catch (Exception e) { String error = "Failed to invoker action method `" + incomingMessage.getAction() + "` at channel `" + incomingMessage.getChannel() + "` "; log.error(error, e); session.error(error); } } }
ChannelHandlerResolver实现
@Component public class ChannelHandlerResolver { private static final Logger log = LoggerFactory.getLogger(ChannelHandlerResolver.class); private static final AntPathMatcher antPathMatcher = new AntPathMatcher(); // The key is the channel ant-like path pattern, value is the corresponding invoker private final Map<String, ChannelHandlerInvoker> invokers = new HashMap<>(); private ApplicationContext applicationContext; public ChannelHandlerResolver(ApplicationContext applicationContext) { this.applicationContext = applicationContext; this.bootstrap(); } public ChannelHandlerInvoker findInvoker(IncomingMessage incomingMessage) { ChannelHandlerInvoker invoker = null; Set<String> pathPatterns = invokers.keySet(); for (String pathPattern : pathPatterns) { if (antPathMatcher.match(pathPattern, incomingMessage.getChannel())) { invoker = invokers.get(pathPattern); } } if (invoker == null) { return null; } return invoker.supports(incomingMessage.getAction()) ? invoker : null; } private void bootstrap() { log.info("Bootstrapping channel handler resolver"); Map<String, Object> handlers = applicationContext.getBeansWithAnnotation(ChannelHandler.class); for (String handlerName : handlers.keySet()) { Object handler = handlers.get(handlerName); Class<?> handlerClass = handler.getClass(); ChannelHandler handlerAnnotation = handlerClass.getAnnotation(ChannelHandler.class); String channelPattern = ChannelHandlers.getPattern(handlerAnnotation); if (invokers.containsKey(channelPattern)) { throw new IllegalStateException("Duplicated handlers found for chanel pattern `" + channelPattern + "`."); } invokers.put(channelPattern, new ChannelHandlerInvoker(handler)); log.debug("Mapped channel `{}` to channel handler `{}`", channelPattern, handlerClass.getName()); } } }
更新2
目前我已经通过添加@Inherited注解、使用AnnotationUtils.findAnnotation()方法遍历父类方法的方式,让@ChannelHandler和@Action注解可以正常识别,但还无法获取方法参数上的@ChannelValue自定义注解值,执行Annotation[][] allParameterAnnotations = actionMethod.getParameterAnnotations();时返回null。
更新3(已解决)
只需要在ChannelHandler实现类(例如BoardChannelHandler)上添加@Aspect注解即可解决问题。
解答
核心故障点
该问题是Spring版本迭代后Bean代理逻辑变化导致的:
- 新版SpringBoot默认启用CGLIB代理所有Bean,你从ApplicationContext里拿到的
ChannelHandler实例实际上是CGLIB生成的代理类对象,不是你编写的原实现类对象。 - JDK原生的
getAnnotation()、getParameterAnnotations()方法只能读取当前类直接声明的注解,代理类默认不会继承原实现类的自定义注解,更不会保留原方法的参数注解,所以直接调用原生方法会返回null。 - 你之前用
AnnotationUtils解决了类和方法注解的读取问题,但参数注解的读取你还是用了原生方法,因此还是不兼容代理场景。
加@Aspect解决的原理
@Aspect是Spring切面专用注解,Spring扫描到该注解后,会把对应类标记为切面组件,不会对其进行普通业务Bean的CGLIB代理,你拿到的就是原实现类的实例,所以原生的注解读取方法都能正常拿到值。
其他可选替代方案
如果你的ChannelHandler不需要作为切面使用,也可以选以下更符合业务语义的方案:
- 统一用Spring提供的工具类读取参数注解,替换原生的
getParameterAnnotations()调用:
// 循环内读取参数注解的代码替换为 MethodParameter param = new MethodParameter(actionMethod, i); ChannelValue channelValueAnno = param.getParameterAnnotation(ChannelValue.class); Payload payloadAnno = param.getParameterAnnotation(Payload.class);
- 给所有自定义注解添加
@Inherited元注解,让代理类可以继承原类的注解 - 给
ChannelHandler实现类添加@Scope(proxyMode = ScopedProxyMode.NO),直接禁用该类的代理
内容的提问来源于stack exchange,提问作者camel38
相关产品推荐
相关产品推荐

