Tomcat源码分析-类加载器
类加载器
在分析 tomcat 类加载之前,我们简单的回顾下 java 体系的类加载器
- 启动类加载器(Bootstrap ClassLoader):加载对象是java的核心类库,把一些的 java 类加载到 jvm 中,它并不是我们熟悉的
ClassLoader
,而是 jvm 层面由 C/C++ 实现的类加载器,负责加载 $JAVA_HOME/jre/lib 目录下 jvm 指定的类库,它是无法被 java 应用程序直接使用的 - 扩展类加载器(Extension Classloader):它是一个 ClassLoader 实例,父加载器是启动类加载器,它负责加载 $JAVA_HOME/jre/lib/ext 目录的类库
- 应用类加载器(Application ClassLoader):又叫做系统类加载器(System ClassLoader),负责加载用户类路径(-cp参数)指定的类库,可以通过
ClassLoader.getSystemClassLoader()
获取,它也是由启动类加载器加载的 - 自定义类加载器:应用程序根据自己的需求开发的类加载器,可以继承
ClassLoader
,当然也可以不继承
下图描述了类加载器的关系图,其中自定义类加载器有N多个
我们知道 java.lang.ClassLoader
有双亲委派机制(准确的说是单亲,因为只有一个parent),这只是 java 建议的规范,我们也可以不遵循这条规则,但是建议遵循该规则。此外,有一点需要注意的是,类加载器不局限于 ClassLoader
,我们也可以自己实现一个类加载器,只要你加载出来的 Class 符合 jvm 规范即可
我们在日常开发工作中,经常会遇到类冲突的情况,明明 classpath 下面的类有这个方法,但是一旦跑线上环境就出错,比如NoSuchMethodError
、NoClassDefFoundError
、NoClassDefFoundError
等。我们可以使用 jvm 参数 -verbose:class
方便地定位该问题,使用该参数可以快速地定位某个类是从哪个jar包加载的,而不是一味地埋头苦干,求百度,找Google。下面是使用 -verbose:class
jvm 参数的部分日志输出
[Loaded org.springframework.context.annotation.CommonAnnotationBeanPostProcessor from file:/D:/tomcat/webapps/touch/WEB-INF/lib/spring-context-4.3.7.RELEASE.jar]
[Loaded com.alibaba.dubbo.rpc.InvokerListener from file:/D:/tomcat/webapps/touch/WEB-INF/lib/dubbo-2.5.3.jar]
我们有必要了解下关于类加载有几个重要的知识点:
- 在 Java 中我们用完全类名来标识一个类,而在 JVM 层面,使用完全类名 + CloassLoader 对象实例 ID 作为唯一标识,因此使用不同实例的类加载器,加载的两个同名的类,他们的类实例是不同的,并且不能强制转换
- 在双亲委派机制中,类加载器查找类时,是一层层往父类加载器查找的,最后才查看自己,如果都找不到则会抛出异常,而不是一层层往下找的
- 每个运行中的线程都有一个
CloassLoader
,并且会从父线程中继承(默认是应用类加载器),在没有显式声明由哪个类加载器加载类时(比如 new 关键字),将默认由当前线程的类加载器加载该类
由于篇幅有限,关于类加载的过程这里不再展开了,可以参考厮大的博客
- Java虚拟机类加载机制:Java虚拟机类加载机制_朱小厮的博客-CSDN博客_加载、验证、准备、初始化、和卸载这5个阶段的顺序是确定的,类的加载
tomcat 类加载器
根据实际的应用场景,我们来分析下 tomcat 类加载器需要解决的几个问题
- 为了避免类冲突,每个 webapp 项目中各自使用的类库要有隔离机制
- 不同 webapp 项目支持共享某些类库
- 类加载器应该支持热插拔功能,比如对 jsp 的支持、webapp 的 reload 操作
为了解决以上问题,tomcat设计了一套类加载器,如下图所示。在 Tomcat 里面最重要的是 Common 类加载器,它的父加载器是应用程序类加载器,负责加载 ${catalina.base}/lib
、${catalina.home}/lib
目录下面所有的 .jar 文件和 .class 文件。下图的虚线部分,有 catalina 类加载器、share 类加载器,并且它们的 parent 是 common 类加载器,默认情况下被赋值为 Common 类加载器实例,即 Common 类加载器、catalina 类加载器、 share 类加载器都属于同一个实例。当然,如果我们通过修改 catalina.properties
文件的 server.loader
和 shared.loader
配置,从而指定其创建不同的类加载器
我们先从 Bootstrap
这个入口说起,在执行 init
的时候会实例化类加载器,在初始化类加载器之后立即设置线程上下文类加载器(Thread Context ClassLoader)为 catalina 类加载器,接下来是为 Catalina
组件指定父类加载器。为什么要设置线程上下文的类加载器呢?一方面,很多诸如 ClassUtils
之类的编码,他们在获取 ClassLoader
的时候,都是先尝试从 Thread 上下文中获取 ClassLoader
,例如:ClassLoader cl = Thread.currentThread().getContextClassLoader();
另一方面,在没有显式指定类加载器的情况下,默认使用线程的上下文类加载器加载类,由于 tomcat 的大部分 jar 包都在 ${catalina.hom}/lib
目录,因此需要将线程类加载器指定为 catalina 类加载器,否则加载不了相关的类。
双亲委派模型存在设计上的缺陷,在某些应用场景下,例如加载 SPI 实现(JNDI、JDBC等),如果我们严格遵循双亲委派的一般性原则,使用应用程序类加载器,由于这些 SPI 实现在厂商的 jar 包中,所以应用程序类加载器不可能认识这些代码啊,怎么办?为了解决这个问题,Java 设计团队引入了一个不太优雅的设计:Thread Context ClassLoader
,有了这个线程上下文类加载器,我们便可以做一些“舞弊”的事情了,JNDI 服务可以使用这个类加载器加载 SPI 需要的代码,JDBC、JAXB 也是如此。这样,双亲委派模型便被破坏了。
Bootstrap.java
public void init() throws Exception {// 初始化commonLoader、catalinaLoader、sharedLoader,关于ClassLoader的后面再看initClassLoaders();// 设置上下文类加载器为 catalinaLoader Thread.currentThread().setContextClassLoader(catalinaLoader);SecurityClassLoad.securityClassLoad(catalinaLoader);// 反射方法实例化Catalina,后面初始化Catalina用了很多反射,不知道意图是什么Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");Object startupInstance = startupClass.getConstructor().newInstance();//TODO 为Catalina对象设置其父加载器为shared类加载器,默认情况下就是catalina类加载器// 引用Catalina实例catalinaDaemon = startupInstance;
catalina.properties 文件的相关配置如下所示
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
server.loader=
我们再来看下创建类加载器的代码,首先是创建 common 类加载器,从 catalina.properties
中读取 common.loader
配置作为 common 类加载器的路径。我们注意到 common.loader
中存在 ${catalina.base}
、${catalina.home}
这样的占位符,在读取配置之后,tomcat 会进行替换处理,同理 server.loader
、shared.loader
也可以使用这样的占位符,或者系统变量作为占位符,有兴趣的童鞋可以参考下 Bootstrap.replace(String str)
源码,如果在项目中有相同的场景的话,可以直接 copy 该代码。
Bootstrap.javaprivate void initClassLoaders() {try {// 从catalina.properties中读取common.loader配置作为common类加载的路径commonLoader = createClassLoader("common", null);if( commonLoader == null ) {commonLoader=this.getClass().getClassLoader();}// 如果未指定server.loader和shared.loader配置,则catalina和shared类加载器都是common类加载器catalinaLoader = createClassLoader("server", commonLoader);sharedLoader = createClassLoader("shared", commonLoader);} catch (Throwable t) {handleThrowable(t);log.error("Class loader creation threw exception", t);System.exit(1);}
接下来,我们再来看下 tomcat 是如何创建 common 类加载器的。关键代码如下所示,在创建类加载器时,会读取相关的路径配置,并把路径封装成 Repository
对象,然后交给 ClassLoaderFactory
创建类加载器。
Bootstrap.java
private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {// 从catalina.propeties中读取配置,并替换 catalina.home、或者catalina.base,或者环境变量String value = CatalinaProperties.getProperty(name + ".loader");value = replace(value);// 遍历目录,并对路径进行处理List<Repository> repositories = new ArrayList<>();String[] repositoryPaths = getPaths(value);for (String repository : repositoryPaths) {//TODO 将路径封装成 Repository 对象}return ClassLoaderFactory.createClassLoader(repositories, parent);
我们再进一步对 ClassLoaderFactory
进行分析,都是细节上的处理,比如利用文件路径构造带有明显协议的 URL 对象,例如本地文件的标准 URL 是 file:/D:/app.jar
。另外,在创建 URLClassLoader
的时候还需要考虑 jdk 对权限控制的影响,因此 tomcat 利用 AccessController
创建 URLClassLoader
,由此可见 tomcat 编码的严谨性。而我们在实际的开发过程中,有时候需要自定义类加载器,但往往不会考虑权限控制这块,所以在对类加载器进行编码时需要注意一下
ClassLoaderFactory.java
public static ClassLoader createClassLoader(List<Repository> repositories, final ClassLoader parent)throws Exception {Set<URL> set = new LinkedHashSet<>();if (repositories != null) {for (Repository repository : repositories) {// 对不同类型的 Repository 对象进行处理,将路径转换为URL类型// 因为 URL 类型带有明显的协议,比如jar:xxx、file:xxx}}// 将对应的路径组装成 URLfinal URL[] array = set.toArray(new URL[set.size()]);// 在创建 URLClassLoader 需要考虑到 AccessController 的影响return AccessController.doPrivileged(new PrivilegedAction<URLClassLoader>() {public URLClassLoader run() {if (parent == null) return new URLClassLoader(array);else return new URLClassLoader(array, parent);}});
}private static URL buildClassLoaderUrl(File file) throws MalformedURLException {String fileUrlString = file.toURI().toString();fileUrlString = fileUrlString.replaceAll("!/", "%21/"); // 转换成URL编码return new URL(fileUrlString);
OK,前面介绍了 tomcat 创建类加载器的过程,接下来我们看下 tomcat 类加载器的具体应用场景
WebappClassLoader
在前面,我们介绍了 tomcat 类加载器的设计,每个 webapp 使用单独的类加载器完成我们开发的 webapp 应用程序的类加载,而每一个 webapp 对应一个 WebappClassLoader
。tomcat7 默认使用 WebappClassLoader
类加载器,而 tomcat8 默认使用 ParallelWebappClassLoader
,支持并行加载类的特性,这也算是 tomcat8 做的一些优化吧,而实际上也是利用 jdk 的功能,需要同时满足以下两点才支持并行加载类,并且一旦注册了并行加载的能力,就不能回退了
1、 没有创建调用者的实例
2、 调用者的所有超类(除了类对象)都是并行注册的
基于上面两点,因此,ParallelWebappClassLoader
在 static 代码块中注册并行加载机制,而它的父类 URLClassLoader
父类也是具有并行能力的,关键代码如下所示:
public class ParallelWebappClassLoader extends WebappClassLoaderBase {static {boolean result = ClassLoader.registerAsParallelCapable();if (!result) {log.warn(sm.getString("webappClassLoaderParallel.registrationFailed"));}}// 省略无关代码...
WebappClassLoader
的类图如下所示,其中 WebappClassLoaderBase
实现了主要的逻辑,并且继承了 Lifecycle
,在 tomcat 组件启动、关闭时会完成资源的加载、卸载操作,例如在 start
过程会读取我们熟悉的 /WEB-INF/classes
、/WEB-INF/lib
资源,并且记录每个 jar 包的时间戳方便重载 jar 包;而在组件 stop
的时候,会清理已经加载的资源;destory
时会显式地触发 URLClassLoader.close()
。这个 Lifecycle
真是无处不在啊
单独的类加载器是无法获取 webapp 的资源信息的,因此 tomcat 引入了 WebappLoader
,便于访问 Context
组件的信息,同时为 Context
提供类加载的能力支持,下面我们分析下 WebappLoader
的底层实现
WebappLoader
我们先来看看 WebappLoader
几个重要的属性,内部持有 Context
组件,并且有个我们熟悉的 reloadable
参数,如果设为 true,则会开启类的热加载机制
public class WebappLoader extends LifecycleMBeanBaseimplements Loader, PropertyChangeListener {private WebappClassLoaderBase classLoader = null; // 默认使用ParallelWebappClassLoaderprivate Context context = null;private String loaderClass = ParallelWebappClassLoader.class.getName();private ClassLoader parentClassLoader = null; // 父加载器,默认为 catalina 类加载器private boolean reloadable = false; // 是否支持热加载类private String classpath = null;
在 tomcat 中,每个 webapp 对应一个 StandardContext
,在 start
过程便会实例化 WebappLoader
,并且调用其 start
方法完成初始化,包括创建 ParallelWebappClassLoader
实例,然后,还会启动 Context
的子容器。注意,这两个过程,都会将线程上下文类加载器指定为 ParallelWebappClassLoader
类加载器,在完成 webapp 相关的类加载之后,又将线程上下文类加载器设置为 catalina 类加载器。Context
容器的启动过程,这里便不再重复了,感兴趣的童鞋请查看前面的博文 webapp源码分析
StandardContext.javaprotected synchronized void startInternal() throws LifecycleException {// 实例化 Loader 实例,它是 tomcat 对于 ClassLoader 的封装,用于支持在运行期间热加载 class if (getLoader() == null) {WebappLoader webappLoader = new WebappLoader(getParentClassLoader());webappLoader.setDelegate(getDelegate());setLoader(webappLoader); // 使用了读写锁控制并发问题}// 将 Loader 中的 ParallelWebappClassLoader 绑定到当前线程中,并返回 catalian 类加载器ClassLoader oldCCL = bindThread();try {if (ok) {// 如果 Loader 是 Lifecycle 实现类,则启动该 LoaderLoader loader = getLoader();if (loader instanceof Lifecycle) {((Lifecycle) loader).start();}// 设置 ClassLoader 的各种属性setClassLoaderProperty("clearReferencesRmiTargets", getClearReferencesRmiTargets());// 省略……// 解除线程上下文类加载器绑定unbindThread(oldCCL);oldCCL = bindThread();// 发出 CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,主要目的是加载 Context 的子容器fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);// 启动子容器for (Container child : findChildren()) {if (!child.getState().isAvailable()) {child.start();}}}} finally {// Unbinding threadunbindThread(oldCCL);}
而 WebappLoader
在 stop
的时候,会销毁 WebappClassLoader
,并且进行回收,促使 jvm 卸载已加载的类
WebappLoader.java@Override
protected void stopInternal() throws LifecycleException {// 省略不相关代码...if (classLoader != null) {try {classLoader.stop();} finally {classLoader.destroy();}}classLoader = null; // help gc
相关文章:

Tomcat源码分析-类加载器
类加载器 在分析 tomcat 类加载之前,我们简单的回顾下 java 体系的类加载器 启动类加载器(Bootstrap ClassLoader):加载对象是java的核心类库,把一些的 java 类加载到 jvm 中,它并不是我们熟悉的 ClassLoader&#x…...

MySQL进阶篇之视图/存储过程/触发器
今天我们主要来快速学习视图,存储过程,触发器四个方面的内容,一起加油学习吧,还有半年就有秋招了,要加快速度了,迫在眉睫,冲吧,兄弟们。 目录 1、视图 2、存储过程 3、存储函数 4、…...

【一看就会】实现仿京东移动端页面滚动条布局
简单粗暴直接上代码: <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta http-equiv"X-UA-Compatible" content"IEedge"> <meta name"viewport" content&q…...

网易的“草长莺飞二月天”:增长稳健,加码研发,逐浪AI
2月23日,网易发布了2022年第四季度财报。 这是网易与暴雪分道扬镳后的首份财报,加上近期AIGC热度扩散至游戏、教育等各个领域,网易第四季度业绩及其对于GPT等热门技术的探索受到市场关注。 根据财报,第四季度,网易营…...

NPC内网穿透教程-入门
安装 安装包安装 releases下载 下载对应的系统版本即可,服务端和客户端是单独的 源码安装 安装源码 go get -u ehang.io/nps 编译 服务端go build cmd/nps/nps.go 客户端go build cmd/npc/npc.go docker安装 server安装说明 client安装说明 启动 服务端 下…...

【Linux修炼】14.磁盘结构/文件系统/软硬链接/动静态库
每一个不曾起舞的日子,都是对生命的辜负。 磁盘结构/文件系统/软硬链接/动静态库前言一.磁盘结构1.1 磁盘的物理结构1.2 磁盘的存储结构1.3 磁盘的逻辑结构二.理解文件系统2.1 对IO单位的优化2.2 磁盘分区与分组2.3 分组的管理方法2.4 文件操作三.软硬链接3.1理解硬…...

Spring源码分析:创建 BeanDefinition 流程
一、前期准备1.1 环境依赖<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.1.7.RELEASE</version></dependency><dependency><groupId&…...

Linux 练习一(思维导图 + 练习过程)
文章目录一、Linux 用户管理及文件操作第一段练习记录:主要对用户进行删除添加设置密码等操作第二段练习记录:主要包括权限设置和查找命令第三段练习记录:关于文件的命令练习第四段练习记录:查找命令及查看内存命令的使用二、Linu…...

高德地图基础教程超详细版
在当前社会,对于地图的使用是很必须的,所以对于程序员来说也是需要掌握的技能,目前主流的又百度地图和高德地图,但是我建议使用高德地图,因为百度地图的API着实不好用吖,不好理解,对于开发人员来…...

基于A7核开发板的串口实现控制LED亮灭
1.通过操作Cortex-A7核,串口输入相应的命令,控制LED灯进行工作 1>例如在串口输入led1on,开饭led1灯点亮 2>例如在串口输入led1off,开饭led1灯熄灭 3>例如在串口输入led2on,开饭led2灯点亮 4>例如在串口输入led2off,开饭led2灯熄灭 5>例如…...

HyperGBM用Adversarial Validation解决数据漂移问题
本文作者:杨健,九章云极 DataCanvas 主任架构师 数据漂移问题近年在机器学习领域来越来越得到关注,成为机器学习模型在实际投产中面对的一个主要挑战。当数据的分布随着时间推移逐渐发生变化,需要预测的数据和用于训练的数据分布…...

关基系统三月重保安全监测怎么做?ScanV提供纯干货!
三月重保当前,以政府、大型国企央企、能源、金融等重要行业和领域为代表的关键信息基础设施运营单位都将迎来“网络安全大考”。 对重要关基系统进行安全风险监测并收敛暴露面,响应监管要求进行安全加固,重保期间实时安全监测与数据汇报等具体…...

RK3588关键电路 PCB Layout设计指南
1、音频接口电路 PCB 设计(1)所有 CLK 信号建议串接 22ohm 电阻,并靠近 RK3588 放置,提高信号质量;(2)所有 CLK 信号走线不得挨在一起,避免串扰;需要独立包地,…...

二分边界详细总结
一、查找精确值 从一个有序数组中找到一个符合要求的精确值(如猜数游戏)。如查找值为Key的元素下标,不存在返回-1。 //这里是left<right。 //考虑这种情况:如果最后剩下A[i]和A[i1](这也是最容易导致导致死循环的…...

STM32---备份寄存器BKP和 FLASH学习使用
BKP库函数 学习BKP,首先就是知道BKP每一个函数的作用然后如何使用即可 使用备份域的作用只需要操作上面的两个函数即可,其余的都是它的其他功能 BKP简介 备份寄存器是42个16位的寄存器,可用来存储84个字节的用户应用程序数据。他们处在备份…...

Python-生成元组和字典
1.生成元组元组是元素按顺序组合后的产物,元组对象的类型是tuple型含有两个元素的元组成为数据对元组可以包含任意数量和任意类型的元素,其元素总数可以为0、1、2等,并且元素的先后顺序是由意义的。另外,元组中的元素类型没有必要…...

I.MX6ULL内核开发10:设备树
目录 一、设备树简介 二、设备树源码 三、获取设备树信息 1、增加设备节点 2、内核编译设备树 3、替换设备树文件 4、查看设备树节点 5、在驱动中获取节点的属性 6、编译驱动模块 7、加载模块 一、设备树简介 设备树的作用是描述一个硬件平台的硬件资源。这个“设备树…...

【大数据】记一次hadoop集群missing block问题排查和数据恢复
问题描述 集群环境总共有2个NN节点,3个JN节点,40个DN节点,基于hadoop-3.3.1的版本。集群采用的双副本,未使用ec纠删码。 问题如下: bin/hdfs fsck -list-corruptfileblocks / The list of corrupt files under path…...

国产音质好的蓝牙耳机有哪些?国产音质最好的耳机排行
随着时间的推移,真无线蓝牙耳机逐渐占据耳机市场的份额,成为人们日常生活中必备的数码产品之一。蓝牙耳机品牌也多得数不胜数,哪些国产蓝牙耳机音质好?下面,我们从音质出来,来给大家介绍几款国产蓝牙耳机&a…...

CTFer成长之路之XSS的魔力
XSS的魔力CTF XSS闯关 题目描述: 你能否过关斩将解决所有XSS问题最终获得flag呢? docker-compose.yml version: "3.2"services:xss:image: registry.cn-hangzhou.aliyuncs.com/n1book/web-xss:latestports:- 3000:3000启动方式 docker-compose up -…...

行锁、表锁、主键外键、表之间的关联关系
Java知识点总结:想看的可以从这里进入 目录2.4、行锁、表锁2.5、主键、外键2.5.1、主键2.5.2、外键2.6、表的关联关系2.4、行锁、表锁 MyISAM默认采用表级锁,InnoDB默认采用行级锁。 表锁:开销小,加锁快,不会出现死锁…...

JavaScript 进阶(面试必备)--charater4
文章目录前言一、深浅拷贝:one: 浅拷贝:two:深拷贝二、异常处理:one: throw 抛异常:two: try /catch 捕获异常:three:debugger三、处理thisthis指向 :one:普通函数this指向this指向 :two: 箭头函数this指向3.2 改变this:one: call():two: apply():three: bind()四、性能优化:on…...

ARM+FPGA架构开发板PCIE2SCREEN示例分析与测试-米尔MYD-JX8MMA7
本篇测评由电子发烧友的优秀测评者“zealsoft”提供。 本次测试内容为米尔MYD-JX8MMA7开发板其ARM端的测试例程pcie2screen并介绍一下FPGA端程序的修改。 01. 测试例程pcie2screen 例程pcie2screen是配合MYD-JX8MMA7开发板所带的MYIR_PCIE_5T_CMOS 工程的测试例&#…...

51单片机入门 - SDCC / Keil_C51 会让没有调用的函数参与编译吗?
Small Device C Compiler(SDCC)是一款免费 C 编译器,适用于 8 位微控制器。 不想看测试过程的话可以直接划到最下面看结论:) 关于软硬件环境的信息: Windows 10STC89C52RCSDCC (构建HEX文件&…...

OpenCV只含基本图像模块编译
编译OpenCV4.5.5只含基本图像模块,环境为Windows10 x64CMake3.23.3VS2019。默认编译选项编译得到的OpenCV库往往大几百MB甚至上GB,本文配置下编译得到的库压缩后得到的zip包大小仅6.25MB,适合使用OpenCV基本图像功能模块的项目移植而不牵涉其…...

Java实现阴历日历表(附带星座)
准备工作 1.无敌外挂(GitHub直达源码) Nobb 直击灵魂 https://github.com/xuyishanBD/Java_create_calendar.git2.maven配置(如果没有走上面的捷径) <dependencies><dependency><groupId>net.sourceforge.javacsv</groupId><artifactId>javac…...

Python入门之最基础
Python入门之最基础 IDLE有两种模式,一种是交互模式,通俗讲就是写一个代码,会得到相应的反馈,另一种为编辑模式. 注意事项: 标点符号一定要用英文符号 要注意缩进 dir(builtins)可以看到python所有的内置函数&#…...

浏览器缓存策略
先走强缓存,再走协商缓存 强缓存 不发送请求,直接使用缓存的内容 状态码200 当前会话没有关闭的话就是走memory cache,否则就是disk cache 由响应头的 Pragma(逐渐废弃,优先级最高),catch-…...

高清无码的MP4如何采集?python带你保存~
前言 大家早好、午好、晚好吖 ❤ ~ 又是我,我又来采集小姐姐啦~ 这次我们采集的网站是(看下图): 本文所有模块\环境\源码\教程皆可点击文章下方名片获取此处跳转 话不多少,我们赶快开始吧~ 第三方模块: requests >>> pip install requests 如果安装python第三方模块…...

python+pytest接口自动化(1)-接口测试基础
接口定义一般我们所说的接口即API,那什么又是API呢,百度给的定义如下:API(Application Programming Interface,应用程序接口)是一些预先定义的接口(如函数、HTTP接口),或…...