手撕spring bean的加载过程
这里我们采用手撕源码的方式,开始探索spring boot源码中最有意思的部分-bean的生命周期,也可以通过其中的原理理解很多面试以及工作中偶发遇到的问题。
springboot基于约定大于配置的思想对spring进行优化,使得这个框架变得更加轻量化,集成各种starter组件时使其能够更加全面。
1、SpringApplication启动类的配置与软件包的反射加载
通常我们在建立一个新的spring boot项目时,利用idea脚手架生成模板内部会自带一个标注有SpringApplication注解的启动类,如下所示:
/*** @author : spring* {@code @description:}* {@code @date} : 2024/2/4* {@code @modified} By: spring* {@code @project:} spring-plus*/
@SpringApplication(scanBeanPackagePath = "com/hlc/springplus/test")
public class ApplicationStarter {public static void main(String[] args) {ApplicationContext applicationContext = new DefaultApplicationContext(ApplicationStarter.class);TestBean contextBean = (TestBean) applicationContext.getBean("test");contextBean.test();}
}
由于本文的代码全部都是手撕,所以会与spring boot的源码有所不同,个人主义完美凸显。
言回正传,用过spring boot的人都知道它主要的特色基础是它的“容器”的概念,我们可以通过配置文件、注解、导入以及反射实例化后调用通用应用上下文注入的方式将我们的bean交给spring容器管理,那么这里启动类启动后“容器”是怎么识别出我们标准或配置的bean信息同时将其实例化、配置属性、配置名称.....的呢?
那么下面就是一个过程
获取启动类上标注注解中的包路径值
SpringApplication annotation = appconfig.getAnnotation(SpringApplication.class);String beanPackagePath = annotation.scanBeanPackagePath();
String path = beanPackagePath.replace('.', '/');
这里的path就是我们实际的包路径,为什么需要将.替换城/呢?实际上我们配置的包路径是软件包中的相对路径,并不是Resource获取时规定的路径格式。
获取当前类的类加载器并根据路径加载URL获取文件
ClassLoader classLoader = DefaultApplicationContext.class.getClassLoader();URL resource = classLoader.getResource(path);File file = new File(resource.getFile());
DefaultApplicationContext就是我当前类的名称,后续串完全部的流程会将全部的代码挂出的,这里的类加载器获取资源的方式是比较常用的。
需要注意的是这里拿到的file有可能是文件夹,也可能是文件。
通过文件夹或文件夹获取字节码
for (File item : files) {int begin = item.getAbsolutePath().indexOf("com");int end = item.getAbsolutePath().indexOf('.');String className = item.getAbsolutePath().substring(begin, end).replace('\\', '.');try {Class<?> clazz = Class.forName(className);if (clazz.isAnnotationPresent(Component.class)) {beanClazzList.add(clazz);//收集后置处理器(意图是收集后置处理器而不是收集bean对象)if (BeanPostprocessor.class.isAssignableFrom(clazz)) {beanPostprocessorList.add((BeanPostprocessor) clazz.getDeclaredConstructor().newInstance());}}} catch (Exception e) {throw new RuntimeException(e.getMessage());} }
上面的类路径获取方式以及判断字节码是否实现了接口BeanPostprocessor的判断、字节码是否标注了注解Component的判断都是比较常用的方法。
那么通过此三步就将全部需要加载的字节码文件都获取到我们的成员变量beanClazzList列表中去了。
2、ApplicationContext接口的定义以及相关注解的配置
虽然我们解决了待加载bean的字节码列表的收集问题,但是spring boot的容器我们还没有加载出来,也没有实现相关注解的配置,注解标注了bean的身份、名称、类型、加载方式、加载条件、加载顺序、依赖关系等。
ApplicationContext接口的定义
public interface ApplicationContext extends BeanFactory {Object getBean(String beanName);<T> void registerBean(T bean, Class<T> clazz, String name);
}
ApplicationContext接口的释义是“应用上下文”,在计算机科学中,上下文表示进程在执行过程中系统内部的资源情况与中断向量表的记录情况,总之代表的是进程所处的逻辑环境。这里顾名思义ApplicationContext代表的也自然就是bean所处的环境。也就是我们口中的spring boot的容器。
对于基本的ApplicationContext能力而言,它应当具备获取bean对象与注册bean对象的能力。所以这里定义的两个基础能力接口。在考虑如何实现它之前,我们还需要配置以下bean相关的其他注解:
组件注解(标注类型为bean组件)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Component {String name() default "";
}
注入注解(标注为某类型注入某bean的属性值)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoWired {String value() default "";
}
作用域注解(标注单例、原型等生存周期的bean类型)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Scope {String value();
}
懒加载注解(标注即用即加载还是立刻加载)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Lazy {
}
初始化方法注解(标注bean的初始化方法)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InitBeanMethod {
}
3、实现ApplicationContext接口与bean加载原理
上述我们定义了相关接口与注解,接下来我们实现容器上下文接口以及描述bean是如何加载到容器内部管理的。
那么这里我们就先将实现ApplicationContext接口的DefaultApplicationContext.java代码放在下方:
public class DefaultApplicationContext implements ApplicationContext {private final Class<?> appconfig;private List<Class<?>> beanClazzList = new LinkedList<>();private Map<String, Object> singletonBeanMap = new ConcurrentHashMap<>();private Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();private List<BeanPostprocessor> beanPostprocessorList = new LinkedList<>();public DefaultApplicationContext(Class<?> appconfig) {this.appconfig = appconfig;//1、扫描启动类注解的字节码列表scanBeansByPackage(beanClazzList);//2、注册bean的BeanDefinition初始配置信息initBeanDefinition(beanClazzList, beanDefinitionMap);//3、实例化单例bean并存入map中instanceSingletonBeans(beanDefinitionMap, singletonBeanMap);}@Overridepublic Object getBean(String beanName) {BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);if (null != beanDefinition && "prototype".equals(beanDefinition.getScope())) {try {return beanDefinition.getBeanClazz().getDeclaredConstructor().newInstance();} catch (InstantiationException | IllegalAccessException | InvocationTargetException |NoSuchMethodException e) {throw new RuntimeException(e.getMessage());}}return singletonBeanMap.get(beanName);}@Overridepublic <T> void registerBean(T bean, Class<T> clazz, String name) {singletonBeanMap.put(name, bean);}/*** 扫描bean的字节码列表** @param beanClazzList bean的字节码列表(待填充)*/protected void scanBeansByPackage(List<Class<?>> beanClazzList) {if (null != appconfig && appconfig.isAnnotationPresent(SpringApplication.class)) {SpringApplication annotation = appconfig.getAnnotation(SpringApplication.class);if (null != annotation) {String beanPackagePath = annotation.scanBeanPackagePath();String path = beanPackagePath.replace('.', '/');ClassLoader classLoader = DefaultApplicationContext.class.getClassLoader();URL resource = classLoader.getResource(path);if (resource != null) {File file = new File(resource.getFile());if (file.isDirectory()) {File[] files = file.listFiles();if (files != null) {for (File item : files) {loadAndFilterBeanClazzes(beanClazzList, item);}}} else {loadAndFilterBeanClazzes(beanClazzList, file);}}} else {throw new RuntimeException("Annotation SpringApplication is not exist");}} else {throw new RuntimeException("Annotation SpringApplication is not exist and appconfig is null");}}/*** 加载bean的字节码列表并过滤** @param beanClazzList bean的字节码列表(待填充)* @param item 文件或文件夹*/private void loadAndFilterBeanClazzes(List<Class<?>> beanClazzList, File item) {int begin = item.getAbsolutePath().indexOf("com");int end = item.getAbsolutePath().indexOf('.');String className = item.getAbsolutePath().substring(begin, end).replace('\\', '.');try {Class<?> clazz = Class.forName(className);if (clazz.isAnnotationPresent(Component.class)) {beanClazzList.add(clazz);//收集后置处理器(意图是收集后置处理器而不是收集bean对象)if (BeanPostprocessor.class.isAssignableFrom(clazz)) {beanPostprocessorList.add((BeanPostprocessor) clazz.getDeclaredConstructor().newInstance());}}} catch (Exception e) {throw new RuntimeException(e.getMessage());}}/*** 注册bean的BeanDefinition初始配置信息** @param beanClazzList bean的字节类型列表* @param beanDefinitionMap bean的BeanDefinition初始配置信息池子*/private void initBeanDefinition(List<Class<?>> beanClazzList, Map<String, BeanDefinition> beanDefinitionMap) {if (null != beanClazzList && !beanClazzList.isEmpty()) {for (Class<?> clazz : beanClazzList) {BeanDefinition beanDefinition = new BeanDefinition();Component component = clazz.getAnnotation(Component.class);Scope scope = clazz.getAnnotation(Scope.class);Lazy lazy = clazz.getAnnotation(Lazy.class);beanDefinition.setBeanClazz(clazz);beanDefinition.setLazy(null != lazy);beanDefinition.setScope(null != scope ? scope.value() : "prototype");String beanName = component.name();if (beanName.isEmpty()) {beanName = clazz.getSimpleName();}beanDefinitionMap.put(beanName, beanDefinition);}}}/*** 实例化单例bean** @param beanDefinitionMap bean定义信息* @param singletonBeanMap 单例bean池子*/private void instanceSingletonBeans(Map<String, BeanDefinition> beanDefinitionMap, Map<String, Object> singletonBeanMap) {if (null != beanDefinitionMap && !beanDefinitionMap.isEmpty()) {for (Class<?> clazz : beanDefinitionMap.values().stream().map(BeanDefinition::getBeanClazz).toList()) {if (clazz.isAnnotationPresent(Scope.class) && "prototype".equals(clazz.getAnnotation(Scope.class).value())) {continue;}if (!clazz.isAnnotationPresent(Lazy.class)) {//实例化beantry {Component component = clazz.getAnnotation(Component.class);String beanName = component.name();if (null == beanName || beanName.isEmpty()) {beanName = clazz.getSimpleName();}//1、实例化beanObject newInstance = clazz.getDeclaredConstructor().newInstance();//2、属性填充attributeAutoWiredPadding(clazz, newInstance);//3、aware能力透传awareBeanInstancePadding(newInstance);//4、初始化//4.1、后置处理器 初始化前执行for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {newInstance = beanPostprocessor.beforeInitialization(newInstance, beanName);}//4.2、初始化bean执行//检查是否实现初始化Bean的接口initializeBeanInstancePadding(newInstance);//检查是否配置过init方法initBeanMethodInstancePadding(newInstance);//4.3、后置处理器能力 初始化后执行for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {newInstance = beanPostprocessor.afterInitialization(newInstance, beanName);}singletonBeanMap.put(beanName, newInstance);} catch (InvocationTargetException | NoSuchMethodException | InstantiationException |IllegalAccessException e) {throw new RuntimeException(e.getMessage());}}}}}/*** bean的属性填充** @param beanClazz bean的字节类型* @param newInstance 实例化的bean*/private void attributeAutoWiredPadding(Class<?> beanClazz, Object newInstance) {if (null != beanClazz) {Field[] fields = beanClazz.getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(AutoWired.class)) {field.setAccessible(true);Class<?> declaringClass = field.getType();AutoWired autoWired = field.getAnnotation(AutoWired.class);String name = autoWired.value();if (null == name || name.isEmpty()) {name = declaringClass.getSimpleName();}Object fieldBean = singletonBeanMap.get(name);if (null == fieldBean) {List<Class<?>> beanClazzList = new LinkedList<>();beanClazzList.add(declaringClass);initBeanDefinition(beanClazzList, beanDefinitionMap);Map<String, BeanDefinition> definitionMap = new HashMap<>();definitionMap.put(name, beanDefinitionMap.get(name));instanceSingletonBeans(definitionMap, singletonBeanMap);try {field.set(newInstance, singletonBeanMap.get(name));} catch (IllegalAccessException e) {throw new RuntimeException(e.getMessage());}} else {try {field.set(newInstance, fieldBean);} catch (IllegalAccessException e) {throw new RuntimeException(e.getMessage());}}}}}}/*** bean的Aware接口的实现类填充** @param bean bean实例对象*/private void awareBeanInstancePadding(Object bean) {if (null != bean) {if (bean instanceof Aware) {if (bean instanceof ApplicationContextAware) {((ApplicationContextAware) bean).setApplicationContext(this);}if (bean instanceof BeanNameAware) {((BeanNameAware) bean).setBeanName();}}}}/*** bean的初始化方法填充** @param bean 实例化的bean*/private void initializeBeanInstancePadding(Object bean) {if (null != bean) {if (bean instanceof InitializingBean) {((InitializingBean) bean).afterPropertiesSet();}}}/*** bean的初始化方法填充** @param newInstance 实例化的bean*/private void initBeanMethodInstancePadding(Object newInstance) {if (null != newInstance) {Method[] methods = newInstance.getClass().getDeclaredMethods();for (Method method : methods) {if (method.isAnnotationPresent(InitBeanMethod.class)) {method.setAccessible(true);try {method.invoke(newInstance);} catch (IllegalAccessException | InvocationTargetException e) {throw new RuntimeException(e.getMessage());}}}}}private void sortBeanInstanceClazzList() {}
}
这里我们着重描述一下关于bean加载原理的这一块代码:
private void instanceSingletonBeans(Map<String, BeanDefinition> beanDefinitionMap, Map<String, Object> singletonBeanMap) {if (null != beanDefinitionMap && !beanDefinitionMap.isEmpty()) {for (Class<?> clazz : beanDefinitionMap.values().stream().map(BeanDefinition::getBeanClazz).toList()) {if (clazz.isAnnotationPresent(Scope.class) && "prototype".equals(clazz.getAnnotation(Scope.class).value())) {continue;}if (!clazz.isAnnotationPresent(Lazy.class)) {//实例化beantry {Component component = clazz.getAnnotation(Component.class);String beanName = component.name();if (null == beanName || beanName.isEmpty()) {beanName = clazz.getSimpleName();}//1、实例化beanObject newInstance = clazz.getDeclaredConstructor().newInstance();//2、属性填充attributeAutoWiredPadding(clazz, newInstance);//3、aware能力透传awareBeanInstancePadding(newInstance);//4、初始化//4.1、后置处理器 初始化前执行for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {newInstance = beanPostprocessor.beforeInitialization(newInstance, beanName);}//4.2、初始化bean执行//检查是否实现初始化Bean的接口initializeBeanInstancePadding(newInstance);//检查是否配置过init方法initBeanMethodInstancePadding(newInstance);//4.3、后置处理器能力 初始化后执行for (BeanPostprocessor beanPostprocessor : beanPostprocessorList) {newInstance = beanPostprocessor.afterInitialization(newInstance, beanName);}singletonBeanMap.put(beanName, newInstance);} catch (InvocationTargetException | NoSuchMethodException | InstantiationException |IllegalAccessException e) {throw new RuntimeException(e.getMessage());}}}}}
到这里我们已经了解在获取待加载bean的字节码列表之后,我们需要将bean的配置信息存储到我们的beanDefinitionMap中,再根据beanDefinitionMap将其中的单例bean信息加载成一个个bean放入单例bean map中,这里的存储key统一都是beanName。
看以上代码我们不难分析出通过bean配置信息加载bean的过程中,一个bean需要经过6步周期性工作才会被放入容器中给我们使用。以下是图示:
相关文章:

手撕spring bean的加载过程
这里我们采用手撕源码的方式,开始探索spring boot源码中最有意思的部分-bean的生命周期,也可以通过其中的原理理解很多面试以及工作中偶发遇到的问题。 springboot基于约定大于配置的思想对spring进行优化,使得这个框架变得更加轻量化&#…...

Linux系统中安装JDK
天行健,君子以自强不息;地势坤,君子以厚德载物。 每个人都有惰性,但不断学习是好好生活的根本,共勉! 文章均为学习整理笔记,分享记录为主,如有错误请指正,共同学习进步。…...

Stable Diffusion 模型下载:国风3 GuoFeng3
文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八案例九案例十推荐提示词下载地址模型介绍 欢迎使用GuoFeng3模型 - 这是一个中国华丽古风风格模型,也可以说是一个古风游戏角色模型,具有2.5D的质感。 条目内...

VUE项目导出excel
导出excel主要可分为以下两种: 1. 后端主导实现 流程:前端调用到导出excel接口 -> 后端返回excel文件流 -> 浏览器会识别并自动下载 场景:大部分场景都有后端来做 2. 前端主导实现 流程:前端获取要导出的数据 -> 把常规数…...

Springboot 整合 Elasticsearch(二):使用HTTP请求来操作ES
📁前情提要:Springboot整合Elasticsearch(一):Linux下安装 Elasticsearch 8.x 目录 一、使用 elasticsearch-head 插件连接 1、下载压缩包 2、在 chrome 浏览器中添加扩展程序 3、修改IP地址,点击连接 …...
npm 淘宝镜像到期
npm 淘宝镜像到期了 npm ERR! request to https://registry.npm.taobao.org/cnpm failed, reason: certificate has expired npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED npm ERR! request to https://registry.npm.taobao.org/cnpm failed, reason: cer…...

计算机网络——新型网络架构:SDN/NFV
1. 传统节点与SDN节点 1.1 传统节点(Traditional Node) 这幅图展示了传统网络节点的结构。在这种设置中,控制层和数据层是集成在同一个设备内。 以太网交换机:在传统网络中,交换机包括控制层和数据层,它不仅负责数据包的传输&…...

【精选】java继承进阶,子类继承父类(内存图、内存分析工具)
🍬 博主介绍👨🎓 博主介绍:大家好,我是 hacker-routing ,很高兴认识大家~ ✨主攻领域:【渗透领域】【应急响应】 【python】 【VulnHub靶场复现】【面试分析】 🎉点赞➕评论➕收藏…...

Google Play上架:因行为透明度被拒审或下架的政策自查(基于区块链的内容)
近期很多朋友的项目出现因行为透明度问题被谷歌拒审或者已经上架的包被下架甚至封号,今天解释一下为什么会被封号下架,根据是什么? 目录 政策发布时间与截止时间政策内容政策背景政策解析和问题讲解政策发布时间与截止时间 基于区块链的内容相关政策,于2023-07-12 公布,…...
备战蓝桥杯---搜索(剪枝)
何为剪枝,就是减少搜索树的大小。 它有什么作用呢? 1.改变搜索顺序。 2.最优化剪枝。 3.可行性剪枝。 首先,单纯的广搜是无法实现的,因为它存在来回跳的情况来拖时间。 于是我们可以用DFS,那我们如何剪枝呢&#…...

ResizeObserver的使用
这篇说下ResizeObserver API。ResizeObserver接口监视 Element 内容盒或边框盒或者 SVGElement 边界尺寸的变化。 ResizeObserver避免了通过回调函数调整大小时,通常创建的无限回调循环和循环依赖项。它只能通过在后续的帧中处理 DOM 中更深层次的元素来做到这一点…...

CleanMyMac X 4.14.7帮您安全清理Mac系统垃圾
CleanMyMac X 4.14.7是一款强大的 Mac 清理、加速工具和健康卫士,可以让您的 Mac 再次恢复巅峰性能。 移除大型和旧文件、卸载应用,并删除浪费磁盘空间的无用数据。 5倍 更多可用磁盘空间 CleanMyMac X 4.14.7帮您安全清理Mac系统垃圾 CleanMyMac X 4.14.7一键深度扫描mac系统…...

C语言实现memcpy、memmove库函数
目录 引言一、库函数介绍二、库函数详解三、源码实现1.memcpy源码实现2.memmove源码实现 四、测试1.memcpy函数2.memmove函数 五、源码1.memcpy源码2.memmove源码 六、参考文献 引言 关于memcpy和memmove这两个函数,不论是算法竞赛还是找工作面试笔试,对…...

MySQL数据库④_表的约束(主键_自增长_唯一键_外键等)
目录 1. 约束概念和常见的约束 2. 空属性null/not null 2. 默认值default 3. 列描述comment 4. 自动填充zerofill 5. 主键primary key 5.1 主键 5.2 复合主键 6. 自增长auto_increment 7. 唯一键unique key 8. 外键forei…...
SpringBoot过滤器获取请求的参数
一、背景 在项目开发过程中,需要对于某些接口统一处理。 这时候就需要获取请求的报文,再对获取的报文进行统一处理。 二、了解过滤器 首先了解一下过滤器拦截器的区别: JAVA中的拦截器、过滤器:https://blog.csdn.net/qq_38254…...

幻兽帕鲁mac可以玩吗?
《幻兽帕鲁》(英文:Palworld)是一款近期在 Steam 爆红的动作冒险生存游戏,游戏设置在一个居住着「帕鲁」的开放世界中,玩家可以战斗并捕捉帕鲁,也能用它们来建造基地、骑乘和战斗。 不过目前《幻兽帕鲁》仅…...

webstorm、vscode、HBuilder配置eslint检查
你们好,我是金金金。 场景 每个人写的代码都有自己所属的风格,所以项目中统一代码风格特别重要,新开的项目中如何快速配置ESLint呢? 安装 npm install --save-dev eslint ----安装eslintnpm install --save-dev eslint-plugin-vu…...

大数据知识图谱之深度学习——基于BERT+LSTM+CRF深度学习识别模型医疗知识图谱问答可视化系统
文章目录 大数据知识图谱之深度学习——基于BERTLSTMCRF深度学习识别模型医疗知识图谱问答可视化系统一、项目概述二、系统实现基本流程三、项目工具所用的版本号四、所需要软件的安装和使用五、开发技术简介Django技术介绍Neo4j数据库Bootstrap4框架Echarts简介Navicat Premiu…...
年底个人总结
年底个人总结 前言:又到了年底,在游戏行业工作了接近10年,想想也应该把自己做过的东西做一个总结。 从14年在北京毕业,懵懂的我在机缘巧合下遇到了陈g,我行业的领路人,在他的带领下我进入到了游戏行业。 当…...

jsp教材管理系统Myeclipse开发mysql数据库web结构java编程计算机网页项目
一、源码特点 JSP 教材管理系统是一套完善的java web信息管理系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发,数据库为Mysql5.0&…...

国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...
Vite中定义@软链接
在webpack中可以直接通过符号表示src路径,但是vite中默认不可以。 如何实现: vite中提供了resolve.alias:通过别名在指向一个具体的路径 在vite.config.js中 import { join } from pathexport default defineConfig({plugins: [vue()],//…...

【Linux系统】Linux环境变量:系统配置的隐形指挥官
。# Linux系列 文章目录 前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变量的生命周期 四、环境变量的组织方式五、C语言对环境变量的操作5.1 设置环境变量:setenv5.2 删除环境变量:unsetenv5.3 遍历所有环境…...
django blank 与 null的区别
1.blank blank控制表单验证时是否允许字段为空 2.null null控制数据库层面是否为空 但是,要注意以下几点: Django的表单验证与null无关:null参数控制的是数据库层面字段是否可以为NULL,而blank参数控制的是Django表单验证时字…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...
数据库——redis
一、Redis 介绍 1. 概述 Redis(Remote Dictionary Server)是一个开源的、高性能的内存键值数据库系统,具有以下核心特点: 内存存储架构:数据主要存储在内存中,提供微秒级的读写响应 多数据结构支持&…...

Redis上篇--知识点总结
Redis上篇–解析 本文大部分知识整理自网上,在正文结束后都会附上参考地址。如果想要深入或者详细学习可以通过文末链接跳转学习。 1. 基本介绍 Redis 是一个开源的、高性能的 内存键值数据库,Redis 的键值对中的 key 就是字符串对象,而 val…...