Pinpoint 源码分析(一)

pinpoint 是什么?

这个问题还是详见官方文档,我写得应该不会比它更好了。

原文:https://github.com/naver/pinpoint/wiki/Technical-Overview-Of-Pinpoint

译文:https://github.com/skyao/learning-pinpoint/blob/master/design/technical_overview.md

论文:https://ai.google/research/pubs/pub36356

简单的说它可以用来追踪分布式系统的执行链路,记录执行时间、参数、异常等各种信息,可以作性能分析、监控预警、故障分析等用途。

假如读者已经对pinpoint有直观的认识,从githup download了相应源码,在本机编译运行quickstart应用,体验过了quickstart中pinpoint采集testApp接口调用数据,上报collect,在web中查看到了链路图,详细查看某个方法的执行链路。那么可以开始阅读本源码分析文章。

Interceptor 源码分析
如果已经阅读了pinpoint的介绍文章,那么就应该知道,pinpoint使用了java字节码增强技术ASM(ASM的详细信息可以自行google),配合java探针技术,可以在应用类装载前,改写类结构,比如增加字段、增加访问方法get/set、增加方法、改写方法。pinpoint就是通过这一系列技术达到对客户代码无侵入,用户使用pinpoint无需改写应用代码,无需增加锚点,pinpoint自动帮你完成一切。pinpoint的plugin就是完成对用户代码改写、增强的模块,pinpoint提供了很多内置插件,用户也可以按规范编写自己的插件。而我们马上要分析的Interceptor就是插件的基础,通过Interceptor来对方法做功能增强。

com.navercorp.pinpoint.bootstrap.interceptor 包下是拦截器相关的框架类,本文主要分析该包中的类。Interceptor 是其中的接口,它没有任何方法,是一个标记接口。
下面的AroundInterceptor、ApiIdAwareAroundInterceptor、StaticAroundInterceptor都继承自Interceptor。
AroundInterceptor

方法环绕拦截器,在方法执行的前后增加拦截,如果原方法体执行出现异常,也将被拦截

public interface AroundInterceptor extends Interceptor {

    void before(Object target, Object[] args);

    void after(Object target, Object[] args, Object result, Throwable throwable);
}

例:客户代码

public String say(String name){
    //do something
    return "hello "+name;
}

如果一个AroundInterceptor拦截以上方法,那么客户代码会被改写为如下,以下示例原理,不一定与实际实现一致。

public String say(String name){
    AroundInterceptor interceptor = InterceptorRegistry.getInterceptor(1);
    interceptor.before(this,new Object[]{name});
    try {
       //do something
       
       Object result = "hello "+name;
       interceptor.after(this,new Object[]{name},result,null);
       return result;
    } catch (Throwable t) {
       interceptor.after(this,new Object[]{name},null,t);
       throw t;
    }
}

首先,在方法入口处会插入获取拦截器的代码,所有拦截器都注册在InterceptorRegistry中,每个注册的拦截器会被分配相应的id,此处通过id获取到相应的拦截器。原方法体被执行前先执行拦截器before方法,将当前对象及方法入参传入before。原方法体被try catch包裹,捕获所有原方法体的异常,如果有异常则先调用拦截器after方法,将异常信息传入,然后再原样抛出异常;如果方法正常返回,则所有返回语句都会被拦截,待after方法执行过后才返回。
这种改写技术,前面已经提到是ASM,这种技术要求开发人员对字节码要非常熟悉,一旦改写失败,那整个class都无法正常装载。如果将这种API暴露给插件开发人员,那对插件开发人员要求过高,这也就是为何pinpoint会抽象出Interceptor的原因,使用interceptor简化了改写字节码过程,用户只需要实现interceptor本身逻辑,改写字节码部分由pinpoint封装,执行期间会自动调用到用户编写的interceptor。

StaticAroundInterceptor

public interface StaticAroundInterceptor extends Interceptor {

void before(Object target, String className, String methodName, String parameterDescription, Object[] args);

void after(Object target, String className, String methodName, String parameterDescription, Object[] args, Object result, Throwable throwable);

}

静态方法的环绕拦截器,与AroundInterceptor类似,只不过是针对静态方法的拦截。

ApiIdAwareAroundInterceptor

public interface ApiIdAwareAroundInterceptor extends Interceptor {
void before(Object target, int apiId, Object[] args);
void after(Object target, int apiId, Object[] args, Object result, Throwable throwable);
}

关注apiId的环绕拦截器,拦截方法会多一个apiId。aware命名的接口在spring源码中也经常见到,表示实现类关注对某种目标,框架会把实现类关注的东西注入或方法参数传递给它。这里表示实现类对apiId关注。

com.navercorp.pinpoint.bootstrap.interceptor 包下还有 ExceptionHandler 接口

public interface ExceptionHandler {
void handleException(Throwable t);
}

这个接口用于处理拦截器方法发生的异常,我们插入了before、after方法,如果这些方法发生异常而不处理,那将影响到原始方法体的执行逻辑,因此有必要对拦截器方法发生的异常做处理。

ExceptionHandleAroundInterceptor 类,这是一个装饰器模式的类,用于装饰其它拦截器类,主要做异常处理的功能增强。
该包下还有各种以数字结尾的类,如:AroundInterceptor0、ExceptionHandleAroundInterceptor0等,其它包下也有类似的类,这些类都和不带数字的类功能类似,主要处理方法不同参数个数。

scope、ExecutionPolicy

下面再看scope包下的类,这里要解释的两个概念:scope、ExecutionPolicy。
scope 作用域或作用范围:它起到的作用是将多个拦截器关联在一起。
ExecutionPolicy 执行策略:它控制拦截器是否执行。

下面我通过一个示例说明pinpoint中为何要引入这样的两个概念,这两个概念又是如何解决相关的问题。

假如有2个方法a和b。a、b两个方法都要被pinpoint用于做分布式链路跟踪,因此a、b都会被改写并加上环绕拦截器。a方法在某些情况下有可能调用b方法,或a方法在某些情况下会递归调用自身,无论以上哪种情况,加在a、b方法上的拦截器都有可能重复执行,比如a方法内调用了b方法,那a和b的拦截器都要执行。有时候这种重复执行并不是我们想要的,我们只想a上的拦截器执行一遍就可以了,如果再调用b方法,b的拦截器不执行,如果递归调用a自身,那也不再执行。
所以这里就产生了一个需求,a和b的拦截器本来是独立的,现在需要有一种办法将他们关联起来,需要有个一类似上下文的记录,比如a拦截器已经执行了,b的拦截器能感知到,然后不执行。或a递归时,能感知到已经在递归了,不重复执行拦截器。解决这个需求的关键就是通过scope,同名scope可以将拦截器关联,记录上下文信息,并决定拦截器是否被执行。
再回到上面的a、b方法,我们之前的需求是只想让拦截器执行一次,假如我们的确有需求,要记录每一次的执行情况呢?或者我们的需求是只第一次不执行,后面的每次都执行?
ExecutionPolicy 就是为解决我们上述两种需求的,下面我们通过源码来分析。

scope包下,我们可以看到InterceptorScope和InterceptorScopeInvocation,还有一个ExecutionPolicy枚举类。

public interface InterceptorScope {
String getName();
InterceptorScopeInvocation getCurrentInvocation();
}

public interface InterceptorScopeInvocation {
String getName();

boolean tryEnter(ExecutionPolicy policy);//以给定执行策略尝试进入拦截器
boolean canLeave(ExecutionPolicy policy);
void leave(ExecutionPolicy policy);

boolean isActive();

Object setAttachment(Object attachment);//同scope的拦截器之间可以传递参数
Object getAttachment();
Object getOrCreateAttachment(AttachmentFactory factory);
Object removeAttachment();
}

public enum ExecutionPolicy {
ALWAYS, //总是执行拦截器
BOUNDARY, //边界处执行拦截器
INTERNAL //与BOUNDARY相反,边界处不执行,内部调用时执行拦截器
}

通过以上接口的设计,我们可以看出以下信息
scope 是通过名字进行标识,名字相同scope就相同。
scope 可以获取到 InterceptorScopeInvocation
InterceptorScopeInvocation 中包含拦截器执行与否的控制逻辑,这个控制逻辑可以有
ExecutionPolicy 来指定。
拦截器之间可以通过 scope 传递参数。

在自定义拦截器时,如何获取 InterceptorScope 呢?只需要在拦截器构造函数中,将
InterceptorScope 作为参数传入即可。自定义拦截器的初始化时交给框架的,框架会解析拦截器构造函数的参数,并传入相应的值。

比如:

com.navercorp.pinpoint.plugin.httpclient3.interceptor.HttpMethodBaseExecuteMethodInterceptor
构造函数:

public HttpMethodBaseExecuteMethodInterceptor(TraceContext traceContext, MethodDescriptor methodDescriptor, InterceptorScope interceptorScope)

该拦截器在 com.navercorp.pinpoint.plugin.httpclient3.HttpClient3Plugin 被添加到指定方法,代码如下

InstrumentMethod execute = target.getDeclaredMethod(“execute”, “org.apache.commons.httpclient.HttpState”, “org.apache.commons.httpclient.HttpConnection”);
if (execute != null) {
execute.addScopedInterceptor(HttpMethodBaseExecuteMethodInterceptor.class, HttpClient3Constants.HTTP_CLIENT3_METHOD_BASE_SCOPE, ExecutionPolicy.ALWAYS);
}

自定义拦截器会被com.navercorp.pinpoint.profiler.objectfactory.AutoBindingObjectFactory 的工厂方法初始化,

Interceptor interceptor = (Interceptor) factory.createInstance(interceptorClass, providedArguments, interceptorArgumentProvider);

初始化的过程中,会通过拦截器的构造函数,先解析构造函数的参数,然后注入相应的值。熟悉springMvc的应该很清楚这种方式,springMvc中的controller方法,也是通过这种形式自动注入参数的。因此,如果自定义拦截器依赖 InterceptorScope ,那么相应的值会自动注入。 InterceptorScope 在pinpoint中有默认的实现:请见profile模块中com.navercorp.pinpoint.profiler.interceptor.scope 包下的两个类 DefaultInterceptorScope 和 DefaultInterceptorScopeInvocation。

初始化自定义拦截器之后,自定义拦截器会被装饰,作功能增强,

com.navercorp.pinpoint.profiler.interceptor.factory.AnnotatedInterceptorFactory 的

public Interceptor newInterceptor(Class<?> interceptorClass, Object[] providedArguments, ScopeInfo scopeInfo, InstrumentClass target, InstrumentMethod targetMethod) 

方法返回值
wrap(interceptor, scopeInfo, interceptorScope);

装饰器就是
com.navercorp.pinpoint.bootstrap.interceptor.scope 包下的 相关类。

至此,我们基本分析完了pinpoint 的 interceptor ,这是pinpoint无侵入实现的基础。下一篇文章我们看看通过拦截器,pinpoint为应用增加了什么代码,使得pinpoint具备了分布式跟踪的能力。

Pinpoint 的 DefaultSqlParser 类是用于解析 SQL 语句的工具类。它主要用于解析 SQL 语句中的表名、列名、条件语句等信息。解析过程中会涉及到正则表达式的使用。 下面是 DefaultSqlParser 类的源码解析: ``` public class DefaultSqlParser implements SqlParser { private static final String[] EMPTY_STRING_ARRAY = new String[0]; @Override public ParsedSql parse(String sql) { if (sql == null) { return new ParsedSql(EMPTY_STRING_ARRAY, EMPTY_STRING_ARRAY, false, false); } String newSql = SqlParsingUtils.removeComments(sql); newSql = SqlParsingUtils.replaceQuotationMark(newSql); List<String> parameters = new ArrayList<>(); StringBuilder parameterBuilder = new StringBuilder(); Set<String> tables = new LinkedHashSet<>(); Set<String> columns = new LinkedHashSet<>(); boolean inSingleQuoteString = false; boolean inDoubleQuoteString = false; boolean inParameter = false; int length = newSql.length(); for (int i = 0; i < length; i++) { char c = newSql.charAt(i); boolean isLast = (i + 1) == length; if (inSingleQuoteString) { if (c == '\'') { if (isLast || newSql.charAt(i + 1) != '\'') { inSingleQuoteString = false; } else { i++; } } } else if (inDoubleQuoteString) { if (c == '\"') { if (isLast || newSql.charAt(i + 1) != '\"') { inDoubleQuoteString = false; } else { i++; } } } else if (inParameter) { if (c == '?') { parameters.add(parameterBuilder.toString().trim()); parameterBuilder.setLength(0); inParameter = false; } else { parameterBuilder.append(c); } } else { if (c == '\'') { inSingleQuoteString = true; } else if (c == '\"') { inDoubleQuoteString = true; } else if (c == '?') { inParameter = true; } else if (c == ' ') { // skip } else if (c == ',') { // skip } else if (Character.isLetter(c) || c == '_') { // table name or column name int j = i + 1; for (; j < length; j++) { if (!(Character.isLetterOrDigit(newSql.charAt(j)) || newSql.charAt(j) == '_')) { break; } } String word = newSql.substring(i, j); String lowerCaseWord = word.toLowerCase(); if (SqlParsingUtils.TABLE_HINTS.contains(lowerCaseWord)) { // skip table hints } else if (SqlParsingUtils.QUERY_HINTS.contains(lowerCaseWord)) { // skip query hints } else if (SqlParsingUtils.TABLE_KEYWORDS.contains(lowerCaseWord)) { // table name i = j - 1; for (; j < length; j++) { if (newSql.charAt(j) == ' ' || newSql.charAt(j) == '\t') { continue; } else if (newSql.charAt(j) == '(') { i = j; break; } else { tables.add(word); break; } } } else { // column name columns.add(word); i = j - 1; } } } } return new ParsedSql(tables.toArray(new String[tables.size()]), columns.toArray(new String[columns.size()]), !parameters.isEmpty(), inParameter); } } ``` 其中,parse() 方法接收一个 SQL 语句作为参数,并返回 ParsedSql 对象,该对象包含解析后的表名、列名、参数等信息。 在解析过程中,首先会移除 SQL 语句中的注释和替换引号,然后遍历 SQL 语句的每个字符,根据不同的字符类型进行相应的处理。具体来说: - 如果当前字符在单引号字符串中,则跳过; - 如果当前字符在双引号字符串中,则跳过; - 如果当前字符是问号,则表示当前字符为参数,将 inParameter 标记设置为 true,并将参数添加到 parameters 集合中; - 如果当前字符是空格或逗号,则跳过; - 如果当前字符是字母或下划线,则表示当前字符为表名或列名。在往后遍历时,如果遇到空格或括号,则表示当前字符为表名,否则为列名; - 如果当前字符不属于上述任何一种类型,则跳过。 在解析过程中,还需要注意以下几点: - 如果 SQL 语句中出现了嵌套的单引号或双引号,则需要特殊处理; - 如果 SQL 语句中出现了注释,则需要移除; - 如果 SQL 语句中出现了参数,则需要将 inParameter 标记设置为 true,并将参数添加到 parameters 集合中; - 如果 SQL 语句中出现了表名或列名,则需要将其添加到 tables 或 columns 集合中。 最后,将解析得到的表名、列名、参数等信息封装到 ParsedSql 对象中,并返回即可。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值