百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

如何以低侵入方式获取业务系统使用的二方包版本号

csdh11 2025-03-03 18:10 23 浏览

在软件开发的世界里,依赖管理是一项至关重要却又常常被忽视的工作。最近,领导给我布置了一个特别的任务 —— 了解我们部门提供给业务方的核心二方包及其版本号,以便为后续的兼容升级工作提供有力支持。这看似简单的需求,背后却蕴含着不少技术挑战和思考。在本文中,我将与大家分享在完成这个任务过程中的探索与实践,希望能为正在面临类似问题的你提供一些启发。

知识小科普:一方包、二方包、三方包的区别

在深入探讨获取二方包版本号的方法之前,我们先来科普一下一方包、二方包和三方包的概念。

一方包

一方包指的是本工程中各模块之间的相互依赖。它们就像是一个团队内部成员之间的协作,只在项目内部发挥作用,是项目实现自身功能的重要组成部分。比如,在一个电商项目中,用户模块、订单模块、支付模块等之间的相互调用所依赖的代码包,就属于一方包。一方包的使用范围局限于项目内部,对项目的功能实现起着关键作用。

二方包

二方包是公司内部的依赖库,通常是公司内部其他项目发布的 jar 包。可以把它想象成公司内部不同部门之间的协作,虽然不在同一个项目组,但都在公司这个大环境下,为了实现公司的整体目标而共享资源。例如,公司的基础服务团队开发了一套通用的日志记录组件,其他业务部门在开发项目时可以直接引用这个二方包,实现统一的日志记录功能。二方包的使用范围限于公司内部,能够提高公司内部开发的效率和代码的复用性。

三方包

三方包是来自公司之外的开源库,像大家熟知的 apache、google 等发布的依赖就属于三方包。它们就像是外界提供的各种工具和资源,供整个行业或公众使用。比如,在开发 Web 应用时,我们常常会使用 Apache 的 HttpComponents 库来处理 HTTP 请求,这个库就是一个典型的三方包。三方包的使用范围广泛,能够帮助我们快速集成各种功能,加速项目的开发进程。

总结来说,一方包、二方包和三方包的主要区别在于权限范围和使用范围:一方包限于项目内部,二方包限于公司内部,而三方包则是面向公众或行业开放的。

回到正题:获取业务系统使用的二方包版本号的挑战

由于核心二方包是我们部门开发提供的,我们清楚地知道有哪些核心二方包。但问题在于,我们并不了解业务方到底使用了哪些二方包以及相应的版本。这就好比我们生产了一堆工具,却不知道客户具体使用了哪些工具以及工具的版本。而获取业务系统使用的二方包及其版本号,就成为了我们完成领导需求的关键所在。接下来,我将详细介绍几种获取二方包版本号的方法及其优缺点。

方法一:拉通业务方获取信息

最直接的方法就是拉通各个业务方,让他们提供正在使用的二方包及其版本。这个方法看似简单可行,但在实际操作中却存在不少问题。首先,这个需求是我们发起的,对于业务方来说,他们只是配合方,这件事情的优先级对他们来说可能并不高。他们可能有自己手头更重要的业务任务需要处理,很难将大量的时间和精力投入到我们的这个需求中。其次,业务方在配合时会考虑投入产出比。如果我们不能清晰地向他们阐述这件事情对他们的价值和收益,他们可能会对配合工作产生抵触情绪,导致沟通成本大幅增加。因此,这种方法虽然直接,但效率较低,并不是最佳选择。

方法二:埋点上报方式获取信息

如何获取二方包版本

获取二方包版本的关键在于读取META-INF/MANIFEST.MF文件中的Implementation-Version属性。这个属性记录了二方包的版本信息。虽然在某些情况下,Implementation-Version可能为空,但对于我们提供的二方包来说,这个问题并不存在。因为我们在构建二方包时,在pom文件的 GAV(GroupId、ArtifactId、Version)配置中引入了生成Implementation-Version的插件。具体配置如下:

 
        
            
                org.apache.maven.plugins
                maven-jar-plugin
                3.2.0
                
                    
                        
                            true
                        
                    
                
            
        
    

使用该插件会生成形如下内容

Manifest-Version: 1.0
Implementation-Title: Spring Cloud Alibaba Sentinel
Implementation-Version: 2.1.0.RELEASE
Built-By: yizhan
Implementation-Vendor-Id: com.alibaba.cloud
Created-By: Apache Maven 3.5.0
Build-Jdk: 1.8.0_131
Implementation-URL: https://github.com/alibaba/spring-cloud-alibaba/sp
 ring-cloud-alibaba-sentinel
Implementation-Vendor: Pivotal Software, Inc.

那么,如何获取Implementation-Version呢?最容易想到的方法就是解析MANIFEST.MF文件。下面是一个解析工具类的示例代码:

public final class VersionNoFetcher {

	private VersionNoFetcher() {
	}

	/**
	 * Return the full version string of the present codebase, or {@code null}
	 * if it cannot be determined.
	 * @return the version or {@code null}
	 * @see Package#getImplementationVersion()
	 */
	public static VersionMeta getVersion(ClassLoader classLoader,String libraryClassName) {
		if(classLoader == null){
			classLoader = Thread.currentThread().getContextClassLoader();
		}
		return determineVersion(classLoader,libraryClassName);
	}


	private static VersionMeta determineVersion(ClassLoader classLoader,String libraryClassName) {
		try {
		Class libraryClass = Class.forName(libraryClassName,true,classLoader);
		String implementationVersion = libraryClass.getPackage().getImplementationVersion();
		if (implementationVersion != null) {
			return new VersionMeta(implementationVersion, libraryClassName,libraryClass.getPackage().getName());
		}
		VersionMeta versionMeta = getVersionMetaFromCodeSource(libraryClassName, libraryClass);
		if(versionMeta != null){
			return versionMeta;
		}
		return getVersionMetaFromManifest(classLoader,libraryClass);

		} catch (Exception ex) {
			return new VersionMeta();
		}

	}

	private static VersionMeta getVersionMetaFromCodeSource(String libraryClassName, Class libraryClass) throws IOException, URISyntaxException {
		String implementationVersion;
		CodeSource codeSource = libraryClass.getProtectionDomain().getCodeSource();
		if (codeSource == null) {
			return null;
		}
		URL codeSourceLocation = codeSource.getLocation();

		URLConnection connection = codeSourceLocation.openConnection();
		if (connection instanceof JarURLConnection) {
			implementationVersion =  getImplementationVersion(((JarURLConnection) connection).getJarFile());
			if (implementationVersion != null){
				return new VersionMeta(implementationVersion, libraryClassName, libraryClass.getPackage().getName());
			}
		}
		try (JarFile jarFile = new JarFile(new File(codeSourceLocation.toURI()))) {
			implementationVersion = getImplementationVersion(jarFile);
			if (implementationVersion != null){
				return new VersionMeta(implementationVersion, libraryClassName, libraryClass.getPackage().getName());
			}
		}
		return null;
	}


	private static VersionMeta getVersionMetaFromManifest(ClassLoader classLoader,Class libraryClass) {

		try (InputStream is = classLoader.getResourceAsStream("META-INF/MANIFEST.MF")) {
			if (is == null) {
				return new VersionMeta();
			}

			// 创建Manifest对象并从输入流中读取
			Manifest manifest = new Manifest(is);
			// 获取主属性集
			Attributes attributes = manifest.getMainAttributes();

			// 从属性集中获取版本号
			String version = attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
			if (version != null) {
				return new VersionMeta(version, libraryClass.getName(), libraryClass.getPackage().getName());
			}
		} catch (Exception e) {

		}
		return new VersionMeta();
	}

	private static String getImplementationVersion(JarFile jarFile) throws IOException {
		return jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
	}

}

2、如何上报

获取到二方包版本后,我们需要将这些信息上报到指定的服务端。一种常见的上报方式是通过 HTTP 请求。以下是一个上报的参考示例代码:

public final class VersionNoUtils {
    private static final String APP_ID_KEY = "appid";
    private static final String VERSION_NO_KEY = "versionNo";


    private VersionNoUtils(){}

    public static void reportVersion2RemoteSvc(String url,String appId,ClassLoader classLoader,String libraryClassNames)  {
        if(!StringUtils.hasText(libraryClassNames) || !StringUtils.hasText(url)){
            return;
        }
        Map params = new HashMap<>();
        params.put(APP_ID_KEY,appId);
        if(libraryClassNames.contains(EXTENSION_SEPARATOR)){
            String[] libClassNames = libraryClassNames.split(EXTENSION_SEPARATOR);
            for(String libClassName : libClassNames){
                addVersionForClass(classLoader, libClassName,params);
            }
        }else{
            addVersionForClass(classLoader, libraryClassNames,params);
        }

        sendVersion2RemoteSvc(url, params);

    }


 
    private static void sendVersion2RemoteSvc(String url, Map params) {
        if(!params.isEmpty()){
            try {
                String jsonContent = JsonConverter.mapToJson(params);
                HttpUtils.getInstance().postJson(url, jsonContent);
            } catch (IOException e) {

            }
        }
    }
    }

3、何时上报?

确定了获取版本和上报的方式后,接下来就是选择合适的上报时机。以下是几种常见的上报时机及其优缺点分析:

a、 在业务方项目编译期期间进行上报

可以利用 APT(Annotation Processing Tool)或者自定义 Maven 插件在编译期进行上报。这种方式的优点是对项目运行的性能影响最低,因为在编译期就完成了上报工作,不会对项目的运行时性能产生额外的开销。然而,它的缺点也很明显,一旦在编译期出现问题,排查起来会非常困难。因为编译期的错误信息往往比较复杂,涉及到编译工具和插件的配置等多个方面,定位问题的根源需要花费大量的时间和精力。

b、 在业务方项目 install 或者 deploy 期间进行上报

利用自定义 Maven 插件在业务方执行install或者deploy操作时进行上报。这种方式可以确保在项目发布到本地仓库或远程仓库之前完成版本号的上报。但是,这种方式需要业务方在他们的项目中引用相应的插件。对于业务方来说,引入这个插件可能不会给他们带来直接的收益,反而可能会增加项目的复杂性和维护成本。因此,业务方可能不太愿意配合这种方式。

c、 在业务方项目启动时上报

可以利用 Spring 提供的扩展点,在业务方项目启动时进行上报。Spring 提供了丰富的扩展点,如CommandLineRunner、ApplicationListener等,我们可以通过实现这些接口来完成上报工作。这种方式的优点是相对简单易行,而且对业务方的侵入性较小。我们可以将上报代码直接内嵌到我们提供的二方包中,业务方在使用二方包时,上报功能会自动生效,对业务方基本上是无感的。当然,这种无感是相对的,业务方通过抓包或者监控手段还是可以发现上报请求的存在。以下是一个在项目启动时上报的示例代码:

@Component
public class VersionFetchCommandLineRunner implements CommandLineRunner {

    ExecutorService executorService = Executors.newSingleThreadExecutor(new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "report-version-thread");
        }
    });

    @Override
    public void run(String... args) throws Exception {
        executorService.execute(() -> {
            reportVersion2RemoteSvc("http://localhost:8080/version/report", "10000", Thread.currentThread().getContextClassLoader(), "org.apache.catalina.startup.Tomcat,org.springframework.beans.factory.BeanFactory");
        });

    }
}

上报的内容示例如下:

{"org.apache.catalina.startup": "9.0.21", "appid": "10000", "org.springframework.beans.factory": "5.1.8.RELEASE"}

d、 在业务方项目关闭时上报

可以使用 JVM 钩子函数,或者监听 Spring 的关闭事件,在业务方项目关闭时进行上报。这种方式的优点是可以确保在项目运行的整个生命周期结束时完成上报工作,获取到项目在运行过程中使用的二方包版本信息。但是,它的缺点是如果项目在运行过程中出现异常终止等情况,可能会导致上报失败。而且,在项目关闭时进行上报,可能会对项目的关闭过程产生一定的影响,增加项目关闭的时间。
综合比较以上几种上报时机,在业务方项目启动或者关闭时进行上报是相对可行的方式。尤其是在项目启动时上报,通过将上报代码内嵌到二方包中,可以实现对业务方的低侵入性,同时保证上报工作的顺利进行。

总结

获取业务系统使用的二方包版本号这个需求,在大多数业务开发场景中可能并不常见,但在开发基础组件或进行依赖管理时却非常重要。通过本文介绍的埋点上报方式,我们可以以较低的侵入性获取到业务方使用的二方包及其版本号。在实施过程中,需要注意以下几点:首先,上报操作一定要使用异步方式,避免对业务造成堵塞;其次,如果使用自定义 Maven 插件进行上报,要注意类加载器的问题,因为 Maven 插件的类加载器是自定义类加载器;最后,在开发公共组件时,可以考虑添加HasFeatures功能,将公共组件的能力封装进去,方便后续的功能扩展和管理。

至于什么是HasFeatures,可以查看我之前的文章聊聊如何感知项目引入哪些功能特性

示例代码链接

本文涉及的示例代码已上传至 GitHub,你可以通过以下链接获取:
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-fetch-version

希望本文的内容能够对你有所帮助。如果你在实践过程中遇到任何问题,或者有更好的解决方案,欢迎在评论区留言交流。让我们一起在技术的道路上不断探索前行!

相关推荐

探索Java项目中日志系统最佳实践:从入门到精通

探索Java项目中日志系统最佳实践:从入门到精通在现代软件开发中,日志系统如同一位默默无闻却至关重要的管家,它记录了程序运行中的各种事件,为我们排查问题、监控性能和优化系统提供了宝贵的依据。在Java...

用了这么多年的java日志框架,你真的弄懂了吗?

在项目开发过程中,有一个必不可少的环节就是记录日志,相信只要是个程序员都用过,可是咱们自问下,用了这么多年的日志框架,你确定自己真弄懂了日志框架的来龙去脉嘛?下面笔者就详细聊聊java中常用日志框架的...

物理老师教你学Java语言(中篇)(物理专业学编程)

第四章物质的基本结构——类与对象...

一文搞定!Spring Boot3 定时任务操作全攻略

各位互联网大厂的后端开发小伙伴们,在使用SpringBoot3开发项目时,你是否遇到过定时任务实现的难题呢?比如任务调度时间不准确,代码报错却找不到方向,是不是特别头疼?如今,随着互联网业务规模...

你还不懂java的日志系统吗 ?(java的日志类)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

谈谈枚举的新用法--java(java枚举的作用与好处)

问题的由来前段时间改游戏buff功能,干了一件愚蠢的事情,那就是把枚举和运算集合在一起,然后运行一段时间后buff就出现各种问题,我当时懵逼了!事情是这样的,做过游戏的都知道,buff,需要分类型,且...

你还不懂java的日志系统吗(javaw 日志)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

Java 8之后的那些新特性(三):Java System Logger

去年12月份log4j日志框架的一个漏洞,给Java整个行业造成了非常大的影响。这个事情也顺带把log4j这个日志框架推到了争议的最前线。在Java领域,log4j可能相对比较流行。而在log4j之外...

Java开发中的日志管理:让程序“开口说话”

Java开发中的日志管理:让程序“开口说话”日志是程序员的朋友,也是程序的“嘴巴”。它能让程序在运行过程中“开口说话”,告诉我们它的状态、行为以及遇到的问题。在Java开发中,良好的日志管理不仅能帮助...

吊打面试官(十二)--Java语言中ArrayList类一文全掌握

导读...

OS X 效率启动器 Alfred 详解与使用技巧

问:为什么要在Mac上使用效率启动器类应用?答:在非特殊专业用户的环境下,(每天)用户一般可以在系统中进行上百次操作,可以是点击,也可以是拖拽,但这些只是过程,而我们的真正目的是想获得结果,也就是...

Java中 高级的异常处理(java中异常处理的两种方式)

介绍异常处理是软件开发的一个关键方面,尤其是在Java中,这种语言以其稳健性和平台独立性而闻名。正确的异常处理不仅可以防止应用程序崩溃,还有助于调试并向用户提供有意义的反馈。...

【性能调优】全方位教你定位慢SQL,方法介绍下!

1.使用数据库自带工具...

全面了解mysql锁机制(InnoDB)与问题排查

MySQL/InnoDB的加锁,一直是一个常见的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?下面是不同锁等级的区别表级锁:开销小,加锁快;不会出现死锁;锁定粒度...

看懂这篇文章,你就懂了数据库死锁产生的场景和解决方法

一、什么是死锁加锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。任何事务都需要获得相应对象上的锁才能访问数据,读取数据的事务通常只需要获得读锁(共享锁),修改数据的事务需要获...