title: Spring Boot custom annotation design not working
body: <p>I am following a tutorial by PacktPublishing where some annotations are used in an example,
  but the code is from 2018 and there have probably been some changes.</p>
  <p>Spring does not recognize the Annotation when creating a bean.</p>
  <p>Specifically, here is an annotation design that just does not work for me locally:
  <a href="https://github.com/PacktPublishing/Building-Applications-with-Spring-5-and-Vue.js-2/blob/master/Chapter12/vuejs.spring-boot.mysql/src/main/java/com/taskagile/web/socket/handlers/BoardChannelHandler.java" rel="nofollow noreferrer">link</a></p>
  <p>Some important code snippets are:</p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-html lang-html prettyprint-override"><code>@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 "";
  
  }</code></pre>
  </div>
  </div>
  </p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-html lang-html prettyprint-override"><code>@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);
    }
  }</code></pre>
  </div>
  </div>
  </p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-html lang-html prettyprint-override"><code>@Target({ElementType.METHOD})
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  public @interface Action {
  
    /**
     * The action pattern. It needs to be an exact match.
     * &lt;p&gt;For example, "subscribe"
     */
    String value() default "";
  }</code></pre>
  </div>
  </div>
  </p>
  <p><strong>Can you see what the issue is here? Is there some other annotation missing for newer versions
  of Spring?</strong></p>
  <p><strong>UPDATE - adding other necessary code.</strong></p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-html lang-html prettyprint-override"><code>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&lt;String, Method&gt; actionMethods = new HashMap&lt;&gt;();
  
    public ChannelHandlerInvoker(Object handler) {
      Assert.notNull(handler, "Parameter `handler` must not be null");
  
      Class&lt;?&gt; 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&lt;?&gt;[] 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 &lt; parameterTypes.length; i++) {
          Class&lt;?&gt; 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);
      }
    }
  }</code></pre>
  </div>
  </div>
  </p>
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="false">
  <div class="snippet-code">
  <pre class="snippet-code-html lang-html prettyprint-override"><code>@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&lt;String, ChannelHandlerInvoker&gt; invokers = new HashMap&lt;&gt;();
  
    private ApplicationContext applicationContext;
  
    public ChannelHandlerResolver(ApplicationContext applicationContext) {
      this.applicationContext = applicationContext;
      this.bootstrap();
    }
  
    public ChannelHandlerInvoker findInvoker(IncomingMessage incomingMessage) {
      ChannelHandlerInvoker invoker = null;
      Set&lt;String&gt; 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&lt;String, Object&gt; handlers = applicationContext.getBeansWithAnnotation(ChannelHandler.class);
      for (String handlerName : handlers.keySet()) {
        Object handler = handlers.get(handlerName);
        Class&lt;?&gt; 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());
      }
    }
  }
  
  
  &lt;!-- begin snippet: js hide: false console: true babel: false --&gt;</code></pre>
  </div>
  </div>
  </p>
  <p><strong>UPDATE 2</strong></p>
  <p>I have managed to make ChannelHandler and Action annotations work by adding @Inherited annotation and using AnnotationUtils.findAnnotation() which traverses its super methods if the annotation is not directly present on the given method itself.</p>
  <p><strong>However, I haven't managed to access custom annotation value of type parameter (ChannelValue)</strong></p>
  <p>Here, <code>Annotation[][] allParameterAnnotations = actionMethod.getParameterAnnotations();</code></p>
  <p>returns null value.</p>
  <p><strong>UPDATE 3 -&gt; SOLVED</strong></p>
  <p>Just add @Aspect annotation to your ChannelHandler implementation (e.g.
  &quot;BoardChannelHandler&quot;).</p>