一. 背景

今天无心写代码,整理下文章看。应用对于第三方的依赖较多,由于第三方接口测试环境可靠性不高,容易导致测试人员测试堵塞;需要特定场景的数据,但是依赖相对复杂,伪造数据的成本较高等情况,对于接口,数据库,redis等Mock的需求还是比较大的。目前公司内部不同部门有多套Mock方案,但是都没有摆脱对代码的侵入,可扩展性不高。基于目前大部分服务已经是Java技术栈的前提情况下,通过JavaAgent修改字节码的方式达到Mock的目的的条件逐渐成熟,虽然该方案开发入门较高,但从可维护,推广简易,成本效益等角度看是值得尝试的。

二. 一些提示以及注意事项

  1. 必须基于JVM技术栈
  2. 目前实现SOA接口的录制,Mock,以及方法级别的耗时分析。
  3. Soa 通过DUBBO,自研SOA的方式提供。
  4. 数据库查询,redis查询等JAVA API相对稳定(暂未实现,架构同理)
  5. 链路监控(架构同理)

三. 方案架构

  1. 架构

整体架构在目标应用层面主要划分为三部分,

  • Javaagent部分,主要Transform应用代码,加载Plugin逻辑
  • Plugin,负责数据收集,发送给消息队列,同步应用信息等
  • 应用,通过反射的方式调用Plugin的Hook,利用Javaagent将相关逻辑编织进应用code space。
    服务端,主要是用来数据管理,用户管理,ceph管理和其他操作的Portal
    服务端和客户端通过long-poll http,消息队列方式进行通信。后面计划自定义Transport层,通过TCP协议传输来提高通信效率和减少资源的利用

java 视频车流量统计 java流量监控_java 视频车流量统计


3. classloader 方案

Classloader的实现,通过自定义Classloader来实现与应用代码空间的隔离,保证对应用不产生污染。

java 视频车流量统计 java流量监控_List_02

  1. 扩展类库加载

在项目进行Transform的时候初始化自定义类加载,尝试Attach路径下的PluginJar。

java 视频车流量统计 java流量监控_List_03


SPI 核心探测实现

public class SPI {

    private static final Logger LOG = LoggerFactory.getLogger(SPI.class);

    private static final String SPI_BASE_PATH = "META-INF/snake/services";

    public static List<String> detect(JarFile jarFile, String name) {
        JarEntry jarEntry = jarFile.getJarEntry(SPI_BASE_PATH + "/" + name);
        if (jarEntry != null) {
            try (InputStream is = jarFile.getInputStream(jarEntry)) {
                List<String> entries = new LinkedList<>();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
                String readLine = null;
                while ((readLine = bufferedReader.readLine()) != null) {
                    if (readLine.length() > 5) {
                        entries.add(readLine.trim());
                    }
                }
                return entries;
            } catch (IOException e) {
                LOG.error(e.getMessage(), e);
                return Collections.emptyList();
            }
        }
        return Collections.emptyList();
    }
}

PluginAttacher核心实现

/**
 * @author lonelyangel.jcw@gmail.com
 * @date 2020/5/6
 * MTEAT-INF/snake/service/xxxx.xxx.Pulgin
 */
public class PluginAttacher {

    private static final Logger LOG = LoggerFactory.getLogger(PluginAttacher.class);

    private final LinkedList<File> paths = new LinkedList<>();

    private final ReentrantLock jarScanLock = new ReentrantLock();

    private final ClassLoader classLoader;

    private static final AtomicBoolean ATTACHED = new AtomicBoolean(false);

    public PluginAttacher(final String path, ClassLoader classLoader) {
        this.paths.add(new File(path));
        this.classLoader = classLoader;
    }

    public void attemptAttach() {
        if (ATTACHED.get()) {
            return;
        }
        ATTACHED.set(true);
        load().forEach(Plugin::attach);
    }

    private List<Plugin> load() {
        List<Jar> jars = allJars();
        List<String> detected = new LinkedList<>();
        for (Jar jar : jars) {
            detected.addAll(SPI.detect(jar.jarFile, Plugin.class.getName()));
        }
        return detected.stream().map(d -> {
            try {
                Class<?> klass = classLoader.loadClass(d);
                Constructor<Plugin> constructor = (Constructor<Plugin>) klass.getConstructor();
                return constructor.newInstance();
            } catch (ClassNotFoundException | NoSuchMethodException
                    | InstantiationException | IllegalAccessException
                    | InvocationTargetException e) {
                LOG.error(e.getMessage(), e);
                return null;
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());

    }


    private List<Jar> allJars() {
        final LinkedList<Jar> allJars = new LinkedList<>();
        jarScanLock.lock();

        for (File path : paths) {
            if (path.exists() && path.isDirectory()) {
                String[] jarNames = path.list((dir, name) -> name.endsWith(".jar"));
                if (jarNames != null) {
                    for (String jarName : jarNames) {
                        File jarFile = new File(path, jarName);
                        try {
                            Jar jar = new Jar(new JarFile(jarFile), jarFile);
                            allJars.add(jar);
                        } catch (IOException ex) {
                            LOG.error(ex.getMessage(), ex);
                        }
                    }
                }
            }
        }
        jarScanLock.unlock();
        return allJars;
    }

    private class Jar {
        private JarFile jarFile;

        private File sourceFile;

        public Jar(JarFile jarFile, File sourceFile) {
            this.jarFile = jarFile;
            this.sourceFile = sourceFile;
        }
    }

}

五. 当前功能支持

  1. 基于JavaAgent,对开发无感,无侵入式Mock
  2. 支持自研Soa服务,DUBBO服务的SOA接口Mock。
  3. 支持接口返回报文的录制,可支持一次访问记录的链路日志回放
  4. 服务方法调用链以及方法耗时记录

六. 后续

  • 这篇文章主要讲下整个系统的大体架构和一些想法,具体实现方案和实现技术在后续文章详细介绍
  • 暂时想到的一些点,JavaAgent技术,字节码注入技术(ASM,Javassist,Bytebuddy),Pulsar相关介绍,SOA-Dubbo简要介绍,方法耗时收集,Mock技术方案,全链路监控等。