如何减少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)));
}
}