前言:
起因很简单,公司需要一个通用服务。把其他服务比较高频会用到的功能 例如 发短信,邮件,生成表格,等等。单独抽取出来形成一个服务。以免重复编写
我们编写好项目后,会写一个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 自己搬石头砸自己脚了。