Java环境中的ext扩展库引发的血案

837 阅读9分钟

故事发展

最近,因为公司服务器的Java环境原因从而影响项目的开发。项目中需要对接一个外部所提供的API接口,而API的请求参数和响应参数是使用RSA非对称加密算法来传输数据。在加密算法中需要使用到bcprov-jdk15on的依赖包。

刚好在公司中加密算法也是使用的RSA非对称加密,所以也是使用到了bcprov-jdk15on依赖。这不,问题就出现了。API接口所需要的依赖版本更高一点,而公司使用的是低版本的。本来问题很好解决的,只要在maven里面升级一下bcprov-jdk15on依赖的版本即可。

就这样,在本地环境码好代码之后。打包部署到测试环境,进行代码测试环节。然后就出现......找不到资源的异常(Unknown Source)

java.lang.NoSuchMethodError: org.bouncycastle.asn1.gm.GMNamedCurves$1.getCurve()Lorg/bouncycastle/math/ec/ECCurve;
	at org.bouncycastle.asn1.gm.GMNamedCurves$1.createParameters(Unknown Source) ~[bcprov-jdk15to18-1.72.jar:1.72.0]
	at org.bouncycastle.asn1.x9.X9ECParametersHolder.getParameters(Unknown Source) ~[bcprov-jdk15on-156.jar.back:1.56.0]
	at org.bouncycastle.asn1.gm.GMNamedCurves.getByOID(Unknown Source) ~[bcprov-jdk15to18-1.72.jar:1.72.0]
	at org.bouncycastle.asn1.gm.GMNamedCurves.getByName(Unknown Source) ~[bcprov-jdk15to18-1.72.jar:1.72.0]
	at com.chinapay.secss.sm.SM2Util.<clinit>(SM2Util.java:112) ~[chinapaysecure-sm-1.0.jar:na]
	at com.xgdfin.a0133.util.SM2Utils.init(SM2Utils.java:50) ~[xgdfina0133service-2.0.0.jar:na]
	at com.xgdfin.a0133.dubbo.impl.A0133DubboServiceImpl.business1635(A0133DubboServiceImpl.java:439) ~[xgdfina0133service-2.0.0.jar:na]
	at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:46) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:72) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:64) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:75) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.filter.TimeoutFilter.invoke(TimeoutFilter.java:42) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter.invoke(TraceFilter.java:78) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.filter.ContextFilter.invoke(ContextFilter.java:70) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:132) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.filter.EchoFilter.invoke(EchoFilter.java:38) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol$1.reply(DubboProtocol.java:113) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.handleRequest(HeaderExchangeHandler.java:84) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:170) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:52) [dubbo-2.8.4.jar:2.8.4]
	at com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:82) [dubbo-2.8.4.jar:2.8.4]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_11]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_11]
	at java.lang.Thread.run(Thread.java:745) [na:1.8.0_11]

我明明在maven加入了bcprov-jdk15on的依赖啊,怎么会找不到资源呢。而且也检查了,加密包是成功打包了。看着报错的异常信息,一行一行仔细排查了一下。等等,这个1.56.0版本的加密包是哪里来的,我在maven里面引入的是1.72.0的版本啊。看着这个1.56.0版本的加密包我陷入了沉思......

首先我想到的是,是不是其他的依赖包下面有自带的1.56.0版本的加密包。然后我就使用idea对依赖检查了一番,哎,也没有看到哪个依赖包下面自带1.56.0版本的加密包依赖啊。而且我本地启动时完全没问题,没有任务的1.56.0加密包版本的出现。管他三七二十一,先在maven里面全局排除一下1.56.0版本的加密包,然后再重新打包部署一番。

果然,测试环境又出现这个问题。排除依赖没有生效,还是存在Unknown Source问题。这下我凌乱了。没办法,只能又重新回去检查一下项目依赖和代码,最终还是没检查出哪里来的问题。

忙了一上午,到了饭点了,还是先干饭吧。带着这个问题就下楼去了,饭都吃的郁闷。干完饭、抽完烟、蹲完坑、午休完。该解决的问题还是得解决......

实在没得办法,我拿出测试环境得启动日志,排查一下是哪里加载得1.56.0得依赖包。

org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:zookeeper.version=3.4.6-1569965,builton02/20/201409:09GMT
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:host.name=localhost
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:java.version=1.8.0_11
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:java.vendor=OracleCorporation
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:java.home=/home/jdk1.8.0_11/jre
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:java.class.path=.:./resources/:./data/:./lib/common-utils-2.0.0.jar:./lib/commons-collections-3.2.1.jar:./lib/aspectjrt-1.8.0.jar:./lib/zkclient-0.1.jar:./lib/xgd-credit-common-2.0.0.jar:./lib/guava-11.0.1.jar:./lib/commons-codec-1.6.jar:./lib/cglib-nodep-3.1.jar:./lib/spring-oxm-4.0.3.RELEASE.jar:./lib/bcprov-jdk15to18-1.72.jar:./lib/commons-digester-2.1.jar:./lib/aopalliance-1.0.jar:./lib/spring-aop-4.0.3.RELEASE.jar:./lib/commons-lang3-3.1.jar:./lib/json-lib-2.3-jdk15.jar:./lib/TDBASE-1.0.0-release.jar:./lib/gson-2.8.0.jar:./lib/slf4j-api-1.6.1.jar:./lib/chinapaysecure-sm-1.0.jar:./lib/log4j-1.2.16.jar:./lib/ognl-3.0.jar:./lib/javassist-3.18.2-GA.jar:./lib/dubbo-2.8.4.jar:./lib/jaxen-1.1.6.jar:./lib/spring-tx-4.0.3.RELEASE.jar:./lib/netty-3.7.0.Final.jar:./lib/dom4j-1.6.1.jar:./lib/xml-apis-1.0.b2.jar:./lib/aspectjweaver-1.8.0.jar:./lib/jline-0.9.94.jar:./lib/zookeeper-3.4.6.jar:./lib/logback-core-1.1.1.jar:./lib/commons-lang-2.3.jar:./lib/spring-core-4.0.3.RELEASE.jar:./lib/jsr305-1.3.9.jar:./lib/TDCOMM-1.0.0-release.jar:./lib/xgdfina0133service-2.0.0.jar:./lib/spring-context-support-4.0.3.RELEASE.jar:./lib/spring-expression-4.0.3.RELEASE.jar:./lib/spring-web-4.0.3.RELEASE.jar:./lib/tdcommext-2.0.0-release.jar:./lib/spring-context-4.0.3.RELEASE.jar:./lib/commons-logging-1.1.3.jar:./lib/hutool-all-4.6.10.jar:./lib/spring-beans-4.0.3.RELEASE.jar:./lib/ezmorph-1.0.6.jar:./lib/javax.inject-1.jar:./lib/logback-ext-spring-0.1.2.jar:./lib/logback-classic-1.1.1.jar:./lib/commons-beanutils-1.8.0.jar:./lib/fastjson-1.1.15.jar
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:java.library.path=:/usr/local/opencvNEW/lib:/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:java.io.tmpdir=/tmp
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:java.compiler=<NA>
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:os.name=Linux
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:os.arch=amd64
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:os.version=2.6.32-754.6.3.el6.x86_64
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:user.name=root
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:user.home=/root
org.apache.zookeeper.Environment 100 logEnv - Clientenvironment:user.dir=/data/xgdjf/xgdfina0133service
org.apache.zookeeper.ZooKeeper 438 <init> - Initiatingclientconnection,connectString=127.0.0.1:10001,127.0.0.1:10002,127.0.0.1:10003sessionTimeout=60000watcher=org.I0Itec.zkclient.ZkClient@74e5d307
o.a.zookeeper.ClientCnxn$SendThread 975 logStartConnect - Openingsocketconnectiontoserver127.0.0.1/127.0.0.1:10002.WillnotattempttoauthenticateusingSASL(unknownerror)
o.a.zookeeper.ClientCnxn$SendThread 852 primeConnection - Socketconnectionestablishedto127.0.0.1/127.0.0.1:10002,initiatingsession
o.a.zookeeper.ClientCnxn$SendThread 1235 onConnected - Sessionestablishmentcompleteonserver127.0.0.1/127.0.0.1:10002,sessionid=0x286c4a65361000a,negotiatedtimeout=40000

根据上面的启动日志,可以确定了java.class.path里面是没有加载1.56.0版本的依赖包的,只有1.72.0的依赖包。那么这个是哪里加载的呢,突然,就看到上面的java.home这一行,加载了/home/jdk1.8.0_11/jre。看到这里,感觉答案离我越来越近了。带着这个疑问,我毫不犹豫的 cd 进这个目录,果不其然,哈哈,原来你在这里面的啊。最终,我在jre里面的/lib/ext目录下找到这个1.56.0版本的依赖包。

根据双亲委派机制,首先会委托父类加载器来尝试加载该类,如果父类加载器无法加载该类,子类加载器才会尝试加载,所以才会加载出来1.56.0版本得依赖包里面的方法。为什么要把依赖放在扩展库里面啊,难不成是为了方便吗,这坑给我挖的。

2a8ec20750b8f9ef2b9c46b5d8af9a9.png

我赶紧将这个低版本的加密包给移除,换上高版本的依赖。重新启动项目,这下终于没有出现这个Unknown Source异常了。虽然现在测试环境直接替换一下没问题,但是到正式环境的时候,担心直接给替换高版本的,会影响到其他项目的正常运行。所以还是得想个方法来解决一下这个问题。

三种方案

  • 搞一个新的Java环境,扩展库里面没有这个低版本的加密包

  • 单独新建一个新的jre路径,指定jre路径的加载启动

  • 在启动项目时,跳过jre里面的ext目录下载的加密依赖jar包

毫无疑问,第一种方案可以排除了,完全不考虑。而第二种方案可以是可以,但也是有点麻烦。最好的解决方案就是第三种,直接启动时就排除ext目录下的加密包。跳过低版本的加密,通过maven依赖下的加载项目。(大家如果有更好的方案可以相互交流一下)

指定jre路径加载


java -Djava.home=<jre_path> -jar <jar_file_name>

其中,<jre_path>是jre的安装路径,<jar_file_name>是需要运行的Java应用程序jar包

指定跳过ext下目录的jar包


java -Xbootclasspath/a:<path_to_jar> -jar <jar_file_name>

其中,<path_to_jar>是需要排除的jar包的路径,<jar_file_name>是需要运行的Java应用程序jar包

最终解决方案

最最重要的事情来了,异常原因找到了,也给出了解决方案。当决定使用第三种方案的时候,和运维沟通了一下,启动的时候加上以上的命令。在运维启动之前,然后就去正式环境的jdk中检查了一下ext目录下的依赖包,呵呵....结果没有,是的,没看错,也没听错。正式环境的竟然没有1.56.0版本的依赖包的存在,突然就好难受,这谁能想到测试环境有,然后正式环境没有的,测试环境和正式环境竟然不同,啊,好蛋疼。

问题既然出现了,那就总结一下吧,随便复习一下其中的问题,不然真就白折腾了,说不出来的难受。

Java环境的jdk目录介绍

在Java中,jdk目录是Java Development Kit(JDK)的安装目录。该目录包含Java开发所需的所有文件、库和工具。

  • bin目录:包含Java编辑器(javac.exe)、Java虚拟机(java.exe)等可执行文件

  • include目录:包含用于本地开发的文头件和库文件

  • jre目录:包含Java Runtime Environment(JRE),其中包含Java类库和Java虚拟机

  • lib目录:包含jdk和jre所需的库文件

  • src.zip文件:包含Java类库的源代码

除此之外,还有一些其他的目录和文件,如示例代码、文档和许可证文件等。

jre目录介绍

jre目录是Java Runtime Environment(JRE)的安装目录。jre是Java程序的运行环境,它包含Java虚拟机(JVM)和Java类库等组件,可让用户在没有开发环境的情况下运行Java程序。

  • bin目录:包含Java运行时环境的可执行文件,如java.exe和javaw.exe等

  • conf目录:包含jre的配置文件,如jvm.cfg和security文件等

  • lib目录:包含jre所需要的类库文件和扩展库文件

  • plugin目录:包含Java插件,用于浏览器中运行Java applet程序

  • fonts目录:包含jre使用的字体文件

  • images目录:包含jre使用的图像文件

  • jre/bin/java.exe文件:jre使用的Java虚拟机可执行文件

  • jre/lib/rt.jar文件:jre使用的Java类库文件,包含了Java核心类库的所有类文件

  • jre/lib/ext目录:jre使用的扩展库文件

jre/lib/ext目录

jre的lib/ext目录是Java Runtime Environment(JRE)所使用的扩展库目录。在Java中,扩展库是指Java标准之外的第三方库,可以通过jre的lib/ext目录来加载这些库

该目录通常包含一些jar文件,这些jar文件包含Java类文件和相关资源文件。当jar启动时,会自动加载lib/ext目录中的扩展库,这些库可以被Java程序所调用和使用。

jre的lib/ext目录中的扩展库通常由第三方开发者提供,并且这些库必须遵循Java扩展机制的规则。具体说,jar文件必须包含一个名为META-INF/MANIFEST.MF的清单文件,并指定类路径(Class-Path)和扩展类路径(Extension-List)。

需要注意的是,虽然可以将第三方库放在jre的lib/ext目录下,但是这种方式并不推荐使用。因为这样做可能会导致程序和jre版本之间的兼容性问题。而且扩展库中的类也可能会和jre的核心库中的类冲突。为了避免这些问题,最好将扩展库放在程序本身的classpath中,或将其打包成独立的jar文件并在程序运行时动态加载。

双亲委派机制

image.png

Java中的双亲委派是一种类加载机制,用于保证Java程序的稳定性和安全性

加载一个类先由应用类加载器委托给扩展类加载器,再由扩展类委托给启动类加载器,如果启动类加载器加载不了的话,则由扩展类加载器加载,如果扩展类加载器也加载不了的话,则由应用类加载器加载,如果连应用类加载器都找不到的话,则报ClassNotFound异常。

源码


    protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查这个classsh是否已经加载过了
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        // bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    
                }
                if (c == null) {
                    //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

双亲委派的优点在于,可以避免类的重复加载和冲突,同时也可以保证程序的安全性。如果有人想要篡改String类的话,类加载优先使用启动类加载器加载,发现已经加载过了,所以不会加载你自己写的String类,因此Java程序也就不会被恶意的类库替换。

加载到内存中的类叫做运行时类,一个运行时类就是一个Class示例

  • 启动类加载器:是用来加载jdk/jre/lib下的核心类库,比如rt.jat、resource.jar等

  • 扩展类加载器:是用来加载jdk/jre/lib/ext下的扩展类中的jar包和.class文件

  • 应用类加载器:是用来加载classpath下的jar包和.class文件

类加载

image.png

类加载是指将一个类的二进制代码从文件系统或其他地方读取到内存中,并将其转化为Java虚拟机中的Class对象的过程。当程序需要使用某个类时,Java虚拟机会检查这个类是否已经被加载,如果没有加载,则会触发类加载的过程。

  • 加载:类加载器首先会查找类文件,并读取类的二进制数据到内存中。这个过程可以从文件系统、网络、ZIP文件等地方获取类文件的字节码数据。

  • 链接:在加载完成之后,类加载器会进行链接,包括校验、准备和解析。校验是确保字节码符合JVM规范,例如验证方法调用是否存在,访问修饰符是否正确等。准备是为了类变量分配内存并设置默认初始值。解析是将符号引用转换为直接引用。

  • 初始化:当类加载完成链接后,就可以对类进行初始化,也就是执行类的静态初始化器、镜头初始化块和镜头字段初始化等操作。这个过程会将类的字节码转换成可执行的机器码,并将类的实例化对象放入堆内存中。

如果在加载、链接和初始化过程中发送错误,类加载器会抛出异常。如果类加载成功,那么Java虚拟机会生成一个Class对象来代表这个类,并将它储存在方法区中,提供程序使用。