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和message
private 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 -
*/
@Override
public 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
@Override
public 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 detail
return 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 {
@Bean
public 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);
}
};
}
@Slf4j
public static class FeignRule extends RoundRobinRule {
@Override
public 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;
}
}
@Bean
public 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和message
private 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);
}
}
@Override
public 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的url
if (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 SynchronousMethodHandler
boolean 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 disconnected
final 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-null
private 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);
}
}
@Override
public 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 12
response = 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) {
// skip
e.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();
}
@Override
public 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;
}
}
@Override
public 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