从一次简单的http远程调用设计来看代码功力

1.背景与需求

        在微服务体系中,API网关具有如签名验证、权限验证、请求转发、流量限制等作用。最近在做一个项目时,就遇到需要对接API网关的功能场景,应用初始化API网关带的客户端,并基于这个带有签名验证功能的客户端跟微服务通信并获取接口数据。

2.实现思路

       应用项目基于SpringBoot搭建,客户端初始化定义如下:

@Configuration
public class ClientConfig {

    @Value("${epaas.accesskey}")
    public String epassAccessKey;
    @Value("${epaas.secretkey}")
    public String secretKey;
    @Value("${epaas.domain}")
    public String domain;

    @Bean(initMethod="init")
    public ExecutableClient executableClient(){
        ExecutableClient executableClient = ExecutableClient.getInstance();
        executableClient.setDomainName(domain);
        executableClient.setProtocal("https");
        executableClient.setAccessKey(epassAccessKey);
        executableClient.setSecretKey(secretKey);
        return executableClient;
    }

}
复制代码

        主要接口实现如下,主要是填充url和请求参数,并构建客户端请求,然后将返回的数据拼装起来返回到上层调用,这样实现后基本都是可以基本满足功能需求。

public class DataPanelImpl implements DataPanelService {

    @Resource
    private ExecutableClient executableClient;

    @Override
    public List<ResultDTO> searchEmploy(String searchKey) {
        String api = "/xxxxx";
        GetClient getClient = executableClient.newGetClient(api);
        ....
        String apiResult = getClient.get();
        BaseDTO<List<ReportResultDTO>> result;
        result = JSON.parseObject(apiResult,
                new TypeReference<BaseDTO<List<ReportResultDTO>>>(){}.getType() );        if( result!=null && result.getSuccess()==true ) {
            return result.getContent();
        }
        return null;
    }
}
复制代码

3.优化思路

3.1 监控怎么弄?

        通常这里说的监控是指针对http请求做日志拦截处理,一种是动态的一种是静态的。先说静态 的方式如下,这种蛮力流非常香,针对请求前后log打印,快速简单,C-V操作妥妥的。

try {
    String apiResult = getClient.get();
    if (apiResult != null) {
        EpassBaseDTO<DeptAllIndicatorsStatisticsDTO> result =
                JSON.parseObject(apiResult, new TypeReference<EpassBaseDTO<DeptAllIndicatorsStatisticsDTO>>(){}.getType());
        if (result == null || !result.getSuccess()) {
            String errorMessage = (result != null ? result.getErrorMsg() : "");
            logger.error("[epass-Agoal] api response fail or null, message:{}", errorMessage);
            BusinessException.throwException(ErrorCode.SYSTEM_ERROR,
                    "[epass-Agoal Data Analysis] api response fail or null" + errorMessage);
        }
        return result.getContent();
    }
} catch (Exception ex) {
    String exMessage = ex.getMessage();
    logger.error(exMessage, ex);
    BusinessException.throwException(ErrorCode.SYSTEM_ERROR, "[epass-Agoal] request happen exception message:{}" + exMessage);
}

return null;
复制代码

         如果是对批量添加操作呢?需要对接的接口有二三十个,估计C_V也够呛呀!小伙伴们很快就会想到AOP,不错!AOP日志拦截一下方法就可以了呀,如下所示,这个切面做了请求前后与计时的操作,基本可以满足请求监控的操作了,到了这里其实大家都还算满足了,但是又发现另外一个问题,有实现类里面居然还会去调用诸如数据库操作与RPC接口调用,这就导致请求耗时是不准确的,在排查问题的时候有一定的阻碍,当时我就在想有没有更深入的方法去做这个client的监控?

@Aspect
@Component
public class EmpassTransportLogAspect {

    ThreadLocal<Long> startTime = new ThreadLocal<>();

    @Pointcut("execution(* com.xxx.execute*(..))")
    public void clientExecuteLog() {}

    @Before("clientExecuteLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        if (log.isInfoEnabled()) {
            startTime.set(System.currentTimeMillis());
            Object[] args = joinPoint.getArgs();
            StringBuilder sb = new StringBuilder();
            for (Object arg : args) {
                if (arg != null) {
                    sb.append(arg.toString());
                }
            }
            log.info("[client] request params:{}", sb);
        }
    }

    @AfterReturning(returning = "ret", pointcut = "clientExecuteLog()")
    public void doAfterReturning(Object ret) throws Throwable {
        if (log.isInfoEnabled()) {
            log.info("client cost time:{}", (System.currentTimeMillis() - startTime.get()));
            log.info("client receive:{}", ret);
            startTime.remove();
        }
    }
}
复制代码

         动态增强代理马上登场了,在做Client对象实例时使用了前置Bean处理器,将这个对象使用字节码改写工具的进行了增强,并插入切面相关的代码,至此整个客户端的请求过程都能监控到并且是准确的。大家都在想为啥要搞的这么复杂呢?有没有炫技的成分呢?哈哈哈,并没有,这就是我不想写很多重复代码的原因,技术复杂性与使用的简单性,这两个需要综合考量实现成本。但是可以探究的东西为什么不深入去探究呢?

@Slf4j
@Component
public class EpassClientBeanProcessor implements BeanPostProcessor, BeanFactoryAware {

    private BeanFactory beanFactory;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Object ret = bean;
        if (bean instanceof ExecutableClient) {
            ret = proxy(bean);
            log.info("custom proxy bean:{}", beanName);
        }
        return ret;
    }
    ....

    private Object proxy(Object bean) {
        Class<?> beanClass = bean.getClass();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(beanClass);
        enhancer.setCallback(new LogInterceptor(bean));
        enhancer.setInterfaces(beanClass.getInterfaces());
        enhancer.setClassLoader(beanClass.getClassLoader());

        Constructor<?> defaultConstructors =
                ConstructorUtils.getMatchingAccessibleConstructor(beanClass, Object.class);

        if (defaultConstructors == null) {
            return createProxy(enhancer, beanClass);
        } else {
            return enhancer.create();
        }
    }

    private Object createProxy(Enhancer enhancer, Class<?> beanClass) {
        Constructor<?> constructor = beanClass.getConstructors()[0];
        Class<?>[] types = constructor.getParameterTypes();
        Object[] args = new Object[types.length];
        for (int i = 0; i < types.length; i++) {
            args[i] = this.beanFactory.getBean(types[i]);
        }
        return enhancer.create(types, args);
    }
    ...

    /**
     * 方法切面
     */
    public static class LogInterceptor implements MethodInterceptor {
        ...
        @Override
        public Object intercept(Object o,
                                Method method, 
                                Object[] args, 
                                MethodProxy methodProxy) throws Exception {
            Object ret;
            try {
                String methodName = method.getName();
                if (methodName.equals("execute")) {
                    StringBuilder sb = new StringBuilder();
                    for (Object arg : args) {
                        if (arg != null) {
                            sb.append(arg.toString());
                        }
                    }
                    log.info("[epass-client] request params:{}", sb);
                    startTime.set(System.currentTimeMillis());
                }
                ret = method.invoke(target, args);
                if (methodName.equals("execute")) {
                    log.info("[epass-client] receive:{} cost time:{}", ret,
                            (System.currentTimeMillis() - startTime.get()));
                }
            } catch (Exception ex) {
                log.error("className:{} method:{} error", target.getClass(), method.getName(), ex);
                throw ex;
            } finally {
                startTime.remove();
            }
            return ret;
        }
    }
}
复制代码

3.2 重复代码怎么优化?

       这个接口只截取了一半的方法,在最初的实现版本里面都是硬编码形式进行的,而且有大量的重复代码。

        其实都可以分为几个步骤 解析参数 -> 构造请求 -> 收到响应 -> 解析并返回四大步骤,唯一不同的是请求参数与url与响应的内容解析返回。其实这个跟使用Mybatis是类似的,需要编写mybatis对应的接口和mapper XML文件即可,并不需要手动编写mapper接口的实现。这里mybatis就用到了JDK动态代理,并且将生成的接口代理对象动态注入到Spring容器中。相同的技术原理还应用在各大RPC框架中。我觉得自己也可以基于这个功能场景撸一个简单的httpk客户端请求框架,直接上最后效果。

public interface DataPane {
   @EpassRequest(url = "/xxxx")
   List<EmpAndMastersDto> getManagers(@EpassParam(name = "workNos") List<String> workNos);
   .....
}
复制代码

就相当于写个接口定义,然后底层会根据注解去扫描生成代理对象。

@Component
public class ServiceBeanDefinitionRegistry implements BeanDefinitionRegistryPostProcessor,
        ResourceLoaderAware, ApplicationContextAware {
    ......
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        Set<Class<?>> beanClazzList = scannerPackages("com.dingtalk.goal.biz.facade");
        beanClazzList.stream().filter(clazz -> hasEpassServiceAnnotation(clazz))
                .forEach(beanClazz -> {
                    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(beanClazz);
                    GenericBeanDefinition definition = (GenericBeanDefinition) builder.getRawBeanDefinition();
                    definition.getConstructorArgumentValues().addGenericArgumentValue(beanClazz);
                    definition.setBeanClass(ServiceFactory.class);
                    definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
                    registry.registerBeanDefinition(beanClazz.getSimpleName(), definition);
                });
    }
}
复制代码

       真正实现客户端请求是在代理对象中,当调用上层方法时真实调用的是invoke方法。到这里是基本达到我想要的代码效果了,不要重复代码,相同的实现思路可以看下retrofix-starter的实现,最后把这个封装成一个starter组件贡献给了这个网关团队。

public class ServiceProxy<T> implements InvocationHandler {
    ...
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        EpassRequest epassRequest = method.getAnnotation(EpassRequest.class);
        String url = epassRequest.url();
        EpassRquestType requestType = epassRequest.type();

        Class<?> returnType = method.getReturnType();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        Class<?>[] parameterTypes = method.getParameterTypes();
        Map<Integer, ParameterValue> indexParameterMap = parseParameterMap(args, parameterAnnotations, parameterTypes);

        String apiResult = null;
        try {
            logRequestParams(args);
            startTime.set(System.currentTimeMillis());
            apiResult = doRequest(url, requestType, indexParameterMap);
            log.info("[epass-client] receive:{} cost time:{}", apiResult, (System.currentTimeMillis() - startTime.get()));
            return validateAndParseApiResult(apiResult, returnType);
        } catch (Exception ex) {
            log.info("[epass-client] receive:{} exception:{} cost time:{}", apiResult, ex.getMessage(),
                    (System.currentTimeMillis() - startTime.get()));
            String exMessage = ex.getMessage();
            log.error(exMessage, ex);
            BusinessException.throwException(ErrorCode.SYSTEM_ERROR, "[epass-Agoal] request happen exception message:{}" + exMessage);
        }
        return null;
    }
    ...
}
复制代码

4.总结

       本文讲述的是在日常业务开发设计中的一个基础实现,使用http客户端调用外部接口并返回,利用Spring框架特性与设计模式的一些思路进行优化改进,最后实现出一个简单的带监控的简单请求框架。凡事往前一步,会收获更多!


作者:代码的色彩
链接:https://juejin.cn/post/6951743635122012196
来源:掘金
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值