分类 默认分类 下的文章

由http header Keep-Alive 引出的问题

由于建立一个网络连接相对于本地操作十分昂贵, 所以系统应该尽量重用一个已经建立的连接.

因此, 在http 1.0的规范中定义了 Keep-Alive header, 一个典型的例子:

Keep-Alive: timeout=5, max=1000

这里的 timeout 表示如果没有后续的重用, 让这个连接最短逗留(idle) 5秒才能关闭它. 若有重用, 这从最后一个重用重新开始计时.
这里的 max 表示如果这个连接被一直重用, 但是这个连接最多只能发送1000个请求(或响应), 一旦超过这个数目, 即使能还没等idle 5秒, 也要关闭它. 所以它表示最多可以通过这个连接发送多少请求(或响应).

http 1.1

http 1.1 默认是持久化连接, 所以默认暗含是 Keep-Alive 的, 即便不发送这个头字段. 如果不在 http 的请求和响应中发送这个头字段, 那么连接的两端又该如何控制 idle 的timeout 和 一个连接最多可以发送的请求(或响应)的数目呢?

既然是为了重用, 那就最大化重用. 是不是一直重用某个连接, 一直不关闭就好了呢?

timeout 的合理性

  1. 从资源管理的角度. 若服务器端一直保持连接, 那么每个新用户都保持一个连接, 即便用户几分钟没数据传输, 这会导致服务器资源(连接, 内存等)很快被耗尽. 若客户端程序(浏览器, 代码客户端)也会有一样的问题.
  2. 从公平的角度. 若一个连接一直保持, 那么后来的新用户肯定无法再连接上.
  3. 从安全和性能的角度. 若保持很多没有数据的连接, 可能会有潜在的性能降级或者内存泄漏问题.

设置一个合理的 timeout 值, 保证一旦一段时间没数据传输, 就关闭这个连接.

max 请求的合理性

既然有了 timeout 可以保证 idle 了就关闭, 同时为了最大化重用, 那么是不是不需要 max 了呢?
考虑一种情况: timeout 是5秒, 但是每当 idle 4秒的时候, 就有一个新的请求(或响应), 那么这个连接将一直会被重用. 那么一直重用又有什么不好呢?

在理想的情况一下, 一直有数据固定频率的传输, 永久的使用这个连接其实是非常美好的一种状态.

但是 max 也有其它合理的地方:

  1. 避免某些写的不好的代码把某些请求(request)资源绑定在这个连接之上, 导致内存泄漏, 若关掉连接则全部释放.
  2. 公平性. 比如某些抢票的网站, 避免某些人一直先保持一个连接, 导致后面的人根本连不上. max 保证多少次请求之后, 一定关闭.
  3. 对于某些负载均衡器(Loader Balancer), 如果某个特定IP上一直保持特别多的连接, 会导致不再均衡.

服务端的 timeout 和 max

虽然 http 1.1 不再设置 Keep-Alive 头字段, 默认是持久连接. 但是连接的2端仍然要对合理的这2种机制做处理. 下面列举一些常见的服务端的处理和配置.

Tomcat web 服务器

Tomcat 是一个常用的 Java web 服务器. 在今天(20240605)最新的 10.1 版本里面的 http Connector 的配置里, 就能看到关于 timeout 和 max 的配置:
keepAliveTimeout: 默认60秒.
maxKeepAliveRequests : 默认100.

nginx 服务器

nginx 的配置分别是:
keepalive_timeout: 默认75秒.
keepalive_requests: 默认 1000.
不过, nginx 还有一个 keepalive_time: 类似max, 它从时间角度约束一个连接从开始建立最长存活多久, 区别于idle timeout.

envoy proxy

对于 Envoy proxy 没有对应的完全一致的概念, 不过它有另外2个参数:
common_http_protocol_options.idle_timeout:
stream_idle_timeout:
更多内容参考: https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts
https://github.com/envoyproxy/envoy/issues/8652

客户端的 timeout 和 max

既然对于 http 1.1 默认是持久的, 那么客户端也是暗含的, 不发送 Keep-Alive 的, 那么客户端是怎么处理的呢?

chrome 浏览器

没有找到相关文档.

Apache httpClient

在 4.5 版本 2.6 节 连接管理部份(https://hc.apache.org/httpcomponents-client-4.5.x/current/tutorial/html/connmgmt.html), 有下面一段描述:
“If the Keep-Alive header is not present in the response, HttpClient assumes the connection can be kept alive indefinitely”

JDK HttpClient

对于取值的大概处理:
https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L892-L937

max 的取值:

https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L902C25-L902C82

keepAliveConnections = p.findInt("max", usingProxy?50:5);

timeout 的取值

https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L906C37-L918

    /*
     * The timeout if specified by the server. Following values possible
     *  0: the server specified no keep alive headers
     * -1: the server provided "Connection: keep-alive" but did not specify a
     *     a particular time in a "Keep-Alive:" headers
     * -2: the server provided "Connection: keep-alive" and timeout=0
     * Positive values are the number of seconds specified by the server
     * in a "Keep-Alive" header
     */
OptionalInt timeout = p.findInt("timeout");
if (timeout.isEmpty()) {
    keepAliveTimeout = -1;
} else {
    keepAliveTimeout = timeout.getAsInt();
    if (keepAliveTimeout < 0) {
        // if the server specified a negative (invalid) value
        // then we set to -1, which is equivalent to no value
        keepAliveTimeout = -1;
    } else if (keepAliveTimeout == 0) {
        // handled specially to mean close connection immediately
        keepAliveTimeout = -2;
    }
}                          

max 用完之后, 关闭连接:

https://github.com/openjdk/jdk/blob/e1870d360e05c372e672b519d7de2a60c333675b/src/java.base/share/classes/sun/net/www/http/HttpClient.java#L443-L453

客户端总结

客户端也会考虑 连接idle 的timeout 和 max 请求数, 但是很多情况下, 它没有服务端那么紧迫, 但是客户端也有这些机制, 只是没有那么透明.

urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate

最近一个项目把 python 从 3.9 升级到3.11, 于是把 3.9 删除, 使用 brew install python@3.11, 其它都正常, 就是使用 requests 访问各个https 地址的时候, 就报错:

Failed to send a request to Slack API server: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)>

因为出错的是访问 slack api(其实跟访问那个url没关系), 所以找到了这个帖子: https://github.com/slackapi/bolt-python/issues/673.

可是里面提到的 Certificates.command 对于我的安装路径根本就不存在. 于是怀疑 brew 安装有问题, 重新安装, 没看到任何错误, 运行程序还少一样的错.

最终根据一篇帖子的描述, 找到了答案.

启动python, 运行下面的脚本:

supra@host tools % python -V
Python 3.11.9
supra@host tools % python
Python 3.11.9 (main, Apr  2 2024, 08:25:04) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import _ssl; _ssl.get_default_verify_paths()
('SSL_CERT_FILE', '/usr/local/etc/openssl@3/cert.pem', 'SSL_CERT_DIR', '/usr/local/etc/openssl@3/certs')

可以看到它用的证书在/usr/local/etc/openssl@3/cert.pem, 发现这个是一个软链接, 链接到 ../ca-certificates/cert.pem, 可是这个文件夹都不存在.

于是新建这个文件夹, 并且把正确的证书放到那里, 然后就工作了.

IEEE 754 浮点表示

IEEE 754 定义了计算机如何表示和存储非整数的数据, 如: 12.084, 0.3, -0.0043. 对于整数, 我们只要把十进制的整数转换成二进制, 并且在最前面设置它的正数/负数的符号, 就很容易的存储.

浮点数转成二进制

浮点数转成二进制分成2部份: 整数部分和小数部份.

  1. 对于整数部分, 直接转成二进制, 比如 12(十进制) = 1100 (二进制).
  2. 对于小数部份, 0.5(十进制) = 0.1(二进制), 0.25(十进制) = 0.01(二进制), 0.125(十进制) = 0.001(二进制), 所以可以看到对于小数部份是通过把1整除的方式获得的. 所以0.375(十进制) = 1/4 + 1/8 = 0.25 + 0.125 = 0.01(二进制) + 0.001(二进制) = 0.011(二进制).

所以:

12.125
   12 = 8 + 4 = 1100
   0.125 = 1/8 = 0.001
12.125 = 1100.001

科学计数法

科学记数法是一种记数的方法。 把一个数表示成a与10的n次幂相乘的形式(1≤|a|<10,a不为分数形式,n为整数),这种记数法叫做科学记数法。
例如:12.125 = 1.2125×10^1, 19971400000000=1.99714×10^13, 0.00345 = 3.45×10^-3.
所以它把一个数分成大于等于1小于10的科学计数部份和指数部分.

对于二进制, 同样适用科学计数法, 只不过二进制表示成科学计数法整数部分除了0之外, 只能有1. 比如:

12.125 = 1100.001 = 1.100001×2^3
0.375 = 0.011 = 1.1×2^-2

IEEE754 浮点表示

IEEE754 规定了单精度(32位)浮点数和双精度(64位)浮点数以及其他浮点数. 我们这里主要看单精度, 其它原理类似.

对于32位的浮点数, 32位分成 1位符号位(正负) + 8位指数 + 23位 科学计数数值.
符号位 0 表示正, 1 表示负.
比如:
12.125 = 1100.001 = 1.100001×2^3, 它的符号位0(正数), 指数部分是3 (二进制11), 科学计数数值是1.100001(二进制).
-0.375 = 0.011 = 1.1×2^-2, 它的符号位数1(负数), 指数部分是-2(-10二进制), 科学计数数值是1.1(二进制).

需要进一步明确的地方:

  1. 8位指数从 00000000 ~ 11111111, 即0 ~ 255, 但是这样对于是负数的指数无法表示, 所以需要调整这个值从0 ~ 255表示成-127 ~ 128. 即0表示 0-127 = -127(指数), 127 - 127 = 0(指数为0), 255 - 127 = 128. 但实际使用的时候, -127128被用作特殊值处理, 实际可能的值只能是 -126 ~ 127.
  2. 对于数值部分, 因为是科学计数法, 所以除了0之外, 其它时候都是1, 所以这个1可以去掉, 所以23位全部用来表示点之后的部份. 即 1.001101 只要使用 001101 它, 1.101 只要使用 101. 0表示成 23个0.

所以:
12.125 = 1100.001 = 1.100001×2^3 -> 符号是0, 指数部分原本是3,转成127+3=130,即二进制 10000010, 科学计数部分去掉点之前的1, 即是100001.
-0.375 = 0.011 = 1.1×2^-2 -> 符号是1, 指数部分是-2, 转成127-2=125, 即二进制 1111101, 科学计数部分去掉点之前的1, 即是 1.

对于指数部分不够8位只要前面补0, 对于科学计数部分, 由于是小数点后的, 所以后面补0. 于是:
12.125 = 1100.001 = 1.100001×2^3 => 0 10000010 10000100000000000000000`.
-0.375 = 0.11 = 1.1×2^-1 => 1 01111101 1000000000000000000000

验证

让AI写了一段 C 代码, 来验证一把:

#include <stdio.h>

int main() {
    float num1 = 12.125f;
    float num2 = -0.375f;

    // 将浮点数的二进制表示转换为字节表示
    unsigned char *bytePtr1 = (unsigned char *)&num1;
    unsigned char *bytePtr2 = (unsigned char *)&num2;

    printf("12.125 的二进制表示为:\n");
    for (int i = sizeof(float) - 1; i >= 0; i--) {
        for (int j = 7; j >= 0; j--) {
            printf("%d", (bytePtr1[i] >> j) & 1);
        }
        printf(" ");
    }
    printf("\n");

    printf("-0.375 的二进制表示为:\n");
    for (int i = sizeof(float) - 1; i >= 0; i--) {
        for (int j = 7; j >= 0; j--) {
            printf("%d", (bytePtr2[i] >> j) & 1);
        }
        printf(" ");
    }
    printf("\n");

    return 0;
}

跑一台机器

12.125 的二进制表示为:
01000001 01000010 00000000 00000000
-0.375 的二进制表示为:
10111110 11000000 00000000 00000000

通过已有网站内容借助GPT来回答问题

最近学习 chatGPT 文档的时候,看到这么一篇文章 How to build an AI that can answer questions about your website. 它讲的是如何用你现有的网站来做一个AI bot 来回答有关你网站的问题. 这种场景很常见: 比如你公司有很多很有用的文档存放在某个站点, 或者你有一个专门针对某个主题的blog网站,又或者某个产品的详细使用说明在线文档. 当有了GPT的工具后, 我们把这些站点的内容作为context送给GPT,然后GPT以这些context为基础来回答用户的问题.

下面我们就以我的个人网站为例,以 openai 的chatGPT API为工具, 构建这么一个问答程序.

步骤概括

总的来看, 我们要做下面的一些步骤:

  1. 把整个网站下载下来.
  2. 把有用的网页文档里面的核心内容取出来.
  3. 把这些取出来的核心文本内容做 text embeding, 并放入向量数据库.
  4. 当有问题的时候, 先使用问题的内容去搜索向量数据库, 把搜索的结果做为 context, 连同问题一并送给 chatGPT 获取答案.
    下面我们就给出具体的代码和步骤.

把整个网站下载下来

使用 wget 命令很容易去下载一个网站.

$ wget -r https://www.tianxiaohui.com 
        ...省略...
FINISHED --2024-01-06 19:42:12--
Total wall clock time: 11m 28s
Downloaded: 3611 files, 133M in 3m 37s (625 KB/s)

通过 -r 参数, wget会把整个网站都下载下来, 并且按照网页的路径分类. 可以看到这里下载了3611个文件, 133M. 但是我的网站明显没有这么多文章, 这里面包含很多图片的链接, 有些分类的页面.

把有用的网页文档里面的核心内容取出来.

通过人工浏览这些页面, 我们可以看到我们只需要特定的含有整篇文章的html页面, 有些分类页面(比如2023年3月的文章合集)是不需要的, 一篇文章的html被加载之后, 我们只需要取其中文章的部分, 不需要菜单链接和旁边的分类链接. 所以我们有下面的代码:

import os

from bs4 import BeautifulSoup


def fetch_docs(folder: str = None):
    # 遍历并过滤以 .html 结尾的文档
    html_docs = [f for f in os.listdir(folder) if f.endswith('.html')]

    txts = []
    for html_doc in html_docs:
        with open(folder + "/" + html_doc, 'r') as file:
            # 使用BeautifulSoup 解析html
            soup = BeautifulSoup(file, 'html.parser')
            # 只取其中文章的部分, 有些分类页面没有文章部分, 这里就会放弃
            post = soup.find('div', class_='post-content')
            if post:
                # 替换掉很多分隔符
                txt = post.get_text().replace("\n", "").replace("\t", " ").replace("\r", "");
                # print(txt) 查看具体文本, 方便跟多加工
                txts.append(txt)
            else:
                print("not find article from " + html_doc)
    print(len(txts))
    return txts
    

fetch_docs(“/Users/eric/Downloads/blogs/www.tianxiaohui.com/index.php/Troubleshooting”)

把这些取出来的核心文本内容做 text embeding, 并放入向量数据库.

我们使用openAI 的 embedding, 并使用 FAISS 做为向量库来进行相似性搜索.

from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS


embeddings_model = OpenAIEmbeddings()
vectorstore = FAISS.from_texts(
    fetch_docs("/Users/eric/Downloads/blogs/www.tianxiaohui.com/index.php/Troubleshooting"), embedding=embeddings_model
)

从向量数据库获取相关内容调用 GPT 生成答案

首先我们把向量库FAISS设置为 retriever, 然后检索相关文档, 然后把问题和相关文档组成的context 给chatGPT, 获取答案.

from langchain.prompts import PromptTemplate
from langchain.chat_models import ChatOpenAI


question = "如何分析Java应用OOM问题?"
retriever = vectorstore.as_retriever(search_kwargs={"k": 3, "score_threshold": .5})
docs = retriever.get_relevant_documents(question)
doc_contents = [doc.page_content for doc in docs]

prompt = PromptTemplate.from_template("here is the question: {query}, this is the contexts: {context}")
chain = prompt | ChatOpenAI()
result = chain.invoke({"query": question, "context": doc_contents})
print(result)

总结

通过上面几十行代码, 我们就能把一个现有的知识网站, 做成了一个初级版本的可以回答问题的智能AI了.

静态代码分析工具 Spoon 使用

官网: https://spoon.gforge.inria.fr/launcher.html
它是基于 Eclipse JDT 打造的静态代码分析工具. 它把原代码拆解成包,模块,类,方法,语句,表达式,各种语法单元, 各个语法单元又形成了父子包含等关系. 可以对原代码编译, 检查, 分析, 过滤, 替换, 转换 等.

AST 语法树的元素

语法树包含的各类语法基本单元: https://spoon.gforge.inria.fr/structural_elements.html
详细的代码块形成的各个基本单元: https://spoon.gforge.inria.fr/code_elements.html

setup project

可以分析一个基本的 Java 项目, 一个 Maven 项目, 或者一个 Jar包(通过反编译).

# 分析一个 Maven project 的 source code
MavenLauncher launcher = new MavenLauncher("/Users/tianxiaohui/codes/myProj", SOURCE_TYPE.APP_SOURCE,"/Users/tianxiaohui/apache-maven-3.9.1/");

launcher.getEnvironment().setComplianceLevel(17);
launcher.getEnvironment().setNoClasspath(true); //有些类没提供, 比如 servlet jar 里面的类
launcher.buildModel();

CtModel model = launcher.getModel();

3种情况:

  1. 有源代码 reference.getDeclaration() = reference.getTypeDeclaration()
  2. 没有源代码, 只有binary(jar). reference.getDeclaration() = null, reference.getTypeDeclaration() 反射得来, isShadow = true.
  3. 没有源代码, 也没有binary. reference.getDeclaration() = reference.getTypeDeclaration() = false.
    上面的 getTypeDeclaration 适用于 getFieldDeclaration, getExecutableDeclaration.

常见的代码分析

返回原代码中所有的包和类

for(CtPackage p : model.getAllPackages()) {
  System.out.println("package: " + p.getQualifiedName());
}
// list all classes of the model
for(CtType<?> s : model.getAllTypes()) {
  System.out.println("class: " + s.getQualifiedName());
}

找到一个方法的定义

当你知道一个方法名的时候, 你要查看这个类具体的定义, 可以通过下面的查找方法.

    public static void findMethodDefinition(CtModel ctModel, String clazzName, String methodName) {
        CtClass<?> foundClass = ctModel.getElements(new TypeFilter<CtClass<?>>(CtClass.class) {
            @Override
            public boolean matches(CtClass<?> clazz) {
                return clazz.getQualifiedName().equals(clazzName);
            }
        }).stream().findFirst().orElse(null);

        if (foundClass != null) {
            //System.out.println("Found class definition: " + foundClass);
            foundClass.getMethodsByName(methodName).forEach(m -> {
                System.out.println("Found method definition: " + m.getSignature());
                System.out.println(m.toString());
            });
        } else {
            System.out.println("Class definition not found for: " + clazzName);
        }
    }

    public static void findInvocationPoints(CtModel ctModel, String className, String methodName) {
        System.out.println(" Method " + className + "." + methodName + " is called by:");
        findInvocation(ctModel, className, methodName, 0, false);
    }

查找一个类实例是在哪里构造的

有时候我们要查找某个类是在哪里被初始化的, 可以通过下面的代码获得.

    public static void findNewClassConstruct(CtModel ctModel, String clazzName) {
        long start = System.currentTimeMillis();
        ctModel.getRootPackage().getElements(new TypeFilter<>(CtConstructorCall.class)).forEach(e -> {
            if (e.getExecutable().getDeclaringType().getQualifiedName().equals(clazzName)) {
                System.out.println(clazzName + "is created at: " + e.getPosition());
            }
        });
        System.out.println("time0: " + (System.currentTimeMillis() - start));
    }
    public static void findNewClassConstruct1(CtModel ctModel, String clazzName) {
        long start = System.currentTimeMillis();
        ctModel.getRootPackage().getElements(new TypeFilter<>(CtConstructorCall.class)).forEach(e -> {
            System.out.println(e.getType());
            String type = e.getType().toString();
            if (type.equals(clazzName)) {
                System.out.println(clazzName + "is created at: " + e.getPosition());
            }
        });
        System.out.println("time1: " + (System.currentTimeMillis() - start));
    }

查找一个方法的调用点

一个方法被调用的时候, 它声明的类型可能是它本身的类型, 或者它实现的接口类型, 或者直接/非直接父类的类型. 为了查看完整的可能性, 要能要去每个父类, 实现的接口都去查看一遍. 下面的方法只是查看当前类型的直接调用.

    /**
     *  here we only find the invocation with the exactly class name and method name, not the declared method in
     *  Interface and parent class.
     *  Sometimes you want to find all the invocation points of a method, include the declared method in Interface and parent class.
     *  ex:
     *    IHello hello = new Hello();
     *    hello.sayHello();
     *    In this case, the invocation point of sayHello() is in IHello, not in Hello.
     * @param ctModel
     * @param className
     * @param methodName
     * @param depth
     * @param recursive
     */
    private static void findInvocation(CtModel ctModel, String className, String methodName, int depth, boolean recursive) {

        List<CtInvocation<?>> invocations = ctModel.getElements(new TypeFilter<CtInvocation<?>>(CtInvocation.class) {
            @Override
            public boolean matches(CtInvocation<?> element) {
                return element.getExecutable().getSignature().toString().equals(methodName) && containsRefType(element.getReferencedTypes(), className);
            }
        });

        for (CtInvocation<?> invocation : invocations) {
            CtExecutable<?> caller = invocation.getParent(CtExecutable.class);
            if (caller != null) {
                for (int i = 0; i < depth; i++) {
                    System.out.print("\t");
                }
                System.out.print(" - " + caller.getParent(CtClass.class).getPackage() + "." + caller.getParent(CtClass.class).getSimpleName() + "." + caller.getSignature());
                if (caller.getThrownTypes().size() > 0) {
                    System.out.print(" throws ");
                    System.out.print(caller.getThrownTypes().stream().map(t -> t.toString()).collect(Collectors.joining( ", ")));
                }
                System.out.println(" at line " + invocation.getPosition().getLine());
                if (recursive) {
                    findInvocation(ctModel, caller.getParent(CtClass.class).getQualifiedName(), caller.getSignature(), 1 + depth, recursive);
                }
            }
        }
    }

找到所有抛出异常的代码

下面的代码找出所有抛出异常的代码点. 当然你可以根据异常的类型去过滤.

    public static void findThrowStatements(CtModel ctModel) {
        List<CtThrow> throwStatements = ctModel.getElements(new TypeFilter<>(CtThrow.class));

        // Process each throw statement
        for (CtThrow throwStatement : throwStatements) {
            CtExecutable<?> executable = throwStatement.getParent(CtExecutable.class);
            if (executable != null) {
                System.out.println(executable.getParent(CtClass.class).getPackage() + " - " + executable.getParent(CtClass.class).getSimpleName() + "." + executable.getSimpleName()
                        + " at line " + throwStatement.getPosition().getLine());
            }
        }
    }

找到包含特定注解的类或方法

有时候你想找到特定注解的类, 比如有些注解定义了系统的所有API, 有些注解标注了系统将要废弃的API.

findWithAnnotation(ctModel, javax.ws.rs.ApplicationPath.class).forEach(clazz -> {
            System.out.println(clazz.getAnnotation(javax.ws.rs.ApplicationPath.class).value());
        });


public static List<CtClass> findWithAnnotation(CtModel ctModel, Class<? extends Annotation> annotationType) {
        return ctModel.getRootPackage().getElements(new AnnotationFilter<>(annotationType));

列出某个函数调用的其它函数列表

给出特定函数, 我们可以列出当前函数使用了其他哪些函数

public static void findNextCalls(CtModel ctModel, String pkg, String clazz, String methodName) {
        //find this method
        ctModel.getElements(new TypeFilter<CtMethod<?>>(CtMethod.class) {
            @Override
            public boolean matches(CtMethod<?> method) {
                if (!(method.getParent() instanceof CtClass)) {
                    return false;
                }

                CtClass cz = (CtClass) method.getParent();
                String curClazz = cz.getSimpleName();
                String curPkg = null != cz.getPackage() ? cz.getPackage().getQualifiedName() : "-";

                if (method.getSimpleName().equals(methodName)) {
                    System.out.println(curPkg + " - " + curClazz + " - " + method.getSimpleName());
                }
                return pkg.equals(curPkg) && clazz.equals(curClazz) && method.getSimpleName().equals(methodName);
            }
        }).forEach(m -> {
            System.out.println("Method found: " + m.getSimpleName());
            //find next calls
            List<CtInvocation<?>> invocations = m.getElements(new TypeFilter<>(CtInvocation.class));
            for (CtInvocation<?> invocation : invocations) {
                // Check if the invocation is a client call
                CtExecutableReference exec = invocation.getExecutable();
                System.out.println(m.getSimpleName() + " -> " + invocation.getExecutable().getDeclaringType() + "."
                        + invocation.getExecutable().getSignature());
            }
        });
    }