实用开发篇1
实用开发篇1
1、ConfigurationProperties
1.1、工程的搭建
勾选lombok,其他的看情况自己手动添加,因为不是web服务所以不需要勾选web
Finnish
1.2、项目初始化配置
将原先的porperties文件修改为yml文件格式
1.3、@ConfigurationPorperties
1概述
- @ConfigurationPorperties这个注解,是可以对我们的实体类的属性进行属性注入的
- 首先我们得有配置---yml
- 其次我们得有所谓的实体类(Bean包,取什么名字都可以),并且这个对象是被SpringIOC容器所管控的
- 因为这个实体类需要加载yml当中的属性,所以这个需要加载配置的实体类,他是需要被SpringIOC容器所管控的
2、配置文件的编写
# 我们这里单独配置一个供我们这个项目所使用的端口号
services:
ipAddress: 172.16.1.1 # ip地址
port: 8080 # ip地址的端口号
timeout: -1 # 该地址的超时时间用不超时
3、实体类的创建
因为我们是配置类嘛,那就创建目录为config/ServletConfig的java文件
/**
* 这是一个配置类,这个配置类会加载yml文件当中我们所需要的配置
*/
@Component // 你要加载SpringBoot的yml文件,那么你自己得是一个被SpringBoot所管控的容器
@ConfigurationProperties(prefix = "services")
@Data
public class ServletConfig {
/**
* 通过 @ConfigurationProperties,配置了前缀我可以读取到当中的配置
* @Param ipAddress IP地址
* @Param port IP地址的端口号
* @Param timeout IP地址的超时时间
*/
private String ipAddress;
private int port;
private int timeout;
}
4、测试
获取这个Bean,然后我们打印一下地址吧
@SpringBootApplication
public class Demo8SpringbootConfigurationApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Demo8SpringbootConfigurationApplication.class, args);
ServletConfig servletConfig = run.getBean(ServletConfig.class);
System.out.println(servletConfig);
}
}
- 如果使用了@ConfigurationProperties这个注解
- 在类上加上这个注解的话,并且配置好前缀--prefix
- 如果有多级目录的话就 . 就行了
- 该配置类当中的属性名一定要和yml当中的父级目录下(services)的属性名一致,不然无法进行加载
例如我现在将timeout改为timeouts
我再来测试一下
因为是long型的,所以默认值为0
1.4、问题的引出
如果是第三方的配置呢?我应该怎么引入?
- 假设我yml文件当中配置了一条数据源(datasource)相关的配置,并且这个配置需要第三方Bean去加载呢?我应该怎么配置?
- 这就是我们在高级当中需要解决的问题
导入druid数据源的依赖
<!-- 添加一个数据源druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<!-- 这个是第三方的,所以需要版本号 -->
<version>1.1.16</version>
</dependency>
创建配置类dataSource
我们会通过这个配置类,获取到数据源对象 druidDataSource
/**
* 数据源dataSource的配置类
*/
@Configuration // 这是一个配置类
public class dataSource {
// 我这个配置类是获取数据源对象DataSource的
@Bean
public DruidDataSource getDataSource() {
DruidDataSource dataSource = new DruidDataSource();
return dataSource;
}
}
测试方法
- 加载我们的配置类
- 通过配置类调用内部,加载数据源对象的方法--getDataSource()
- 打印我们这个数据源对象的toString方法
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Demo8SpringbootConfigurationApplication.class, args);
ServletConfig servletConfig = run.getBean(ServletConfig.class);
// System.out.println(servletConfig);
// 获取自定义的druid的配置类
dataSource dataSourceConfig = run.getBean(dataSource.class);
// 通过这个配置类的方法获取到我们的数据源对象,并打印
System.out.println(dataSourceConfig.getDataSource());
}
打印出了这么一串结果
- 现在他给我显示的是这么一个格式,是数据源连接池 初始化 好以后的配置值
- 现在这一套配置值并没有被初始化,因为druid只有他真正连接数据库的时候才会去处理,才会启动初始化
- 现在这个样子就是懒加载的样子,内部的属性给你定义好了,因为你没有使用,所以所有值都是0
- 这个不重要,我们现在讲究的是@ConfigurationProperties,给第三方的组件定义值
配置值1,通过get和set
配置类方法的改造
这个数据源他不是一个对象吗,内部肯定有get和set方法嘛,我们这样给他赋值撒
你看它这里有这么多属性,我们随便找几个吧
/**
* 数据源dataSource的配置类
*/
@Configuration // 这是一个配置类
public class dataSource {
// 我这个配置类是获取数据源对象DataSource的
@Bean
public DruidDataSource getDataSource() {
DruidDataSource dataSource = new DruidDataSource();
// 通过set手动对某些属性进行赋值,可以随便设置,因为现在我们不需要连接数据库
// 我们的目的是第三方Bean的属性绑定
dataSource.setDriverClassName("com.mysql.cj.jdbc.driver"); // 设置驱动类名,那就设置个M8的吧
dataSource.setEnable(true); // 布尔类型的,那我设置个true吧
return dataSource;
}
}
重新测试
配置值2--注解方式
yml文件配置
配置我们需要的属性,供配置类的使用
配置类代码改进
- 我们就在配置类的方法上加上@ConfigurationProperties即可,并且加上我们需要的前缀--datasource
- @ConfigurationProperties也可以加在方法上
- 因为@Bean注解是将被他标注的方法的返回值设置为一个组件嘛
- 组件相当于被IOC容器管理的对象嘛,对象的本质就是类嘛,那么我对这个类加上@ConfigurationProperties注解让他去读取配置文件的属性是没问题的吧?
// 我这个配置类是获取数据源对象DataSource的
@Bean
@ConfigurationProperties(prefix = "datasource")
public DruidDataSource getDataSource() {
DruidDataSource dataSource = new DruidDataSource();
// 通过set手动对某些属性进行赋值,可以随便设置,因为现在我们不需要连接数据库
// 我们的目的是第三方Bean的属性绑定
// dataSource.setDriverClassName("com.mysql.cj.jdbc.driver"); // 设置驱动类名,那就设置个M8的吧
dataSource.setEnable(true); // 布尔类型的,那我设置个true吧
return dataSource;
}
测试结果
一样的效果
1.5、总结
- 首先是关于数据源对象
- 有些数据源对象,他是在创建的时候,内部的属性就被初始化好了
- 而有些数据源对象,例如druid,人家创建的时候是采用懒加载的方式,只有他真正连接数据库的时候才会去处理,才会启动初始化
- @ConfigurationProperties,不仅可以为自己定义的组件进行属性绑定,同时也可以对第三方Bean进行属性的绑定
1.6、关于引入@ConfigurationProperties的警告
- 没有配置SpringBoot对于该注解的处理器
- 加上这个即可
<!-- @ConfigurationProperties的注解处理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
2、EnableConfigurationProperties
这个注解加上了Enable,根据我看原理篇的认知,这是跟自动装配有关的,那么他跟--@ConfigurationProperties有什么关系呢?
- 还真有关系,我们在类上使用@ConfigurationProperties注解,说明我们这个类是一个配置类
- 既然是一个配置类,那么这个配置类需要被Spring管控对吧,所以他需要成为SpringIOC容器当中的组件,所以我们需要给这个配置类加上一个注解@Component
- 那么跟我这个@EnableConfigurationProperties有什么关系呢?还真有
2.1、主配置类的设计
- 我们设计一个配置类吧,可以把这个配置类理解为最终的配置类,怎么个最终法?写了就知道了
- 我们这个类做所有需要加载配置文件的类的集中配置
- 首先这个类肯定是要被Spring管控的嘛,所以我给他写个配置注解,不然你怎么管理别人?
1、类BaseConfig的设计
- 在这个注解的内部写上我们需要读取配置文件的类对象,该格式是数组类型的,可以写很多
/**
* 这个类是我们的集中配置类
* 这个类用来加载配置
*/
@Configuration
// 所有配置类我们都放在这里面进行集中的配置,可以减少我们为配置类写@Component吧
@EnableConfigurationProperties({ServletConfig.class})
public class BaseConfig {
}
2、异常产生
- 我们开始跑方法
- 发现了一个异常
- NoUniqueBeanDefinitionException,该Bean不唯一,也就是这个组件是不唯一的,在容器当中找到了两个这样的组件
- 因为我在ServletConfig上加上了@Component注解,他是一个组件
- 也不是说一个类他就只能在容器当中存在一个Bean,你可以通过设置Bean的名字来创建多个同类型不同名的组件
- 那就说明一点,@EnableConfigurationProperties注解会把他数组当中的类对象注册为一个组件,并且注册这个组件的方式采用的是默认创建组件的方式,二者重复自然就会出现这个异常?但我刚刚测了下好像也不是啊
- 使用这个注解的前提
- 被描述的类,必须加上@ConfigurationProperties注解,如果是类下的方法@Bean,然后@ConfigurationProperties,然后将类定义为一个配置类@Configuration,那么也是无法被加载的,因为你是当中的方法被注册为了一个组件,并且在该方法上标注的读取配置文件
- 这样也是不行的
3、问题的引出--松散绑定
如果命名规范的话,我们yml当中配置的测试数据源信息,dataSource应该采用驼峰命名法
但是当引用的时候,这里就报错了,这是为什么呢?
小写的能用,大写的不行,为啥呢
3、宽松绑定or松散绑定
3.1、概述
- 对于我们使用@ConfigurationProperties这个注解的时候,他绑定属性,对于这个名称上的要求是非常灵活的,SpringBoot的开发者为了兼顾大多数编程爱好者的书写习惯,设置了若干种匹配格式
1、什么是松散绑定?
- 用我们最开始的demo举例子,在ServletConfig类中,我们对yml文件进行了读取配置
- 其中以ipAddress举例,我们这个变量是和配置文件当中的变量对应的对吧,完全对应
- 但是这样写也可以形成对应,我就不测试了
- 为什么呢?其实就是内部采用的松散绑定的形式,只要配置类和配置文件相对的配置的,字母是一致的,无论加上下划线,中划线,随意大小写,都是可以识别到的,只要二者小写字母是一致的
- 在上述途中,例举一下常用的几种yml文件变量的写法
- 驼峰命名法:ipAddress
- unline模式:ip_address
- 串模式:ip-address
- 常量写法:IP_ADDRESS
- 这些写法在spring的开发当中都能大量看到,不过更主张使用串模式的开发方式
当我们使用@ConfigurationProperties拉绑定属性的时候,它的名称比较随意,松散,可以支持很多种形式
注意:不是所有注解都支持这种形式的,例如@Value就不行
这也是Boot让我们主推的方式---@ConfigurationProperties
2、之前问题的解答,为什么prefix当中使用驼峰不可以
- 我们来看下这个异常
- 啥意思?
- 就是你用来接收配置文件的名称定义是什么样的?
- 这也是跟松散绑定相关的知识,所以以后遇到这种问题,二话不说直接全部小写即可
- 或者剑走偏锋我就要试试中划线,我来试试
- 嘿嘿~
3、总结
4、问题引出
- timeout在我们这里配置的是超时时间,-1代表用不超时,这样写肯定是不行的,但是如果我想给他设置的超时时间是多少分钟?、多少秒、多少小时?这些应该怎么设置?
- 如果设置的是毫秒,或者分钟,就像下图这样,那么读起来会不会有阅读障碍?
- 有什么方法可以解决嘛?必须有,在JDK1.8中提供了两种数据类型用来解决这类问题
4、常用计量单位应用
4.1、概述
- 上节我们说到关于计量单位的问题,我们之前的yml配置当中,timeout为-1是可以被识别到的,你写其他的也行,例如3、4、5、6、7、8、9啊,等等
- 但是当我写这些数据的时候,例如3,我这是3毫秒呢?还是三秒钟呢?还是三分钟呢?亦或者是三小时?这就说不清了吧?或许根据业务需求,可以对常量进行内部的规划,但,在旁人眼里看来呢?别人不清楚啊,--这种数据类型的描述方式就会出现一些误解
- 在我们JDK8版本中,对于数据类型,如果是Long型的话,那几本就可以确定--该数据类型是一个毫秒类型的
- 在JDK8以后,出现了一系列与单位有关的数据类型,其中有一个数据类型是 --Duration
4.2、Duration
1、概述
参考文档--Duration引用
- JDK8版本后推出的一种数据类型,主要用于计算时间日期,差值之类的
- 该数据类型可以指定时间类型,如果不指定时间类型默认是秒--S
- 可以指定的时间类型有很多
- 该数据类型的共用方法
- 时间转换为时分秒:toSeconds(),to…()等方法
- 两个Duration比较:compareTo()相等返回0,大于返回正数,小于返回负数
- 获取指定单位的数值:get(TemporalUnit unit)
- 获取Duration中所有单位:getUnits()
- 获取Duration绝对值:abs()
- 给指定Temporal添加一个Duration:addTo(Temporal temporal)
- getNano() 获取纳秒数,获取的是纳秒部分的值,而不是转换成纳秒
- getSeconds() 获取秒数,获取的是秒部分的值,而不是转换成秒
- plusSeconds(), plus…() 加法 增加时间(时分秒),返回新实例
- dividedBy(long divisor) 除法,返回一个新的Duration实例,内部转换成秒来实现
- multipliedBy(long multiplicand) 惩罚,返回一个新的Duration实例,内部同样也是转换成秒来实现
- minus…()相关方法 减法:注意是用你输入参数去减,返回的是新的Duration实例
- 四则运算都是返回的新实例,并没有对旧实例进行修改,这是需要注意的,所以千万别忽略了返回值。
- 返回一个负的Duration实例:negated() 比如 PT1.4S 返回 PT-1.4S
- isZero() 判断是否是 ZERO 实例
- withSeconds() 和 withNanos() 两个方法通过传入秒数或者纳秒数来构造一个Duration副本
2、新增属性区分默认和设置类型
/**
* 这是一个配置类,这个配置类会加载yml文件当中我们所需要的配置
*/
// @Component("cinfigurat") // 你要加载SpringBoot的yml文件,那么你自己得是一个被SpringBoot所管控的容器
@Data
@ConfigurationProperties(prefix = "servers")
public class ServletConfig {
/**
* 通过 @ConfigurationProperties,配置了前缀我可以读取到当中的配置
* @Param ipAddress IP地址
* @Param port IP地址的端口号
* @Param timeout IP地址的超时时间
*/
private String ipAddress;
private int port;
private int timeouts;
// JDK8版本之后推出的数据类型,多用于时间日期,差值计算
@DurationUnit(ChronoUnit.DAYS) // 该注解可以指定需要的时间类型
private Duration serversTimeOut; // 服务器超时时间类型
// 我这里写一个默认的Duration类型的值,等会会一起打印
private Duration defaultDuration;
}
3、yml文件配置
servers:
ipAddress: 172.16.1.1
port: 8080
timeout: -1
servers-timeout: 5
default-duration: 3
4、打印输出
- 这里我们可以看到,没有设置时间类型的话默认是秒
- 如果设置的时间超过小时的话,例如天,那么会转换为小时的样式
5、报错分析
- 我这里将事件设置为了weeks,也就是多少周,爆了这么一个错误
- failed to convert java.lang.Integer to @org.springframework.boot.convert.DurationUnit java.time.Duration (caused by java.lang.IllegalArgumentException: Unknown unit Years)
4.3、DataSize
1、概述
- 这个变量是配置存储容量的,说简单点就是存储空间的大小
- datasize,数据的大小,默认单位是Byte,也就是比特
- 如果是读取配置文件的话,那么可以在配置文件当中写MB,KB,GB,TB等单位来描述
- 也可以通过指定该DataSize的类型来指定存储空间的大小
2、yml文件配置
3、类的属性添加
/**
* 这是一个配置类,这个配置类会加载yml文件当中我们所需要的配置
*/
// @Component("cinfigurat") // 你要加载SpringBoot的yml文件,那么你自己得是一个被SpringBoot所管控的容器
@Data
@ConfigurationProperties(prefix = "servers")
public class ServletConfig {
private String ipAddress;
private int port;
private int timeouts;
// JDK8版本之后推出的数据类型,多用于时间日期,差值计算
@DurationUnit(ChronoUnit.DAYS) // 该注解可以指定需要的时间类型
private Duration serversTimeOut; // 服务器超时时间类型
// 我这里写一个默认的Duration类型的值,等会会一起打印
private Duration defaultDuration;
// 内存空间大小描述--DataSize
// 默认描述
private DataSize serversDataSizeDefault;
// 在配置文件当中指定描述
private DataSize serversDataSizeOne;
// 通过注解指定内存大小的类型,这里指定为MB
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize serversDataSizeTow;
}
4、测试
是没问题的
5、Bean的属性校验
5.1、概述
如果Bean当中的属性能够做校验,那么就可以有效的帮我们做数据安全性的保障了
- 对于数据校验来说,他实际上是各个业务系统中,必不可少的一个环节
- 不管你是什么样的数据,总之在进行使用之前,做一套有效的数据校验,做一套配置也、好业务数据也好、具有很强的安全性,不至于出现预料之外的错误
- 对于这个数据校验的功能,是一个新功能吗?--不是的
- 在Java2EE中规范了一套数据校验的API,我们只需要使用即可
5.2、使用校验框架
1、导入框架依赖
在pom.xml当中引入相关依赖,他是一个接口,而非实现类,是一套标准的校验框架
<!-- 数据类型校验框架-JSR303 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
在SpringBoot的版本管理当中默认使用的是2.0.1.Final
2、在什么地方使用?
导入依赖后,想让哪儿有校验的功能,在哪儿开启校验的功能?
开启对当前Bean属性注入的校验
配置我们的校验规则
- 例如Max和Min注解,最大or最小
- 该注解是属于 javax.validation.constraints下的,别用错了
代码
/**
* 这是一个配置类,这个配置类会加载yml文件当中我们所需要的配置
*/
@Data
@ConfigurationProperties(prefix = "servers")
@Validated // 2、开启对当前Bean的属性注入的校验
public class ServletConfig {
/**
* 通过 @ConfigurationProperties,配置了前缀我可以读取到当中的配置
* @Param ipAddress IP地址
* @Param port IP地址的端口号
* @Param timeout IP地址的超时时间
*/
private String ipAddress;
// 3、配置校验规则,端口号不能超过8888
@Max(value = 8888,message="端口号最大不能够超过8888!")
@Min(value = 5000,message="端口号最小不能低于5000!")
private int port;
private int timeouts;
}
3、小小测试一波(报错原因)
- 在我们测试的时候出现了这么一个异常
- The Bean Validation API is on the classpath but no implementation could be found
- 这个Bean组件,校验API(Validation)在我们的类路径下没有实现类,或者说,它的实现类不能被发现
- 接口给你了,得有实现类
4、配置校验规则
- 第二步我只是对这个Bean开启了校验功能,但是怎么校验,如何校验,那就需要我们自行配置了
- 接口给你了,得有实现类
- 就像你用Servlet,对应谁实现的?TomCat实现的
- 使用JDBC连接Mysql,对应是谁实现的? JDBC是接口,Mysql驱动才是实现类
- 我现在要使用校验框架,是谁实现的?没有实现类,得我们自己来
- 继续看报错
- 例如Hibernate校验器,将其添加到类路径中
5、Hibernat
- Hibernat是一个校验引擎,我们需要让Hibernat作为我们校验框架(Validation)的实现类
- 在pom文件引入对应依赖
- 使用Hibernate框架提供的校验器做实现类
<!-- Hibernate校验引擎,使用Hibernate框架提供的校验器做实现类 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
关系理解
- 他俩的关系可以这么理解
- 上面是jdbc,下面是mysql驱动
- 上面servlet,下面是tomcat
- 那么上面是我们的校验框架(Validation),那么下面就是咱们这个校验框架的实现类(Hibernate)
6、再次运行
- 当我们再次运行的时候,确实是成功运行了
- 那么我们将端口号的值重新配置成超过我们约定范围的值--Max
- 在我们这个配置类(ServletConfig)绑定属性(properties)的发生的绑定异常,下面是提示内容
7、关于校验框架
- 对于这个校验框架的讲解还只是一个比较片面的理解,实际开发的时候,这个校验框架的作用是非常大的
- 这期篇幅有限,所以就暂时说这么多
- 最后一个,当我们点进这个@Max注解当中的时候,我们看下别人的包
- 这里是有非常多的注解供我们使用的
8、总结
- 使用校验框架和校验框架的实现
- 在需要校验的地方添加上@Validated,开启校验框架的功能
- 详细配置我们的校验规则
6、进制数的转换
6.1、问题回顾
- 我记得我刚刚开始使用SpringBoot的时候,有一天在做到SpringBoot整合第三方技术的时候
- 我刚好在那天学习到整合Mybatis,做Web项目嘛,不连数据库怎么行?
- 在yml配置文件当中配置数据源--datasource的时候,我遇见了一个问题
- 我个人有做笔记的习惯嘛,我喜欢写一步做一步,在那天这个错误的信息被我留存下来了
- 这就是我当时配置数据源的配置,我遇到了一个问题,我死活都连不上数据库
- 我通过cmd终端连接我的数据库,没问题
- 通过图形界面化工具Navicat连接也没问题
- 但是就是使用SpringBoot配置我的数据库连接的时候,连不上,密码是错的
- 我很郁闷,但是那天我也找到了问题的所在,只是我不明白为什么会出现这个问题
6.2、问题复盘
- 我将原先的demo给大家来复个盘
- 这是咱的yml配置文件,我现在将它(password)设置为int类型的
- 同时,这里有个实体test,我让他来读取我这个配置文件当中的值
- 接下来,咱们开始打印输出测试一下结果,看看发生了什么事情?
- 我明明写的是 010115,为什么打印出来是4173呢?
6.3、问题出现的原因
上述这种情况,其实任何开发人员都有可能遇见的,所以这里需要提一下
- 在我们学习yaml语法规则的时候
- 信息量不大,但是有几个信息容易忽略
- int值支持,二进制,八进制,十六进制
- 二进制是什么:0b1010_0111_1010_1110,这种类型的
- 八进制的语法:以0开头,后面跟上(0-7)=>0 (0-7)=>0(10115)
- 十六进制呢:0x开,(0-9,a-f)跟
我们打开计算器,测试一下,为什么010115 会变成 4173呢?
- 现在明白了吧,我的密码刚好是以0开头的,然后又恰好后面的数字都只包含0-7,他是一个标准的八进制,才会在八转十的时候遇见这个问题
- 只能说太倒霉加上太巧了,但凡密码不是以0开头的都不至于这样,当然,如果是0x开的那遇到了也没辙对吧
- 那么他的隐藏性主要在哪里?
6.4、问题的隐藏性
- 我们后台接受数据的时候,password是按照字符串来接收的
- 他识别到了0开的数据(0(010115))以后,并且又是一个纯数字,他默认把它按照数值进行解析=>八进制转十进制
- 转换完毕得到结果了=>4175,那直接将其转换为字符串即可,从而注入到我们的实体类配置项当中
- 最终就出现了我们这么一个问题
6.5、总结
- 像我们这个问题最终引发的后果就是,连不上数据库,其实和连数据库有关系吗?没有关系?
- 就是因为这个地方的格式转化问题,你能赖别人吗?
- 人家SpringBoot都推荐了,如果是纯数字结构的,那么你用字符串给他包上就行了
- 我们这里
- 恰巧没用字符串包上、
- 恰巧又刚好是一个八进制,这个八进制被转换成十进制了、
- 恰巧,转化出来的密码又刚好可以和String类型时配上、
- 恰巧,转化出来的十进制密码跟你数据库当中的密码完全不一样、
- 恰巧,你连不上数据库
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何在 .NET 中 使用 ANTLR4
· 后端思维之高并发处理方案
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· Cursor预测程序员行业倒计时:CTO应做好50%裁员计划
· 想让你多爱自己一些的开源计时器
· 大模型 Token 究竟是啥:图解大模型Token
· 用99元买的服务器搭一套CI/CD系统
· 当职场成战场:降职、阴谋与一场硬碰硬的抗争