`
wzucxd
  • 浏览: 27713 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

OSGi Bundle Convert插件原理

 
阅读更多

本文首发于Infoq中文站(4.2):http://www.infoq.com/cn/articles/osgi-bundle-convert-plugin-principle

1.引言

普通的web应用要转换为OSGi应用,经常会遇到应用中依赖的Jar是非标准的情况,这些Jar可能只遵守了部分OSGi规范,甚至Manifest信息是空的。这种情况在OSGi 应用中根本无法使用这个非标准的Jar做为Bundle,故必须要将这个非标准的Jar转换成遵守OSGi规范的Bundle。另外用Maven管理的仓库,由于不同开发者对规范的理解不同,在仓库中也存在了各种规范或者不规范的Jar,如果我们能很好的将Maven仓库中的Jar转换成标准的Bundle,Maven仓库也就转换成对应的OSGi Bundle仓库,对于非OSGi的应用而言也就可以很方便的利用Maven仓库,普通web应用也可以平滑的切换到OSGi环境。

2.转换Bundle的方式

要将非标准的Jar转换成OSGi Bundle,最核心的也就是如何将Bundle中的Import-Package和Export-Package内容重写,对此转换Bunlde的方式可划分为3种方式:1.固定版本方式转换;2.非固定版本方式转换;3.固定版本和非固定版本混合使用方式转换;

2.1固定版本方式

主要依据Jar对应pom依赖树(mvn dependency:tree)进行转换,而对于pom中无法确定的版本,则需要依靠Bundle模块pom的依赖仲裁来强制选择一个版本,这种情况最终转换好的Bundle Import-Package和Export-Package是固定版本的;固定版本方式转换有点类似于maven中的Jar版本强依赖,这个Bundle必须import指定的固定版本Bundle,这样做的目的也在于使得转换后的每个Bundle依赖环境是一个独立的集合,而且集合之间没有任何冲突可言。

2.2非固定版本方式

依据Jar文件中*.class的package和import package信息转换,最终转换后的Bundle不指定Import-Package版本和Export-Package版本;这种情况也就是目前经常用的不指定版本范围或者无版本package,特别是无版本package,在OSGi环境中,将会使用Verison>=0.0.0的方式选择Bundle,只要最新版本可用,就会使用最新版本替换新环境。

2.3固定版本和非固定版本混合使用方式

依据pom依赖树转换,遇到无法转换的版本不继续处理,最终转换后的Bunlde中Import-Package和Export-Package会存在固定依赖版本、无版本信息或者版本区间。例:

Import-Package: org.apache.commons.collections.comparators;version="[3.2.0,3.2.0]
",org.apache.commons.collections.keyvalue;version="[3.2.0,3.2.0]
",org.apache.commons.collections.list;version="[3.0.0,3.2.0]
",org.apache.commons.collections.set;version="[3.0.0,3.2.0]
",org.apache.commons.logging

3.maven-bundle-plugin插件转换的Bundle

通常OSGi环境的开发相对比较复杂,从上面也可以看出对于Bundle的转换更加复杂,于是业界有提供这样的Maven插件maven-bundle-plugin(具体见参考资料[1])来做这个事情,所采用的转换方式正好是固定版本和非固定版本混合使用方式。

其实maven-bundle-plugin是使用bnd插件完成对Bundle的转换。这个Maven插件对于Bundle的转换主要使用asm解析,读取Jar文件中的class文件字节码信息,解析class文件中的package和import package信息,最后分析重写Manifest.MF的Import-Package和Export-Package。

这种模式在一个独立项目中使用,可能不会存在什么大问题,但是这个Maven插件转换后的Bundle无法很好处理几种特殊应用场景,下面从OSGi Bundle的2种依赖情况来详细分析下采用这种转换模式转换后的Bundle问题。

3.1转换为固定版本依赖

alt

(图1)

如图1所示,假如是多人多部门协作开发的环境,那么就很有可能不同Bundle是由不同开发角色开发的,这时候BundleA1.0.0要求说必须使用BundleA1 1.0.1,BundleB 1.0.0要求说必须使用BundleA1 1.0.0。但是,他们所依赖的BundleA1的是冲突的,所以这时需要指定BundleA和BundleB依赖各自对应版本的BundleA1来解决,此时2个BundleA1同时在一个应用环境中存在并同时提供服务,固定版本方式的Bundle依赖很好解决了冲突问题。不过这种情况下,BundleA和BundleB同时存在会造成2个Bundle之间通信出现问题,这是由于同一个类(BundleA1中的类)进行类型转换时会因为classLoader不同而造成ClassCastException异常。在第4节“插件的改进”中将会详细描述如何解决固定版本模式出现的问题。

3.2转换为非固定版本依赖

alt

(图2)

在多人协作开发或者分布式应用环境中,如果BundleA和BundleB不指定依赖的BundleA1版本,那么在OSGi应用环境中,应用会使用verison>=0.0.0的方式选择Bundle,只要最新版本的Bundle可用,就会使用最新版本更新环境。如图2所示,BundleA 1.0.0 和BundleB 1.0.0均选择了最新版本的BundleA1 1.0.1安装,假如BundleA1提供了1.0.0和1.0.1,而且这2个版本是冲突的。BundleB 1.0.0会要求必须使用BundleA1 1.0.0,这时候的应用环境就成了图2这种情况,会造成最后安装的BundleA1环境不稳定,导致 BundleB 1.0.0功能出问题。反之,使用BundleA1 1.0.0,那么BundleA将会出现问题。 maven-bundle-plugin插件对于非遵守OSGi规范的Jar转换大多都会这种形式,因此在OSGi应用环境开发过程中尽量使用固定版本方式,虽然这种情况的开发相对困难些,但是改进好插件还是能够很好的帮助我们规避掉以后维护固定版本的工作量,而且能辅助我们很好的定位Bundle依赖出现的问题。

3.3插件的功能问题

如果要转换的Jar本身是OSGi Bundle,而且MANIFEST.MF的Import-Package中存在内容,原先Maven的maven-bundle-plugin插件则会继续使用该Import-Package。当然Import-Package的内容会遇到一些不规范的版本,特别是从未接触的开发人员,模仿着写,Import-Package写的有些问题,依赖信息错了,甚至由于开发人员疏忽写了些错误版本,这时候需要我们去修正为正确的信息,同理Export-Package也存在同样的问题。

另外MANIFEST.MF中没有Import-Package内容时:maven-bundle-plugin利用asm来解析*.class文件里import package来提取,提取方式如下所示:

alt

(图3)

图3 BunldeA中只有这个类,那么BundleA的Import-Package为java.lang.reflect,org.objectweb.asm,com.wzucxd.classloader。

可以看出这种由maven-bundle-plugin插件转换的版本信息丢失很明显,再则由于bnd根据maven pom依赖进行转换,如果pom中有exclusions写法,那么当转换Bundle过程中,认为这个Bundle package依赖是不需要的,Import-Package会去除这个package,对于所依赖的Bundle来说可能会造成deploy失败。如BundleA1.0.0依赖的BundleA1 1.0.1需要com.wzucxd.classloader这个package,但是由于在BundleA1.0.0的pom中把BundleA1 1.0.1对应的com.wzucxd.classloader package exclusion出去了,而BundleA1.0.0的package中import进来,这种情况就有可能造成BundleA1 1.0.1 deploy失败了。

总之,maven-bundle-plugin插件转换后的Bundle,在应用运行过程中会存在的上述问题,故可以针对这几个改进措施来弥补插件的不足:

  1. 允许为所有package强制指定固定版本
  2. 修正OSGi Bundle Jar中不正确的Import-Package和Export-Package
  3. 解决exclusions等情况的版本丢失问题
  4. 解决可选版本问题,正确使用resolution:=optional

4.插件的改进

从Maven插件的不足中,我们也看到了核心问题在于该如何解决全部采用固定版本Bundle依赖时的转换问题。针对图1固定版本依赖这个问题,我们可以将冲突类放到bootdetegation中,但是这种解决方式不建议,这样做也就把这个类让WebAppClassLoder加载了,让应用方去解决maven冲突,所有Bundle中的这种冲突都会需要解决,显然这样不是一个好方式,这样就回到了原始社会。但是我们可以采用固定版本方式结合extra模式,这样可以很好的解决这个Bundle交互问题,将所有Bunlde做成固定版本的Import-Package到extra中,extra中指定的版本会用加载OSGi Framework的classloader加载,具体可以看下参考资料[2]里的内容,这种方式避免了大量使用bootdelegation来解决类型转换问题。

因此下面将具体介绍如何转换固定版本Import-Package和Export-Package的方法。

4.1 Export-Package转换

alt

(图4)

从上面总结下来,对于Export-Package中的package版本修正,遇到没有使用版本或者错误信息的package时可以选择指定成当前转换Jar的pom version。

处理方法:

转换过程可采用后序遍历方式逐级转换Jar,如图4所示,逐级转换asm-all,org.apache.felix.ipojo.metadata->org.apache.felix.ipojo。

  1. 将Jar中所有的package提取出来
  2. 继续把MANIFEST.MF中的Private-Package的package信息收集起来。

    另外在Bnd中,对应下面的代码可用来获取当前Jar的所有package,但是这个package是包含Private-Package的:

    alt

    (图5)

    最后将这些packages 减去Private-Package,并带有当前转换Jar的version的内容作为Export-Package。

  3. 解析当前Jar文件对应的pom文件,用pom version覆盖掉原先的version值,让所有Export-Package的version对应具体实际使用的版本。

    重新生成的Export-Package和MANIFEST.MF文件中原先的Export-Package数据这这个时候进行合并,于是最终对应的version值都会改成和该Jar对应的版本。

如griffin.core.module:1.0.5的Export-Package:

alt

(图6)

处理后还存在无版本的package,则从Export-Package中去除,因为这种情况的package并不是他自己提供的,是由于原始的OSGi Bundle Jar写的不规范

uses语法的使用:

转换后的Bundle在使用过程中,有时会遇到一个问题:interface类中使用了其他package的类,而这个interface类在实现类型的Bundle中却没有使用到。

如下图7和图8中的TemplateEngine interface(接口Bundle内)和HelloImpl类(实现Bundle内),在HelloImpl类中import了com.wzucxd.griffin.core.module.engine.TemplateEngine接口,但是不使用TemplateEngine接口。这时候实现的Bundle将会生成Export-Package:com.wzucxd.griffin.core.module.engine;uses:=com.wzucxd.griffin.core.module.context;version="1.0.5"

表示com.wzucxd.griffin.core.module.engine这个package要使用的时候,必须先install包含com.wzucxd.griffin.core.module.context package的Bundle;这样做的目的也是为了让Bundle依赖能够更加完整些。

alt

(图7)

alt

(图8)

4.2 Import-Package转换

对于Import-Package的转换要求更为严格,必须要在Export-Package转换后再进行转换,因为Import-Package的内容必须是被Export过的package。

转换方法:

使用4.1的方式转换好Export-Package

  1. 第1次后序遍历,转换时候先后序遍历转换所有的Jar,(maven dependency:tree可以参照maven的DependencyTreeBuilder实现);
  2. 转换Jar时,读取该Jar里所有java文件的字节码,分析class文件中的import内容,将所有import内容提取出来。并记录下所有依赖树里的package和Jar version信息,为V1<package,version> map集合。
  3. 在所有Jar转换完成后,将转换完成后的V1<package,version>与第1步Export-Package转换完成后记录的package version合并为V2<package,version>
  4. 第2次后序遍历,将Import-package的版本信息用第2步合并的V2<package,version>数据进行重写。这时候正常来说版本信息已经全部为固定版本信息。
resolution:=optional的使用:

第2次转换中会遇到一些异常情况,例如遇到原先没有版本,但是这个版本在maven仲裁中有定义,那么可使用maven仲裁的版本作为固定版本,这种情况需要加上resolution:=optional;转换时遇到maven仲裁后也无版本的,也就是说这个package无定义,在这个Bundle环境中是独立的,那么这个信息可能是Private-Package或者原先是OSGi Bundle中错误的Import,没有任何意义,这种Package去除掉,当然也可以使用resolution:=optional;表示这个package在install的时候是可选的,应用真正需要的时候再去install。

另外由于是根据pom转换Bundle,故遇到pom中含有这几种配置时也要用resolution:=optional:optional、exclude jar、exclude java类型的配置。因为在这几种情况下转换Bundle,是不可能知道使用的Import-Package版本,版本信息只有在maven仲裁后才知道(也有可能是开发者最后具体指定使用的版本),当前你也可以不写版本信息,但是这样不建议。resolution:=optional使用和Export-Package中的use有点类似,有些jar采用spi方式开发,只定义了接口,但是实现由具体的jar来做,实现的jar可以有多个,如common.logging,可以有多个log日志系统,那么这个接口使用必须都import进去,具体使用哪个由应用来决定。

例net.sf.json-lib_2.2.0的MANIFEST.MF

alt

在这里可以看到org.apache.oro.text.regex;resolution:=optional;version="[0.0.0,0.0.0]",这里的resolution:=optional;用法表示这个package是非必须的依赖,可有也无,只有需要使用的时候才install,当应用中使用有export这个package的Bundle时,那么会提前install这个依赖Bundle,而且少这个package的时候也可以install net.sf.json-lib。

4.3 OSGi Bundle转换插件使用设计

这次我们从Convert插件使用上来看看,大多都会以什么习惯去使用Convert插件:

1.在pom中指定<packaging>bundle</packaging>

2.引入plugin(如图9)

alt

(图9)

注:Instructions中可选属性有

  • Private-Package:将需要使用的package内容全部打包到Bundle中,私有package的内容由Bundle自身的classloader加载,但是不建议使用,容易出现被指定的package在WebAppClassloader加载过
  • Import-Package:指定bundle的import,import如果写明具体版本,格式为<Import-Package> xxx.xxx.xxx;version="[1.0.0,1.0.0]", xxx.xxx.xxx;version="[xxx,xxx]"</Import-Package>,将这个Import-Package信息覆盖bnd插件转换后的Import-Package
  • Export-Package:对外暴露的package,可被其他bundle import使用,Export-Package的所有package必须带版本,而且建议和pom的版本一致,版本也可以自己指定
  • Ignore-Package:这里的Ignore-Package作用:转换的时候过滤掉这些package信息,在所有Bundle转换之前,可以提前配置了一个bnd.properties文件,里面指定所有Import-Package不需要import的package,里面的内容基本为jdk里的package,因为jdk刚开始由WebAppClassLoader加载, 当然可以为每个Bundle默认写上Ignore-Package。
  • Bundle-SymbolicName:Bundle的名字,建议用${project.groupId}+${project.artifactId}的组合
  • Interface类型的Bundle在开发过程一般只写Export-Package就好,如果接口Bundle作为二方库方式开发也可以,在Bundle.implement中引用接口pom的时候,插件在转换Bundle.implement之前转换对接口做Bundle Convert,为接口Bundle中的MANIFEST.MF写入正确的Import-Package和Export-Package信息。
  • Bundle.implement作为实现Bundle,一般不需要写Export-package/Import-Package,一些特殊的Bundle,如这个Bundle.implement实现使用了动态代理的service,那么大多可以在Import-Package中指定这个service interface的package来解决。

插件编译参数设计,为了提升效率,必要的编译参数也是需要的:

插件编译参数设计,为了提升效率,必要的编译参数也是需要的:
//编译前是否清理缓存目录
maven install -Dbundle.clean=true/(false)
//是否编译snapshot
maven install -Dsnapshot.rebuild=(true)/false
//只编译指定的bundle
maven install -D buildBundle=com.common.util

这样对Bundle的Import-Package和Export-Package可以进行一定程度的自定义了,但是我们还是更希望尽量一次性完整的转换好Bundle,用插件来完全负责转换Import-Package和Export-Package。

4.4 MANIFEST.MF中其他信息的定义

Bundle-Convert:表示从普通Jar转为OSGi Bundle后的表示

Bundle-Build:表示原先是标准的OSGi Bundle,如自己开发的标准Bundle.implement

Bundle-Sha1:表示这个Bundle的唯一版本信息,计算方式:BundleConvertUtil.getSha1(File file);具体sha1或者md5计算方法很多,这里的具体作用还为了以后将转换后的Bundle保存到一个组件仓库中避免重复Bundle的多次转换,也可以用来区分一个Bundle是否被多次编译。

5.总结

改进后的Convert插件也还存在一个问题,pom转换为OSGi Bundle时出现的Jar版本冲突,允许2个版本都可以使用,这里的方法是使用自己解决冲突的方式,插件转换时用了maven仲裁的版本。这里的版本就相当坑,maven使用树状最短路径版本,而OSGi Bundle使用的是图状关联版本,故这个时候使用多个版本的OSGi Bundle会使用version="[xxx,xxx]"区间值表达,但是在我们这里还是建议使用maven仲裁选择一个固定版本。当然固定版本选择是为了支持extra使用,所以在这个插件使用的时候,不建议应用方在pom中的Jar依赖与接口Bundle的pom依赖有冲突,冲突需要应用方提前先解决,当然这个做法也是合理的,因为应用新引入一个接口Jar的时候,pom依赖有冲突那么需要提前解决。在插件中也可以在转换过程中就将有冲突的Package提前抛出异常告知开发者,在编译期就让开发者解决掉这个问题。

6.参考资料

[1] apache-felix-maven-bundle-plugin-bnd插件http://felix.apache.org/site/apache-felix-maven-bundle-plugin-bnd.html

[2]Exposing the boot classpath in OSGihttp://spring.io/blog/2009/01/19/exposing-the-boot-classpath-in-osgi/

分享到:
评论

相关推荐

    kernel-devel-4.18.0-553.45.1.el8-10.x86-64.rpm

    Rocky Linux 8.10内核包

    Simulink中三阶单环多位量化Σ-Δ调制器的设计与实现-音频带ADC的应用(复现论文或解答问题,含详细可运行代码及解释)

    内容概要:本文档详细介绍了如何在Simulink中设计一个满足特定规格的音频带ADC(模数转换器)。首先选择了三阶单环多位量化Σ-Δ调制器作为设计方案,因为这种结构能在音频带宽内提供高噪声整形效果,并且多位量化可以降低量化噪声。接着,文档展示了具体的Simulink建模步骤,包括创建模型、添加各个组件如积分器、量化器、DAC反馈以及连接它们。此外,还进行了参数设计与计算,特别是过采样率和信噪比的估算,并引入了动态元件匹配技术来减少DAC的非线性误差。性能验证部分则通过理想和非理想的仿真实验评估了系统的稳定性和各项指标,最终证明所设计的ADC能够达到预期的技术标准。 适用人群:电子工程专业学生、从事数据转换器研究或开发的技术人员。 使用场景及目标:适用于希望深入了解Σ-Δ调制器的工作原理及其在音频带ADC应用中的具体实现方法的人群。目标是掌握如何利用MATLAB/Simulink工具进行复杂电路的设计与仿真。 其他说明:文中提供了详细的Matlab代码片段用于指导读者完成整个设计流程,同时附带了一些辅助函数帮助分析仿真结果。

    计算机课后习题.docx### 【计算机科学】研究生入学考试计算机组成原理专项题库设计:考研复习资源集成与优化

    内容概要:该题库专为研究生入学考试计算机组成原理科目设计,涵盖名校考研真题、经典教材课后习题、章节题库和模拟试题四大核心模块。名校考研真题精选多所知名高校的计算机组成原理科目及计算机联考真题,并提供详尽解析,帮助考生把握考研命题趋势与难度。经典教材课后习题包括白中英《计算机组成原理》(第5版)和唐朔飞《计算机组成原理》(第2版)的全部课后习题解答,这两部教材被众多名校列为考研指定参考书目。章节题库精选代表性考题,注重基础知识与重难点内容,帮助考生全面掌握考试大纲要求的知识点。模拟试题依据历年考研真题命题规律和热门考点,精心编制两套全真模拟试题,并附标准答案,帮助考生检验学习成果,评估应试能力。 适用人群:计划参加研究生入学考试并报考计算机组成原理科目的考生,尤其是需要系统复习和强化训练的学生。 使用场景及目标:①通过研读名校考研真题,考生可以准确把握考研命题趋势与难度,有效评估复习成效;②通过经典教材课后习题的练习,考生可以巩固基础知识,掌握解题技巧;③通过章节题库的系统练习,考生可以全面掌握考试大纲要求的各个知识点,为备考打下坚实基础;④通过模拟试题的测试,考生可以检验学习成果,评估应试能力,为正式考试做好充分准备。 其他说明:该题库不仅提供详细的题目解析,还涵盖了计算机组成原理的各个方面,包括计算机系统概述、数据表示与运算、存储器分层、指令系统、中央处理器、总线系统和输入输出系统等。考生在使用过程中应结合理论学习与实践操作,注重理解与应用,以提高应试能力和专业知识水平。

    __UNI__DB9970A__20250328141034.apk.1

    __UNI__DB9970A__20250328141034.apk.1

    minio-rsc-Rust资源

    rust for minio

    4-4-台区智能融合终端功能模块型式规范(试行).pdf

    国网台区终端最新规范

    《基于YOLOv8的化工管道焊缝缺陷检测系统》(包含源码、可视化界面、完整数据集、部署教程)简单部署即可运行。功能完善、操作简单,适合毕设或课程设计.zip

    资源内项目源码是来自个人的毕业设计,代码都测试ok,包含源码、数据集、可视化页面和部署说明,可产生核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果、标签分布图。都是运行成功后才上传资源,毕设答辩评审绝对信服的保底85分以上,放心下载使用,拿来就能用。包含源码、数据集、可视化页面和部署说明一站式服务,拿来就能用的绝对好资源!!! 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、大作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.txt文件,仅供学习参考, 切勿用于商业用途。

    python源码-1个机器学习相关资源

    一个简单的机器学习代码示例,使用的是经典的鸢尾花(Iris)数据集,通过 Scikit-learn 库实现了一个简单的分类模型。这个代码可以帮助你入门机器学习中的分类任务。

    pyqt离线包,pyqt-tools离线包

    pyqt离线包,pyqt-tools离线包

    《基于YOLOv8的船舶机舱灭火系统状态监测系统》(包含源码、可视化界面、完整数据集、部署教程)简单部署即可运行。功能完善、操作简单,适合毕设或课程设计.zip

    资源内项目源码是来自个人的毕业设计,代码都测试ok,包含源码、数据集、可视化页面和部署说明,可产生核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果、标签分布图。都是运行成功后才上传资源,毕设答辩评审绝对信服的保底85分以上,放心下载使用,拿来就能用。包含源码、数据集、可视化页面和部署说明一站式服务,拿来就能用的绝对好资源!!! 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、大作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.txt文件,仅供学习参考, 切勿用于商业用途。

    SQL常用日期和时间函数整理及使用示例

    SQL常用日期和时间函数整理及在sqlserver测试示例 主要包括 1.查询当前日期GETDATE 2.日期时间加减函数DATEADD 3 返回两个日期中指定的日期部分之间的差值DATEDIFF 4.日期格式转换CONVERT(VARCHAR(10),GETDATE(),120) 5.返回指定日期的年份数值 6.返回指定日期的月份数值 7.返回指定日期的天数数值

    GSDML-V2.3-Turck-BL20-E-GW-EN-20160524-010300.xml

    GSDML-V2.3-Turck-BL20_E_GW_EN-20160524-010300.xml

    T_CPCIF 0225-2022 多聚甲醛.docx

    T_CPCIF 0225-2022 多聚甲醛.docx

    《基于YOLOv8的智能仓储货物堆码倾斜预警系统》(包含源码、可视化界面、完整数据集、部署教程)简单部署即可运行。功能完善、操作简单,适合毕设或课程设计.zip

    《基于YOLOv8的智能仓储货物堆码倾斜预警系统》(包含源码、可视化界面、完整数据集、部署教程)简单部署即可运行。功能完善、操作简单,适合毕设或课程设计

    蚕豆脱壳机设计.zip

    蚕豆脱壳机设计.zip

    附件2-2:台区智能融合终端入网专业检测单位授权委托书.docx

    台区终端电科院送检文档

    Y6一39一No23.6D离心通风机 CAD().zip

    Y6一39一No23.6D离心通风机 CAD().zip

    django自建博客app

    django自建博客app

    附件3-4:台区智能融合终端全性能试验增值税发票开具确认单.docx

    台区终端电科院送检文档

    非开挖水平定向钻机动力头装置设计.zip

    非开挖水平定向钻机动力头装置设计.zip

Global site tag (gtag.js) - Google Analytics