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

2,423 阅读4分钟

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) {
        ....
        if (result == null || !result.getSuccess()) {
        ....
            logger.error("[epass-Agoal] api response fail or null, message:{}",、
                     errorMessage);
        }
        return result.getContent();
    }
} catch (Exception ex) {
    ...
    logger.error(exMessage, ex);
}


         如果是对批量添加操作呢?需要对接的接口有二三十个,估计C_V也够呛呀!小伙伴们很快就会想到AOP。不错!AOP日志拦截一下接口实现方法就可以了呀。如下所示,定义的这个切面做了请求前后与计时的操作,基本可以满足请求监控的操作了,到了这里其实大家都觉得还OKd了,但是又发现另外一个问题,有实现类里面居然还会去调用诸如数据库操作与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框架中。我觉得自己也可以基于这个功能场景撸一个简单的http客户端请求框架直接上最后效果。

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

@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框架特性与设计模式的一些思路进行优化改进,最后实现出一个简单的带监控的简单请求框架。凡事往前一步,会收获更多!