中间件多版本冲突的4种解决方案和我们的选择
背景
在小小的公司里面,挖呀挖呀挖。最近又挖到坑里去了。一个稳定运行多年的应用,需要在里面支持多个版本的中间件客户端;而多个版本的客户端在一个应用里运行时会有同名类冲突的矛盾。在经过询问chatGPT,百度,google,github,和各位大佬的文章后,进行了总结。大概有以下几种解决方案
业界方案1----更改类路径
低版本客户端,更改类路径;然后重新打包编译客户端;这样不同版本客户端,使用的类名就不同了
此解决方案
优点
1、适合简单的小项目:无需编写新代码
2、可能是最快的一种方式;只需要把代码下载下来;更改clients下的类路径,重新编译即可;属于不怎么费脑力,但有点费体力的方式。
缺点
1、遇到版本升级,需要把步骤2的过程重新人肉再来一遍。这个比较的那啥。。。。。。
2、不同版本客户端,可能也会依赖不同版本的三方jar包。这个也是蛮棘手的
此种方式适合小项目或者外包等一锤子买卖
解决方案2 -----自定义ClassLoader
使用ClassLoader进行类的隔离;不同版本客户端和依赖三方包,用不同classLoader进行加载和隔离;完全杜绝版本问题
此解决方案
1、属于自研;对ClassLoader的类加载机制需要有一定的了解
解决方案3------业界开源方案 sofa-ark
sofa-ark是动态热部署和类隔离框架,由蚂蚁集团开源贡献。主要提供应用模块的动态热部署和类隔离能力
提供功能:
1、包冲突的解决(能解决现有项目遇到的多版本问题)
2、合并部署:多个项目分工程开发,但可以合并部署;还支持静态合并部署和动态合并部署
资料传送门:
https://github.com/sofastack/sofa-ark
https://www.sofastack.tech/projects/sofa-boot/sofa-ark-readme/
此解决方案:
优点
1、功能强大不仅能执行类的隔离;还能静动态的热部署;还能进行插件的热插拔;能解决现有工程遇到的问题;文档也挺齐全的
2、性能,稳定性,易用性,应该是所有保障的,毕竟蚂蚁内部也在使用;
缺点
1、虽号称是轻量级,但那是和OSGI这种重型框架相比,在sofa-ark里,也还有蛮多概念比如:Ark Container,ark包,插件包,biz包;如何打ark包,如何打插件包等等;有一定的学习成本,好在文档齐全能降低一定的入门门槛
2、需要对现有工程进行改造,以符合sofa-ark的规范;打包和部署上还需要遵循其规范。对于小公司主打的就是一个“自由”这种状态来说;有一点点束缚和被迫学习了,因为我们都比较的“懒”
此种方式适合大型项目;大型项目开发人员和开发应用众多;而sofa-ark制定了相应的biz包和插件包的开发规范;在代码复杂性,模块化开发,扩展性,项目维护,应用运行期等都进行了综合考虑。
解决方案4------OSGI
属于比较重型解决方案
OSGI 作为业内最出名的类隔离框架,自然是可以被用于解决上述包冲突问题,但是 OSGI 框架太过臃肿,功能繁杂。为了解决包冲突问题,引入 OSGI 框架,有牛刀杀鸡之嫌,反而使工程变得更加复杂,不利于开发。
我们的选择
解决方案这么多,我们该选择哪个方案了?
我们选择方案2。为什么了?
先说说为什么不选择其它方案
方案1: 此种方式不一定是最快的方式;因为后续考虑到每增加一个版本,或者社区有更新,都要去做一次,更名,打包等。还是挺麻烦,不太自动化,属于不费脑子费体力。
方案3: 功能强大:在模块化开发,类隔离,热部署等功能性上无比优异;性能和稳定性也有大公司在背书;但我们目前是个小公司,遇到的业务场景和技术场景,没有那么复杂,强大的功能给我们,我们不一定能用的上,用的不好可能还会被反噬;由于公司技术人员对该框架缺少实际使用经验;并且对该框架的实现原理也没人懂属于需要现学;使用后万一后续出现什么问题,问题定位和维护也挺麻烦;对小公司来说还是“太重”了;并且还需要对现有工程进行改造
方案4: 就不用在详说了,比方案3还重的解决方案
为什么方案2适合我们?
实现难度上: 对我们小公司来说虽然要自己写代码实现;但经过评估大概几百行代码就能搞定,技术上不是那么的高不可攀;
技术熟悉度: 团队内大家对ClassLoader的机制还蛮熟悉;
使用经验上: 有多个同事曾经用classLoader进行过这种隔离机制的实现,但业务场景不同;
复用性上: 写一次代码;后续在遇到多个版本冲突;经过简单的配置即可,不需要修改代码;更不需要修改三方依赖源码,比方案1好
在性能和稳定性上: 性能上不影响运行期,只会影响到代码加载期,所以性能这块还好;而在稳定性上,可通过测试环境长期稳定运行和一定的业务压测进行验证;而恰好我们有这样的测试环境和线上引流进行压测验证工具
设计图
ClassLoader的原理: 从上图可看出,ClassLoader会影响JVM加载类的路径和类的加载顺序;
类的加载路径
bootStrap ClassLoader: 加载%JRE_HOME%\lib 下的jar,比如rt.jar等
Extendtion ClassLoader: 加载%JRE_HOME%\lib\ext 目录下的jar
AppClass ClassLoader: 加载应用classpath下的所有类,即工程里依赖三方jar和工程的class
加载顺序 :双亲委托机制;加载类时,先让父ClasserLoader进行加载,父Classloader找不到,才让子ClassLoader进行查找和加载。
主要想法: 自定义ClassLoader继承URLClassLoader(即图里的AppClassClassLoader);基础类的加载 还是用双亲委托机制,由父类去加载,自定义类实现findClass方法,在该方法里加载指定目录class和jar。
具体工具类-----应该可以拿来即用
该实现类主要参考了Cyber365大佬的文章;然后做了一些改动(简化类,抽取工具类,去除多重if嵌套)
主要由两个类进行实现
-
MiddlewareClassLoader 类 主要做了两件事
1:读取Class文件内容;根据传入类名,从指定url中查找并读取到对应class文件内容
2:生产Class对象: 传入class文件内容,调用底层defineClass方法生产 -
UrlUtils 类: 主要做了一件事
1:根据指定的url,计算出该url和对应子目录下jar的url。
MiddlewareClassLoader 类
public class MiddlewareClassLoader extends URLClassLoader {private URL[] allUrl;public MiddlewareClassLoader(String[] paths){this(UrlUtils.getURLs(paths));}public MiddlewareClassLoader(URL[] urls) {this(urls, MiddlewareClassLoader.class.getClassLoader());}public MiddlewareClassLoader(URL[] urls, ClassLoader parent) {super(urls, parent);this.allUrl = urls;}protected Class<?> findClass(final String name)throws ClassNotFoundException{return loadExtendClass(name);}// /**
// * 不建议重新定义loadClass 方法,打破双亲委派机制,采用逆向双亲委派
// *
// * @param className 加载的类名
// * @return java.lang.Class<?>
// * @author Cyber
// * <p> Created by 2022/11/22
// */
// @Override
// public Class<?> loadClass(String className) throws ClassNotFoundException {
// Class extClazz = loadExtendClass(className);
// if(null != extClazz){
// return extClazz;
// }
// return super.loadClass(className);
// }public Class<?> loadExtendClass(String className) throws ClassNotFoundException {if(null == allUrl){return null;}String classPath = className.replace(".", "/");classPath = classPath.concat(".class");for (URL url : allUrl) {byte[] data = null;ByteArrayOutputStream baos = new ByteArrayOutputStream();InputStream is = null;try {File file = new File(url.toURI());if (!file.exists()) {continue;}JarFile jarFile = new JarFile(file);if (jarFile == null) {continue;}JarEntry jarEntry = jarFile.getJarEntry(classPath);if (jarEntry == null) {continue;}is = jarFile.getInputStream(jarEntry);byte[] buffer = new byte[1024 * 10];int length = -1;while ((length = is.read(buffer)) > 0) {baos.write(buffer, 0, length);}data = baos.toByteArray();System.out.println("********找到classPath=" + classPath + "的jar=" + url.toURI().getPath() + "*******");Class clazz = this.defineClass(className, data, 0, data.length);return clazz;} catch (Exception e) {e.printStackTrace();} finally {try {if (is != null) {is.close();}baos.close();} catch (IOException e) {e.printStackTrace();}}}return null;}
}
工具类UrlUtils
public class UrlUtils {/*** description 通过文件目录获取目录下所有的jar全路径信息** @param paths 文件路径* @return java.net.URL[]* @author Cyber* <p> Created by 2022/11/22*/public static URL[] getURLs(String[] paths) {if (null == paths || 0 == paths.length) {throw new RuntimeException("jar包路径不能为空.");}List<String> dirs = new ArrayList<String>();for (String path : paths) {dirs.add(path);collectDirs(path, dirs);}List<URL> urls = new ArrayList<URL>();for (String path : dirs) {urls.addAll(doGetURLs(path));}URL[] threadLocalurls = urls.toArray(new URL[0]);return threadLocalurls;}/*** description 递归获取文件目录下的根目录** @param path 文件路径* @param collector 根目录* @return void* @author Cyber* <p> Created by 2022/11/22*/private static void collectDirs(String path, List<String> collector) {if (null == path || "".equalsIgnoreCase(path)) {return;}File current = new File(path);if (!current.exists() || !current.isDirectory()) {return;}for (File child : current.listFiles()) {if (!child.isDirectory()) {continue;}collector.add(child.getAbsolutePath());collectDirs(child.getAbsolutePath(), collector);}}private static List<URL> doGetURLs(final String path) {if (null == path || "".equalsIgnoreCase(path)) {throw new RuntimeException("jar包路径不能为空.");}File jarPath = new File(path);if (!jarPath.exists() || !jarPath.isDirectory()) {throw new RuntimeException("jar包路径必须存在且为目录.");}FileFilter jarFilter = new FileFilter() {/*** description 判断是否是jar文件* @param pathname jar 全路径文件* @return boolean* @author Cyber* <p> Created by 2022/11/22*/@Overridepublic boolean accept(File pathname) {return pathname.getName().endsWith(".jar");}};File[] allJars = new File(path).listFiles(jarFilter);List<URL> jarURLs = new ArrayList<URL>(allJars.length);for (int i = 0; i < allJars.length; i++) {try {jarURLs.add(allJars[i].toURI().toURL());} catch (Exception e) {throw new RuntimeException("系统加载jar包出错", e);}}return jarURLs;}
}
kafka发送基础类
@Slf4j
public abstract class AbstractKafkaProducer {private String kafkaClassName = "org.apache.kafka.clients.producer.KafkaProducer";private String stringSerializerClassName = "org.apache.kafka.common.serialization.StringSerializer";private String serializerClassName = "org.apache.kafka.common.serialization.Serializer";private String producerRecordClassName = "org.apache.kafka.clients.producer.ProducerRecord";private MiddlewareClassLoader middlewareClassLoader;private Object producerObject = null;private Method sendMethod = null;private Constructor producerRecordConstructor = null;//类的加载和初始化public void init(String jarPath){middlewareClassLoader = new MiddlewareClassLoader(new String[]{jarPath});try {ClassLoader threadClassLoader = Thread.currentThread().getContextClassLoader();Thread.currentThread().setContextClassLoader(middlewareClassLoader);Class kafkaProduceClazz = middlewareClassLoader.loadClass(kafkaClassName);Class kafkaStringSerializerClazz = middlewareClassLoader.loadClass(stringSerializerClassName);Class kafkaSerializerClazz = middlewareClassLoader.loadClass(serializerClassName);//加载KafkaProducer类Class ProducerRecordClazz = middlewareClassLoader.loadClass(producerRecordClassName);Constructor producerConstructor = kafkaProduceClazz.getConstructor(Map.class, kafkaSerializerClazz, kafkaSerializerClazz);Map<String,Object> produceConfigMap = new HashMap<String,Object>();produceConfigMap.put("retries",3);produceConfigMap.put("retry.backoff.ms",10000);produceConfigMap.put("acks","all");//回调方法,让自来可更改生产端配置addExtendConfig(produceConfigMap);producerObject = producerConstructor.newInstance(produceConfigMap,kafkaStringSerializerClazz.newInstance(),kafkaStringSerializerClazz.newInstance());sendMethod = kafkaProduceClazz.getMethod("send",new Class[]{ProducerRecordClazz});producerRecordConstructor = ProducerRecordClazz.getConstructor(String.class,Object.class);Thread.currentThread().setContextClassLoader(threadClassLoader);System.out.println("========end======");} catch (Exception e) {e.printStackTrace();}}protected abstract void addExtendConfig(Map<String, Object> produceConfigMap);//发送消息public void send(String topic,String msg){try {sendMethod.invoke(producerObject,producerRecordConstructor.newInstance(topic,msg));} catch (Exception e) {throw new RuntimeException(e);}}
}
Kafka09Producer
@Data
public class Kafka09Producer extends AbstractKafkaProducer {private String bootstrapServers;@Overrideprotected void addExtendConfig(Map<String, Object> produceConfigMap) {produceConfigMap.put("bootstrap.servers",bootstrapServers);}}
Kafka32Producer
@Data
public class Kafka32Producer extends AbstractKafkaProducer {private String bootstrapServers;@Overrideprotected void addExtendConfig(Map<String, Object> produceConfigMap) {produceConfigMap.put("bootstrap.servers",bootstrapServers);}}
核心测试代码
@Slf4j
public class MiddlewareClassLoaderTest {public static void main(String[] args) throws InterruptedException {//kafka 0.9的测试Kafka09Producer kafka09Producer = new Kafka09Producer();kafka09Producer.setBootstrapServers(KafkaConfig.getInstance().getProperty("bootstrap.servers"));kafka09Producer.init("/data/app/product-kafka/ext-lib/kafka090");kafka09Producer.send("topic090","msg090:" + System.currentTimeMillis());//kafka 3.2的测试Kafka32Producer kafka32Producer = new Kafka32Producer();kafka32Producer.setBootstrapServers(KafkaConfig.getInstance().getProperty("bootstrap.servers"));kafka32Producer.init("/data/app/product-kafka/ext-lib/kafka32");kafka32Producer.send("topic320","msg320:" + System.currentTimeMillis());Thread.sleep(60 * 1000);}
}
测试结果
kafka09类的加载
kafka32类的加载
总结
ClassLoader除了能加载指定版本jar包外;还可以做热部署和热更新;如果要再次加载同一个类达到热更新;可 new一个classLoader然后loadClass,再用该Class去实例化对象即可。
还有一个困扰新手较久的注意点:Class的加载和Object实例化需要分开去看待,ClassLoader只影响类的加载;类的实例化是另外一个问题。
原创不易,请点赞,留言,关注,收藏 4暴击 ^^
参考资料:
https://blog.csdn.net/briblue/article/details/54973413 ClassLoader类加载机制,类加载顺序
https://juejin.cn/post/7168678691839410213 Cyber365大佬的文章
相关文章:
![](https://img-blog.csdnimg.cn/img_convert/f702ed9c5ac9be14c284482a50e3c81f.png)
中间件多版本冲突的4种解决方案和我们的选择
背景 在小小的公司里面,挖呀挖呀挖。最近又挖到坑里去了。一个稳定运行多年的应用,需要在里面支持多个版本的中间件客户端;而多个版本的客户端在一个应用里运行时会有同名类冲突的矛盾。在经过询问chatGPT,百度,googl…...
![](https://www.ngui.cc/images/no-images.jpg)
对 async/await 的理解
async/await 的理解 async/await 其实是 Generator 的语法糖,它能实现的效果都能用then 链来实现,它是为优化 then 链而开发出来的。从字面上来看,async 是“异步”的简写,await 则为等待,所以很好理解async用于申明一…...
![](https://img-blog.csdnimg.cn/57c97dbee5214e998f14ba59d90c386c.png)
Vue 整合 Element UI 、路由嵌套、参数传递、重定向、404和路由钩子(五)
一、整合 Element UI 1.1 工程初始化 使用管理员的模式进入 cmd 的命令行模式,创建一个名为 hello-vue 的工程,命令为: # 1、目录切换 cd F:\idea_home\vue# 2、项目的初始化,记得一路的 no vue init webpack hello-vue 1.2 安装…...
![](https://www.ngui.cc/images/no-images.jpg)
修改 Ubuntu 系统的时区
修改 Ubuntu 系统的时区 如果 Ubuntu 系统的时区设置不正确,您可以按照以下步骤进行调整: 1. 查看当前的时区设置,可以使用以下命令: timedatectl 这将显示当前系统的日期、时间和时区信息。 2. 如果时区设置不正…...
![](https://img-blog.csdnimg.cn/2febb709a2924662a41f8a9d8e3c1e24.png)
如何离线安装ModHeader - Modify HTTP headers Chrome插件?
如何离线安装ModHeader - Modify HTTP headers Chrome插件? 1.1 前言1.2 打开Chrome浏览器的开发者模式1.3 下载并解压打包好的插件1.4 解压下载好的压缩包1.5 加载插件1.6 如何使用插件? 1.1 前言 ModHeader 是一个非常好用的Chrome浏览器插件,可以用…...
![](https://img-blog.csdnimg.cn/img_convert/1a8e8823417e2c289df822fd12bb9fb1.png)
在Linux中安装MySQL
在Linux中安装MySQL 检测当前系统中是否安装MySQL数据库 命令作用rpm -qa查询当前系统中安装的所有软件rpm -qa|grep mysql查询当前系统中安装的名称带mysql的软件rpm -qa | grep mariadb查询当前系统中安装的名称带mariadb的软件 RPM ( Red-Hat Package Manager )RPM软件包管理…...
![](https://img-blog.csdnimg.cn/6a9cf64c9ac4475e86a8fbb41dc1fbbb.png)
python --windows获取启动文件夹路径/获取当前用户名/添加自启动文件
如何使用Python获取计算机用户名 一、Python自带的getpass模块可以用于获取用户输入的密码,但是它同样可以用来获取计算机用户名。 import getpassuser getpass.getuser() print("计算机用户名为:", user)二、使用os模块获取用户名 Python的…...
![](https://img-blog.csdnimg.cn/e47bb46aa26145edbe0a645bfdb4f3e4.png)
微信云托管(本地调试)⑥:nginx、vue刷新404问题
一、nginx默认路径 1.1、默认配置文件路径:/etc/nginx/nginx.conf 1.2、默认资源路径:/usr/share/nginx/html/index.html 二、修改nginx.conf配置 (注意配置中的:include /etc/nginx/conf.d/*.conf; 里面包了一个server配置文件…...
![](https://img-blog.csdnimg.cn/81349588eebb45ab83e21455dd63950d.png)
数据结构 二叉树(一篇基本掌握)
绪论 雄关漫道真如铁,而今迈步从头越。 本章将开始学习二叉树(全文共一万两千字),二叉树相较于前面的数据结构来说难度会有许多的攀升,但只要跟着本篇博客深入的学习也可以基本的掌握基础二叉树。 话不多说安全带系好&…...
![](https://www.ngui.cc/images/no-images.jpg)
可视化绘图技巧100篇高级篇(四)-南丁格尔玫瑰图(二)
目录 前言 适用场景 不适用场景 堆积式南丁格尔玫瑰图( Nightingale Rose Diagram)...
![](https://img-blog.csdnimg.cn/853f98a8e57949478498d5f52405a9e0.png)
Stable Diffusion - Candy Land (糖果世界) LoRA 提示词配置与效果展示
欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://spike.blog.csdn.net/article/details/132145248 糖果世界 (Candy Land) 是一个充满甜蜜和奇幻的地方,由各种各样的糖果和巧克力构成。在糖果世界,可以看到&…...
![](https://www.ngui.cc/images/no-images.jpg)
ES6学习-module语法
Module语法 CommonJS模块 let { readfile } require(fs) # 等同于 let _fs require(fs) let readfile _fs.readfile //这种加载称为“运行时加载”ES6模块 import { stat, exists, readFile } from fs;这种加载称为“编译时加载”或者静态加载 静态加载带来的各种好处 …...
![](https://img-blog.csdnimg.cn/40b43cc94aa346f080026fb34c6f0458.gif)
Flutter 实现按位置大小比例布局的控件
文章目录 前言一、如何实现?1、数值转成分数2、RowFlexible布局横向3、ColumnFlexible布局纵向 二、完整代码三、使用示例1、基本用法2、四分屏3、六分屏4、八分屏5、九分屏6、414分屏 总结 前言 做视频监控项目时需要需要展示多分屏,比如2x2、3x3、414…...
![](https://img-blog.csdnimg.cn/c9a4526d0ae24ba1bdb9f15971643eec.png#pic_center)
ES6 - 对象新增的一些常用方法
文章目录 1,Object.is()2,Object.asign()3,Object.getOwnPropertyDescriptors()4,Object.setPrototypeOf()和getPrototypeOf()5,Object.keys()、values() 和 entries()6,Object.fromEntries()7,…...
![](https://img-blog.csdnimg.cn/e60a559fc07f4bb6afea55e33f644fb0.png)
半导体存储电路
存储电路 存储单元:只能存储一位数据 寄存器:存储一组数据 存储单元 静态存储单元:包含锁存器和触发器,只要不断电,静态存储单元的状态会一直保持下去。 动态存储单元:利用电容的电荷存储效应来存储数据。…...
![](https://img-blog.csdnimg.cn/02ec15ae43b843bd99bc67d455fb35e7.png)
web前端之CSS操作
文章目录 一、CSS操作1.1 html元素的style属性1.2 元素节点的style属性1.3 cssText属性 二、事件2.1 事件处理程序2.1.1 html事件2.1.2 DOM0事件(适合单个事件)2.1.3 DOM2事件(适合多个事件) 2.2 事件之鼠标事件2.3 事件之Event事…...
![](https://img-blog.csdnimg.cn/e75b7d0ae26f4b47a13ee9f195d9b01e.jpeg)
Python SQLAlchemy ( ORM )
From Python中强大的通用ORM框架:SQLAlchemy:https://zhuanlan.zhihu.com/p/444930067Python ORM之SQLAlchemy全面指南:https://zhuanlan.zhihu.com/p/387078089 SQLAlchemy 文档:https://www.sqlalchemy.org/ SQLAlchemy入门和…...
![](https://img-blog.csdnimg.cn/img_convert/a3da07d86eec32da1b4d82534cb7c129.png)
鉴源实验室丨汽车网络安全运营
作者 | 苏少博 上海控安可信软件创新研究院汽车网络安全组 来源 | 鉴源实验室 社群 | 添加微信号“TICPShanghai”加入“上海控安51fusa安全社区” 01 概 述 1.1 背景 随着车辆技术的不断进步和智能化水平的提升,车辆行业正经历着快速的变革和技术进步。智能化…...
![](https://img-blog.csdnimg.cn/670541e85b624ebb81df71d86f88a07a.png)
分布式链路追踪之SkyWalking详解和实战
SkyWalking 文章目录 SkyWalking1.SkyWalking概述2.SkyWalking架构设计3.SkyWalking部署4.应用程序接入SkyWalking5.SkyWalking配置应用告警5.1.告警规则5.2.Webhook(网络钩子)5.3.邮件告警实践 6.项目自动化部署接入SkyWalking6.1 整体思路6.2 启动参数…...
![](https://img-blog.csdnimg.cn/b579a56f62774eb48f03fce6a8b32a14.png)
【工程实践】使用EDA(Easy Data Augmentation)做数据增强
工程项目中,由于数据量不够,经常需要用到数据增强技术,尝试使用EDA进行数据增强。 1.EDA简介 EDA是一种简单但是非常有效的文本数据增强方法,是由美国Protago实验室发表于 EMNLP-IJCNLP 2019 会议。EDA来自论文《EDA: Easy Data…...
![](https://img-blog.csdnimg.cn/bcef973f63404640ae55a9c59afce8c0.jpeg)
ClickHouse(十三):Clickhouse MergeTree系列表引擎 - ReplicingMergeTree
进入正文前,感谢宝子们订阅专题、点赞、评论、收藏!关注IT贫道,获取高质量博客内容! 🏡个人主页:含各种IT体系技术,IT贫道_Apache Doris,大数据OLAP体系技术栈,Kerberos安全认证-CSDN博客 &…...
![](https://img-blog.csdnimg.cn/92dba3b4570f448dbc11c946153ff33b.png#pic_center)
机器学习笔记之优化算法(十)梯度下降法铺垫:总体介绍
机器学习笔记之优化算法——梯度下降法铺垫:总体介绍 引言回顾:线搜索方法线搜索方法的方向 P k \mathcal P_k Pk线搜索方法的步长 α k \alpha_k αk 梯度下降方法整体介绍 引言 从本节开始,将介绍梯度下降法 ( Gradient Descent,GD ) …...
![](https://img-blog.csdnimg.cn/879c01392cfe4421a2b8be8d3d8d2c00.png)
Selenium 根据元素文本内容定位
使用xpath定位元素时,有时候担心元素位置会变,可以考虑使用文本内容来定位的方式。 例如图中的【股市】按钮,只有按钮文本没变,即使位置变化也可以定位到该元素。 xpath内容样例: # 文本内容完全匹配 //button[text(…...
![](https://img-blog.csdnimg.cn/b06a401d50e945758cb37a253e491e39.png#pic_center)
第17章-Spring AOP经典应用场景
文章目录 一、日志处理二、事务控制三、参数校验四、自定义注解五、AOP 方法失效问题1. ApplicationContext2. AopContext3. 注入自身 六、附录1. 示例代码 AOP 提供了一种面向切面操作的扩展机制,通常这些操作是与业务无关的,在实际应用中,可…...
![](https://img-blog.csdnimg.cn/492f4713a86f464888227ea518c553c4.png#pic_center)
Leetcode周赛 | 2023-8-6
2023-8-6 题1体会我的代码 题2我的超时代码题目体会我的代码 题3体会我的代码 题1 体会 这道题完全就是唬人,只要想明白了,只要有两个连续的数的和,大于target,那么一定可以,两边一次切一个就好了。 我的代码 题2 我…...
![](https://img-blog.csdnimg.cn/725c3f502fc744b2b0a53249502e7c5c.png)
ts中interface自定义结构约束和对类的约束
一、interface自定义结构约束对后端接口返回数据 // interface自定义结构 一般用于较复杂的结构数据类型限制 如后端返回的接口数据// 首字母大写;用分割号隔开 interface Iobj{a:number;b:string } let obj:Iobj {a:1,b:2 }// 复杂类型 模拟后端返回的接口数据 interface Il…...
![](https://img-blog.csdnimg.cn/7c7295fd8ee342ccb45dffdbfdf4b48a.png)
Oracle单实例升级补丁
目录 1.当前DB环境2.下载补丁包和opatch的升级包3.检查OPatch的版本4.检查补丁是否冲突5.关闭数据库实例,关闭监听6.应用patch7.加载变化的SQL到数据库8.ORACLE升级补丁查询 oracle19.3升级补丁到19.18 1.当前DB环境 [oraclelocalhost ~]$ cat /etc/redhat-releas…...
![](https://img-blog.csdnimg.cn/fa53e33f57ee4d46ac3cb4b3e0a4806f.png)
力扣初级算法(二分查找)
力扣初级算法(二分法): 每日一算法:二分法查找 学习内容: 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 2.二分查找流程&…...
![](https://img-blog.csdnimg.cn/2f226454f7ad401aa47ce88e80657341.png#pic_center)
探索未来:直播实时美颜SDK在增强现实(AR)直播中的前景
在AR直播中,观众可以与虚拟元素实时互动,为用户带来更加丰富、沉浸式的体验。那么,直播美颜SDK在AR中有哪些应用呢?下文小编将于大家一同探讨美颜SDK与AR有哪些关联。 一、AR直播与直播实时美颜SDK的结合 增强现实技术在直播中…...
![](https://www.ngui.cc/images/no-images.jpg)
SQL 单行子查询 、多行子查询、单行函数、聚合函数 IN 、ANY 、SOME 、ALL
单行子查询 子查询结果是 一个列一行记录 select a,b,c from table where a >(select avg(xx) from table ) 还支持这种写法,这种比较少见 select a,b,c from table where (a ,b)(select xx,xxx from table where col‘000’ )…...
![](/images/no-images.jpg)
吉林网络推广代运营/优化教程网下载
本文介绍的是如何用 Python 语言实现 12306 自动预定列车票,也就是坊间常说的“抢票”,但个人觉得,这不算是“抢”,只不过是一定程度的自动化。总体设计所谓抢票软件,本质上就是基于浏览器驱动,实现登录、预…...
![](https://img-blog.csdnimg.cn/20200204212201554.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2h5b25naWxmbW1t,size_16,color_FFFFFF,t_70)
wordpress文章编辑函数/企业营销策略分析论文
Qt布局管理(3):内容边距(ContentsMargins)、间距(spacing)和QSpacerItem类 本文为原创文章,转载请注明出处,或注明转载自“黄邦勇帅(原名:黄勇) 本文出自本人原创著作《Qt5.10 GUI完全参考手册》网盘地址: https://p…...
![](https://upload-images.jianshu.io/upload_images/17040667-1eccd9c4931630ac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
做个人网站用什么程序/百度seo 优化
公众号有朋友留言说,不知道自己如何申请微信小程序?下面给大家分享一下自己申请微信小程序的流程,以及后期开发的注意事项。 微信小程序申请开发流程及注意事项 一、微信小程序的申请流程及注意事项 1>申请微信小程序的流程 注意请在电脑端…...
![](https://img-blog.csdnimg.cn/img_convert/f60362ef03ca169caa4ebeb1f7167217.png)
富阳区建设局网站首页/淘宝指数
▼电脑端zoom操作指引▼1、电脑端用户在哪里下载zoom?2、电脑端zoom如何加入会议?3、进入zoom后发现忘记填写正确上课名称,怎么改名?4、如何看到主讲老师或其他参会者?5、听不到声音,怎么办?6、其他注意事…...
![](/images/no-images.jpg)
网站开发4k分辨率/星链seo管理
一、概述 分表是个目前算是比较炒的比较流行的概念,特别是在大负载的情况下,分表是一个良好分散数据库压力的好方法。 首先要了解为什么要分表,分表的好处是什么。为什么要分DB文件,分DB文件的好处?分DB文件的好处是…...
![](/images/no-images.jpg)
类似5173的网站怎么做/如何快速提升自己
一、数组:同一个类型数据的集合,其实他也是一个容器 1、数组的好处:可以自动给数组中的元素从0开始编号,方便操作这些数据 2、数组的定义: 在Java中常见: 格式1: 类型 [] 数组名 new 类型[数组…...