如何减少springcloud微服务接入apm成本

写在前面

apm接入是我为我现有公司编写的微服务治理与监控平台初版,编写了有段时间了,一直在推动公司各java系统接入(非java系统,可基于http上报信息)

如何让apm接入成本最小呢

  • 启动类加上注解即可生效
  • 少许的代码改动

littlehow-apm的接入是依托于feign以及sleuth的,所以如果只需要注解的话,势必要代理feign或者sleuth的调用类以便进行日志环绕信息的收集;但feign包下的多数类都是包级别,并且关键类并没有受spring生命周期管理,所以代理这些类比较难实现,也就是一个注解不容易实现。

还有一个办法是对feign的关键包进行源码改动后重新打包,那么稍显繁琐的一点就是apm移植通用性就不太好

但是如果修改了源码,不重新打包,让其被加载进jvm即可,这样对私有maven仓库的依赖就不复存在了,但是如何保证jvm加载到了自身的类呢,下面将详细说明

关键类说明

自有类加载器(字节码存储与内存,直接进行加载)

package com.littlehow.apm.base;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 字节资源类加载器
 * 使用父加载器来定义加载类
 * 因为此加载器加载的类都是重写三方类的资源
 * 不交给父加载器进行定义的话,父加载器还会继续从资源寻找该类
 * 而因为jar的加载顺序不可控,所以可能加载在不是自己修改后的类(同包同名)
 * 如org.springframework.cloud.netflix.feign.ribbon.LoadBalancerFeignClient
 * @author littlehow
 */
public class ApmClassLoader extends ClassLoader {

    /**
     * classloader中的保护方法,需要使用反射调用
     */
    private static Method DEFINE_CLASS_METHOD ;

    /**
     * 当前类加载器已经加载过的类
     */
    private static final Map<String, Class<?>> loadedClassMap = new HashMap<>();

    /**
     * 加载ApmClassLoader的类加载器
     */
    private static ClassLoader parent;
    static {
        Method[] methods = ClassLoader.class.getDeclaredMethods();
        for (Method method : methods) {
            if ("defineClass".equals(method.getName()) && method.getParameterCount() == 4) {
                DEFINE_CLASS_METHOD = method;
                method.setAccessible(true);
            }
        }
        parent = ApmClassLoader.class.getClassLoader();
    }

    @Override
    public Class<?> loadClass(String className) throws ClassNotFoundException {
        synchronized (this) {
            Class<?> c = loadedClassMap.get(className);
            if (c == null) {
                c = findClass(className);
                loadedClassMap.put(className, c);
            }
            return c;
        }
    }

    @Override
    public Class<?> findClass(String className) throws ClassNotFoundException {
        // 加载后就不再保存
        byte[] bts = ApmClassSourceManager.classes.remove(className);
        if (bts == null || bts.length == 0) {
            // 如果没有资源类,则调用parent加载器进行加载
            return parent.loadClass(className);
        }
        // 定义给父加载器,保证父类不再去自行加载
        Class<?> clazz = executeDefine(parent, className, bts);
        if (clazz == null) {
            throw new ClassNotFoundException(className);
        }
        return clazz;
    }

    /**
     * 加载类
     * 普通项目的类加载器为应用类加载器:
     * @see sun.misc.Launcher$AppClassLoader
     * 但是当项目被打成springboot的jar包后,加载该类的类加载器是
     * @see org.springframework.boot.loader.LaunchedURLClassLoader
     * 该类加载器放置于线程上下文类加载器中
     *
     * @param loader    -  加载器
     * @param className -  类名
     * @param bts       -  字节码
     * @return - 类名对应的class对象
     */
    private Class<?> executeDefine(ClassLoader loader, String className, byte[] bts) {
        try {
            return (Class<?>) DEFINE_CLASS_METHOD.invoke(loader, className, bts, 0, bts.length);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

接入类(需要接入方启动类继承该类)

package com.littlehow.apm.feign;

import com.littlehow.apm.base.ApmClassLoader;
import org.springframework.context.annotation.ComponentScan;

/**
 * 使apm收集功能生效,这里不使用starter主要是需要在启动前优先加载一些类
 *
 * @author littlehow
 */
@ComponentScan(basePackages = {"com.littlehow.apm.base", "com.littlehow.apm.feign"})
public class ApmApplication {
    private static ClassLoader loader = new ApmClassLoader();
    static {
        // 加载资源类(主要为了初始化类)
        try {
            // feign实际调用前后的资源类,里面做了前后advice外接处理, 进行类资源注册
            Class.forName("com.littlehow.apm.feign.SynchronousMethodHandlerSource");
            // feign生成代理核心类,对pathVariable进行提前获取,避免同一接口被解释成多个
            // 如/user/{userNo}/info 如果1001和1002调用,分析会出现两个,所以再此处拿出原始path解析
            // 进行类资源注册
            Class.forName("com.littlehow.apm.feign.ReflectiveFeignSource");

            // 对接口注解扫描忽略的类,如接入其他系统的client,也可能使用RequestMapping注解,这样就可能误把其他系统接口解析成自己的接口
            Class.forName("com.littlehow.apm.feign.FeignMappingIgnored");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }


        //加载类
        try {
            // 优先加载资源类
            loader.loadClass(ReflectiveFeignSource.className);
            loader.loadClass(SynchronousMethodHandlerSource.className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

ReflectiveFeignSource和SynchronousMethodHandlerSource可以看源码中的内容

生成资源类的字节码信息

package source;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class PrintSource {
    public static void main(String[] args) throws IOException {
        //String filePath = "apm-feign/target/classes/org/springframework/cloud/netflix/feign/ribbon/LoadBalancerFeignClient.class";
        //String filePath = "apm-feign/target/classes/feign/ReflectiveFeign$BuildTemplateByResolvingArgs.class";
        //String filePath = "apm-feign/target/classes/org/springframework/cloud/sleuth/instrument/web/client/feign/TraceFeignClient.class";
        String filePath = "apm-feign/target/classes/feign/SynchronousMethodHandler.class";
        byte[] b = Files.readAllBytes(Paths.get(filePath));
        System.out.println(b.length);
        System.out.println(new String(Base64.getEncoder().encode(b)));
    }
}