Feign通过自定义注解实现路径的转义

Feign通过自定义注解实现路径的转义

本文主要讲解如果通过注解实现对路由中的路径进行自定义编码

背景

近期由于项目中需要,所以需要通过Feign封装一个对Harbor操作的sdk信息。
在调用的过程中发现,当请求参数中带有"/"时,Feign默认会将"/"当成路径去解析,而不是当成完整的一个参数解析,实例如下
请求路径为:api/v2.0/projects/{projectName}/repositories
注解参数为:@PathVariable("projectName")
正常请求为:api/v2.0/projects/test/repositories
异常路径为:api/v2.0/projects/test/pro/repositories
相信细心的同学已经发现上面的差异了,正常的{projectName}中对应的值为test,而异常的却对应为test/pro,所以当异常的请求打到harbor的机器时,被解析为api/v2.0/projects/test/pro/repositories,所以会直接返回404
以上就是背景了,所以接下来我们讨论一下解决方案

解决方案

首先我们知道springboot中默认是带有几种注释参数处理器的

@MatrixVariableParameterProcessor @PathVariableParameterProcessor @QueryMapParameterProcessor @RequestHeaderParameterProcessor @RequestParamParameterProcessor @RequestPartParameterProcessor 

因为我们的请求参数是在路径中的,所以默认我们会使用@PathVariableParameterProcessor来标识路径参数,而我们需要转义的参数其实也是在路径中,所以我们先来看一下@PathVariableParameterProcessor是如何实现的

public boolean processArgument(AnnotatedParameterProcessor.AnnotatedParameterContext context, Annotation annotation, Method method) {         String name = ((PathVariable)ANNOTATION.cast(annotation)).value();         Util.checkState(Util.emptyToNull(name) != null, "PathVariable annotation was empty on param %s.", new Object[]{context.getParameterIndex()});         context.setParameterName(name);         MethodMetadata data = context.getMethodMetadata();         String varName = '{' + name + '}';         if (!data.template().url().contains(varName) && !this.searchMapValues(data.template().queries(), varName) && !this.searchMapValues(data.template().headers(), varName)) {             data.formParams().add(name);         }         return true;     } 

其实在源码中,springboot并没有做什么神器的事情,就是获取使用了PathVariable注解的参数,然后再将其添加到fromParams中就可以。
看到这里我们是不是可以想到,既然在这里我们可以拿到对应的参数了,那想做什么事情不都是由我们自己来决定了,接下来说干就干,
首先我们声明一个属于自己的注解,

import org.springframework.core.annotation.AliasFor;  import java.lang.annotation.*;  /**   * @CreateAt: 2022/6/11 0:46  * @ModifyAt: 2022/6/11 0:46  * @Version 1.0  */ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SlashPathVariable {      /**      * Alias for {@link #name}.      */     @AliasFor("name")     String value() default "";      /**      * The name of the path variable to bind to.      * @since 4.3.3      */     @AliasFor("value")     String name() default "";      /**      * Whether the path variable is required.      * <p>Defaults to {@code true}, leading to an exception being thrown if the path      * variable is missing in the incoming request. Switch this to {@code false} if      * you prefer a {@code null} or Java 8 {@code java.util.Optional} in this case.      * e.g. on a {@code ModelAttribute} method which serves for different requests.      * @since 4.3.3      */     boolean required() default true; } 

声明完注解后,我们就需要来自定义自己的参数解析器了,首先继承AnnotatedParameterProcessor

import feign.MethodMetadata; import feign.Util; import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; import org.springframework.web.bind.annotation.PathVariable;  import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedType; import java.lang.reflect.Method; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Collectors;  /**  * @CreateAt: 2022/6/11 0:36  * @ModifyAt: 2022/6/11 0:36  * @Version 1.0  */ public class SlashPathVariableParameterProcessor implements AnnotatedParameterProcessor {      private  static  final Class<SlashPathVariable> ANNOTATION=SlashPathVariable.class;     @Override     public Class<? extends Annotation> getAnnotationType() {         return (Class<? extends Annotation>) ANNOTATION;     }      @Override     public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {         MethodMetadata data = context.getMethodMetadata();         String name = ANNOTATION.cast(annotation).value();         Util.checkState(Util.emptyToNull(name) != null, "SlashPathVariable annotation was empty on param %s.", new Object[]{context.getParameterIndex()});         context.setParameterName(name);         data.indexToExpander().put(context.getParameterIndex(),this::expandMap);         return true;     }      private String expandMap(Object object) {         String encode = URLEncoder.encode(URLEncoder.encode(object.toString(), Charset.defaultCharset()), Charset.defaultCharset());         return encode;     } } 

可以看到上面的代码,我们获取到自定义注解的参数后,将当前参数添加打Param后,并且为当前参数指定自定义的编码格式。
最后,我们再通过Bean的形式将对应的注解添加到容器中

import feign.Contract; import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; import org.springframework.cloud.openfeign.support.SpringMvcContract; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component;  import java.util.ArrayList; import java.util.List;  /**  * @CreateAt: 2022/6/11 0:48  * @ModifyAt: 2022/6/11 0:48  * @Version 1.0  */ @Component public class SlashBean {      @Bean     public Contract feignContract(){         List<AnnotatedParameterProcessor> processors=new ArrayList<>();         processors.add(new SlashPathVariableParameterProcessor());         return new SpringMvcContract(processors);     } } 

最后我们将上面的参数注解PathVariable换成我们自定义的@SlashPathVariable,就大功告成了

最后

通过以上的形式进行注入的话,会注入到Spring全局,所以在使用的过程中需要考虑是否符合场景

如有哪里讲得不是很明白或是有错误,欢迎指正
如您喜欢的话不妨点个赞收藏一下吧🙂

发表评论

相关文章