当前位置: 首页 > news >正文

SpringBoot:详解Bean生命周期和作用域

在这里插入图片描述

🏡浩泽学编程:个人主页

 🔥 推荐专栏:《深入浅出SpringBoot》《java项目分享》
              《RabbitMQ》《Spring》《SpringMVC》

🛸学无止境,不骄不躁,知行合一

文章目录

  • 前言
  • 一、生命周期
  • 二、作用域
  • 总结


前言

前面我们讲诉了将Bean正确地装配到IoC容器,却未讲诉IoC如何装配和销毁Bean。本篇文章主要讲诉一下Bean的生命周期和作用域。


一、生命周期

  • Bean 的生命周期的过程, 它大致分为Bean定义、Bean 的初始化、 Bean 的生存期和 Bean 的销毁4个部分。 其中 Bean 定义过程大致如下:

    • Spring 通过我们的配置,如@ComponentScan 定义的扫描路径去找到带有@Component 的类,
      这个过程就是一个资源定位的过程。
    • 一旦找到了资源,那么它就开始解析,并且将定义的信息保存起来。注意,此时还没有初始
      化Bean,也就没有Bean 的实例,它有的仅仅是Bean 的定义
    • 然后就会把Bean 定义发布到 Spring IoC 容器中。 此时, IoC 容器也只有Bean 的定义,还是
      没有Bean 的实例生成
  • 完成了这3 步只是一个资源定位并将Bean 的定义发布到IoC容器的过程,还没有Bean实例的生成,更没有完成依赖注入。在默认的情况下, Spring会继续去完成Bean 的实例化和依赖注入,这样从IoC 容器中就可以得到一个依赖注入完成的Bean。 但是,有些Bean会受到变化因素的影响,这时我们倒希望是取出 Bean 的时候完成初始化和依赖注入换句话说就是让那些 Bean 只是将定义发布到IoC 容器而不做实例化和依赖注入, 当我们取出来的时候才做初始化和依赖注入等操作。

Spring Bean的初始化过程:
在这里插入图片描述
ComponentScan 中还有一个配置项 lazyI nit,只可以配置 Boolean 值,且默认值为 false,也就是默认不进行延迟初始化,因此在默认的情况下Spring会对Bean进行实例化和依赖注入对应的属性值

引入例子:人类(Person)有时候利用一些动物(Animal)去完成一些事情,比方说狗(Dog)是用来看门的,猫(Cat)是用来抓老鼠的.。

代码如下:

//定义人类接口
public interface Person {void service();void setAnimal(Animal animal);
}
//定义动物接口
public interface Animal {void user();
}
//定义狗
@Component
public class Dog implements Animal {@Overridepublic void user() {System.out.println("狗【" + Dog.class.getSimpleName() + "】是用来看门的");}
}
//定义年轻人
@Component
public class YoungPerson implements Person {@Autowiredprivate Animal animal = null;@Overridepublic void service() {this.animal.user();}@Overridepublic void setAnimal(Animal animal) {this.animal = animal;}
}
//定义猫
@Component
public class Cat implements Animal{@Overridepublic void user() {System.out.println("猫【" + Cat.class.getSimpleName() + "】是抓老鼠的");}
}//定义配置类
@Configuration
@ComponentScan("com.dragon.restart")//所有的包和类都在restart下
public class AppConfig {
}

此时没有配置lazyInit的情况进行断点测试如下:
在这里插入图片描述
可以看到在断点处,我们并没有获取Bean 的实例,而日志就已经打出了,可见它是在SpringIoC容器初
始化时就执行了实例化和依赖注入。为了改变这个情况,我们在配置类AppConfig的@ComponentScan
中加入lazylnit 配置,如下面的代码:

@Configuration
@ComponentScan(value = "com.dragon.restart",lazyInit = true)
public class AppConfig {
}

在这里插入图片描述
在这里插入图片描述

就可以发现在断点处“延迟依赖注入”这行并不会出现在日志中,只有运行过断点处才会出现这行日志,这是因为我们把它修改为了延迟初始化, Spring并不会在发布Bean定义后马上为我们完成实例化和依赖注入。

如果仅仅是实例化和依赖注入还是比较简单的,还不能完成进行自定义的要求。 为了完成依赖注入的功能, Spring 在完成依赖注入之后,还提供了一系列的接口和配置来完成Bean初始化的过程,让我们学习这个过程。 Spring在完成依赖注入后,还会进行如下图所示流程来完成它的生命周期:
在这里插入图片描述

图中描述的是整个IoC容器初始化Bean 的流程,作为开发者,需要注意这些流程。除此之外,还需要注意以下两点:

  • 这些接口和方法是针对什么而言的。 对于上图, 在没有注释的情况下的流程节点都是针对单个Bean 而言的,但是BeanPostProcessor 是针对所有 Bean 而言的,这是我们需要注意的地方。
  • 即使你定义了 ApplicationContextAware 接口,但是有时候并不会调用,这要根据你的 IoC 容器来决定。 我们知道, Spring IoC 容器最低的要求是实现 BeanFactory 接口,而不是实现ApplicationContext 接口 。 对于那些没有实现 ApplicationContext 接口的容器,在生命周期对应的ApplicationContextAware 定义的方法也是不会被调用的,只有实现了 ApplicationContext 接口的容器,才会在生命周期调用 ApplicationContextAware 所定义的 setApplicationContext方法。

现在改造一下YoungPerson类:

@Component
public class YoungPerson implements Person, BeanNameAware , BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean {private Animal animal = null;@Overridepublic void service() {this.animal.user();}@Autowired@Qualifier("dog")@Overridepublic void setAnimal(Animal animal) {System.out.println("延迟依赖注入");this.animal = animal;}@Overridepublic void setBeanName(String name) {System.out.println ("【" + this.getClass().getSimpleName() + "】调用BeanNameAware的setBeanName");}@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {System.out.println ("【" + this.getClass().getSimpleName() + "】调用BeanFactoryAware的setBeanFactory");}@Overridepublic void destroy() throws Exception {System.out.println ("【" + this.getClass().getSimpleName() + "】调用DisposableBean方法");}@Overridepublic void afterPropertiesSet() throws Exception {System.out.println ("【" + this.getClass().getSimpleName() + "】调用InitializingBean方法的afterPropertiesSet方法");}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {System.out.println ("【" + this.getClass().getSimpleName() + "】调用ApplicationContextAware方法的setApplicationContext方法");}@PostConstructpublic void init () {System.out.println("【" + this.getClass().getSimpleName() + "】注解@PostConstruct定义的自定义初始化方法");}@PreDestroypublic void destroyl () {System.out.println("【" + this.getClass().getSimpleName() + "】注解@PreDestroy定义的自定义销毁方法");}
}

这样,这个 B巳an 就实现了生命周期中单个 Bean 可以实现的所有接口, 并且通过注解@PostConstruct 定义了初始化方法,通过注解@PreDestroy 定义了销毁方法。 为了测试 Bean 的后置处理器, 这里创建一个类BeanPostProcessorExampIe,如下:

/*** @Version: 1.0.0* @Author: Dragon_王* @ClassName: BeanPostProcessorExample* @Description: TODO描述* @Date: 2024/1/20 23:34*/
public class BeanPostProcessorExample implements BeanPostProcessor {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {System.out.println("BeanPostProcessor调用"+"postProcessBeforeinitialization方法,参数【"+bean.getClass().getSimpleName()+"】【"+beanName+"】");return bean;}@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {System.out.println("BeanPostProcessor调用"+"postProcessAfterinitialization方法,参数【"+bean.getClass().getSimpleName()+"】【"+beanName+"】");return bean;}
}

注意,这个Bean后置处理器将对所有的Bean有效,运行测试如下:
测试类:

AnnotationConfigApplicationContext ctx =new AnnotationConfigApplicationContext(AppConfig.class) ;ctx.close();
2024-01-20T23:43:23.135+08:00  INFO 748 --- [           main] c.d.restart.RestartApplicationTests      : Starting RestartApplicationTests using Java 19 with PID 748 (started by ThundeRobot in E:\IDEA_projects\restart)
2024-01-20T23:43:23.136+08:00  INFO 748 --- [           main] c.d.restart.RestartApplicationTests      : No active profile set, falling back to 1 default profile: "default"
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【RestartApplication$$SpringCGLIB$$0】【restartApplication】
BeanPostProcessor调用postProcessAfterinitialization方法,参数【RestartApplication$$SpringCGLIB$$0】【restartApplication】
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【AppConfig$$SpringCGLIB$$0】【appConfig】
BeanPostProcessor调用postProcessAfterinitialization方法,参数【AppConfig$$SpringCGLIB$$0】【appConfig】
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【Cat】【cat】
BeanPostProcessor调用postProcessAfterinitialization方法,参数【Cat】【cat】
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【Dog】【dog】
BeanPostProcessor调用postProcessAfterinitialization方法,参数【Dog】【dog】
延迟依赖注入
【YoungPerson】调用BeanNameAware的setBeanName
【YoungPerson】调用BeanFactoryAware的setBeanFactory
【YoungPerson】调用ApplicationContextAware方法的setApplicationContext方法
BeanPostProcessor调用postProcessBeforeinitialization方法,参数【YoungPerson】【youngPerson】
【YoungPerson】注解@PostConstruct定义的自定义初始化方法
【YoungPerson】调用InitializingBean方法的afterPropertiesSet方法
BeanPostProcessor调用postProcessAfterinitialization方法,参数【YoungPerson】【youngPerson】
BeanPostProcessor 调用 postProcessBeforeinitialization 方法,参数 【Cat】【cat】BeanPostProcessor 调用 postProcessAfterinitialization 方法, 参数 【Cat】【cat】2024-01-20T23:43:24.044+08:00  INFO 748 --- [main] c.d.restart.RestartApplicationTests      : Started RestartApplicationTests in 1.142 seconds (process running for 1.772)YoungPerson】注解@PreDestroy定义的自定义销毁方法
【YoungPerson】调用DisposableBean方法

从日志可以看出,对于Bean后置处理器(BeanPostProcessor)而言, 它对所有的 Bean 都起作用,而其他的接口则是对于单个Bean起作用。我们还可以注意到BussinessPerson执行的流程是上图所画出的流程。有时候Bean 的定义可能使用的是第三方的类,此时可以使用注解@Bean来配置自定义初始化和销毁方法,如下所示:

@Bean(InitMethod =”Init”, destroyMethod = ”destroy” )

二、作用域

在介绍IoC 容器最顶级接口 BeanFactory 的时候, 可以看到 isSingleton 和 isPrototype 两个方法。其中,isSingleton 方法如果返回 true,则 Bean 在 loC 容器中以单例存在,这也是 Spring IoC 容器的默认值;如果 isPrototype 方法返回 true,则当我们每次获取 Bean 的时候, IoC 容器都会创建一个新的 Bean,这显然存在很大的不同,这便是Spring Bean 的作用域的问题。在一般的容器中, Bean都会存在单例(Singleton)和原型(Prototype)两种作用域, Java EE 广泛地使用在互联网中,而在 Web容器中, 则存在页面(page)、请求(request)、会话 (session)和应用(application) 4 种作用域对于页面(page),是针对 JSP 当前页面的作用域,所以 Spring是无法支持的。为了满足各类的作用域,在Spring 的作用域中就存在如表所示的几种类型。

作用域类型使用范围作用域描述
singleton所有Spring 应用默认值, loC 容器只存在单例
prototype所有Spring 应用每当从IoC 容器中取出一个 Bean,则创建一个新的Bean
sessionSpring Web 应用HTTP 会话
applicationSpring Web 应用Web 工程生命周期
requestSpring Web 应用Web 工程单次请求 (request)
globalSessionSpring Web 应用在一个全局的HTTPSession 中, 一个 Bean 定义对应一个实例。 实践中基本不使用
  • 前四个最常用
  • 对于application作用域,完全可以使用单例来替代。

下面我们探讨单例 (Singleton)和原型(prototype)的区别

首先定义一个类

@Component 
//@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 
public class ScopeBean { }

这是一个简单的类, 可以看到这里声明作用域的代码已经被注释掉了, 这样就是启用默认的作用域,实际就是单例。为了证明作用域的存在,我们进行一下测试:

AnnotationConfigApplicationContext ctx=new AnnotationConfigApplicationContext (AppConfig.class);ScopeBean scopeBeanl = ctx.getBean (ScopeBean.class);ScopeBean scopeBean2 = ctx.getBean (ScopeBean .class);System.out.println (scopeBeanl == scopeBean2) ;

在这里插入图片描述
从测试的结果来看,显然scopeBeanl 和 scopeBean2 这两个变量都指向了同一的实例,所以在IoC容器中, 只有一个ScopeBean 的实例。 然后取消代码中作用域代码的注释,进行同样的测试, 则可以看到scopeBeanl == scopeBean2 返回的将是 false,而不再是 true, 那是因为我们将Bean 的作用域修改为了 prototype,这样就能让IoC 容器在每次获取Bean 时,都新建一个Bean的实例返回给调用者。
在这里插入图片描述
这里的 ConfigurableBeanFactory 只能提供单例 ( SCOPE_ SINGLETON )和原型 ( SCOPE_PROTOTYPE)两种作用域供选择, 如果是在 SpringMVC环境中,还可以使用 WebApplicationContext去定义其他作用域, 如请求(SCOPE REQUEST)、 会话 (SCOPE_SESSION) 和应用 (SCOPE_APPLICATION)。 例如,下面的代码就是定义请求作用域:

@Component 
@Scope(WebApplicationContext.SCOPE_REQUEST) 
public class ScopeBean { }

总结

以上就是Bean生命周期和作用域的讲解。

相关文章:

SpringBoot:详解Bean生命周期和作用域

🏡浩泽学编程:个人主页 🔥 推荐专栏:《深入浅出SpringBoot》《java项目分享》 《RabbitMQ》《Spring》《SpringMVC》 🛸学无止境,不骄不躁,知行合一 文章目录 前言一、生命周期二…...

【图解数据结构】顺序表实战指南:手把手教你详细实现(超详细解析)

🌈个人主页:聆风吟 🔥系列专栏:图解数据结构、算法模板 🔖少年有梦不应止于心动,更要付诸行动。 文章目录 一. ⛳️线性表1.1 🔔线性表的定义1.2 🔔线性表的存储结构 二. ⛳️顺序表…...

WordPress怎么禁用文章和页面古腾堡块编辑器?如何恢复经典小工具?

现在下载WordPress最新版来搭建网站,默认的文章和页面编辑器,以及小工具都是使用古腾堡编辑器(Gutenberg块编辑器)。虽然有很多站长说这个编辑器很好用,但是仍然有很多站长用不习惯,觉得操作太难了&#xf…...

【HarmonyOS】掌握布局组件,提升应用体验

从今天开始,博主将开设一门新的专栏用来讲解市面上比较热门的技术 “鸿蒙开发”,对于刚接触这项技术的小伙伴在学习鸿蒙开发之前,有必要先了解一下鸿蒙,从你的角度来讲,你认为什么是鸿蒙呢?它出现的意义又是…...

第4周:Pytorch——综合应用和实战项目 Day 28-30: 学习资源和社区参与

第4周:综合应用和实战项目 Day 28-30: 学习资源和社区参与 在这个阶段,我们将探索更多的学习资源并鼓励参与PyTorch和TensorFlow的社区,以进一步提升技术和融入开发者社群。 学习资源: 论文:阅读最新的机器学习和深度…...

TypeScript教程(一)在vscode中的配置TypeScript环境

TypeScript教程(一)在vscode中的配置TypeScript环境 文章目录 TypeScript教程(一)在vscode中的配置TypeScript环境一、前言二、具体步骤1、Node.js安装2、TypeScript安装3、helloworld 一、前言 未来的开发者们请上座&#xff0c…...

sshpass的安装与使用

一.简介 1.定义: ssh 登陆不能在命令行中指定密码,sshpass 的出现则解决了这一问题。它允许你用 -p 参数指定明文密码,然后直接登录远程服务器,它支持密码从命令行、文件、环境变量中读取。 2.使用 sshpass 原因 使用 sshpass…...

Excel·VBA合并工作簿2

其他合并工作簿的方法,见之前的文章《ExcelVBA合并工作簿》 目录 8,合并文件夹下所有工作簿中所有工作表,按表头汇总举例 8,合并文件夹下所有工作簿中所有工作表,按表头汇总 与之前的文章《ExcelVBA合并工作簿&#x…...

linux内核原理--分页,页表,内核线性地址空间,伙伴系统,内核不连续页框分配,内核态小块内存分配器

1.分页,页表 linux启动阶段,最初运行于实模式,此阶段利用段寄存器,段内偏移,计算得到物理地址直接访问物理内存。 内核启动后期会切换到保护模式,此阶段会开启分页机制。一旦开启分页机制后,内…...

【MongoDB】下载安装、指令操作

目录 1.下载安装 2.指令 2.1.基础操作指令 2.2.增加 2.3.查询 2.4.修改 2.5.删除 前言: 关于MongoDB的核心概念请移步: 【文档数据库】ES和MongoDB的对比-CSDN博客 1.下载安装 本文以安装Windows版本的mongodb为例,Linux版本的其实…...

k8s-pvc/pv扩容记录

背景 一次聊天过程中,对方提及pvc的扩容,虽然有注意过 storageclass 有个AllowVolumeExpansion的配置(有些csi插件是不支持该配置的,比如local-volume-provisoner),但是没有实际用过,所以还是心…...

关于Unity插件TriLib使用的一点儿心得

最近做一个项目的时候,由于要求动态加载fbx或者glb等格式文件,而我们自己开发加载插件难度又有点大,所以最后使用了TriLib这个插件,现在说一点使用心得。 由于文件加载之后要对加载的内容进行复制,比如加载一个柱子&am…...

计算机二级Python基本排序题-序号45(补充)

1. 文件"singup.txt”中保存了若干条参加运动会学生的报名记录,每条记录的形式为“班级号_学号”,例如"A1_12”,将每个班级报名情按参加运动会人数从多到少排列(假设不存在人数相同的情况)并输出&#xff0c…...

响应式Web开发项目教程(HTML5+CSS3+Bootstrap)第2版 例4-6 fieldset

代码 <!doctype html> <html> <head> <meta charset"utf-8"> <title>fieldset</title> </head><body> <form action"#"><fieldset><legend>学生信息</legend>姓名&#xff1a;&…...

html渲染优先级

在前端开发中&#xff0c;优先布局是指在设计和构建页面时&#xff0c;将页面的各个部分按照其重要性和优先级进行排序&#xff0c;并依次进行布局和开发。这种方法可以帮助开发团队在项目初期就确定页面结构的核心部分&#xff0c;从而更好地掌控项目的整体进度和优先级。且确…...

linux 更新镜像源

打开终端&#xff0c;备份一下旧的 源 文件&#xff0c;以防万一 cd /etc/apt/ ls sudo cp sources.list sources.list.bak ls然后打开清华大学开源软件镜像站 搜索一下你的linux发行版本&#xff0c;我这里是ubuntu发行版本 点击这个上面图中的问号 查看一下自己的版本号&a…...

【征服Redis12】redis的主从复制问题

从现在开始&#xff0c;我们来讨论redis集群的问题&#xff0c;在前面我们介绍了RDB和AOF两种同步机制&#xff0c;那你是否考虑过这两个机制有什么用呢&#xff1f;其中的一个重要作用就是为了集群同步设计的。 Redis是一个高性能的键值存储系统&#xff0c;广泛应用于Web应用…...

php函数 一

一 自动加载 1.1 __autoload(string $class) 类自动加载&#xff0c;7.2版本之后废弃。可使用sql_autoload_register()注册方法实现。 类自动加载&#xff0c;无返回值。 #php7.2之前function __autoload($class) {if(strpos($class, CI_) ! 0){if (file_exists(APPPATH . …...

监督学习 - 梯度提升回归(Gradient Boosting Regression)

什么是机器学习 梯度提升回归&#xff08;Gradient Boosting Regression&#xff09;是一种集成学习方法&#xff0c;用于解决回归问题。它通过迭代地训练一系列弱学习器&#xff08;通常是决策树&#xff09;来逐步提升模型的性能。梯度提升回归的基本思想是通过拟合前一轮模…...

【工具】使用ssh进行socket5代理

文章目录 shellssh命令详解正向代理&#xff1a;反向代理&#xff1a;本地 socks5 代理 shell ssh -D 3333 root192.168.0.11 #输入密码 #3333端口已经使用远程机进行转发设置Windows全局代理转发 socks127.0.0.1 3333如果远程机为公网ip&#xff0c;可通过搜索引擎查询出网…...

golang循环变量捕获问题​​

在 Go 语言中&#xff0c;当在循环中启动协程&#xff08;goroutine&#xff09;时&#xff0c;如果在协程闭包中直接引用循环变量&#xff0c;可能会遇到一个常见的陷阱 - ​​循环变量捕获问题​​。让我详细解释一下&#xff1a; 问题背景 看这个代码片段&#xff1a; fo…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

STM32F4基本定时器使用和原理详解

STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...

多模态大语言模型arxiv论文略读(108)

CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文标题&#xff1a;CROME: Cross-Modal Adapters for Efficient Multimodal LLM ➡️ 论文作者&#xff1a;Sayna Ebrahimi, Sercan O. Arik, Tejas Nama, Tomas Pfister ➡️ 研究机构: Google Cloud AI Re…...

【7色560页】职场可视化逻辑图高级数据分析PPT模版

7种色调职场工作汇报PPT&#xff0c;橙蓝、黑红、红蓝、蓝橙灰、浅蓝、浅绿、深蓝七种色调模版 【7色560页】职场可视化逻辑图高级数据分析PPT模版&#xff1a;职场可视化逻辑图分析PPT模版https://pan.quark.cn/s/78aeabbd92d1...

GitHub 趋势日报 (2025年06月06日)

&#x1f4ca; 由 TrendForge 系统生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日报中的项目描述已自动翻译为中文 &#x1f4c8; 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

逻辑回归暴力训练预测金融欺诈

简述 「使用逻辑回归暴力预测金融欺诈&#xff0c;并不断增加特征维度持续测试」的做法&#xff0c;体现了一种逐步建模与迭代验证的实验思路&#xff0c;在金融欺诈检测中非常有价值&#xff0c;本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...

基于PHP的连锁酒店管理系统

有需要请加文章底部Q哦 可远程调试 基于PHP的连锁酒店管理系统 一 介绍 连锁酒店管理系统基于原生PHP开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。 技术栈 phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册/登录/注销 2 个人中…...