成熟的互联网公司内部一般都会有多个线上环境,像在 JD,就有测试环境,预发布环境,生产环境。开发过程通常是现在本地编写代码,功能差不多了提到测试环境,再到预发布联调,测试通过再提交上线包部署到生产环境。但这是理想状况,实际开发中会有上下游系统联调的问题。

JD 的项目绝大多数都已经服务化了,服务的提供者和消费者分别在服务中心注册,消费者就能调用服务者的接口。但由于 JD 内部系统繁多,各有不同的开发团队维护各自的项目,除了生产环境和预发布环境能保证各系统间能互联互通,很多情况下,本地运行或在测试环境上运行时,没法调用到服务提供者的接口,这就很尴尬了,因为测试资源的不到位,只能上预发布环境进行上下游系统的对接联调,这是很烦人的,比较好的开发方案是,如果测试环境不完善,就把预发布环境上服务接口的真实数据截流并重定向到本地文件,把它打包成一个本地的测试数据源,以后直接在本地运行就行了。

如何拦截数据?这就需要 AOP 大显身手了。Spring AOP 可以通过 BeanNameAutoProxyCreatoraaaa 自动代理目标 bean,属性 beanNamesinterceptorNames 分别设置要代理的目标 bean 列表和拦截器数组。这样就很方便的实现了对目标 bean 的切入拦截。

简单说下具体的实现流程:

  • 当线上运行时,通过拦截器对目标 bean 内部方法的拦截,将方法调用的结果持久化到结果文件中;
  • 当本地运行时,拦截器就不走远程调用,而是直接从结果文件中读取真实的调用结果。

下面给出大致的拦截服务调用数据的代码:

<!-- spring-aop-config.xml -->
<beans>
  <!-- method interceptor -->
  <bean id="rpcInterceptor" class="com.isudox.utils.RpcInterceptor">
    <property name="mode" value="online"/>
    <property name="fileName" value="/home/sudoz/dev/local-rpc-data.properties"/>
  </bean>
  <!-- auto proxy -->
  <bean id="rpcAutoProxyCreator"
        class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames">
      <list>
        <value>remoteService1</value>
        <value>remoteService2</value>
        <value>remoteService3</value>
        <value>remoteService4</value>
      </list>
    </property>
    <property name="interceptorNames">
      <list>
        <value>rpcInterceptor</value>
      </list>
    </property>
  </bean>
</beans>
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import com.alibaba.fastjson.JSON;

public class RpcInterceptor implements MethodInteceptors {

    private static final Logger logger = LoggerFactory.getLogger(RpcInterceptor.class);

    private String mode = "local";  // local || online

    private String fileName = "/home/sudoz/dev/local-rpc-data.properties"

    private static File rpcResultFile = null;

    private Properties rpcResultProperties;
    
    public RpcInterceptor() {}

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object result;
        String methodName = invocation.getMethod().getName();
        Class<?> returnType = invocation.getMethod.getReturnType();
        String className = invocation.getMethod().getDeclaringClass().getSimpleName();
        Object[] args = invocation.getArguments();
        
        String key = className + "." + methodName;
        if (StringUtils.equals(getMode(), "local")) {
            String value = rpcResultProperties.getProperty(key);
            if (StringUtils.isNotBlank(value)) {
                result = JSON.parseObject(value, returnType);
            } else {
                result = invocation.proceed();
            }
        } else {
            result = invocation.proceed();
            String value = JSON.toJSONString(result);
            this.appendRpcResult2File(key, value);
        }

        return result;
    }

    public void appendRpcResult2File(String k, String v) {
        try {
            Properties properties = new Properties();
            if (rpcResultFile.exists()) {
                FileInputStream fis = new FileInputStream(rpcResultFile);
                properties.load(fis);
            }
            FileOutputStream fos = new FileOutputStream(rpcResultFile);
            properties.setProperty(k, v);
            properties.store(fos, "blabla...");
            fos.flush();
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("appendRpcResult2File failed, {}", e);
        } finally {
            fos.close();
        }
    }

    public String getMode() {
        return mode;
    }

    public void setMode(String mode) {
        this.mode = mode;
    }

    public String getfileName() {
        return fileName;
    }

    public void setfileName(String fileName) {
        this.fileName = fileName;
    }

}

在线上预发布环境跑一次,简单的通过 AOP 就一劳永逸的解决了联调测试的一大困扰。要是线上有一个专门做服务调用数据生成的应用,所有系统和开发人员都能从上面生成真实的测试数据,那就更好了。