@RequestPart版本不兼容问题解决方案

     前言:

        起因很简单,公司需要一个通用服务。把其他服务比较高频会用到的功能 例如 发短信,邮件,生成表格,等等。单独抽取出来形成一个服务。以免重复编写

        我们编写好项目后,会写一个SDK来简化调用过程。但是为了图省事这个sdk用了openfegin

导致了,兼容到业务项目的时候。发生了冲突。而业务项目jar包不好升级,我们通用服务的jar包又不好降级。就很难受

     起因:

        说来简单,通用项目的 openfeign 版本为 3.0.6  业务系统的版本为 2.1.3

        低版本的不支持此注解: @RequestPart。关键是对于不认识的注解 feign 的逻辑会认为这是一个Body 当此注解在sdk出现了两次后就会报错:Method has too many Body parameters。。。后面省略

 有代码为证:

        

 我们就看需要的部分,逻辑很清晰:

1.processAnnotationsOnParameter 方法 没有认出 注解 返回了 false  

2.由于返回false  data.bodyIndex(i); 被置为非空

3.继续循环,再一次碰到无法解析的注解 发现 check data.bodyIndex(i); 已不为空

4.报错:Method has too many Body parameters。。。后面省略

深入:

processAnnotationsOnParameter:

既然是 processAnnotationsOnParameter 这个方法没有认出来注解,我们就去看看这个方法里干啥了

路径:SpringMvcContract.class -》processAnnotationsOnParameter 为此方法实现

 逻辑:

1.通过注解类型获取 AnnotatedParameterProcessor 的实现类

2.查看是否为空

3.整个逻辑唯一一次可能改变 isHttpAnnotation 布尔值的可能。而这个改变的方法是 实现类自己写的。

所有通过观察。2.1.3 版本的jar包中,根本没有这个注解,所以在步骤 2 时。根本没拿到。造成启动报错。

annotatedArgumentProcessors:

既然是从这里没有取到 那么我们来看看这个Map是什么时候初始化的,又是如何初始化的

//罪魁祸首的Map
private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;

//初始化方法
public SpringMvcContract(
			List<AnnotatedParameterProcessor> annotatedParameterProcessors) {
		this(annotatedParameterProcessors, new DefaultConversionService());
	}

//初始化Map
public SpringMvcContract(
			List<AnnotatedParameterProcessor> annotatedParameterProcessors,
			ConversionService conversionService) {
		Assert.notNull(annotatedParameterProcessors,
				"Parameter processors can not be null.");
		Assert.notNull(conversionService, "ConversionService can not be null.");

        /**
        * 主要的地方开始
        */
		List<AnnotatedParameterProcessor> processors;
		if (!annotatedParameterProcessors.isEmpty()) {
			processors = new ArrayList<>(annotatedParameterProcessors);
		}
		else {
			processors = getDefaultAnnotatedArgumentsProcessors();
		}
        //主要的地方end

		this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
		this.conversionService = conversionService;
		this.convertingExpanderFactory = new ConvertingExpanderFactory(conversionService);
	}

我们主要看上面代码的 if 判断  初始化这个代码的时候 或传入一个集合,如果集合为空,则用默认的逻辑初始化此Map 如果集合不为空 则直接用传入的集合初始化Map。

那么,到现在就有点放心了。既然溜了口子判断。那么也就证明了,一定有方法可以扩展。没有这个注解,我们从 3.0.6的jar包里扒下来扩展上去就行了 哈哈

ps:我们来对比一下  2.1.3 与 3.0.6 以后得jar包,这个默认初始化map的集合有什么不同:

可以看出,3.0.6 比 2.1.3多出了两个类型  这其中就有我需要的 RequestPartParameterProcessor

//2.1.3
private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {

		List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();

		annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
		annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
		annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
		annotatedArgumentResolvers.add(new QueryMapParameterProcessor());

		return annotatedArgumentResolvers;
	}


//3.0.6
private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {

		List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();

		annotatedArgumentResolvers.add(new MatrixVariableParameterProcessor());
		annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
		annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
		annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
		annotatedArgumentResolvers.add(new QueryMapParameterProcessor());
		annotatedArgumentResolvers.add(new RequestPartParameterProcessor());

		return annotatedArgumentResolvers;
	}

解决:

扩展注解

我们既然知道了它的逻辑,也看到了预留了扩展点。于是我们有了思路 从3.0.6上扒代码,扩展到2.1.3上

1.创建我们需要的注解

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestPart {

	/**
	 * Alias for {@link #name}.
	 */
	@AliasFor("name")
	String value() default "";

	/**
	 * The name of the part in the {@code "multipart/form-data"} request to bind to.
	 * @since 4.2
	 */
	@AliasFor("value")
	String name() default "";

	/**
	 * Whether the part is required.
	 * <p>Defaults to {@code true}, leading to an exception being thrown
	 * if the part is missing in the request. Switch this to
	 * {@code false} if you prefer a {@code null} value if the part is
	 * not present in the request.
	 */
	boolean required() default true;

}

2.从高版本扒此注解的处理类

/**
 * RequestPart 处理器
 * @author wangyuanquan
 * @data 2023/7/25 15:49
 */
public class RequestPartParameterProcessor implements AnnotatedParameterProcessor {

    private static final Class<RequestPart> ANNOTATION = RequestPart.class;

        @Override
        public Class<? extends Annotation> getAnnotationType() {
            return ANNOTATION;
        }

        @Override
        public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
            int parameterIndex = context.getParameterIndex();
            MethodMetadata data = context.getMethodMetadata();

            String name = ANNOTATION.cast(annotation).value();
            checkState(emptyToNull(name) != null, "RequestPart.value() was empty on parameter %s", parameterIndex);
            context.setParameterName(name);

            data.formParams().add(name);
            Collection<String> names = context.setTemplateParameter(name, data.indexToName().get(parameterIndex));
            data.indexToName().put(parameterIndex, names);
            return true;
        }

}

3.扩展 此处注意不能只放入自己的扩展注解,人家曾经的也要放进去。至于为什么上面逻辑阐述的很清楚,我就不在赘述了

/**
 * 将RequestPart 处理器 扫入配置
 * @author wangyuanquan
 * @data 2023/7/26 13:34
 */
@Configuration
public class RequestPartBean {

    @Bean
    public Contract feignContract(){
        List<AnnotatedParameterProcessor> processors = new ArrayList<>();
        processors.add(new RequestPartParameterProcessor());
        processors.add(new PathVariableParameterProcessor());
        processors.add(new RequestParamParameterProcessor());
        processors.add(new RequestHeaderParameterProcessor());
        processors.add(new QueryMapParameterProcessor());
        return new SpringMvcContract(processors);
    }
}

SDK中兼容 这是自己的业务了,用不到的可以忽略 判断是否 能从Bean中取值,取到的话,就按照业务方定义的来。取不到就用当前jar包的

 @Bean
    public ComponentFeignClient componentFeignClient() {

        Contract contract;
        try{
            contract = applicationContext.getBean(Contract.class);
        }catch (Exception e){
            contract = null;
        }

        return new ComponentFeignClient(componentProperties,contract);
    }



 public ComponentFeignClient(ComponentProperties properties, Contract contract) {
        if(properties == null) {
            throw new IllegalArgumentException("properties are not null");
        }
        this.properties = properties;

       if(contract == null){
           mvcContract = new SpringMvcContract();
       }else{
           mvcContract = (SpringMvcContract)contract;
       }
        init();
    }

启动成功了。但事情刚刚完成了一半

SpringEncoder:

我们知道OpenFeign有两个重要的组件  Encoder、Decoder。我们要想到既然低版本不支持这个注解,那么这两个组件会不会也需要兼容? 中途验证不聊了。直接上结论:Encoder 需要做兼容

我们来对比一下 2.1.3 与 3.0.6 的逻辑

/**
 * 2.1.3
 */

@Override
public void encode(Object requestBody, Type bodyType, RequestTemplate request)
			throws EncodeException {
		// template.body(conversionService.convert(object, String.class));
		if (requestBody != null) {
			Class<?> requestType = requestBody.getClass();
			Collection<String> contentTypes = request.headers().get("Content-Type");

			MediaType requestContentType = null;
			if (contentTypes != null && !contentTypes.isEmpty()) {
				String type = contentTypes.iterator().next();
				requestContentType = MediaType.valueOf(type);
			}

            //重点代码开始
			if (bodyType != null && bodyType.equals(MultipartFile.class)) {
				if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
                    //重点中的重点
					this.springFormEncoder.encode(requestBody, bodyType, request);
					return;
				}
            //重点代码结束
				else {
					String message = "Content-Type \"" + MediaType.MULTIPART_FORM_DATA
							+ "\" not set for request body of type "
							+ requestBody.getClass().getSimpleName();
					throw new EncodeException(message);
				}
			}

.......后面的省略



/**
 * 3.0.6
 */

@Override
public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
		// template.body(conversionService.convert(object, String.class));
		if (requestBody != null) {
			Collection<String> contentTypes = request.headers().get(HttpEncoding.CONTENT_TYPE);

			MediaType requestContentType = null;
			if (contentTypes != null && !contentTypes.isEmpty()) {
				String type = contentTypes.iterator().next();
				requestContentType = MediaType.valueOf(type);
			}
            //重点代码开始
			if (isFormRelatedContentType(requestContentType)) {
                //重点中的重点
				springFormEncoder.encode(requestBody, bodyType, request);
				return;
			}
            //重点代码结束
			else {
				if (bodyType == MultipartFile.class) {
					log.warn("For MultipartFile to be handled correctly, the 'consumes' parameter of @RequestMapping "
							+ "should be specified as MediaType.MULTIPART_FORM_DATA_VALUE");
				}
			}
			encodeWithMessageConverter(requestBody, bodyType, request, requestContentType);
		}
	}

本人个人见解。2.1.3写的有问题。因为它判断参数是否为 MultipartFile 类型 但实际上 除非人为干涉 否则在这步逻辑中 不可能出现 类型为 MultipartFile  并且我试验过。就算干涉改成 MultipartFile类型进去了 springFormEncoder 也会报错

好在,springFormEncoder 中的逻辑 两个版本是基本一致的 于是这里用了一个简单但不太好的做法。等待报错。报错后 用 springFormEncoder 进行解析。实在是偷懒了,大家可以不这么写

实现自己的 Encoder

于是我们实现自己的 Encoder  至此大功告成

/**
 * @author wangyuanquan
 * @data 2023/7/27 12:55
 */
@Slf4j
public class MySpringEncoder extends SpringEncoder {

    private SpringFormEncoder springFormEncoder;

    public MySpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
        super(messageConverters);
        springFormEncoder = new SpringFormEncoder();
    }

    @Override
    public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
        try{
            //不报错 则用默认的
            super.encode(requestBody, bodyType, request);
        }catch (Exception e){
            //报错了直接改到解析
            springFormEncoder.encode(requestBody, bodyType, request);
        }
    }
}


//注册自己的Encoder
 private final Encoder encoder = new MySpringEncoder(() -> new HttpMessageConverters(new MappingJackson2HttpMessageConverter(new ObjectMapper())));
//省略部分代码

 private <T> T createFeignClient(Class<T> clazz, SetterFactory setterFactory, FallbackFactory<? extends T> fallbackFactory) {
        return HystrixFeign.builder()
                .client(new ApacheHttpClient())
                 //注册进自己的encoder
                .encoder(encoder) 
                .decoder(decoder)
                .contract(mvcContract)
                .options(new Request.Options(properties.getTimeoutMillis(), properties.getTimeoutMillis()))
                .setterFactory(setterFactory)
                //默认是Logger.NoOpLogger
                .logger(new Slf4jLogger(clazz))
                //默认是Logger.Level.NONE
                .logLevel(Logger.Level.FULL)
                .target(clazz, properties.getBaseUrl(), fallbackFactory);
    }

最后:

其实SDK中不应该用Feign 自己搬石头砸自己脚了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值