同大多数关系型数据库一样,日志文件是MySQL数据库的重要组成部分。MySQL有几种不同的日志文件,通常包括错误日志文件,二进制日志,通用日志,慢查询日志,等等。这些日志可以帮助我们定位mysqld内部发生的事件,数据库性能故障,记录数据的变更历史,用户恢复数据库等等。
「MySQL」MySQL事务处理与并发控制
「MySQL」MySQL索引的使用及优化
「MySQL」MySQL性能优化基础
什么? 微信没有年度账单? 前端 nodejs 撸起来~ [接口实现]
最近逛掘金看见一片文章 非官方统计 2018 微信年度账单实现,作者利用调试微信获取到了 2018
年的所有消费明细,并根据类型进行分类统计,作文一个前端,便萌生了用 nodejs
实现一遍的想法,于是乎呢,就有了这篇文章了。
sofa-ark类隔离技术分析调研
问题痛点
最近维护了一段时间的组件包,在向同事进行推广的时候,经常会听到身边会有类似的抱怨:
- 我靠,为啥你们的包带了一大堆乱七八糟的依赖,把我的classpath都给污染的不像个样子了。
- 我靠,你们这个包依赖的xx包的版本跟我们自己依赖的xx包的版本不一样啊,会不会出锅?
- 我靠,我这个代码编译的时候没问题啊,为啥一用你们的组件就报一堆的NoSuchMethodError啊。
有时候自己写代码的时候也会遇到一些容易被忽略的问题:
- 哎,我看人家引这个包的时候都会exclude一些依赖,我要不要也学着exclude一下呢?不管了,先一起粘过来试试看。
- 哎,怎么我的classpath里的某个包有三四个不同的版本啊,跑的时候到底用的是哪个版本啊?不管了,反正差不多,先跑跑看。
- 哎,我怎么直接就可以用这个类了,这个类是哪个包引的?不管了,反正IDE爸爸提示我能用,先用着再说。
大多数情况下,忽略这些问题一般也不会造成太大的影响,就算出了线上bug,一般也能很快定位问题、强制指定一下依赖的版本号就好。但是不得不说,依赖调解是普通java开发人员经常会遇到的问题,无谓的浪费了我们大量的时间和精力。尤其是当项目越来越大,用到的组件越来越多时,内耗就更加明显。
业内方案
大概研究了一下,目前对于本地依赖调解也没有什么很好的银弹。绝大多数情况下,我们都是在流程、文档上来进行把控。而从技术角度说,或许有如下几种方案供我们选择。
代码内嵌
这个方法很老土,但是也好用,就是把依赖到的包以源代码的形式放在自己项目的包中,而不是以依赖的形式引入。这样的好处就是组件一定可以依赖到自己想要引入的包,不会收到业务方引入的包的影响。不过坏处就是缺少了灵活性,业务方无法修改组件包的任何依赖。
OSGI技术
使用OSGI技术,用felix、karaf或者Jigsaw这样的容器对jar包进行暴露和隔离。
OSGI技术实际上是对代码进行了更高一级的抽象,将“模块”作为一个基本单位,用Bundle包对jar包再进行一级权限管理,将一些导入或导出的资源配置在Manifest文件里。这样我们就可以在Bundle层面进行功能的引入和暴露。同时也在bundle层面引入了Activator的概念,用来向外部模块暴露自身的服务、或者是注册一些生命周期的代码。
目前的OSGI技术社区还是比较完整的,很多知名公司的大型企业软件都在用了这个技术。不过其实大家采用这个技术主要还是为了做应用的热加载和热更新,仅仅用来做依赖隔离确实有点杀鸡用牛刀了。而且对于OSGI容器自身就是一个守护进程,他的使用、管理和维护都会有额外的代价。因此一般来说我们都认为OSGI技术太”重”,不适合小公司、小项目、或者是使用很多小项目组成大项目的互联网公司使用。
不过OSGI技术给我们提供了一个不错的思路,总结下来有如下几点:
- 对Jar包进行更高级的抽象,并支持对类和资源访问控制。
- 程序运行在一个容器中,由容器来启动和管理各个业务组件。
- 每个业务组件有一个独立的ClassLoader,因此不同业务组件之间的依赖不会互相影响。
- 支持组件向容器进行服务的注册,以及服务的互相调用。
Gradle5.0特性
Gradle项目也对依赖调解这个问题做了一个不错的尝试。在Gradle5.0中引入了”java-library”插件,试图让组件开发方在编写组件代码时,主动控制好自己的依赖是否暴露给组件接入方(这个插件在老版本中也有,不过只有在5.0版本中在真正有用)。
什么意思呢?就是原本我们使用gradle来引包的时候是这么写的:
1 | dependencies { |
这么做的话,当组件接入方接入这个组件的时候,就会默认通过传递依赖也依赖了这两个包,这两个包就会出现在组件接入方开发人员的IDE的classpath里。
但是,如果组件开发方只希望将httpclient包传递依赖给接入方而不愿意将commons-lang3包暴露给接入方(毕竟commons-lang3在3.5版本的api变动还是有很多坑的),这时候就可以用Gradle5.0中”java-library”插件的新特性了,我们可以将dependencies重写为:
1 | dependencies { |
使用”java-library”插件之后,”compile”可以被”implementation”和”api”替代。对于组件本身来说,通过”api”或”implementation”引入的包都会被添加到classpath中。而”api”和”implementation”的唯一区别就是,在组件接入方的classpath中(实际上是compileClasspath),将只会出现通过”api”引入的包,而不会出现通过”implementation”引入的包。也就是说,当业务方接入了拥有上面这种依赖的组件时,他只会引入httpclient包,而不会引入commons-lang3包。
咦?那么问题来了,少引入了一个包岂不是肯定会在运行时报错么?他gradle又是通过什么方式来传递依赖控制的信息呢?毕竟打成jar包后包里可是没有任何依赖信息的。
其实”java-library”做的事情很简单,就是在将jar包上传maven仓库时修改了pom.xml文件中各个依赖包的scope。对于通过”api”引入的依赖来说,scope就是compile;而对于通过”implementation”引入的依赖来说,他的scope就变成了runtime。
比如在上面的例子中,打包上传后的pom.xml中的依赖就大概是这样:
1 | <dependencies> |
因此业务方在接入这个组件时,他的compileclasspath当然不会引入runtime的依赖了。但是要注意,在运行时,依旧还是会出现依赖冲突。。。
说白了,gradle5.0的这个功能对于解决依赖冲突这个问题来说,实在是有点饮鸩止渴,他实际上是将编译期的依赖冲突暂时隐藏了起来,等到运行时再暴露出来。我们的IDE中的classpath是干净了,但是该调解的冲突还是要调解,否则就会在运行时给你好看,那这又跟续一秒有啥区别。
当然,gradle的出发点是好的,至少通过api跟implementation让组件开发者明确了自己到底要暴露哪些包。况且通过这种方式来区分,要比写在注释或文档中进行区分要方便多了,而且好歹classpath干净了。
Sofa-ark项目
事实上,为了从根本上解决类冲突问题,我们还是需要OSGI那种通过ClassLoader进行类隔离的思路。但是OSGI还是太”重”了,有没有稍微”轻”一点的技术呢?还好蚂蚁金服给我们提供了他们的一个不错的实践——Sofa-ark项目。
sofa-ark项目从概念上其实并没有什么石破天惊的独创,可以说他就是用FatJar技术去实现OSGI的功能。下面我们主要就来分析一下这个sofa-ark项目,在讲sofa-ark项目之前,我们先讲一下FatJar技术。
Fat-jar技术
OSGI技术刚才大概讲过,那么FatJar技术又是什么呢?我们知道,在用打包插件对springboot项目进行打包时,打出来的那一个jar包是可以直接通过java -jar
直接运行的。可要知道,这并不是天经地义的事情,通常情况下,如果要运行一个jar包,至少得满足两个条件:
- 在jar包中的Manifest文件中要通过”Main-Class”属性,告诉jvm去启动哪个类的main函数。
- 在jvm的classpath中要有项目所有依赖的jar包。
但我们要知道,在用springboot打包插件进行打包的时候,我们并没有指定Main-Class
,而且也没有将依赖下载到classpath的过程。这是因为这一切都是由打包插件帮我们做掉了。在打出来的包里,已经包含了所有配置信息,以及依赖的jar包。这个包一般被称为executable-jar
或者fat-jar
。
springboot的一个典型的fat-jar大抵如此:
1 | example.jar |
在对项目进行打包的时候,插件会把所有依赖的包下载好,放到一个自定义的文件夹下(这里是BOOT-INF/lib
)。但是仅仅放进来也是不够的,因为jvm并不知道要去这里拿这些依赖。为此,在启动真正的项目之前,我们还要想办法加载这些依赖。所以你会发现在jar包中有一块org.springframework.boot.loader
包的代码,并且如果你打开MANIFEST.MF查看你会发现类似下面的配置:
1 | Manifest-Version: 1.0 |
也就是说,jar包在启动时实际上启动的是springboot自己的JarLauncher,通过这个JarLauncher去加载lib
下的依赖,然后去启动Start-Class
配置对应下的类(这个配置实际上是在用插件打包时扫描@SpringBootApplication注解得到的)。
以上就是Fat-Jar技术的基本原理,其实核心就在于要定义一套Jar包的文件规范,并且写一个打包插件按照这个规范打包,然后写一个Launcher进行解包、用依赖包配置ClassLoader、用反射调用实际main函数。
需要注意的是,如果仅仅是在IDE中运行代码,是完全感知不到打包逻辑的,因为IDE会自动帮你下载Jar包、指定classPath。
Sofa-ark的工作原理
讲了那么多准备知识,接下来就进入正题,讲一讲sofa-ark项目到底是怎么一回事。
在继续阅读之前,最好先去看一下sofa-ark项目的基本使用,否则可能会有阅读难度。
sofa-ark项目自称是“一款基于Java实现的轻量级类隔离加载容器”。在这个项目中,会打三种类型的包:
- Ark包
- Ark Biz包
- Ark Plugin包
Ark Plugin
Ark Plugin包我们可以理解为一个组件,与普通组件包不同的是,Ark Plugin包原则上是不包含任何类文件的(除非是shade的场景,这在0.4.0以后的版本才出现),他只是一些依赖的集合,并且通过MANIFEST文件对这些依赖集合中的类进行访问控制。事实上,一个Ark Plugin项目在代码层面其实完全可以当成一个普通的项目(在不注册服务的情况下)。Ark Plugin项目最大的特点就在于他的打包。
包结构
一个典型的Ark Plugin包解压后大致如下:
1 | . |
可以看到这个包的结构很简单,主要是以下几个部分:
com/alipay/ark/plugin/mark
文件来表示这是一个Ark Plugin包。MANIFEST.MF
文件,用来保存需要export或者import的包或者类,以及用于服务注册的Activator类。lib/
文件夹,用来保存项目依赖到的jar包,以及项目本身的jar包。conf/export.index
文件,通过解析MANIFEST文件,并扫描lib/
文件夹,用于存储实际导出的类。
MANIFEST文件
MANIFEST.MF文件样例如下:
1 | Manifest-Version: 1.0 |
MANIFEST文件中的配置基本都是打包插件直接控制的,默认的sofa-ark-plugin-maven-plugin
(注意中间有一个plugin)的配置如下:
1 | <plugins> |
lib文件夹
lib/
目录下存放的jar包,不仅包含了项目依赖的包,而且包含了项目自身的jar包。也就是说,Ark Plugin的包和原生jar包的关系并不是”代替”或是”扩展”,而是”包含”。
当然,由于plugin包和原生jar包是共用的同一个maven坐标,因此,我们就要用maven的”classifier”属性来区别。默认原生jar包的”classifier”就是空,而默认plugin包的”classifier”就是”ark-plugin”(由于版本不一致问题,最好还是在打包插件中显式指定一下classifier)。
当我们将打出来的包上传到Nexus或者其他maven包管理中心时,事实上我们会上传一个pom文件、两个jar包、以及一些自动生成的校验文件:
1 | ├── plugin-test-1.0-20181203.131726-1-ark-plugin.jar |
但是你会发现,Ark Plugin包和原生jar包事实上还是共用了同一个pom文件!这就意味着,当业务项目依赖了Ark Plugin包时,我们仍然会默认传递依赖进这个项目的间接依赖。这样的结果就是,即使Ark Plugin项目已经将自己的依赖添加到了jar包的lib目录下,但是这些依赖依然会出现在业务项目的classpath中。但是我们又不能将这些依赖全部排除掉,毕竟这里面还有业务方真正用到的那些类。
这显然不是我们期望看到的局面,虽然在运行时类是隔离的,但是在编译期,那些间接依赖还是会充斥业务开发人员的IDE中。
为了解决这个问题,在0.4.0版本后,在打包Ark Plugin时,增加了一个shade属性,用于指定需要shade进来的jar包的maven坐标。
所谓shade,就是指将某个jar包中所有的内容,复制到当前的jar包中。这就相当于我们在Ark plugin包中添加进了被shade进来的jar包的所有内容。这样做的好处就是,即使业务在使用时exclude了Ark Plugin包的所有依赖,业务在编译期仍然能正常使用那些shade进来的类。当然,这样做的前提是,我们需要在Ark Plugin包中导出这些类、告诉Ark Container,这些类是使用这个Ark Plugin来加载的。
因此,当我们在编写Ark Plugin时,我们应当遵循这样的规范,就是将那些需要暴露给业务方的接口作为一个模块,shade进Ark Plugin中,然后将这个模块导出,而将那些内部逻辑需要用到的一切都隐藏起来。当业务方调用这个Ark Plugin时,可以放心的将他所有的依赖排除掉。这样就既做到了编译期不引入间接依赖,又做到了运行期的依赖隔离。
export.index文件
conf/export.index
文件是根据MANIFEST文件中配置的export
的信息扫描lib
文件夹中的jar包得到的。一个典型的样例如下:
1 | com.example.test.sofa.TestLib1 |
在Ark Plugin包中,最重要的其实就是这个conf/export.index
文件,就是他告诉容器“到底什么类需要通过这个Ark Plugin进行加载”,如果有多个plugin 声明了相同的类,那么则会根据priority进行选择,判断一个类到底是用哪一个插件的ClassLoader来加载。
Ark Biz
Ark Biz包实际上就是一个业务的包,是在多个ark plugin的基础上真正执行业务逻辑的基本单位。在用sofa-ark-maven-plugin
打包时,会同时生成两个包,一个是Ark包,一个就是Ark Biz包。Ark Biz包的基本结构如下:
1 | . |
首先肯定会有个com/alipay/sofa/ark/biz/mark
文件,告诉容器这是一个biz包。同时,我们也要注意到,biz包是不会将ark plugin依赖添加到自己的lib文件夹下的。
然后我们来看下Manifest文件:
1 | Manifest-Version: 1.0 |
虽然在他的MANIFEST文件中有”Main-Class”属性,指定了要执行的类,这个包只能算是半个可执行文件。因为他缺少了实际运行时的容器,所以如果要强行跑的话肯定是会报错。
慢着,我们在打包时都没有指定Main-Class,如果是非SpringBoot项目,甚至都没有加@SpringBootApplication注解,那么打包插件是怎么知道我要运行的是哪一个类呢?其实是打包插件做了一个很”呆”的处理:
com.alipay.sofa.ark.tools.MainClassFinder:
1 | private String getMainClassName() { |
事实上他会去找有@SpringBootApplication注解的类,这一点与SpringBoot的打包插件类似;如果找不到,那么就检测所有的类中有Main函数的,如果找到且只找到一个就皆大欢喜,否则就报错给你看。。。(平时喜欢在main函数里写测试用例而且还不记得删的同学要注意了哈)
回到上面的话题,既然Biz包都跑不动,我们为啥要生成他呢?事实上从这就看出sofa-ark项目的本质了。sofa-ark项目其实是一个以Ark Container为核心的容器生态。Ark Container可以被理解为OSGI中的那个守护进程,用来管理业务包和插件包,只不过Ark Container不是一个守护进程而只是一个启动类罢了。既然是容器,那么就肯定要支持多应用,Container就要和Biz解耦,从而做到一个Container可以运行多个Biz和多个Plugin。
对于单个应用的项目,我们当然可以不要Ark Biz包,而是直接用Container包装起来,做成一个可执行文件。但是如果需要多个业务同时运行,我们就可以以Biz包的形式,一个一个往容器里加了。
Ark包
包结构
有了上面的基础,Ark包就很好理解了,它实际上就是一个Ark Container实例,里面存放了需要加载的Biz和Plugin。包结构大致如下:
1 | . |
这个包实际上就是shade进了Ark Container的Jar包,在SOFA-ARK/biz
和SOFA-ARK/plugin
目录下分别保存了需要加载的业务包和插件包。
Manifest文件
他的Manifest文件大致如下:
1 | Manifest-Version: 1.0 |
在执行Ark包时,他会启动ArkLauncher,用来启动Ark Container并加载业务包和插件包(每一个业务包和插件包都有自己独立的ClassLoader)然后再去启动每一个业务包。
类加载
最后我们再来简单看下,Ark Container是如何用ClassLoader进行运行时类隔离的。
以BizClassLoader为例,BizClassLoader用来控制每一个Biz加载类的逻辑。显然默认的委托模型是不中的,BizClassLoader采用了如下逻辑:
com.alipay.sofa.ark.container.service.classloader.BizClassLoader:
1 | @Override |
可以说这段代码写的逻辑很清楚,当Biz在运行时发现一个类需要被加载时,他会按照如下步骤搜索:
- 如果已加载过,那就返回已加载好的那个类。
- 如果这个类是JDK自己的,那么就用JDKClassLoader去加载。
- 如果这个类是属于Ark容器的,那么就用ArkClassLoader去加载。
- 如果这个类是某个插件export的,那么就用ExportClassLoader去加载。
- 如果这个类是我业务自己的,那么就用当前的ClassLoader直接loadClass就好。
- 否则就去试试是不是用了某个java agent。
- 再找不到就报错给你看。
其他能力
上面的文章中,我们只是专注于使用sofa-ark进行类隔离。事实上他也支持类似OSGI的那种服务发布、热加载和热部署。
服务发布
利用Activator,你可以很方便的以jvm服务的形式发布plugin的服务。不过这不是我使用的重点,我也就没有过多研究。
热加载
当你启动ark包时,你会发现你的1234端口开放了一个telnet接口,当然这个端口默认是没有任何用的。不过当你引入了sofa-jarslink项目,你就真的可以像使用OSGI容器一样的利用这个端口动态管理你的Biz和Plugin了。
启动方式
非Springboot应用
在非SpringBoot中,Sofa-ark的Biz通过引入sofa-ark-support-starter
,并且在主函数中显式launch来启动。
1 | public class TestMain { |
注意到这个launch方法一定要是main函数的第一个方法。否则在他之前的代码就会被执行两次。
如果是在IDE中启动,那么这个launch方法会自己起一个Ark容器,然后再用反射重新调用自己的main函数。
如果是打包后启动,那么创建Ark容器的任务就交给了启动类了了,这个launch方法将不做任何事情。
SpringBoot应用
在SpringBoot中,Sofa-ark的Biz通过引入sofa-ark-springboot-starter
来启动。这个starter注册了spring的启动事件,并且在启动事件中调用SofaArkBootstrap.launch函数。其他的过程就和非SpringBoot应用类似了。
IDE启动和打包启动的区别
为什么前面我们要区分IDE启动和非IDE启动呢?因为这两种启动方式不只是启动逻辑不同,执行逻辑也不一样。
在IDE中启动时,由于主类的静态代码块是会在容器启动之前就会加载一次的。可是在容器起来之后,由于容器会用心的ClassLoader再反射调用主类的main函数,因此他又会被加载一次,这一点需要额外注意。
但是,如果是打包后启动呢?容器的启动是在启动类中完成的、而不是主类,因此主类的静态代码块就会正常只被执行一次,这样就不会有问题了。
已知问题
关于sofa-ark项目,前前后后研究了一个礼拜的样子,也稍微有了一些使用心得,再总结一下这个项目的不足:
- 用fatJar技术做依赖隔离其实并不优雅。因为不同插件实在是会引入很多重复的包,会导致最终的jar包很大。虽然plugin层可以复用,但是还是会有一些重复jar包的。
- 目前采用的日志框架是蚂蚁内部的框架,实在是太丑陋,而且不支持关闭,用起来很难受。
- 没有官方gradle插件,且开发组据说一时半会也不会考虑支持。当然,写起来其实不困难,为了内部使用,我没办法就只能自己花了点时间写了一套,先自己用一段时间看看。
- 社区不够完善。这个项目刚开源也没多久,用的人也不是很多,所谓的社区也就只有github的issue了。而且看上去开发组也很忙,很多特性的支持都很慢。
- 对应用侵入性比较强,有风险。为了运行sofa-ark,项目的整体打包逻辑都要变,而且代码都是跑在Ark Container里面,对于一个社区都不完善的不是很成熟的项目来说,这么用还是有很大风险的。
- 对依赖进行导入导出的管理引入了一定的复杂性,还是有一定学习成本的。
最后再强调一点,sofa-ark项目并不是解决依赖冲突问题的银弹。对于一个已有的、已经使用了很多富客户端二方包的项目,迁移到sofa-ark后,你会经常发现很多”ClassCastException”。这是因为由不同Classloader创建的类是不一样的,不能将一个ClassLoader创建的实例,赋给由另一个ClassLoader声明的类。在springboot项目中,这种问题经常会发生。
因此,在已有的生态中仅仅是为了做依赖隔离而接入sofa-ark并不是一件省心的事情,各位还要慎用。
参考资料
gradle 5.0 The Java Library Plugin
「ElasticStack」Beats+Logstash+Elasticsearch+Kibana基础整合
「ElasticStack」ElasticSearch聚合分析与数据建模
「ElasticStack」ElasticSearch分布式特性 与 Search机制
java运行期的版本控制方案
前言
前两天我们组负责的一个组件发生了一个与jar包版本号有关的线上bug,最近没啥事情,就顺便分析了一下。
其实是个非常无脑的小bug:commons-lang3
包中有一堆@since 3.5
的新增方法,我们的组件依赖了3.5
版本以上的一个包;业务方依赖了我们的这个组件,同时也直接依赖了一个3.5
版本以下的包。在gradle打包的时候,由于老版本的是直接依赖,新版本的是间接依赖,直接依赖优先级高于间接依赖,因此最终采用的是老版本的包。这就导致在运行期调用新方法的时候会报NoSuchMethod
的错。
虽然问题很简单,但毕竟也是一个影响了GMV的线上事故(可怕),值得吸取一波教训。
方案
一般来说,在比较大的项目里,依赖冲突这种事情几乎是无法避免的。一般来说,这种问题的解决方法大多是下面几种:
- 对于业务方来说,写代码的时候小心一点,遇到不同依赖的时候,有意识的检查一下依赖树,尽量使用较新的包,并且代码上线之前需要在测试环境充分测试。
- 对于组件开发方来说,在写接入文档的时候要同时指明依赖的包的最低版本号,清楚地告诉接入方最低的依赖,然后再由接入方手动指定。
- 采用容器技术,比如OSGI、Jigsaw、Karaf这些容器,对jar包再进行一层权限控制。这是一种十分重量级的方法,一般项目得上了一定的规模才会使用。
- 采用ClassLoader隔离技术,各个包都使用自己的classLoader,互相不影响。这种方法其实很像是容器技术的阉割版,逻辑上很像容器,对jar包再做一层隔离控制。不过这种方式一般不是很优雅,有点像hack,因此目前看起来没什么像样的完整解决方案。稍微像样点的大概就是阿里最近搞的Sofa ark,功能挺强大,但是用起来也比较复杂,对jar包的侵入性也很强。
各个方法其实都不是很方便,那就换一个思路,既然避免问题比较困难,那就尽量早点暴露问题。编译错误或者启动错误肯定会比运行时不知道啥时候报错更让人放心。因此根据fail fast
原则,我们应当保证在不增加沟通成本的情况下,快速暴露问题。
分析
既然很多依赖冲突问题在编译、打包时都不会报错,那就只能尽量在启动时报错了。因此对于一个稳定的组件来说,做一个运行时的启动检查也就有一定的合理性了。
为了能在运行时进行依赖检查,肯定要想办法在运行时获得某个包的版本号。那如何在打包时把版本信息写在jar包里,然后再读出来呢?这就要从JarFile的加载说起了。
源码分析
加载一个jarFile,当然是要用ClassLoader,比如对于URLClassLoader。根据之前对ClassLoader的分析,查询下源码就会发现如下的加载流程:
URLClassLoader在
loadClass
时,根据双亲委托模型,最终会用findClass(String name)
方法用于查询特定类。findClass(String name)
方法会调用defineClass(String name, Resource res)
方法用于加载特定类,并通过ucp.getResource
去加载JarFile
: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
30protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}JarFile
中定义了一个Manifest
对象,用于存储Jar包的元信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public
class JarFile extends ZipFile {
private SoftReference<Manifest> manRef;
private JarEntry manEntry;
private JarVerifier jv;
private boolean jvInitialized;
private boolean verify;
// indicates if Class-Path attribute present (only valid if hasCheckedSpecialAttributes true)
private boolean hasClassPathAttribute;
// true if manifest checked for special attributes
private volatile boolean hasCheckedSpecialAttributes;
// Set up JavaUtilJarAccess in SharedSecrets
static {
SharedSecrets.setJavaUtilJarAccess(new JavaUtilJarAccessImpl());
}
/**
* The JAR manifest file name.
*/
public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";可见这里明确指定了MANIFEST文件的路径。
ManiFest
类中定义了一个Attributes
对象,用来保存一些关键的特征:1
2
3
4
5
6
7
8
9
10
11
12
13public class Manifest implements Cloneable {
// manifest main attributes
private Attributes attr = new Attributes();
// manifest entries
private Map<String, Attributes> entries = new HashMap<>();
/**
* Constructs a new, empty Manifest.
*/
public Manifest() {
}Attributes
对象定义了一个叫Name
的内部类,用来保存一些内定的属性(关键):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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226public static class Name {
private String name;
private int hashCode = -1;
/**
* Constructs a new attribute name using the given string name.
*
* @param name the attribute string name
* @exception IllegalArgumentException if the attribute name was
* invalid
* @exception NullPointerException if the attribute name was null
*/
public Name(String name) {
if (name == null) {
throw new NullPointerException("name");
}
if (!isValid(name)) {
throw new IllegalArgumentException(name);
}
this.name = name.intern();
}
private static boolean isValid(String name) {
int len = name.length();
if (len > 70 || len == 0) {
return false;
}
for (int i = 0; i < len; i++) {
if (!isValid(name.charAt(i))) {
return false;
}
}
return true;
}
private static boolean isValid(char c) {
return isAlpha(c) || isDigit(c) || c == '_' || c == '-';
}
private static boolean isAlpha(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
private static boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
/**
* Compares this attribute name to another for equality.
* @param o the object to compare
* @return true if this attribute name is equal to the
* specified attribute object
*/
public boolean equals(Object o) {
if (o instanceof Name) {
Comparator<String> c = ASCIICaseInsensitiveComparator.CASE_INSENSITIVE_ORDER;
return c.compare(name, ((Name)o).name) == 0;
} else {
return false;
}
}
/**
* Computes the hash value for this attribute name.
*/
public int hashCode() {
if (hashCode == -1) {
hashCode = ASCIICaseInsensitiveComparator.lowerCaseHashCode(name);
}
return hashCode;
}
/**
* Returns the attribute name as a String.
*/
public String toString() {
return name;
}
/**
* <code>Name</code> object for <code>Manifest-Version</code>
* manifest attribute. This attribute indicates the version number
* of the manifest standard to which a JAR file's manifest conforms.
* @see <a href="../../../../technotes/guides/jar/jar.html#JAR_Manifest">
* Manifest and Signature Specification</a>
*/
public static final Name MANIFEST_VERSION = new Name("Manifest-Version");
/**
* <code>Name</code> object for <code>Signature-Version</code>
* manifest attribute used when signing JAR files.
* @see <a href="../../../../technotes/guides/jar/jar.html#JAR_Manifest">
* Manifest and Signature Specification</a>
*/
public static final Name SIGNATURE_VERSION = new Name("Signature-Version");
/**
* <code>Name</code> object for <code>Content-Type</code>
* manifest attribute.
*/
public static final Name CONTENT_TYPE = new Name("Content-Type");
/**
* <code>Name</code> object for <code>Class-Path</code>
* manifest attribute. Bundled extensions can use this attribute
* to find other JAR files containing needed classes.
* @see <a href="../../../../technotes/guides/jar/jar.html#classpath">
* JAR file specification</a>
*/
public static final Name CLASS_PATH = new Name("Class-Path");
/**
* <code>Name</code> object for <code>Main-Class</code> manifest
* attribute used for launching applications packaged in JAR files.
* The <code>Main-Class</code> attribute is used in conjunction
* with the <code>-jar</code> command-line option of the
* <tt>java</tt> application launcher.
*/
public static final Name MAIN_CLASS = new Name("Main-Class");
/**
* <code>Name</code> object for <code>Sealed</code> manifest attribute
* used for sealing.
* @see <a href="../../../../technotes/guides/jar/jar.html#sealing">
* Package Sealing</a>
*/
public static final Name SEALED = new Name("Sealed");
/**
* <code>Name</code> object for <code>Extension-List</code> manifest attribute
* used for declaring dependencies on installed extensions.
* @see <a href="../../../../technotes/guides/extensions/spec.html#dependency">
* Installed extension dependency</a>
*/
public static final Name EXTENSION_LIST = new Name("Extension-List");
/**
* <code>Name</code> object for <code>Extension-Name</code> manifest attribute
* used for declaring dependencies on installed extensions.
* @see <a href="../../../../technotes/guides/extensions/spec.html#dependency">
* Installed extension dependency</a>
*/
public static final Name EXTENSION_NAME = new Name("Extension-Name");
/**
* <code>Name</code> object for <code>Extension-Name</code> manifest attribute
* used for declaring dependencies on installed extensions.
* @deprecated Extension mechanism will be removed in a future release.
* Use class path instead.
* @see <a href="../../../../technotes/guides/extensions/spec.html#dependency">
* Installed extension dependency</a>
*/
@Deprecated
public static final Name EXTENSION_INSTALLATION = new Name("Extension-Installation");
/**
* <code>Name</code> object for <code>Implementation-Title</code>
* manifest attribute used for package versioning.
* @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779">
* Java Product Versioning Specification</a>
*/
public static final Name IMPLEMENTATION_TITLE = new Name("Implementation-Title");
/**
* <code>Name</code> object for <code>Implementation-Version</code>
* manifest attribute used for package versioning.
* @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779">
* Java Product Versioning Specification</a>
*/
public static final Name IMPLEMENTATION_VERSION = new Name("Implementation-Version");
/**
* <code>Name</code> object for <code>Implementation-Vendor</code>
* manifest attribute used for package versioning.
* @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779">
* Java Product Versioning Specification</a>
*/
public static final Name IMPLEMENTATION_VENDOR = new Name("Implementation-Vendor");
/**
* <code>Name</code> object for <code>Implementation-Vendor-Id</code>
* manifest attribute used for package versioning.
* @deprecated Extension mechanism will be removed in a future release.
* Use class path instead.
* @see <a href="../../../../technotes/guides/extensions/versioning.html#applet">
* Optional Package Versioning</a>
*/
@Deprecated
public static final Name IMPLEMENTATION_VENDOR_ID = new Name("Implementation-Vendor-Id");
/**
* <code>Name</code> object for <code>Implementation-URL</code>
* manifest attribute used for package versioning.
* @deprecated Extension mechanism will be removed in a future release.
* Use class path instead.
* @see <a href="../../../../technotes/guides/extensions/versioning.html#applet">
* Optional Package Versioning</a>
*/
@Deprecated
public static final Name IMPLEMENTATION_URL = new Name("Implementation-URL");
/**
* <code>Name</code> object for <code>Specification-Title</code>
* manifest attribute used for package versioning.
* @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779">
* Java Product Versioning Specification</a>
*/
public static final Name SPECIFICATION_TITLE = new Name("Specification-Title");
/**
* <code>Name</code> object for <code>Specification-Version</code>
* manifest attribute used for package versioning.
* @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779">
* Java Product Versioning Specification</a>
*/
public static final Name SPECIFICATION_VERSION = new Name("Specification-Version");
/**
* <code>Name</code> object for <code>Specification-Vendor</code>
* manifest attribute used for package versioning.
* @see <a href="../../../../technotes/guides/versioning/spec/versioning2.html#wp90779">
* Java Product Versioning Specification</a>
*/
public static final Name SPECIFICATION_VENDOR = new Name("Specification-Vendor");
}这里定义了大量的属性名,当一个jarfile的Manifest文件中有这些属性,这些属性就会被识别。
在加载了上面的JarFile之后,
defineClass(String name, Resource res)
方法会继续调用一系列的definePackage
方法,用于定义Package
类: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
69protected Package definePackage(String name, Manifest man, URL url)
throws IllegalArgumentException
{
String path = name.replace('.', '/').concat("/");
String specTitle = null, specVersion = null, specVendor = null;
String implTitle = null, implVersion = null, implVendor = null;
String sealed = null;
URL sealBase = null;
Attributes attr = man.getAttributes(path);
if (attr != null) {
specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
sealed = attr.getValue(Name.SEALED);
}
attr = man.getMainAttributes();
if (attr != null) {
if (specTitle == null) {
specTitle = attr.getValue(Name.SPECIFICATION_TITLE);
}
if (specVersion == null) {
specVersion = attr.getValue(Name.SPECIFICATION_VERSION);
}
if (specVendor == null) {
specVendor = attr.getValue(Name.SPECIFICATION_VENDOR);
}
if (implTitle == null) {
implTitle = attr.getValue(Name.IMPLEMENTATION_TITLE);
}
if (implVersion == null) {
implVersion = attr.getValue(Name.IMPLEMENTATION_VERSION);
}
if (implVendor == null) {
implVendor = attr.getValue(Name.IMPLEMENTATION_VENDOR);
}
if (sealed == null) {
sealed = attr.getValue(Name.SEALED);
}
}
if ("true".equalsIgnoreCase(sealed)) {
sealBase = url;
}
return definePackage(name, specTitle, specVersion, specVendor,
implTitle, implVersion, implVendor, sealBase);
}
protected Package definePackage(String name, String specTitle,
String specVersion, String specVendor,
String implTitle, String implVersion,
String implVendor, URL sealBase)
throws IllegalArgumentException
{
synchronized (packages) {
Package pkg = getPackage(name);
if (pkg != null) {
throw new IllegalArgumentException(name);
}
pkg = new Package(name, specTitle, specVersion, specVendor,
implTitle, implVersion, implVendor,
sealBase, this);
packages.put(name, pkg);
return pkg;
}
}这样,当从JarFile中加载一个类的时候,就顺便加载了他的Manifest文件,然后加载了Package对象。我们再来看Package对象的方法:
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151public class Package implements java.lang.reflect.AnnotatedElement {
/**
* Return the name of this package.
*
* @return The fully-qualified name of this package as defined in section 6.5.3 of
* <cite>The Java™ Language Specification</cite>,
* for example, {@code java.lang}
*/
public String getName() {
return pkgName;
}
/**
* Return the title of the specification that this package implements.
* @return the specification title, null is returned if it is not known.
*/
public String getSpecificationTitle() {
return specTitle;
}
/**
* Returns the version number of the specification
* that this package implements.
* This version string must be a sequence of nonnegative decimal
* integers separated by "."'s and may have leading zeros.
* When version strings are compared the most significant
* numbers are compared.
* @return the specification version, null is returned if it is not known.
*/
public String getSpecificationVersion() {
return specVersion;
}
/**
* Return the name of the organization, vendor,
* or company that owns and maintains the specification
* of the classes that implement this package.
* @return the specification vendor, null is returned if it is not known.
*/
public String getSpecificationVendor() {
return specVendor;
}
/**
* Return the title of this package.
* @return the title of the implementation, null is returned if it is not known.
*/
public String getImplementationTitle() {
return implTitle;
}
/**
* Return the version of this implementation. It consists of any string
* assigned by the vendor of this implementation and does
* not have any particular syntax specified or expected by the Java
* runtime. It may be compared for equality with other
* package version strings used for this implementation
* by this vendor for this package.
* @return the version of the implementation, null is returned if it is not known.
*/
public String getImplementationVersion() {
return implVersion;
}
/**
* Returns the name of the organization,
* vendor or company that provided this implementation.
* @return the vendor that implemented this package..
*/
public String getImplementationVendor() {
return implVendor;
}
/**
* Returns true if this package is sealed.
*
* @return true if the package is sealed, false otherwise
*/
public boolean isSealed() {
return sealBase != null;
}
/**
* Returns true if this package is sealed with respect to the specified
* code source url.
*
* @param url the code source url
* @return true if this package is sealed with respect to url
*/
public boolean isSealed(URL url) {
return url.equals(sealBase);
}
/**
* Compare this package's specification version with a
* desired version. It returns true if
* this packages specification version number is greater than or equal
* to the desired version number. <p>
*
* Version numbers are compared by sequentially comparing corresponding
* components of the desired and specification strings.
* Each component is converted as a decimal integer and the values
* compared.
* If the specification value is greater than the desired
* value true is returned. If the value is less false is returned.
* If the values are equal the period is skipped and the next pair of
* components is compared.
*
* @param desired the version string of the desired version.
* @return true if this package's version number is greater
* than or equal to the desired version number
*
* @exception NumberFormatException if the desired or current version
* is not of the correct dotted form.
*/
public boolean isCompatibleWith(String desired)
throws NumberFormatException
{
if (specVersion == null || specVersion.length() < 1) {
throw new NumberFormatException("Empty version string");
}
String [] sa = specVersion.split("\\.", -1);
int [] si = new int[sa.length];
for (int i = 0; i < sa.length; i++) {
si[i] = Integer.parseInt(sa[i]);
if (si[i] < 0)
throw NumberFormatException.forInputString("" + si[i]);
}
String [] da = desired.split("\\.", -1);
int [] di = new int[da.length];
for (int i = 0; i < da.length; i++) {
di[i] = Integer.parseInt(da[i]);
if (di[i] < 0)
throw NumberFormatException.forInputString("" + di[i]);
}
int len = Math.max(di.length, si.length);
for (int i = 0; i < len; i++) {
int d = (i < di.length ? di[i] : 0);
int s = (i < si.length ? si[i] : 0);
if (s < d)
return false;
if (s > d)
return true;
}
return true;
}可以发现,package类有很多get方法,这些方法基本和Attributes类的Name内部类中定义的名字一样,也就是说Package类能直接获取到Manifest文件中定义的变量。
与此同时,我们发现他也有一个isCompatibleWith
方法,这个方法很有意思,他会将给定的一个版本号字符串与Specification-Version
的值进行比较,用于判断当前的jar的版本是否不低于给定的版本号。
利用这个方法,我们就可以非常方便的在类加载时做一个验证,断言当前运行的版本号一定不低于我们给定的一个版本号。
打包分析
不过问题来了,随便打开几个包的Manifest文件,我这里以fastjson
为例:
1 | Manifest-Version: 1.0 |
我们发现这个文件非常简单,并没有之前定义的那些attributes。这样一来,package类也肯定是解析不到类似的方法的。那么我们如何在打包的时候加入这些信息呢?
如果是用gradle打包的话,这就用到了gradle的java插件的一个功能了。在给定的项目下添加一个打包命令的一个配置:
1 | jar.manifest.attributes("Specification-Version": '1.0.0') |
这样就可以将”Specification-Version”这个属性加到打包后的jarFile里了,详见参考资料中的gradle docs。
不过需要注意的是,这个值一定要是点号分隔的数字,不要加任何其他字符,否则调用isCompatibleWith方法时就会抛异常。
因此一般来说,我会这样进行配置,用以兼容以”-SNAPSHOT”结尾的版本号:
1 | jar.manifest.attributes("Specification-Version": version.split("-")[0]) |
使用分析
打完包之后,我们就可以很happy的在组件启动时,进行版本检查:
1 | static{ |
其中的”1.2.2”可以配置在Lion或者Apollo这样的配置中心里,以统一管理。
不过蛋疼的是,不是所有的第三方包的jarfile里都自带版本信息的,比如上面的fastjson。。。