feign 优雅记录日志
·
feign 优雅记录日志
场景
每次通过 feign 接口调用 时,都需要进行 try{}catch{} 操作,进行相同操作,所以有很多冗余
方案一
通过 AOP 拦截 FeignBlockingLoadBalancerClient, 缺点就是 只能拦截走客户端负载的请求,普通请求拦截不到
@Component
@Aspect
@Slf4j
public class FeignClientAspect {
/**
* AOP 只能 切 FeignBlockingLoadBalancerClient ,
* feign.Client.Default 是内部类,
* ApacheHttpClient 是 final 类
* @param pjp
* @return
* @throws Throwable
*/
@Around("execution(* org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient.execute(..))")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Request request = null;
if (args.length > 0 && args[0] instanceof Request) {
request = (Request) args[0];
}
String url = request.url();
byte[] requestBody = isFileUploadRequest(request) ? null : request.body();
Map<String, Collection<String>> queries = request.requestTemplate().queries();
String requestBodyString = requestBody != null ? new String(requestBody) : "";
Response response = null;
try {
log.info("feign.request: url: {}, query: {}, body: {}", url, JSON.toJSONString(queries), requestBodyString);
// 执行请求
Object proceed = pjp.proceed();
if (proceed instanceof Response) {
response = (Response) proceed;
}
// 打印响应信息
byte[] responseBodyBytes = Util.toByteArray(response.body().asInputStream());
log.info("feign.response: {}", new String(responseBodyBytes));
InputStream newInputStream = new ByteArrayInputStream(responseBodyBytes);
return Response.builder()
.status(response.status())
.reason(response.reason())
.headers(response.headers())
.request(response.request())
.body(newInputStream, response.body().length())
.build();
} catch (Throwable e) {
log.info("feign.error: query: {}, body: {}", JSON.toJSONString(queries), requestBodyString, e);
throw e;
}
}
private boolean isFileUploadRequest(Request request) {
// 检查请求方法是否为 POST
if (!Request.HttpMethod.POST.name().equalsIgnoreCase(request.method())) {
return false;
}
// 检查 Content-Type 是否为 multipart/form-data
Collection<String> contentType = request.headers().get("Content-Type");
if (contentType != null) {
Optional<String> first = contentType.stream().filter(a -> a.toLowerCase().contains("multipart/form-data")).findFirst();
return first.isPresent();
}
return false;
}
}
方案二
通过创建 代理 Client , 缺点就是不仅要配置 客户端负载 client, 而且要配置普通 client
@Slf4j
public class LogClient implements Client {
private Client delegate;
public LogClient(Client delegate) {
this.delegate = delegate;
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
String url = request.url();
byte[] requestBody = isFileUploadRequest(request) ? null : request.body();
Map<String, Collection<String>> queries = request.requestTemplate().queries();
String requestBodyString = requestBody != null ? new String(requestBody) : "";
try {
log.info("feign.request: url: {}, query: {}, body: {}", url, JSON.toJSONString(queries), requestBodyString);
// 执行请求
Response response = delegate.execute(request, options);
// 打印响应信息
byte[] responseBodyBytes = Util.toByteArray(response.body().asInputStream());
log.info("feign.response: {}", new String(responseBodyBytes));
InputStream newInputStream = new ByteArrayInputStream(responseBodyBytes);
return Response.builder()
.status(response.status())
.reason(response.reason())
.headers(response.headers())
.request(response.request())
.body(newInputStream, response.body().length())
.build();
} catch (Exception e) {
log.info("feign.error: query: {}, body: {}", JSON.toJSONString(queries), requestBodyString, e);
throw e;
}
}
private boolean isFileUploadRequest(Request request) {
// 检查请求方法是否为 POST
if (!Request.HttpMethod.POST.name().equalsIgnoreCase(request.method())) {
return false;
}
// 检查 Content-Type 是否为 multipart/form-data
Collection<String> contentType = request.headers().get("Content-Type");
if (contentType != null) {
Optional<String> first = contentType.stream().filter(a -> a.toLowerCase().contains("multipart/form-data")).findFirst();
return first.isPresent();
}
return false;
}
}
配置
//使用httpclient
@Bean
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
LoadBalancerClientFactory loadBalancerClientFactory) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new FeignBlockingLoadBalancerClient(new LogClient(delegate), loadBalancerClient, loadBalancerClientFactory);
}
//默认
@Bean
public Client feignClient(LoadBalancerClient loadBalancerClient,
LoadBalancerClientFactory loadBalancerClientFactory) {
Client delegate = new Client.Default(null,null);
return new FeignBlockingLoadBalancerClient(new LogClient(delegate), loadBalancerClient, loadBalancerClientFactory);
}
这个可以记录完整的请求url , 请求参数带参数名称,响应等等
方案三
通过 BeanPostProcessor 创建代理对象
定义标记接口
public interface FeignClientMarker {
}
后置处理
@Slf4j
@Component
public class FeignClientBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof FeignClientMarker) {
Class<?> clientClass = bean.getClass();
String clientName = beanName.substring(beanName.lastIndexOf(".")+1);
return Proxy.newProxyInstance(clientClass.getClassLoader(), clientClass.getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Arrays.asList("toString","equals").contains(method.getName())){
return method.invoke(bean,args);
}
String name = clientName.concat(".").concat(method.getName());
try {
// 记录请求参数
log.info("feign.{}.request: {}",name, formatParams(args));
Object result = method.invoke(bean, args);
// 记录响应结果
log.info("feign.{}.response: {}", name, JSON.toJSONString(result));
return result;
} catch (FeignException fe) {
// 处理Feign异常
log.info("feign.{}.exception", name, fe);
throw fe;
} catch (Exception e) {
// 处理其他异常
log.info("feign.{}.error", name, e);
throw e;
}
}
});
}
return bean;
}
private static String formatParams(Object[] args) {
if (args == null || args.length == 0) {
return null;
}
StringBuilder sb = new StringBuilder();
for (Object arg : args) {
if (arg != null && !isPrimitiveOrWrapper(arg.getClass())) {
try {
sb.append(JSON.toJSONString(arg)).append(", ");
} catch (Exception e) {
sb.append(arg.toString()).append(", ");
}
} else {
sb.append(arg.toString()).append(", ");
}
}
return sb.substring(0, sb.length() - 2); // 去掉最后一个逗号和空格
}
private static boolean isPrimitiveOrWrapper(Class<?> clazz) {
return clazz.isPrimitive() ||
clazz.equals(Boolean.class) || clazz.equals(Integer.class) || clazz.equals(Character.class) ||
clazz.equals(Byte.class) || clazz.equals(Short.class) || clazz.equals(Double.class) ||
clazz.equals(Long.class) || clazz.equals(Float.class);
}
}
这个只能记录 方法名称,请求参数【不带参数名称】,响应
方案四
重写 feign 的 Logger 接口,个人不喜欢,这要求开启 debug 模式,而且打印东西很多
方案五
使用AOP, 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignLog {
}
@Pointcut("@annotation(feignLog)")
public void pointcut(FeignLog feignLog) {
}
@Around("pointcut(feignLog)")
public Object log(ProceedingJoinPoint pjp, FeignLog feignLog) throws Throwable {
// 获取注解
Object result = null;
MethodSignature signature;
Method method = null;
//这种方式不通拿到参数名称,只能拿到参数值
// !!!!因为生成代理对象时,参数名称用的是索引值 ,如 arg0,arg1这样
Map<String, Object> param = getParam(pjp);
long l = System.nanoTime();
try {
log.info("{}.reuqest: {}", method.getName(), JSON.toJSONString(param));
// 执行方法
result = pjp.proceed();
} catch (Throwable e) {
log.error("{}.error.response: {} ms - {}", method.getName(), ((System.nanoTime() - l) / 1000), JSON.toJSONString(param), e);
throw e;
}
log.info("{}.response:{} ms -{}", method.getName(), ((System.nanoTime() - l) / 1000), JSON.toJSONString(result));
return result;
}
/**
* 获取参数列表
*/
private static Map<String, Object> getParam(ProceedingJoinPoint joinPoint) {
// 参数值
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
if (parameterNames != null) {
Map<String, Object> paramMap = new HashMap<>(parameterNames.length);
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
return paramMap;
}
return Collections.emptyMap();
}
这种方式不通拿到参数名称,只能拿到参数值,因为生成代理对象时,参数名称用的是索引值 ,如 arg0,arg1这样!!!
feign 调用 wrapper
feign 调用 结果 一般是带有格式包装的,可以包装一下
@Slf4j
public class FeignClientWrapper {
/**
* 如果异常,则返回 null
* @param supplier
* @return
* @param <T>
*/
public static <T> T execute(Supplier<ResponseResult<T>> supplier) {
try {
ResponseResult<T> rs = supplier.get();
return rs != null ? rs.getData() : null;
} catch (Exception e) {
return null;
}
}
/**
* 如果有异常,直接抛出
* @param supplier
* @return
* @param <T>
*/
public static <T> T executeWithExp(Supplier<ResponseResult<T>> supplier) {
ResponseResult<T> rs = supplier.get();
return rs != null ? rs.getData() : null;
}
}
更多推荐



所有评论(0)