如何使用Spring创建自定义请求映射注解以添加前缀?

huangapple go评论76阅读模式
英文:

How to make custom request mapping annotation with spring to add prefixes?

问题

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping("/api/v1")
@interface APIv1 {
    @AliasFor(annotation = RequestMapping.class, attribute = "value")
    String[] uriComponents() default {};

    @AliasFor(annotation = RestController.class)
    String value() default "";
}
英文:

Using spring boot 2 on java 11, I want to make a custom annotation for each REST API version (eg: "/api/v1/") that can be joined with subsequent URI components as below:

@APIv1("/users/")    // this annotation should prepend "/api/v1/{argument}"
public class UserController {
    @GetMapping("/info")
    public String info() {return "This should be returned at /api/v1/users/info/";}

    /* More methods with mappings */
}

The problem is I don't know how to define that @APIv1 annotation.
From what I've searched, I referenced https://stackoverflow.com/a/51182494/ to write the following:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping("/api/v1")
@interface APIv1 {
    @AliasFor(annotation = RestController.class)
    String value() default "";
}

But this cannot handle arguments. Doing the same as above will route to /api/v1/info/ whether the argument is given or not. It's better than nothing since I can switch the method annotation to @GetMapping("/users/info"), but I was wondering if there was a way to combine the constant with an argument to reduce repetition across method annotations within the same controller class.

答案1

得分: 0

@APIv1中你定义了:

@RequestMapping("/api/v1")

所以它按照你的要求在运行。

英文:

In @APIv1 you defined:

@RequestMapping("/api/v1")

So it is working as you told it to.

答案2

得分: 0

使用自定义注解将前缀路径单独保存并在 WebMvcConfigurer 中进行连接后,我尝试了一些类似问题中提出的不同解决方案,最终我采用了以下方法:

控制器中的用法:

@APIv1("/users")
public class UserController {
    @GetMapping("/info")
    public String info() { return "This should be returned at /api/v1/users/info/"; }

    /* 更多带有映射的方法 */
}

自定义注解在另一个包中定义如下(例如:com.example.myannotations):

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrefixMapping {
    /**
     * 要添加到路径映射的前缀
     */
    String value() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@RestController
@RequestMapping
@PrefixMapping("/api/v1") // 使用此注解时要添加的前缀
public @interface APIv1 {
    /**
     * {@link RequestMapping#value} 的别名。
     */
    @AliasFor(annotation = RequestMapping.class)
    String value() default "";
}

并定义一个 WebMvcConfigurer,在启动时搜索注解:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                return true;
            }
        };
        provider.addIncludeFilter(new AnnotationTypeFilter(PrefixMapping.class));

        String pkgName = "com.example.myannotations"; // 自定义注解所在的包名
        provider.findCandidateComponents(pkgName).forEach(bean -> {
            try {
                String className = bean.getBeanClassName();
                Class<? extends Annotation> clz = (Class<? extends Annotation>) Class.forName(className);

                String prefix = clz.getDeclaredAnnotation(PrefixMapping.class).value();
                configurer.addPathPrefix(prefix, HandlerTypePredicate.forAnnotation(clz));
            } catch (ClassNotFoundException | ClassCastException e) {
                e.printStackTrace(System.err);
            }
        });
    }

}

现在,如果我想要一个具有不同前缀的注解,可以定义该注解而无需更改其他任何内容。

在编写 WebConfig 类时让我困扰的一件事是,尽管应该有函数可用于从 findCandidateComponents(pkgName) 返回的 BeanDefinition 获取注解类的 Class 对象,但是获取这个对象时遇到了困难。

我尝试过的函数                                  -> resulting Class<?>::getName() or null

- ClassUtils.getUserClass(bean.getClass())         -> org.springframework.context.annotation.ScannedGenericBeanDefinition
- AopUtils.getTargetClass(bean.getClass())         -> java.lang.Class - 如果使用 bean 而不是 bean.class()
- AopProxyUtils.ultimateTargetClass(bean.getClass()) -> java.lang.Class - 如果使用 bean 而不是 bean.class()
- AopProxyUtils.proxiedUserInterfaces(bean.getClass()) -> [java.io.Serializable, java.lang.reflect.GenericDeclaration, java.lang.reflect.Type, java.lang.reflect.AnnotatedElement] - 如果使用 bean 而不是 bean.getClass()
- bean.getResolvableType().resolve()               -> null
- bean.getResolvableType().toClass()               -> java.lang.Object
- bean.getResolvableType().getRawClass()           -> null
英文:

After trying around with different solutions proposed in similar questions, I settled on using my custom annotation to hold the prefix path separately and join them in WebMvcConfigurer

Usage in controllers:

@APIv1(&quot;/users&quot;)
public class UserController {
    @GetMapping(&quot;/info&quot;)
    public String info() {return &quot;This should be returned at /api/v1/users/info/&quot;;}

    /* More methods with mappings */
}

where the custom annotations are defined as follows (in a separate package, ie: com.example.myannotations):

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrefixMapping {
    /**
     * The prefix to prepend to path mappings
     * */
    String value() default &quot;&quot;;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@RestController
@RequestMapping
@PrefixMapping(&quot;/api/v1&quot;)    // Prefix to add when using this annotation
public @interface APIv1 {
    /**
     * Alias for {@link RequestMapping#value}.
     */
    @AliasFor(annotation = RequestMapping.class)
    String value() default &quot;&quot;;
}

And define a WebMvcConfigurer that will search for annotations on startup

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                return true;
            }
        };
        provider.addIncludeFilter(new AnnotationTypeFilter(PrefixMapping.class));

        String pkgName = &quot;com.example.myannotations&quot;;    // Package name where the custom annotations are defined in
        provider.findCandidateComponents(pkgName).forEach(bean -&gt; {
            try {
                String className = bean.getBeanClassName();
                Class&lt;? extends Annotation&gt; clz = (Class&lt;? extends Annotation&gt;) Class.forName(className);

                String prefix = clz.getDeclaredAnnotation(PrefixMapping.class).value();
                configurer.addPathPrefix(prefix, HandlerTypePredicate.forAnnotation(clz));
            } catch (ClassNotFoundException | ClassCastException e) {
                e.printStackTrace(System.err);
            }
        });
    }

}

Now if I want an annotation with different prefix, I can define the annotation without changing anything else.

One thing that troubled me when writing the WebConfig class was getting the Class object for the annotation class from the BeanDefinition returned by findCandidateComponents(pkgName) even though there were supposed to be functions for getting them available.

Functions I tried                                       -&gt; resulting Class&lt;?&gt;::getName() or null

- ClassUtils.getUserClass(bean.getClass())              -&gt; org.springframework.context.annotation.ScannedGenericBeanDefinition
- AopUtils.getTargetClass(bean.getClass())              -&gt; java.lang.Class    - ScannedGenericBeanDefinition if bean instead of bean.class()
- AopProxyUtils.ultimateTargetClass(bean.getClass())    -&gt; java.lang.Class    - ScannedGenericBeanDefinition if bean instead of bean.class()
- AopProxyUtils.proxiedUserInterfaces(bean.getClass())  -&gt; [java.io.Serializable, java.lang.reflect.GenericDeclaration, java.lang.reflect.Type, java.lang.reflect.AnnotatedElement]    - [org.springframework.beans.factory.annotation.AnnotatedBeanDefinition] if bean instead of bean.getClass()
- bean.getResolvableType().resolve()                    -&gt; null
- bean.getResolvableType().toClass()                    -&gt; java.lang.Object
- bean.getResolvableType().getRawClass()                -&gt; null

huangapple
  • 本文由 发表于 2023年2月18日 18:21:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/75492671.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定