问题现象
最近一个老的微服务引入了page-helper 6.0版本的 jar 包之后,微服务发布到环境上去之后直接启动不起来了,报 no such constructor 错误,但是,这个微服务在发布到环境上的时候在本地 Idea 里面启动过,但是在 Idea 里面启动缺没有任何报错。如下图所示: 

问题复现
使用下面的 pom 文件引入依赖包,然后将项目打包项目为 jar 包启动就可以复现问题,配置如下:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.18.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>2.3.3</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
但是直接在 Idea 里面启动项目,则可以看到是可以正常启动的。如下图所示:
问题原因
启动报错原因
从上面报错的堆栈信息可以看到是在 AbstractDialect 的 57 行报的错,但是查看这行代码,实际上就是通过 lambda 表达式创建了一个 DefaultCountSqlParser 对象,代码如下: 
同时查看 DefaultCountSqlParser 的源码发现,它本身也没有定义任何有参数的构造函数,那它应该是默认有无参构造函数的,那为啥会报 no such constructor 错误呢?问题定位到这里有点定位不下去了。代码如下: 
后来又再次去看了报错的堆栈日志,发现在它的下面还有 Caused by: java.lang.VerifyError: Bad return type 报错。如下图所示: 
然后拿着这个报错去问了下 AI,AI 的回答:可能是 jar 包编译时的版本和运行时的版本不一致,且涉及到了方法签名的变更。
同时查看上面报错的堆栈信息提到了 DefaultCountSqlParser 的 sqlToCount() 方法,原因是:net/sf/jsqlparser/statement/select/PlainSelect is not assignable to net/sf/jsqlparser/statement/select/Select。
然后查看这个方法,发现它方法的返回值声明类型是 net/sf/jsqlparser/statement/select/Select,方法内部返回的是 net/sf/jsqlparser/statement/select/PlainSelect 类型。如下图所示:
查看 net/sf/jsqlparser/statement/select/PlainSelect 确实和 net/sf/jsqlparser/statement/select/Select 没有继承关系,同时发现当前实际引入的 jsqlparser 的版本是 1.1 的版本。然后去 maven 搜索这个 jar 包,发现 pagehelper-spring-boot-starter 实际想要引入的 jsqlparser 版本是 4.7 的版本,但是因为引入了 mybatis-plus 的 2.3.3 版本,它引入了 jsqlparser 的版本是 1.1,同时因为它和 pagehelper-spring-boot-starter 依赖 jsqlparser 的路径长度相同,且它在 pom 文件声明的前面。按照 maven 依赖调节的原则:路径长度相同时会取声明在前的,因此引入了 jsqlparser 的 1.1 版本。如下图所示:

从类加载的过程分析上面的报错:通过 new DefaultCountSqlParser 属于对一个类型的主动引用,会触发类加载,类加载又分为:加载、验证、准备、解析、初始化。
根据《深入理解JVM虚拟机》书中解释:在验证过程中会对方法的字节码进行验证。如下图所示: 
对于本案例中,在验证的过程中,JVM 发现 sqlToCount() 方法声明返回 net/sf/jsqlparser/statement/select/Select ,同时方法体中返回的是 net/sf/jsqlparser/statement/select/PlainSelect 类型,这就要求它们有继承关系,但是因为当前引入的是 jsqlparser 的版本是 1.1 版本,在这个 jar 包它们两个并不具备父子关系,因此校验失败了。
Idea 启动不报错原因
那为啥打包成 jar 包启动就会报错,但是在 Idea 中启动就不报错呢?最开始我以为是 Idea 自动引入了新版本的 jar 包,但是我把 Idea 的启动命令拷贝出来,搜索 classpath 中设置的 jsqlparser 版本,发现它引入的也是 1.1 版本,到这里感觉怎么都解释不通了呀!如下图所示: 
后面又回去翻了翻书,发现书上说有个 -Xverify:none 参数可以关闭校验,巧了的是上面启动命令图中的第一行的最后恰好就是这个参数(-noverify 实际上就是 -Xverify:none 的一个快捷别名)。
真相终于大白了,Idea 默认应该是把类加载的验证这个操作给关闭了,因此通过 Idea 启动的时候不会进行校验,自然也不会报错了。

那如何让 Idea 启用校验呢?在 Idea 的 Run/Debug Configurations 这里把 Disable launch optimization 这个配置相勾选上,然后启动项目,在控制台就可以看到一模一样的报错了。如下图所示:
