0%

一次修改Spring源码的经历

一次修改Spring源码的经历

需求

今天收到一个需求,就是当接口处理业务的过程中报错,需要将报错接口传入的参数打印记录下来,由于异常可以在任何方法抛出无法保证拿到接口参数,所以当时接到这个需求的第一反映就是去全局异常处理的类看看能不能拿到调用栈,再通过调用栈拿到其栈帧的局部变量表里的方法参数。结果当然是不行,我忘记了异常Exception中只记录调用链(stackTrace),拿不到其栈帧,更不要说栈帧内的局部变量表了,局部变量应当在退出方法时就要被销毁了(当然不是马上)。

想到的第二个办法就是使用AOP动态代理,切到所有的接口上,但是担心切的面太大了,有些性能问题(虽然没有什么依据仅凭个人感觉),还是要找一些更好的办法。

InvocableHandlerMethod

**InvocableHandlerMethod.doInvoke()**方法是SpringMVC解析接口后最终调用controller接口的地方。

调用处理流程如下

图片来自https://segmentfault.com/a/1190000014243157

code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(getBridgedMethod());
try {
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
throw new IllegalStateException(formatInvokeError(text, args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
}
}
}

看到这个代码应该就能马上知道怎么实现这个需求了,return getBridgedMethod().invoke(getBean(), args);为最终调用controller的方法,那我们只需在cath中加入自己打印接口参数的逻辑不就可以了。

最终效果是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
protected Object doInvoke(NativeWebRequest request, Object... args) throws Exception {
ReflectionUtils.makeAccessible(this.getBridgedMethod());
try {
return this.getBridgedMethod().invoke(this.getBean(), args);
} catch (IllegalArgumentException var4) {
this.assertTargetBean(this.getBridgedMethod(), this.getBean(), args);
String text = var4.getMessage() != null ? var4.getMessage() : "Illegal argument";
throw new IllegalStateException(this.formatInvokeError(text, args), var4);
} catch (InvocationTargetException var5) {
Throwable targetException = var5.getTargetException();
printInterfaceParams((ServletWebRequest) request, args);
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
} else if (targetException instanceof Error) {
throw (Error) targetException;
} else if (targetException instanceof Exception) {
throw (Exception) targetException;
} else {
throw new IllegalStateException(this.formatInvokeError("Invocation failure", args), targetException);
}
}
}

/**
* 打印报错接口参数
*
* @param request
* @param args
*/

private void printInterfaceParams(ServletWebRequest request, Object... args) {
String requestURI = request.getRequest().getRequestURI();
HttpMethod method = request.getHttpMethod();
String sessionId = request.getRequest().getSession().getId();

log.error(">>>>>>>>>[{}] {}<<<<<<<<<", method, requestURI);

StringBuilder header = new StringBuilder();
Iterator<String> headerNames = request.getHeaderNames();
while (headerNames.hasNext()) {
String name = headerNames.next();
header.append("\n\t").append(name).append(" : ").append(request.getHeader(name));
}
log.error("Header: {}", header);
log.error("InterfaceParams: {}", Arrays.toString(Arrays.copyOf(args, args.length - 1)));

log.error(">>>>>>>>>{}<<<<<<<<<", sessionId);
}

值得注意的是,我将doInvoke的参数列表修改了,新增了一个request参数以便拿到一些请求头参数。但是随便修改源码是非常危险的,由于对源码不够了解,你不知道哪个地方也用这这个方法,随意修改可能会产生一片红,但是这个doInvoke()方法我看了只在本类内部调用,不会产生一片连锁反应。正常修改源码我觉得应当保证只增不减的效果,尽量使用重载和重写等特性。

实验

这样这个需求就写好了,我们来试一次下。

在业务代码中新增org.springframework.web.method.support.InvocableHandlerMethod类,内容就是上面说的。

再写两个方法,一个正常方法一个异常方法

image-20220413162738264

可以看到接口参数遇到异常时正常打印

image-20220413171508914

依赖包

上述的实现是将修改后的源码覆盖,当我们的项目启动时去加载某个类优先加载我们业务代码里的类,其次再加载jar包的类,这和JVM的双亲委派机制有关。所以我们被修改的InvocableHandlerMethod类会被优先加载,而加载到依赖包spring-web内的InvocableHandlerMethod类时就不会被加载了,因为类加载器会认为InvocableHandlerMethod已经加载过了,从而实现了代码覆盖。

这样去做当然可以,但是还是有点不正规,和业务代码放在一块还是有点变扭,所以我们把Spring依赖包更新一下重新做一个,这样

就可以和业务代码分离啦,和正常引入依赖一样。

先说结果以及原因

结果: 没有做成

原因:

  • Spring源码使用的包管理器是gradlew,俺不会没用过,虽然最终是成功打包了,但过程曲折无比。

    下个依赖是真滴满,也不知道怎么加速。刚开始也不知道怎么让gradlewmaven打通,就是gradlew打出来的包安装到maven

  • Spring有很多组合包,比如spring-boot-starter-XXX就是一坨组合包,内部管理了很多依赖以及之间的版本

    不知道为什么我自己做的spring-boot-starter-web依赖包,导入的内部依赖版本号始终和我在spring-boot-starter-web定义的对不上号(强的教学一下,怎么做一个spring-boot-starter-web包)

  • Spring之间的依赖有很强的版本对应关系,比如A版本的web依赖必须要B版本以上的beans依赖

    所以去寻找正确的依赖版本很重要,我从github下的spring-framework版本为5.2.21,他就和spring-boot-autoconfigure:2.6.6冲突。

    想解决要么换spring-boot-autoconfigure(一旦换了又不知道哪里不兼容了),

    要么换和spring-boot-autoconfigure:2.6.6兼容的spring-framework:5.2.8,那我就要把当前的5.2.21退到5.2.8的那次commit,实在太麻烦!

结论:

如果遇到单体的依赖包,没有很强的版本关联,那么可以尝试将修改好的源码做一个新的依赖包。

但是遇到Spring这种建议放弃,我个人觉得太烦了,资料也少。

好的,虽然没有成功,但是其中的过程也应当记录下来,用它对付单体依赖包轻轻松松。

下载源码

我们修改的InvocableHandlerMethodspring-web这个依赖包中,而spring-web又在spring-framework

所以我们去下载spring-framework的源码

image-20220413175340152

源码地址https://github.com/spring-projects/spring-framework

image-20220413180110873

由于我们的依赖包为5.2.8版本,所以我们下5.2.x的源码

git

git clone -b 5.2.x https://github.com/spring-projects/spring-framework.git

IDEA编辑

下载源码用IDEA打开

等待导入下载依赖,过程漫长,只能耐心等待

导入完成后,首先我们改一下版本为version=5.2.21.1.RELEASE,多加一个.1方便区分

image-20220413180240399

其次就是修改源码了,找到InvocableHandlerMethod,加上printInterfaceParams方法

image-20220413180448953

打包

找到Tasks -> publishing -> publishToMavenLocal双击,静静等待即可

官方文档https://github.com/spring-projects/spring-framework/wiki/Build-from-Source

建议是点Tasks -> publishing -> publishToMavenLocal打包安装,也可以点某一个子模块的publishToMavenLocal

但是往往某一个子模块依赖另一个模块,并且它们之间的版本要相同,所以全部打包方便点、

image-20220413180740631

完成后,可以在系统用户的.m2 -> repository,找到打的包

image-20220413181034274

由于我maven仓库不在.m2这里,所以我还得一个个手动复制迁移

制作starter

包完成后我们新建一个工程叫spring-boot-starter-web,因为我的项目代码里spring-webspring-boot-starter-web里,所以我们新建一个覆盖。

image-20220413181901169

把工程内该删的删一下,resources下的META-INF(手动创建)内的文件可以参考原生的spring-boot-starter-web

image-20220413182144265

然后把pom.xml的依赖做一下,版本注意弄个新的,容易区分,格式也可以参考原生的spring-boot-starter-web

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>cn.xpp011</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.2.1.RELEASE</version>
<name>spring-boot-starter-web</name>
<description>Starter for building web, including RESTful, applications using Spring MVC. Uses Tomcat as the default embedded container</description>
<url>https://spring.io/projects/spring-boot</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>info@pivotal.io</email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>https://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-boot.git</connection>
<developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git</developerConnection>
<url>https://github.com/spring-projects/spring-boot</url>
</scm>
<issueManagement>
<system>GitHub</system>
<url>https://github.com/spring-projects/spring-boot/issues</url>
</issueManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.3.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.21.1.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.21.1.RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.21.1.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

然后点击insrall将依赖安装到mavne仓库中

image-20220413182413427

安装完成后就可以在maven中看到starter包啦(我的坐标是cn.xpp011)

image-20220413182542524

测试

新建一个工程,引入我们做的starter包

然后你就可以惊奇的发现,引入的包和做的starter包版本不一致,反而去下了最新的webwebmvc

image-20220413182825801

没办法咱也不知道为啥,只有强制规定为5.2.21.1.RELEASE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.21.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.21.1.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>

然后启动,轻松报错。。。。

应该就是版本冲突的问题,自此我也懒得折腾了(写业务里又不是不能用),放弃。识时务者为俊杰!!!

image-20220413183212778

文章作者:xpp011

发布时间:2022年04月13日 - 14:04

原始链接:http://xpp011.cn/2022/04/13/3138ac3f.html

许可协议: 转载请保留原文链接及作者。