什么是 Java Agent
Java Agent是JVM啟動時(shí)給應(yīng)用程序一種機(jī)會去修改class文件的機(jī)制畜疾,在啟動的VM參數(shù)增加-javaagent 再加上自定義的擴(kuò)展來實(shí)現(xiàn)砌滞,這種自定義擴(kuò)展是一種插件開發(fā)機(jī)制。
使用 Java Agent 的步驟大致如下:
定義一個(gè) MANIFEST.MF 文件哀蘑,在其中添加 premain-class 配置項(xiàng)苫拍。
創(chuàng)建 premain-class 配置項(xiàng)指定的類瓜富,并在其中實(shí)現(xiàn) premain() 方法,方法簽名如下:
public static void premain(String agentArgs, Instrumentation inst){
...
}
將 MANIFEST.MF 文件和 premain-class 指定的類一起打包成一個(gè) jar 包。
使用 -javaagent 指定該 jar 包的路徑即可執(zhí)行其中的 premain() 方法厘托。
如果使用maven打包友雳,可以省去上面繁瑣的操作,配置如下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.preapm.agent.Bootstrap</mainClass>
</manifest>
<manifestEntries>
<Premain-Class>com.preapm.agent.APMAgentPremain</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
實(shí)例
public class TestAgent {
public static void premain(String agentArgs,
Instrumentation inst) {
System.out.println("this is a java agent with two args");
System.out.println("參數(shù):" + agentArgs + "\n");
}
public static void premain(String agentArgs) {
System.out.println("this is a java agent only one args");
System.out.println("參數(shù):" + agentArgs + "\n");
}
}
premain() 方法有兩個(gè)重載铅匹,如下所示押赊,如果兩個(gè)重載同時(shí)存在,【1】將會被忽略包斑,只執(zhí)行【2】
public static void premain(String agentArgs) [1]
public static void premain(String agentArgs,
Instrumentation inst); [2]
agentArgs 參數(shù):-javaagent 命令攜帶的參數(shù)流礁。在前面介紹 SkyWalking Agent 接入時(shí)提到,agent.service_name 這個(gè)配置項(xiàng)的默認(rèn)值有三種覆蓋方式罗丰,其中神帅,使用探針配置進(jìn)行覆蓋,探針配置的值就是通過該參數(shù)傳入的萌抵。
inst 參數(shù):java.lang.instrumen.Instrumentation 是 Instrumention 包中定義的一個(gè)接口找御,它提供了操作類定義的相關(guān)方法。
如何用java agent修改class,比如實(shí)現(xiàn)監(jiān)控功能在getNumber前后增加AOP邏輯绍填?
比如有下面這個(gè)類
public class TestClass {
public int getNumber() { return 1; }
}
接下來需要編寫一個(gè)Transformer類來實(shí)現(xiàn)對類的修改霎桅,下面以javassist為字節(jié)碼注入工具進(jìn)行演示,在方法前后擇增加統(tǒng)計(jì)時(shí)間讨永。
public class TestAgent {
public static void premain(String agentArgs, Instrumentation inst)
throws Exception {
// 注冊一個(gè) Transformer滔驶,該 Transformer在類加載時(shí)被調(diào)用
inst.addTransformer(new Transformer(), true);
inst.retransformClasses(TestClass.class);
System.out.println("premain done");
}
}
Transformer的實(shí)現(xiàn)如下(參考代碼):
class Transformer implements ClassFileTransformer {
public byte[] transform(ClassLoader l, String className,
Class<?> c, ProtectionDomain pd, byte[] b) {
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
byte[] byteCode = classfileBuffer;
className = className.replace('/', '.');
if (!isNeedLogExecuteInfo(className)) {
return byteCode;
}
if (null == loader) {
loader = Thread.currentThread().getContextClassLoader();
}
byteCode = aopLog(loader, className, byteCode);
return byteCode;
}
}
private byte[] aopLog(ClassLoader loader, String className, byte[] byteCode) {
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc;
try {
cc = cp.get(className);
} catch (NotFoundException e) {
cp.insertClassPath(new LoaderClassPath(loader));
cc = cp.get(className);
}
byteCode = aopLog(cc, className, byteCode);
} catch (Exception ex) {
System.err.println(ex);
}
return byteCode;
}
private byte[] aopLog(CtClass cc, String className, byte[] byteCode) throws CannotCompileException, IOException {
if (null == cc) {
return byteCode;
}
if (!cc.isInterface()) {
CtMethod[] methods = cc.getDeclaredMethods();
if (null != methods && methods.length > 0) {
boolean isOpenPojoMonitor = ConfigUtils.isOpenPojoMonitor();
Set<String> getSetMethods = Collections.emptySet();
if (!isOpenPojoMonitor) {
getSetMethods = PojoDetector.getPojoMethodNames(methods);
}
for (CtMethod m : methods) {
if (isOpenPojoMonitor || !getSetMethods.contains(m.getName())) {
aopLog(className, m);
}
}
byteCode = cc.toBytecode();
}
}
cc.detach();
return byteCode;
}
private void aopLog(String className, CtMethod m) throws CannotCompileException {
if (null == m || m.isEmpty()) {
return;
}
boolean isMethodStatic = Modifier.isStatic(m.getModifiers());
String aopClassName = isMethodStatic ? "\"" + className + "\"" : "this.getClass().getName()";
final String timeMethodStr
= ConfigUtils.isUsingNanoTime() ? "java.lang.System.nanoTime()" : "java.lang.System.currentTimeMillis()";
// 避免變量名重復(fù)
m.addLocalVariable("dingjsh_javaagent_elapsedTime", CtClass.longType);
m.insertBefore("dingjsh_javaagent_elapsedTime = " + timeMethodStr + ";");
m.insertAfter("dingjsh_javaagent_elapsedTime = " + timeMethodStr + " - dingjsh_javaagent_elapsedTime;");
m.insertAfter(LOG_UTILS + ".log(" + aopClassName + ",\"" + m.getName()
+ "\",(long)dingjsh_javaagent_elapsedTime" + ");");
}
如何實(shí)現(xiàn)精確配置監(jiān)聽
細(xì)心的讀者可能會發(fā)現(xiàn),雖然上面的方式采用排除法排除了不需要監(jiān)控的類卿闹,但是監(jiān)控范圍還是過廣揭糕,同時(shí),可以看到最后的log實(shí)在是太簡單锻霎,無法實(shí)現(xiàn)一些復(fù)雜的邏輯著角,不具備很好的擴(kuò)展性,那么有沒有更好的方式呢旋恼?
這個(gè)patroller 項(xiàng)目采用精確匹配的方式進(jìn)行監(jiān)控類擴(kuò)展雇寇,配置采用yml配置方式,配置如下:
#基礎(chǔ)的插件包配置蚌铜,會優(yōu)先加載
basePlugins:
pre-agent-common:
jarName: pre-agent-common
pre-zipkin-sdk:
jarName: pre-zipkin-sdk
#第三方插件包配置
plugins:
pre-zipkin-plugin:
jarName: pre-zipkin-plugin
interceptorNames:
- com.preapm.agent.plugin.interceptor.ZipkinInterceptor
loadPatterns:
- com.preapm.agent.constant.BaseConstants
---
#AOP切面配置,類似sparing aop配置
patterns:
#==========================================================一下是具體業(yè)務(wù)配置根據(jù)具體項(xiàng)目路徑修改================================
#業(yè)務(wù)service配置
pre-service:
patterns:
- com.*.service.impl.*
excludedPatterns:
includedPatterns:
#- key: .*(run\(\)|call\(\))
# interceptors:
# - com.preapm.agent.plugin.interceptor.JdkThreadInterceptor
- key: .*
interceptors:
- com.preapm.agent.plugin.interceptor.ZipkinInterceptor
interceptors:
- com.preapm.agent.plugin.interceptor.ZipkinInterceptor
track:
inParam: true #記錄入?yún)? outParam: true #記錄出參
time: -1 #不設(shè)置時(shí)間限制
serialize: fastjson
consMethod: false
#agent測試配置
test:
#插件織入匹配方法路徑嫩海,支持正則表達(dá)式
patterns:
- com.preapm.agent.Bootstrap
#排除匹配路徑
excludedPatterns:
- key: com.preapm.agent.Bootstrap\(\)
includedPatterns:
- key: .*
interceptors:
- com.preapm.agent.plugin.interceptor.ZipkinInterceptor
track:
inParam: false
outParam: true
time: -1
同時(shí)在AOP實(shí)現(xiàn)方面具備很好的擴(kuò)展性冬殃,主要看這個(gè)類
com.preapm.agent.weave.ClassWrapper
代碼如下:
public String beginSrc(ClassLoader classLoader,byte[] classfileBuffer,CtClass ctClass, CtMethod ctMethod) {
String methodName = ctMethod.getName();
List<String> paramNameList = Arrays.asList(ReflectMethodUtil.getMethodParamNames(classLoader,classfileBuffer,ctClass, ctMethod));
try {
//System.out.println("方法名稱:"+methodName+" 參數(shù)類型大小:"+ctMethod.getParameterTypes().length+" paramNameList:"+paramNameList.toArray());
String template = ctMethod.getReturnType().getName().equals("void")
?
"{\n" +
" %s \n" + beforAgent(methodName,paramNameList)+" \n"+
" try {\n" +
" %s$agent($$);\n" +
" } catch (Throwable e) {\n" +
" %s\n" +doError(BaseConstants.THROWABLE_NAME_STR)+
" throw e;\n" +
" }finally{\n" +
" %s\n" + afterAgent(null)+" \n"+
" }\n" +
"}"
:
"{\n" +
" %s \n" +
" Object result=null;\n" +beforAgent(methodName,paramNameList)+" \n"+
" try {\n" +
" result=($w)%s$agent($$);\n" +
" } catch (Throwable e) {\n" +
" %s \n" +doError(BaseConstants.THROWABLE_NAME_STR)+
" throw e;\n" +
" }finally{\n" +
" %s \n" + afterAgent(BaseConstants.RESULT_NAME_STR)+" \n"+
" }\n" +
" return ($r) result;\n" +
"}";
String insertBeginSrc = this.beginSrc == null ? "" : this.beginSrc;
String insertErrorSrc = this.errorSrc == null ? "" : this.errorSrc;
String insertEndSrc = this.endSrc == null ? "" : this.endSrc;
String result = String.format(template,
new Object[] { insertBeginSrc, ctMethod.getName(), insertErrorSrc, insertEndSrc });
//log.info("result:"+result);
return result;
} catch (NotFoundException localNotFoundException) {
log.severe(org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(localNotFoundException));
throw new RuntimeException(localNotFoundException);
}
}
最終實(shí)現(xiàn)是通過如下代碼
package com.preapm.agent.weave.impl.ClassWrapperAroundInterceptor
class ClassWrapperAroundInterceptor
{
//... 其他邏輯
public String beforAgent(String methodName, List<String> argNameList) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("com.preapm.agent.common.bean.MethodInfo preMethondInfo = new com.preapm.agent.common.bean.MethodInfo();").append(line());
//stringBuilder.append("com.preapm.agent.common.bean.MethodInfo preMethondInfo = com.preapm.agent.common.context.AroundInterceptorContext.loader(Thread.currentThread().getContextClassLoader());").append(line());
stringBuilder.append("preMethondInfo.setTarget(this);").append(line());
stringBuilder.append("preMethondInfo.setMethodName(" + toStr(methodName) + ");").append(line());
if (argNameList != null && argNameList.size() != 0) {
stringBuilder.append("preMethondInfo.setArgs($args);").append(line());
stringBuilder.append("String preMethodArgsStr = ").append(toStr(StringUtils.join(argNameList, ",")))
.append(";").append(line());
stringBuilder.append("preMethondInfo.setArgsName(preMethodArgsStr.split(" + toStr(",") + "));")
.append(line());
}
setSerialize(stringBuilder);
setPlugin(stringBuilder);
setTrack(stringBuilder);
stringBuilder.append("com.preapm.agent.common.context.AroundInterceptorContext.start(Thread.currentThread().getContextClassLoader(),preMethondInfo);")
.append(line());
return stringBuilder.toString();
}
}
最后我們看看AroundInterceptorContext
public static void start(ClassLoader classLoader,MethodInfo methodInfo, String... names) {
for (AroundInterceptor i : get(classLoader,names)) {
i.before(methodInfo);
}
}
可以看到叁怪,最終是通過遍歷我們配置的AroundInterceptor接口來實(shí)現(xiàn)擴(kuò)展的功能
終極方案
大家可以發(fā)現(xiàn)以上方案已經(jīng)是非常好了审葬,我們可以通過精確配置需要監(jiān)控的類,同時(shí)可以通過擴(kuò)展將監(jiān)控?cái)?shù)據(jù)使用擴(kuò)展的方式存儲到任何地方,但是這種方案也有其局限性涣觉,首先AroundInterceptor的邏輯實(shí)現(xiàn)可能非常復(fù)雜痴荐,特別是對于一些復(fù)雜調(diào)用鏈的情況,其次這種上報(bào)方式不是很好調(diào)度官册,容易引發(fā)性能問題生兆,同時(shí)對于一些復(fù)雜的采樣,合計(jì)膝宁,匯總等監(jiān)控方式還是需要實(shí)現(xiàn)復(fù)雜的代碼才能實(shí)現(xiàn)鸦难,那么有沒有更好的方案呢?
答案是當(dāng)然有员淫,我推薦skywalking合蔽,如果需要更進(jìn)一步的學(xué)習(xí),可以參考github或者官方文檔介返,原理和上面所講的是類似的拴事,但是上面只是講了客戶的采集,對于服務(wù)端的實(shí)現(xiàn)還需要讀者進(jìn)一步挖掘圣蝎。