You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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不需要作为切面使用,也可以选以下更符合业务语义的方案:

  1. 统一用Spring提供的工具类读取参数注解,替换原生的getParameterAnnotations()调用:
// 循环内读取参数注解的代码替换为
MethodParameter param = new MethodParameter(actionMethod, i);
ChannelValue channelValueAnno = param.getParameterAnnotation(ChannelValue.class);
Payload payloadAnno = param.getParameterAnnotation(Payload.class);
  1. 给所有自定义注解添加@Inherited元注解,让代理类可以继承原类的注解
  2. 给ChannelHandler实现类添加@Scope(proxyMode = ScopedProxyMode.NO),直接禁用该类的代理

内容的提问来源于stack exchange,提问作者camel38

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 01:36:07