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;
    }
}
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐