M功能-open feign的使用-支付系统(四)

target:离开柬埔寨倒计时-219day

在这里插入图片描述

这张图片一直是我idea的背景图,分享出来啦…

前言

支付平台使用的是基于springcloud的微服务,服务之间的调用都是使用openfeign,而我们每个服务对外暴露的接口响应都会在外部封装一层code之类的信息(二进制下载除外,如:图片、文件下载等)。所以我们内部服务调用时也会有这一层的封装。

这层封装产生的问题

封装结构类,所有响应统一使用该封装类

package com.littlehow;import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;/*** @author littlehow* @since 5/27/24 18:13*/
@Slf4j
@Setter
@Getter
@Accessors(chain = true)
public class BaseResp<T> {// 成功的code和messageprivate static final String SUCCESS_RESP_CODE = "0";private static final String SUCCESS_MSG = "success";private static final String TRACE_ID = "X-B3-TraceId";@ApiModelProperty(value = "状态码", required = true)private String code;@ApiModelProperty(value = "返回信息")private String msg;@ApiModelProperty(value = "返回数据", required = true)private T data;@ApiModelProperty(value = "全局调用id")private String traceId;@ApiModelProperty(value = "系统毫秒时间戳")private Long systemTime;private BaseResp() {traceId = MDC.get(TRACE_ID);systemTime = System.currentTimeMillis();}/***  成功后响应的数据* @param data  - 原始接口的返回值* @return - 包装后的数据*/public static <T> BaseResp<T> success(T data) {return new BaseResp().setCode(SUCCESS_RESP_CODE).setMsg(SUCCESS_MSG).setData(data);}/***  失败响应码,国际化也可以基于code来支持* @param code     - 错误码* @param message  - 错误信息* @return*/public static BaseResp fail(String code, String message) {return new BaseResp().setCode(code).setMsg(message);}/***  是否成功,如果失败可以打印日志* @return - true表示成功*/public boolean success() {boolean isSuccess = SUCCESS_RESP_CODE.equals(code);if (!isSuccess) {// 这里也可以打印debug日志,使用级别来控制是否打印该行日志log.error("errorCode:{}, errorMessage={}", code, msg);}return isSuccess;}
}

统一处理类来进行对响应的统一处理

1.成功响应类

package com.littlehow.advice;import com.alibaba.fastjson.JSONObject;
import com.littlehow.BaseResp;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;public class SuccessResponseAdvice implements ResponseBodyAdvice<Object> {private static Map<Method, Boolean /* ignore advice */> ignore = new ConcurrentHashMap<>();/*** 这里可以在接口上定义注解以解除统一包装该接口,如果媒体类相关接口* @param returnType    -* @param converterType -* @return -*/@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {Method method = returnType.getMethod();Boolean value = ignore.get(method);if (value != null) {return value;} else {boolean result = true;Annotation[] annotations = returnType.getMethodAnnotations();for (Annotation annotation : annotations) {if (annotation instanceof IgnoreAdvice) {result = false;break;}}ignore.put(method, result);return result;}}@ResponseBody@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,Class<? extends HttpMessageConverter<?>> selectedConverterType,ServerHttpRequest request, ServerHttpResponse response) {if (body instanceof BaseResp) {return body;} else if (selectedConverterType == StringHttpMessageConverter.class) {// string类型返回对象的话会出现强转异常,因为stringhttpmessageconverter只接收string的参数response.getHeaders().add("content-type", MediaType.APPLICATION_JSON_VALUE);return JSONObject.toJSONString(BaseResp.success(body));}return BaseResp.success(body);}}

1.异常响应类

package com.littlehow.advice;import com.littlehow.BaseResp;
import com.littlehow.error.BizError;
import com.littlehow.error.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.text.MessageFormat;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;/*** 全局异常处理器基类*/
@Slf4j
public class BaseGlobalExceptionHandler {private static Map<String, String> getErrMessageMap(BindingResult bindingResult) {return bindingResult.getAllErrors().stream().collect(Collectors.toMap(error -> {if (error instanceof FieldError) {FieldError fieldError = (FieldError) error;return fieldError.getField();}return error.getObjectName();},item -> Optional.ofNullable(item.getDefaultMessage()).orElse(""),(o1, o2) -> o1));}/*** 拦截自定义的异常*/@ExceptionHandler(BizError.class)@ResponseStatus(value = HttpStatus.OK)public BaseResp<?> handleBizException(HttpServletRequest request, BizError e) {log.error(e.getMessage(), e);return BaseResp.fail(e.getCode(), getLocaleMessage(e.getCode(), e.getMessage(),request, e.getArgs()));}/*** controller入参校验异常*/@ResponseStatus(value = HttpStatus.OK)@ExceptionHandler(value = MethodArgumentNotValidException.class)public BaseResp<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {log.error("MethodArgumentNotValidException: {}, {}", e.getMessage(), e);return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), Objects.toString(getErrMessageMap(e.getBindingResult())));}/*** controller入参校验异常*/@ResponseStatus(value = HttpStatus.OK)@ExceptionHandler(value = HttpMessageNotReadableException.class)public BaseResp<?> methodArgumentNotValidExceptionHandler(HttpMessageNotReadableException e) {log.error("MethodArgumentNotValidException: {}, {}", e.getMessage(), e);String errorMessage = e.getLocalizedMessage();return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), errorMessage);}/*** controller入参校验异常*/@ResponseStatus(value = HttpStatus.OK)@ExceptionHandler(value = ConstraintViolationException.class)public BaseResp<?> validationErrorHandler(ConstraintViolationException e) {log.error("ConstraintViolationException: {}, {}", e.getMessage(), e);return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), e.getMessage());}/*** controller入参校验异常*/@ResponseStatus(value = HttpStatus.OK)@ExceptionHandler(value = BindException.class)public BaseResp<?> bindExceptionHandler(BindException e) {log.error("BindException: {}, {}", e.getMessage(), e);return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), e.getMessage());}@ResponseStatus(value = HttpStatus.OK)@ExceptionHandler(value = IllegalArgumentException.class)public BaseResp<?> bindArgumentException(IllegalArgumentException e) {return BaseResp.fail(ErrorCode.PARAM_ERROR_DESC.getCode(), e.getMessage());}/*** 获取国际化异常信息(支持url中包含lang和浏览器的Accept-Language)*/protected String getLocaleMessage(String code, String defaultMsg, HttpServletRequest request, Object[] params) {// 示例就不国际化的处理放出来了try {return MessageFormat.format(defaultMsg, params);} catch (Exception e1) {return defaultMsg;}
//        try {
//            return messageSource.getMessage(code, params, LocalUtils.getLocale(request));
//        } catch (Exception e) {
//            log.warn("本地化异常消息发生异常: {}, {}", code, params);
//            try {
//                return MessageFormat.format(defaultMsg, params);
//            } catch (Exception e1) {
//                return defaultMsg;
//            }
//        }}
}

3.忽略封装的注解

package com.littlehow.advice;import java.lang.annotation.*;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface IgnoreAdvice {
}

这里封装在服务调用中就比较麻烦了,首先就是接口的定义

在自己的服务中接口的定义一般是下面这样的片段

public interface UserClient {/*** 获取用户信息*/@RequestMapping("/user/get")UserInfoResp getUserInfo(QueryUserInfoReq req);
}public class UserController implements UserClient {/*** 获取用户信息*/public UserInfoResp getUserInfo(@RequestBody @Valid QueryUserInfoReq req) {// skip detailreturn new UserInfoResp();}
}

错误码和异常相关代码

package com.littlehow.error;/*** @author littlehow* @since 5/27/24 17:45*/
public interface IErrCode {/*** 错误码* @return -*/String getCode();/*** 错误信息* @return -*/String getDesc();
}=======================================================package com.littlehow.error;import lombok.Getter;/*** @author littlehow* @since 5/27/24 17:47*/
@Getter
public enum ErrorCode implements IErrCode {SYSTEM_BUSY("SS101", "system busy"),KEY_INVALID("B1001", "非法密码"),PARAM_ERROR_DESC("B1002", "参数错误:{0}"),PARAM_ERROR("B1003", "参数错误"),PARAM_VERIFY_ERROR("B1004", "验签失败"),INVALID_REQUEST("B1005", "非法请求"),DATA_ERROR("B1006", "数据错误"),DATA_DEAL("B1007", "数据已处理"),DATA_EXISTS("B1008", "数据已存在"),DATA_NOT_EXISTS("B1009", "数据不存在"),CALL_RPC_ERROR("B1011", "调用三方接口失败"),;/*** 枚举编码*/private String code;/*** 描述说明*/private String desc;ErrorCode(String code, String desc) {this.code = code;this.desc = desc;}
}==============================================================
package com.littlehow.error;import lombok.Getter;/*** @author littlehow* @since 5/27/24 18:43*/
@Getter
public class DynamicErrorCode implements IErrCode {private final String code;private final String desc;public DynamicErrorCode(String code, String desc) {this.code = code;this.desc = desc;}
}======================================================================
package com.littlehow.error;import org.springframework.util.Assert;import java.text.MessageFormat;/*** @author littlehow* @since 5/27/24 17:49*/
public class BizError extends RuntimeException {protected IErrCode code;private String[] args;/*** @param code 错误码*/public BizError(IErrCode code, String... args) {Assert.notNull(code, "code must be not null");this.args = args;this.code = code;}public String getMessage() {return getFormatMessage();}public String getCode() {return code.getCode();}public String getFormatMessage() {if (args != null && args.length > 0) {return MessageFormat.format(code.getDesc(), args);}return code.getDesc();}public String[] getArgs() {return args;}public String toString() {return "BizError(code=" + code.getCode() + ", desc={}" + getFormatMessage()+")";}
}===========================================================================================
package com.littlehow.error;import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;import java.math.BigDecimal;
import java.util.Collection;/*** @author littlehow* @since 5/27/24 18:39*/
public class BizAssert {public static void isTrue(boolean flag, IErrCode message, String... obj) {if (!flag) {throw new BizError(message, obj);}}public static void notNull(Object obj, IErrCode message, String... argus) {if (obj == null) {throw new BizError(message, argus);}}public static void isNull(Object obj, IErrCode message, String... argus) {if (obj != null) {throw new BizError(message, argus);}}public static void isZero(int data, IErrCode message, String... argus) {if (data != 0) {throw new BizError(message, argus);}}public static void isZero(Long data, IErrCode message, String... argus) {if (data != null && data != 0) {throw new BizError(message, argus);}}public static void isZero(BigDecimal value, IErrCode message, String... argus) {if (value == null || value.compareTo(BigDecimal.ZERO) != 0) {throw new BizError(message, argus);}}public static void notEmpty(Collection collection, IErrCode message, String... argus) {if (CollectionUtils.isEmpty(collection)) {throw new BizError(message, argus);}}public static void isEmpty(Collection collection, IErrCode message, String... argus) {if (!CollectionUtils.isEmpty(collection)) {throw new BizError(message, argus);}}public static void hasText(String value, IErrCode message, String... argus) {if (!StringUtils.hasText(value)) {throw new BizError(message, argus);}}public static void hasAmount(BigDecimal value, IErrCode message, String... argus) {if (value == null || value.compareTo(BigDecimal.ZERO) <= 0) {throw new BizError(message, argus);}}
}

这里就出现一个巨大的问题,因为feign调用该接口的实际返回是 BaseResp<UserInfoResp>, 处理这个就三种方式

  • 1.单独封装对外的client,全部加上BaseResp
  • 2.内部调用的接口加上特定header头,在处理这些的时候统一按原样返回
  • 3.对feign的调用后结果响应写入单独的解码器

第一个在特别多的服务下明显不够优雅,每次都要去手动包装一下,并且调用方也不一定喜欢外面有一层业务无关的东西

第二个有一点比较烦恼的就是,如果我想要抛出特定的错误码,这个就不能实现,还得用httpcode的错误码形式才行,这样对整个系统现有的http响应模式都有冲击

所以就只能选择第三种方式,写解码器来进行处理

对feign的使用以及改造

为什么会改造feign呢,主要有三个原因

  • 1.feign的统一响应处理如果发现返回值是void或者Void等就不会调用自定义解码器,因为我们自己的接口定义void的时候也会被包装BaseResp,所以此处拿不到信息很痛苦,因为可能有错误码在响应中;
  • 2.feign的响应的解码器出现的异常都会被包装一层DecodeException,这个虽然我可以在统一异常那里处理,但是总感觉不爽,还是喜欢我如果自定义了decode,那么异常就应该是干干净净的我抛出来的;
  • 3.就是重试,feign的重试不方便我动态改变,比如动态关掉某个接口的重试,变成不重试,或者改一些接口的重试次数等,所以这里也改动了。

先看定义的解码器/响应类以及自定义retry

package com.littlehow.feign;import com.alibaba.fastjson.JSONObject;
import com.littlehow.BaseResp;
import com.littlehow.error.BizAssert;
import com.littlehow.error.BizError;
import com.littlehow.error.DynamicErrorCode;
import com.littlehow.error.ErrorCode;
import com.netflix.loadbalancer.RoundRobinRule;
import com.netflix.loadbalancer.Server;
import feign.FeignException;
import feign.Retryer;
import feign.Util;
import feign.codec.Decoder;
import io.huione.bakong.common.feign.MyFeignRetry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpStatus;import java.nio.charset.StandardCharsets;
import java.util.Objects;/*** @author littlehow* @since 5/27/24 18:49*/
@Configuration
@Slf4j
@RibbonClients(defaultConfiguration = {FeignRuleConfig.FeignRule.class})
public class FeignRuleConfig {@Beanpublic Decoder decoder() throws FeignException {return (response, type) -> {try {HttpStatus httpStatus = HttpStatus.valueOf(response.status());if (httpStatus.is2xxSuccessful()) {String feignResp = Util.toString(response.body().asReader(StandardCharsets.UTF_8));FeignBaseResp baseResp = JSONObject.parseObject(feignResp, FeignBaseResp.class);// 错误码统一抛出异常,这里的异常会被feign的decodeException包装,所以那里还需要处理一下BizAssert.isTrue(baseResp.success(), new DynamicErrorCode(baseResp.getCode(), baseResp.getMsg()));ResolvableType returnType = ResolvableType.forType(type);if (returnType.getRawClass().isAssignableFrom(BaseResp.class)) {return JSONObject.parseObject(feignResp, type);} else {return JSONObject.parseObject(baseResp.getData(), type);}} else {log.error("resp http status is not 2xx, HttpStatus:{}", httpStatus);throw new BizError(ErrorCode.CALL_RPC_ERROR);}} catch (BizError e) {throw e;} catch (Throwable e1) {log.error("feign client fail,error message:{}", e1.getMessage(), e1);throw new BizError(ErrorCode.SYSTEM_BUSY);}};}@Slf4jpublic static class FeignRule extends RoundRobinRule {@Overridepublic Server choose(Object key) {Server server = super.choose(key);if (Objects.isNull(server)) {log.info("server is null");return null;}log.info("feign rule ---> choose key:{}, final server ip:{}", key, server.getHostPort());return server;}}@Beanpublic Retryer feignRetry() {return new MyFeignRetry();}}==============================================================================================package com.littlehow.feign;import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;/*** @author littlehow* @since 5/27/24 18:13*/
@Slf4j
@Setter
@Getter
@Accessors(chain = true)
public class FeignBaseResp {// 成功的code和messageprivate static final String SUCCESS_RESP_CODE = "0";@ApiModelProperty("状态码")private String code;@ApiModelProperty("返回信息")private String msg;@ApiModelProperty("返回数据,只有这里和BaseResp有区别")private String data;@ApiModelProperty("全局调用id")private String traceId;@ApiModelProperty("系统毫秒时间戳")private Long systemTime;/***  是否成功,如果失败可以打印日志* @return - true表示成功*/public boolean success() {boolean isSuccess = SUCCESS_RESP_CODE.equals(code);if (!isSuccess) {// 这里也可以打印debug日志,使用级别来控制是否打印该行日志log.error("errorCode:{}, errorMessage={}", code, msg);}return isSuccess;}
}======================================================================================
package com.littlehow.feign;import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import feign.FeignExecuteContext;
import feign.MethodMetadata;
import feign.RetryableException;
import feign.Retryer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;/*** @author littlehow* @since 5/27/24 18:47*/
@Slf4j
public class MyFeignRetry extends Retryer.Default {/*** 不重试*/private static MyFeignRetry NEVER = new MyFeignRetry();@Value("${self.feign.retry.enable:false}")private boolean openRetry;@Value("${self.log.detail:false}")private boolean logDetail;private Set<String> noRetry;private Set<String> canRetry;/*** 重试时打印重试的请求接口*/private String requestUrl;/*** 重试时打印调用的类和方法*/private String configKey;/*** 默认构造方法是不支持重试*/public MyFeignRetry() {super(100, TimeUnit.SECONDS.toMillis(1), 1);}/*** 重试两次** @param maxPeriod - 最大等待重试时间*/public MyFeignRetry(long maxPeriod, String requestUrl, String configKey, boolean logDetail) {super(200, maxPeriod, 3);this.requestUrl = requestUrl;this.configKey = configKey;this.logDetail = logDetail;}/*** 自定义重试参数** @param period      - 第一次重试周期* @param maxPeriod   - 最大重试周期, 计算过程为super.continueOrPropagate* @param maxAttempts - 调用次数*/public MyFeignRetry(long period, long maxPeriod, int maxAttempts) {super(period, maxPeriod, maxAttempts);}@Value("${feign.retry.urls.disable:}")public void noRetry(String value) {noRetry = splitRetry(value, "disable");}@Value("${feign.retry.urls.enable:}")public void canRetry(String value) {canRetry = splitRetry(value, "enable");}private Set<String> splitRetry(String value, String flag) {log.info("change {} retry url config {}", flag, value);Set<String> tmp = new HashSet<>();if (StringUtils.hasText(value)) {String[] v = value.split(",");for (String vv : v) {vv = vv.trim();if (StringUtils.hasText(vv)) {tmp.add(vv);}}}return tmp;}/*** 重试继续的判断,不重试则直接抛异常** @param e - 可重试*/public void continueOrPropagate(RetryableException e) {super.continueOrPropagate(e);if (logDetail) {log.info("retry-call-feign-info feign-client={}, url={}", configKey, requestUrl);}}@Overridepublic Retryer clone() {// 判断上下文,选择不同的重试策略MethodMetadata methodMetadata = FeignExecuteContext.getMetadata();if(Objects.isNull(methodMetadata)) {return NEVER;}String request = methodMetadata.template().url();log.info("call-feign-info feign-client={}, url={}", methodMetadata.configKey(), request);if (logDetail) {log.info("call-feign-detail params={}, timeoutConfig={}", getParams(),JSONObject.toJSONString(FeignExecuteContext.getOptions()));}// 无论是否开启 retry,都优先使用确认retry的urlif (canRetry.contains(request)) {return new MyFeignRetry(TimeUnit.SECONDS.toMillis(1), request,methodMetadata.configKey(), logDetail);}if (!openRetry || noRetry.contains(request)) {return NEVER;}// 重试必须每次的初始化对象return new MyFeignRetry(TimeUnit.SECONDS.toMillis(1), request,methodMetadata.configKey(), logDetail);}private String getParams() {Object[] obj = FeignExecuteContext.getArgus();if (obj == null || obj.length == 0) {return "no params";} else {return JSONArray.toJSONString(obj);}}
}

这里的retry是使用了feign的改造类,这里的feign是基于10.10.1的版本,并且改动的类都在feign包下

首先定义feign的简易上下文

package feign;public class FeignExecuteContext {private static final ThreadLocal<Object[]> ARGUS = new ThreadLocal<>();private static final ThreadLocal<MethodMetadata> METADATA = new ThreadLocal<>();private static final ThreadLocal<Request.Options> OPTIONS = new ThreadLocal<>();static void set(Object[] argus, MethodMetadata metadata, Request.Options options) {ARGUS.set(argus);METADATA.set(metadata);OPTIONS.set(options);}static void clear() {ARGUS.remove();METADATA.remove();OPTIONS.remove();}public static Object[] getArgus() {return ARGUS.get();}public static MethodMetadata getMetadata() {return METADATA.get();}public static Request.Options getOptions() {return OPTIONS.get();}
}

其次是feign结果响应类,主要就是屏蔽void的判定和自定义异常类型的添加和判定方法

/*** Copyright 2012-2020 The Feign Authors** Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except* in compliance with the License. You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software distributed under the License* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express* or implied. See the License for the specific language governing permissions and limitations under* the License.*/package feign;import static feign.FeignException.errorReading;
import static feign.Util.ensureClosed;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.concurrent.CompletableFuture;
import feign.Logger.Level;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;/*** The response handler that is used to provide asynchronous support on top of standard response* handling*/
@Experimental
class AsyncResponseHandler {private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L;private final Level logLevel;private final Logger logger;private final Decoder decoder;private final ErrorDecoder errorDecoder;private final boolean decode404;private final boolean closeAfterDecode;// add littlehow 这里是自定义异常类,如果是这些类,则不需要进行decodeException的包装private static final Set<Class<?>> exceptionClass = new HashSet();AsyncResponseHandler(Level logLevel, Logger logger, Decoder decoder, ErrorDecoder errorDecoder,boolean decode404, boolean closeAfterDecode) {super();this.logLevel = logLevel;this.logger = logger;this.decoder = decoder;this.errorDecoder = errorDecoder;this.decode404 = decode404;this.closeAfterDecode = closeAfterDecode;}// add littlehow 添加自定义异常类public static void addExceptionClass(Class<?> e) {exceptionClass.add(e);}boolean isVoidType(Type returnType) {return Void.class == returnType || void.class == returnType;}void handleResponse(CompletableFuture<Object> resultFuture,String configKey,Response response,Type returnType,long elapsedTime) {// copied fairly liberally from SynchronousMethodHandlerboolean shouldClose = true;try {if (logLevel != Level.NONE) {response = logger.logAndRebufferResponse(configKey, logLevel, response,elapsedTime);}if (Response.class == returnType) {if (response.body() == null) {resultFuture.complete(response);} else if (response.body().length() == null|| response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {shouldClose = false;resultFuture.complete(response);} else {// Ensure the response body is disconnectedfinal byte[] bodyData = Util.toByteArray(response.body().asInputStream());resultFuture.complete(response.toBuilder().body(bodyData).build());}} else if (response.status() >= 200 && response.status() < 300) {// add littlehow 这里有调用isVoidType的判定// 如果是void的,那么将直接resultFuture.complete(null),所以需要屏蔽掉;//if (isVoidType(returnType)) {//  resultFuture.complete(null);//} else {final Object result = decode(response, returnType);shouldClose = closeAfterDecode;resultFuture.complete(result);//}} else if (decode404 && response.status() == 404 && !isVoidType(returnType)) {final Object result = decode(response, returnType);shouldClose = closeAfterDecode;resultFuture.complete(result);} else {resultFuture.completeExceptionally(errorDecoder.decode(configKey, response));}} catch (final IOException e) {if (logLevel != Level.NONE) {logger.logIOException(configKey, logLevel, e, elapsedTime);}resultFuture.completeExceptionally(errorReading(response.request(), response, e));} catch (final Exception e) {resultFuture.completeExceptionally(e);} finally {if (shouldClose) {ensureClosed(response.body());}}}Object decode(Response response, Type type) throws IOException {try {return this.decoder.decode(response, type);} catch (FeignException var4) {throw var4;} catch (RuntimeException var5) {// add littlehow 判断是否拥有这些异常类,是的话就原样抛出if (exceptionClass.contains(var5.getClass())) {throw var5;} else {throw new DecodeException(response.status(), var5.getMessage(), response.request(), var5);}}}
}

feign代理的主要处理方法类,主要是添加上下文的设置

/*** Copyright 2012-2020 The Feign Authors** Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except* in compliance with the License. You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software distributed under the License* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express* or implied. See the License for the specific language governing permissions and limitations under* the License.*/
package feign;import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import feign.InvocationHandlerFactory.MethodHandler;
import feign.Request.Options;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import static feign.ExceptionPropagationPolicy.UNWRAP;
import static feign.FeignException.errorExecuting;
import static feign.Util.checkNotNull;final class SynchronousMethodHandler implements MethodHandler {private final MethodMetadata metadata;private final Target<?> target;private final Client client;private final Retryer retryer;private final List<RequestInterceptor> requestInterceptors;private final Logger logger;private final Logger.Level logLevel;private final RequestTemplate.Factory buildTemplateFromArgs;private final Options options;private final ExceptionPropagationPolicy propagationPolicy;// only one of decoder and asyncResponseHandler will be non-nullprivate final Decoder decoder;private final AsyncResponseHandler asyncResponseHandler;private SynchronousMethodHandler(Target<?> target, Client client, Retryer retryer,List<RequestInterceptor> requestInterceptors, Logger logger,Logger.Level logLevel, MethodMetadata metadata,RequestTemplate.Factory buildTemplateFromArgs, Options options,Decoder decoder, ErrorDecoder errorDecoder, boolean decode404,boolean closeAfterDecode, ExceptionPropagationPolicy propagationPolicy,boolean forceDecoding) {this.target = checkNotNull(target, "target");this.client = checkNotNull(client, "client for %s", target);this.retryer = checkNotNull(retryer, "retryer for %s", target);this.requestInterceptors =checkNotNull(requestInterceptors, "requestInterceptors for %s", target);this.logger = checkNotNull(logger, "logger for %s", target);this.logLevel = checkNotNull(logLevel, "logLevel for %s", target);this.metadata = checkNotNull(metadata, "metadata for %s", target);this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target);this.options = checkNotNull(options, "options for %s", target);this.propagationPolicy = propagationPolicy;if (forceDecoding) {// internal only: usual handling will be short-circuited, and all responses will be passed to// decoder directly!this.decoder = decoder;this.asyncResponseHandler = null;} else {this.decoder = null;this.asyncResponseHandler = new AsyncResponseHandler(logLevel, logger, decoder, errorDecoder,decode404, closeAfterDecode);}}@Overridepublic Object invoke(Object[] argv) throws Throwable {try {RequestTemplate template = buildTemplateFromArgs.create(argv);Options options = findOptions(argv);// add littlehow 获取调用的前置信息,这里只是获取一些简单的信息便于下面retryer的调用FeignExecuteContext.set(argv, metadata, options);Retryer retryer = this.retryer.clone();while (true) {try {return executeAndDecode(template, options);} catch (RetryableException e) {try {retryer.continueOrPropagate(e);} catch (RetryableException th) {Throwable cause = th.getCause();if (propagationPolicy == UNWRAP && cause != null) {throw cause;} else {throw th;}}if (logLevel != Logger.Level.NONE) {logger.logRetry(metadata.configKey(), logLevel);}}}} finally {FeignExecuteContext.clear();}}Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {Request request = targetRequest(template);if (logLevel != Logger.Level.NONE) {logger.logRequest(metadata.configKey(), logLevel, request);}Response response;long start = System.nanoTime();try {response = client.execute(request, options);// ensure the request is set. TODO: remove in Feign 12response = response.toBuilder().request(request).requestTemplate(template).build();} catch (IOException e) {if (logLevel != Logger.Level.NONE) {logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));}throw errorExecuting(request, e);}long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);if (decoder != null)return decoder.decode(response, metadata.returnType());CompletableFuture<Object> resultFuture = new CompletableFuture<>();asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,metadata.returnType(),elapsedTime);try {if (!resultFuture.isDone())throw new IllegalStateException("Response handling not done");return resultFuture.join();} catch (CompletionException e) {Throwable cause = e.getCause();if (cause != null)throw cause;throw e;}}long elapsedTime(long start) {return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);}Request targetRequest(RequestTemplate template) {for (RequestInterceptor interceptor : requestInterceptors) {interceptor.apply(template);}return target.apply(template);}Options findOptions(Object[] argv) {if (argv == null || argv.length == 0) {return this.options;}return Stream.of(argv).filter(Options.class::isInstance).map(Options.class::cast).findFirst().orElse(this.options);}static class Factory {private final Client client;private final Retryer retryer;private final List<RequestInterceptor> requestInterceptors;private final Logger logger;private final Logger.Level logLevel;private final boolean decode404;private final boolean closeAfterDecode;private final ExceptionPropagationPolicy propagationPolicy;private final boolean forceDecoding;Factory(Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors,Logger logger, Logger.Level logLevel, boolean decode404, boolean closeAfterDecode,ExceptionPropagationPolicy propagationPolicy, boolean forceDecoding) {this.client = checkNotNull(client, "client");this.retryer = checkNotNull(retryer, "retryer");this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");this.logger = checkNotNull(logger, "logger");this.logLevel = checkNotNull(logLevel, "logLevel");this.decode404 = decode404;this.closeAfterDecode = closeAfterDecode;this.propagationPolicy = propagationPolicy;this.forceDecoding = forceDecoding;}public MethodHandler create(Target<?> target,MethodMetadata md,RequestTemplate.Factory buildTemplateFromArgs,Options options,Decoder decoder,ErrorDecoder errorDecoder) {return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,logLevel, md, buildTemplateFromArgs, options, decoder,errorDecoder, decode404, closeAfterDecode, propagationPolicy, forceDecoding);}}
}

如何让修改的类生效

  • 1.拉取源码重新打包
  • 2.放在项目中
  • 3.编译成字节码,强制加载

我选择的就是强制加载,这里我只贴出加载的形式

package com.littlehow.site;import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import com.littlehow.error;/*** @author littlehow* @since 2024-04-09 18:44*/
public class SourceInitial {// 优先加载需要的资源类public static void initClass() {CustomerClassLoader customerClassLoader = new CustomerClassLoader();List<String> classNames = new ArrayList<>(ClassSourceManager.classSource.keySet());classNames.forEach(className -> {try {Class clazz = customerClassLoader.loadClass(className);if (className.equals("feign.AsyncResponseHandler")) {initException(clazz);}} catch (ClassNotFoundException e) {throw new RuntimeException(e);}});}/** 添加异常类  */private static void initException(Class clazz) {try {Method method = clazz.getDeclaredMethod("addExceptionClass", Class.class);boolean access = method.isAccessible();method.setAccessible(true);method.invoke(null, BizError.class);method.setAccessible(access);} catch (Exception e) {// skipe.printStackTrace();}}
}==============================================================================================
package com.littlehow.site;import java.util.Base64;
import java.util.HashMap;
import java.util.Map;/*** @author littlehow* @since 2024-04-09 17:50*/
public class ClassSourceManager {public static Map<String, byte[]> classSource = new HashMap<>();static {classSource.put("feign.AsyncResponseHandler", Base64.getDecoder().decode("对应的base64字节码"));classSource.put("feign.SynchronousMethodHandler",Base64.getDecoder().decode("对应的base64字节码"));}}================================================================================================
package com.littlehow.site;import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;/*** @author littlehow* @since 2024-04-09 17:45*/
public class CustomerClassLoader extends ClassLoader {/*** classloader中的保护方法,需要使用反射调用*/private static Method DEFINE_CLASS_METHOD ;/*** 当前类加载器已经加载过的类*/private static final Map<String, Class<?>> loadedClassMap = new HashMap<>();/*** 加载主类的类加载器*/private static ClassLoader parent;static {Method[] methods = ClassLoader.class.getDeclaredMethods();for (Method method : methods) {if ("defineClass".equals(method.getName()) && method.getParameterCount() == 4) {DEFINE_CLASS_METHOD = method;method.setAccessible(true);}}parent = CustomerClassLoader.class.getClassLoader();}@Overridepublic Class<?> loadClass(String className) throws ClassNotFoundException {synchronized (this) {Class<?> c = loadedClassMap.get(className);if (c == null) {c = findClass(className);loadedClassMap.put(className, c);}return c;}}@Overridepublic Class<?> findClass(String className) throws ClassNotFoundException {// 加载后就不再保存byte[] bts = ClassSourceManager.classSource.remove(className);if (bts == null || bts.length == 0) {// 如果没有资源类,则调用parent加载器进行加载return parent.loadClass(className);}// 定义给父加载器,保证父类不再去自行加载Class<?> clazz = executeDefine(parent, className, bts);if (clazz == null) {throw new ClassNotFoundException(className);}return clazz;}/*** 加载类* 普通项目的类加载器为应用类加载器:* @see sun.misc.Launcher $AppClassLoader* 但是当项目被打成springboot的jar包后,加载该类的类加载器是* @see org.springframework.boot loader.LaunchedURLClassLoader* 该类加载器放置于线程上下文类加载器中** @param loader    -  加载器* @param className -  类名* @param bts       -  字节码* @return - 类名对应的class对象*/private Class<?> executeDefine(ClassLoader loader, String className, byte[] bts) {try {return (Class<?>) DEFINE_CLASS_METHOD.invoke(loader, className, bts, 0, bts.length);} catch (Throwable e) {throw new RuntimeException(e);}}
}

后记

在这里插入图片描述

feign的调用还是有很多东西没写的,比如灰度调用的实现,上下文灰度在feign里面的实现,其实是在负载均衡器的实现,比如ribbon,或者是feign调用的http连接池的结合公有云组件的一些坑,比如okhttp或者httpclient各自的连接池连接闲置时间默认都在10分钟以上,而aws上的有些组件会对长连接进行管理,闲置250秒就会强制关闭等,导致业务闲置期出现connect reset等错误,反正feign这块的东西特别多,比如自定义错误decode,如404decode,403 、401的http错误码decode等。比如调用前的interceptor切入,比如choose server的逻辑等等,这些只有等有大量时间才能写这个系列,其实之前一段时间还是很想写feign的系列的,但是想要写文的工作量,还要画feign的调用逻辑图等就头大,最终还是放弃了的

今天没有M功能,因为我还是想偶尔出一些源码类的技术文章,虽浅尝辄止,但也算有技术博客的输出,哈哈哈

加油吧littlehow

北京时间:2024-05-27 21:52

金边时间:2024-05-27 20:52

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/17257.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Bash中解析命令行参数的两种样例脚本

文章目录 问题回答以空格分隔选项和参数以等号分隔选项和参数 参考 问题 假设&#xff0c;我有一个脚本&#xff0c;它会被这样一行调用: ./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile或者这个&#xff1a; ./myscript -v -f -d -o /fizz/someOtherFile ./fo…

【NumPy】NumPy实战入门:索引与切片(sort、argsort、searchsorted)详解

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

[NOIP 2014] 寻找道路

[NOIP 2014] 寻找道路 在有向图 G 中&#xff0c;每条边的长度均为 11&#xff0c;现给定起点和终点&#xff0c;请你在图中找一条从起点到终点的路径&#xff0c;该路径满足以下条件&#xff1a; 路径上的所有点的出边所指向的点都直接或间接与终点连通。在满足条件 11 的情…

全局查询筛选器适用场景 以及各场景示例

EF Core中的全局查询筛选器&#xff08;Global Query Filters&#xff09;是一种强大的功能&#xff0c;可以在实体框架的DbContext级别为特定的EntityType设置默认的过滤条件。这些筛选器自动应用于所有涉及到相关实体的LINQ查询中&#xff0c;无论是直接查询还是通过Include或…

统计计算四|蒙特卡罗方法(Monte Carlo Method)

系列文章目录 统计计算一|非线性方程的求解 统计计算二|EM算法&#xff08;Expectation-Maximization Algorithm&#xff0c;期望最大化算法&#xff09; 统计计算三|Cases for EM 文章目录 系列文章目录一、基本概念&#xff08;一&#xff09;估算 π \pi π&#xff08;二&…

【设计模式】JAVA Design Patterns——Commander(指挥官模式)

&#x1f50d;目的 用于处理执行分布式事务时可能遇到的所有问题。 &#x1f50d;解释 处理分布式事务很棘手&#xff0c;但如果我们不仔细处理&#xff0c;可能会带来不想要的后果。假设我们有一个电子商务网站&#xff0c;它有一个支付微服务和一个运输微服务。如果当前运输…

学习图形推理

学习图形推理 1.位置规律1.1平移1.2翻转、旋转2.样式规律2.1加减异同2.2黑白运算3.属性规律3.1对称性3.2曲直性3.3开闭性4.数量规律4.1面4.2线数量4.3笔画数4.4点数量4.5素数量5.空间重构5.1相对面5.2相邻面-公共边5.3相邻面-公共点5.4相邻面-画边法题型 一组图:从左往右找规律…

编程-辅助工具-Git下载

文章目录 1、前言2、Git官网地址3、迅雷下载 1、前言 采用Git能下载github上的代码&#xff0c;其下载是采用官网下载的&#xff0c;但是下载速度比较慢&#xff0c;网上也推荐了镜像的方式&#xff0c;但是有些链接失效了&#xff0c;突然有一天想起用迅雷是不是合适&#xf…

DDR基本原理

1. 简介 DDR SDRAM&#xff08;Double Data Rate Synchronous Dynamic Random Access Memory&#xff0c;双数据率同步动态随机存储器&#xff09;通常被我们称为DDR&#xff0c;其中的“同步”是指内存工作需要同步时钟&#xff0c;内部命令的发送与数据传输都以它为基准。DDR…

【社会信用体系1003】 企业违规新解:社会信用环境改善的实证分析!

今天给大家分享的是来自于国内顶级期刊金融研究2023年发表论文——《社会信用环境改善降低了企业违规吗&#xff1f;——来自“中国社会信用体系建设”的证据》所用到的重要数据集&#xff0c;该文章从企业层面探讨了社会信用系统建设对企业违规行为的影响&#xff0c;更精准地…

牛客NC164 最长上升子序列(二)【困难 贪心+二分 Java/Go/PHP/C++】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/4af96fa010c44638a7e112abf65f7237 思路 贪心二分 所谓贪心&#xff0c;就是往死里贪&#xff0c;所以对于最大上升子序列&#xff0c;结尾元素越小&#xff0c;越有利于后面接上其他的数&#xff0c;也就可能变…

使用 Elasticsearch 作为 Azure OpenAI On Your Data 的向量数据库

作者&#xff1a;来自 Elastic Paul Oremland 背景介绍 最近&#xff0c;微软通过 Azure OpenAI 服务 "On Your Data" 将 Elasticsearch 直接集成到 Azure 中。"On Your Data" 使组织能够利用强大的 AI 模型&#xff08;如 GPT-4 和 RAG 模型&#xff09;…

想知道期权的交易方式有哪些吗?新手必看!

今天期权懂带你了解想知道期权的交易方式有哪些吗&#xff1f;新手必看&#xff01;期权指投资者在支付了一定的权利金之后&#xff0c;将拥有未来某个时间协定价格买入或者卖出的权利。 期权的交易策略方式有哪些&#xff1f; 买入看涨期权&#xff1a; 使用场景&#xff1a…

C# WPF入门学习主线篇(四)—— Button的常用属性

本期来详细介绍一下WPF中Button组件的属性都有哪些 一、准备阶段 首先&#xff0c;打开我们之前创建好的工程。 这是我们之前几期一起做过的工程&#xff0c;现在重新创建一个button&#xff0c;来熟悉一下他的属性。 选中创建的button&#xff0c;点击属性栏 二、接下来介绍…

layui扩展件(xm-select)实现下拉框

layui扩展件&#xff08;xm-select&#xff09;实现下拉框 扩展组件 xm-select 效果图 html代码 <div class"layui-inline"><label class"layui-form-label">职位</label><div class"layui-input-inline" style"wid…

小皮面板中访问不了本地的sqli网站---解决方法

今天想在sqli-labs中做题&#xff0c;却发现自己访问不了网站 1、具体的错误原因如下 2、查了一下&#xff0c;可能是因为自己访问的域名不对 3、修改了域名为&#xff1a;http://sqli-labs:81/Less-2/便可以访问了 4、然后接下来我有遇到一个错误&#xff0c;这个问题是php版…

【onnx问题解决】关键词:found at least two devices、torch.onnx.export

关键词&#xff1a;Expected all tensors to be on the same device, but found at least two devices, cpu and cuda:0! 报错&#xff1a; [34m[1mONNX:[0m export failure ❌ 3.8s: Expected all tensors to be on the same device, but found at least two devices, cpu an…

Amazon云计算AWS之[7]内容推送服务CloudFront

文章目录 CDNCDN简介CDN网络技术 CloudFrontCloudFront基本概念 CDN CDN简介 用户在发出服务请求后&#xff0c;需要经过DNS服务器进行域名解析后得到所访问网站的真实IP&#xff0c;然后利用该IP访问网站。在这种模式中&#xff0c;世界各地的访问者都必须直接和网站服务器连…

openflow协议抓包分析

1、准备实验拓扑&#xff1a; 在Mininet环境中创建一个简单的SDN拓扑&#xff0c;包括控制器、交换机、主机等。 确保拓扑能够正常运行&#xff0c;SDN交换机与控制器建立连接。 采用主机Ubuntu22.04主机&#xff0c;IP地址是192.168.87.130&#xff0c;安装opendaylight控制…

Git标签管理

文章目录 1. 什么是标签2. 创建标签3. 标签删除4. 本地标签推送至远程5. 标签远程删除 1. 什么是标签 标签tag &#xff0c;可以简单的理解为是对某次commit的⼀个标识&#xff0c;相当于起了⼀个别名。 例如&#xff0c;在项目发布某个版本的时候&#xff0c;针对最后一次co…