程序员的出路
最近几天渐渐悟出来一个道理,做技术没有出路。
我在想如果哪天我失业了,没有公司要我,以我现在的经验和能力,我该怎么办?
结果是我什么都干不了,我的经验和能力必须依靠公司才有价值,我在社会上独立生存,跟普通人比,没有任何优势。
所以我觉得做技术,尤其是走技术专家路线,根本没有出路,路越走越窄,最后把自己绑死在公司。
这段时间买了不少课,学了很多思路,打开了视野,也非常认真的思考,找身边厉害的人聊,写出这篇文章,希望可以供你参考。
培养赚钱的能力
赚钱的思维:发现需求,满足需求,推广流量。
普通人能赚到大钱的地方,都是很 low 的。我说几个自己身边的和听过的案例。
有的爸妈上班孩子放学没人接,有人开个自习室把孩子从学校接过来,每个孩子 700 块,一个月赚几万块。
前几年搞视频号,有一哥们做了个租房合同的带货视频爆了,赚了 20 多万。
老婆表姐搞外贸,老早没上班自己开了公司,具体不知道赚了多少钱,但有倒腾鞋子袜子的案例,一年 100 个没有问题。
还有各种卖包装盒、打包盒、塑料袋、花生瓜子、酱菜、小板凳的案例,包括我今天下楼看了下小区周边,我们这条街在金融港正对面,不可否认有些店没过多久倒闭了,但还有些店,招牌已经破的不行还开着在,包括各种餐饮店、收废品、五金、中老年活动中心、中医推拿针灸按摩、宠物店。他们肯定是赚到了钱,而且事情本身没什么门槛。
这些事给我刺激够大的,世界上赚钱的方式不只打工,从大学出来是第一次进入社会,跳出工作是第二次进入社会。
把他们抽象一下,都是发现了某个需求,然后找到了解决方案,再把方案推广让更多人知道。
解决方案各种各样,可以是做个产品(美团、饿了么各种 App 都是),也可以是简单的对接下资源,找个服装厂、鞋厂,甚至直接在 PDD 上代发货,这也是为什么我觉得搞技术没出路原因,解决方案面太窄,资产太重,一个人干不起来。
对我未来有什么启发呢?我分别说说。
发现需求
这个真的难,没抽象出方法论,因为这东西是别人赚钱的底裤,没人会分享,我要有正在赚钱的路子我也不可能说出来,上面说出来的都是过时的。
可行的方案是保持好奇,保持观察,多了解案例,自己多领悟。接地气,多关注衣食住行、吃喝拉撒方面的。
有机会我准备去广州、福建那里有各种代工厂、工业园,找人聊聊积累一些下游资源,多和身边的人聊天换取信息,多认识这个世界,积累各种各样的资源。
满足需求
在职场上,可以往产品经理方向发展。能锻炼自己产品思维,对自己未来创业是有帮助的。
另外搞产品也需要一定的沟通能力,这也能对接上创业时的销售能力。不管搞什么,这两项能力都是必要的。
我在做开发前做过几年销售,而且后期写的一些项目,我也承担了部分产品的职责,往这个方向转,对我来说没有问题,尤其是偏技术的产品,更是信手拈来。
推广流量
往投流方向转,也是可行的。要想赚钱,就得离钱近,投流是离钱最近的。
我了解投流的大致逻辑,ROI 怎么跑正,每个环节数据怎么排查,做增量的大致方向。
这个方面我缺少具体可能遇到的细节经验,或者行业的一些潜规则,平台的规则等。
但其实问题不大,流量方法不是公开的,如果公开的大家都一窝蜂上去投,就会把价格越拉越高,直到超过成本。
重点是如何用更低的价格买到流量,这里面可能有很多野路子、钻空子、薅羊毛等。
而且我之前做视频号的时候,也做过各个赛道,累计有上千万播放和几万粉丝,也算有点经验。
所以往投放方向转,我也有信心做好。
技术本行
再说到技术老本行,我不会去钻研高精尖的东西,就各种大厂面试题里的情况,实话实说工作了 7 年,面试题里面的场景一个都没有遇到,就像知乎上问:什么时候用 ArrayList 什么时候用 LinkList?答曰:工作中用 ArrayList,面试时用 LinkList。
技术上精通基本数据类型、Java 核心、MySQL、分库分表、SpringBoot、Spring Cloud相关、Redis、Kafka、Idea 熟练度等,就差不多了。
重要的还有项目经验、架构设计、业务场景处理那些。我会把这些做成视频发到 B 站上,一方面可以提升自己的技能熟练度,另一方面也能为自己攒点影响力。
然后把精力放在横向扩展上,比如关注 GitHub 各种搭建完就能用的开源项目,思考他们可以用在哪些场景,满足哪些需求。
总之,不管是未来做什么,我的目标是提升自己的赚钱能力,也留意观察、抓住生活中机会。
加油,共勉。
来源:juejin.cn/post/7456417337676595212
2024年终总结:“Fake It Till You Make It”
在这个人人都希望成为**“别人家的孩子”的社会,职场不再仅仅是能力和经验的比拼,越来越多的成功与否取决于你有多会“装”**。
_“Fake It Till You Make It”_______假装自己是个成功人士,直到你的真成功;
这句话就是你在职场打拼的**“生存法则”。但你得明白,不只是说你穿上西装,喝着美式,走进办公室时“演”得像个大老板那么简单。更深层的含义是,如何通过一系列的“装腔作势”提升你的职场竞争力,赢得机会,获得认可,最终实现“真的成功”**。
那么,如何才能通过假装来达成最终的成功呢?接下来,就让我带你走进这个充满**“假装有”与“真成功”**的奇妙世界。
从“低调内敛”到“虚伪高调”:职场的伪装艺术
在职场这个丛林里,初入职场的你,可能会觉得**“踏实肯干、低调内敛”才是好员工的标配。结果你会发现,老板们并不总是青睐于那些“只会做事不吭声”**的人。你低调的态度可能换来的是同事们的忽视,而领导眼中那种总是抢着发言、处处表现自信的人,反而成了职场的宠儿。
这时候,“Fake It Till You Make It”的原则就该派上用场了——先装出自信来。你可以从微笑开始,站直背部,记住要有眼神交流,在开会时积极举手发言,不管发言内容有多无聊。假装自己充满自信,直到你真的有了自信。对,你没有听错,职场上并不是能力最强的人会被选中,而是最懂得“装”的人更容易获得机会。
这种自信的“伪装”并不意味着你要抬高姿态、目中无人,而是要学会在恰当的时机表现自己的优点,让领导和同事看到你是一个值得信赖且能力超群的员工。当你开始给自己打上“成功”的标签时,实际上,你在塑造的,是一个成功的未来——因为别人总是愿意把机会给予那些看起来已经成功的人。
学会“装”——从外部形象到内心成长
_如果说职场上有一种“游戏规则”是“假装自己很牛逼,直到真的牛逼”,那么“装”就是你进入游戏的第一步。_外在的“装”不只是表面功夫,它会逐渐影响你的行为、思维方式,甚至改变你的人际互动模式。
例如,你会发现那些在职场中游刃有余的人,往往外表非常得体。你想要升职加薪,首先要让自己看起来像个管理者,而不仅仅是一个“执行者”。这不意味着你要立即换掉你的整个衣柜,但至少可以从一些小细节入手,比如穿一件干净的白衬衫、戴一副时尚眼镜、换一个有品位的皮包。
此外,职场上,你还得学会用“术语”。在会议中,不妨适时地插入一些行业内的术语或关键词,如“优化流程”、“高效协作”、“数据驱动”,即使你并不完全理解这些术语的含义,也要先把它们抛出来,听起来有点“术业有专攻”的味道,这样能让你在别人面前显得非常专业和有见地。
这种外部“伪装”会潜移默化地影响你的行为方式,甚至改变你在职场中的定位。因为,当你开始装作一个“大咖”时,你会越来越觉得自己“真的”是那个大咖,进而逐渐产生内心的自信,而这种自信,会成为你真正成功的驱动力。
职场社交:假装你是社交达人
在职场中,社交能力也是一个不可忽视的重要技能。其实,很多时候,一个人能否在职场上迅速上升,除了能力,往往还取决于他的“朋友圈”。如果你总是低调、害羞,甚至害怕与别人交往,那么即使你有再高的能力,也可能会因为缺乏人脉而错失机会。
_这时,学会“假装自己是个社交达人”显得尤为重要。
_你不必真的变成一个性格外向、八面玲珑的社交专家,但你可以通过一些技巧来“伪装”自己,逐渐融入到这个职场网络中。比如,在午休时主动邀请同事一起吃饭,或者偶尔给领导发个微信,问问工作上的问题,哪怕只是闲聊几句,都会让你在人际关系上“装腔作势”,形成一个积极的职场形象。
当你开始活跃在社交场合,你就会发现自己变得越来越有话题,越来越能够与不同的人建立联系,这样不仅能增加你获得职位晋升的机会,还能获得更多的资源和支持。而这些人脉,最终会成为你真正成功的助力。
逆袭法则:从“假装做事”到“真正做事”
“Fake It Till You Make It”的过程,从来不仅仅是“装”而已。它的更深层次的意义在于,假装自己的能力,直到你能够真正实现这些能力。而这个过程中,最重要的一步,就是通过“装”来激发行动。
举个例子,假设你刚刚进入公司,作为一个新人,你可能并不具备直接承担大项目的能力,但你可以通过装作“已经准备好接受挑战”的姿态,主动请求领导给你一些重要的任务。在这些任务中,虽然你可能会遇到困难和挑战,但你会发现,通过不断学习、努力和实践,你其实能够逐渐掌握这些技能,最终成为一个真正能做事的人。
这种转变并不是一蹴而就的,而是一个渐进的过程。你通过“假装”自己可以承担更大责任,最后真的承担了这些责任,并且做得越来越好。当你从外部的“假装”进入内心的“真实”时,你的职场竞争力将大大增强。
摆脱“装腔”困境,做最真实的自己
尽管“Fake It Till You Make It”是职场上一个行之有效的策略,但它并非万能。长期依赖“装腔作势”可能会导致身份迷失,甚至产生情感上的空虚感。因此,在职场的“装腔”过程中,最终的目标应当是通过提升自己的能力,真正“做”出成绩,摆脱对表象的过度依赖。
在你“装”成一个自信、成功的职场人之后,不妨逐渐开始寻找自己内心的真实想法,明确自己的职业目标,做出真正符合自己价值观的选择。这个过程中,外在的伪装和内心的成长应该逐步融合,最终让你在职场上不仅仅是一个“假装成功”的人,而是一个“真正成功”的人。
结语:职场的“伪装”是为了更好地成长
“Fake It Till You Make It”这句话,听起来可能有些浮夸,甚至让人觉得不太真实,但它实际上是一种职场中的生存策略。通过外在的“装腔”,你可以提升自信,获得机会,推动自己朝着真正的成功迈进。但最终,只有当你通过不断学习和积累,真正掌握了自己的职业技能,你才会发现,那些“假装”的外衣早已不再需要。
职场就是这样一个既充满竞争又充满机遇的地方。在这片“充满假象”的舞台上,假装自己是一位大咖,并不等于永远是个“假大咖”,而是通过一段时间的“装腔”,让自己在不断进步中成为那个真正的“大咖”。
来源:juejin.cn/post/7455749137974231092
如何实现一个通用的接口限流、防重、防抖机制
介绍
最近上了一个新项目,考虑到一个问题,在高并发场景下,我们无法控制前端的请求频率和次数,这就可能导致服务器压力过大,响应速度变慢,甚至引发系统崩溃等严重问题。为了解决这些问题,我们需要在后端实现一些机制,如接口限流、防重复提交和接口防抖,而这些是保证接口安全、稳定提供服务,以及防止错误数据 和 脏数据产生的重要手段。
而AOP适合在在不改变业务代码的情况下,灵活地添加各种横切关注点,实现一些通用公共的业务场景,例如日志记录、事务管理、安全检查、性能监控、缓存管理、限流、防重复提交等功能。这样不仅提高了代码的可维护性,还使得业务逻辑更加清晰专注,关于AOP不理解的可以看这篇文章。
最近上了一个新项目,考虑到一个问题,在高并发场景下,我们无法控制前端的请求频率和次数,这就可能导致服务器压力过大,响应速度变慢,甚至引发系统崩溃等严重问题。为了解决这些问题,我们需要在后端实现一些机制,如接口限流、防重复提交和接口防抖,而这些是保证接口安全、稳定提供服务,以及防止错误数据 和 脏数据产生的重要手段。
而AOP适合在在不改变业务代码的情况下,灵活地添加各种横切关注点,实现一些通用公共的业务场景,例如日志记录、事务管理、安全检查、性能监控、缓存管理、限流、防重复提交等功能。这样不仅提高了代码的可维护性,还使得业务逻辑更加清晰专注,关于AOP不理解的可以看这篇文章。
接口限流
接口限流是一种控制访问频率的技术,通过限制在一定时间内允许的最大请求数来保护系统免受过载。限流可以在应用的多个层面实现,比如在网关层、应用层甚至数据库层。常用的限流算法有漏桶算法(Leaky Bucket)、令牌桶算法(Token Bucket)等。限流不仅可以防止系统过载,还可以防止恶意用户的请求攻击。
限流框架大概有
- spring cloud gateway集成redis限流,但属于网关层限流
- 阿里Sentinel,功能强大、带监控平台
- srping cloud hystrix,属于接口层限流,提供线程池与信号量两种方式
- 其他:redisson、redis手撸代码
接口限流是一种控制访问频率的技术,通过限制在一定时间内允许的最大请求数来保护系统免受过载。限流可以在应用的多个层面实现,比如在网关层、应用层甚至数据库层。常用的限流算法有漏桶算法(Leaky Bucket)、令牌桶算法(Token Bucket)等。限流不仅可以防止系统过载,还可以防止恶意用户的请求攻击。
限流框架大概有
- spring cloud gateway集成redis限流,但属于网关层限流
- 阿里Sentinel,功能强大、带监控平台
- srping cloud hystrix,属于接口层限流,提供线程池与信号量两种方式
- 其他:redisson、redis手撸代码
本文主要是通过 Redisson 的分布式计数来实现的 固定窗口 模式的限流,也可以通过 Redisson 分布式限流方案(令牌桶)的的方式RRateLimiter。
在高并发场景下,合理地实施接口限流对于保障系统的稳定性和可用性至关重要。
- 自定义接口限流注解类
@AccessLimit
/**
* 接口限流
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AccessLimit {
/**
* 限制时间窗口间隔长度,默认10秒
*/
int times() default 10;
/**
* 时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 上述时间窗口内允许的最大请求数量,默认为5次
*/
int maxCount() default 5;
/**
* redis key 的前缀
*/
String preKey();
/**
* 提示语
*/
String msg() default "服务请求达到最大限制,请求被拒绝!";
}
- 利用
AOP
实现接口限流
/**
* 通过AOP实现接口限流
*/
@Component
@Aspect
@Slf4j
@RequiredArgsConstructor
public class AccessLimitAspect {
private static final String ACCESS_LIMIT_LOCK_KEY = "ACCESS_LIMIT_LOCK_KEY";
private final RedissonClient redissonClient;
@Around("@annotation(accessLimit)")
public Object around(ProceedingJoinPoint point, AccessLimit accessLimit) throws Throwable {
String prefix = accessLimit.preKey();
String key = generateRedisKey(point, prefix);
//限制窗口时间
int time = accessLimit.times();
//获取注解中的令牌数
int maxCount = accessLimit.maxCount();
//获取注解中的时间单位
TimeUnit timeUnit = accessLimit.timeUnit();
//分布式计数器
RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
if (!atomicLong.isExists() || atomicLong.remainTimeToLive() <= 0) {
atomicLong.set(0);
atomicLong.expire(time, timeUnit);
}
long count = atomicLong.incrementAndGet();
;
if (count > maxCount) {
throw new LimitException(accessLimit.msg());
}
// 继续执行目标方法
return point.proceed();
}
public String generateRedisKey(ProceedingJoinPoint point, String prefix) {
//获取方法签名
MethodSignature methodSignature = (MethodSignature) point.getSignature();
//获取方法
Method method = methodSignature.getMethod();
//获取全类名
String className = method.getDeclaringClass().getName();
// 构建Redis中的key,加入类名、方法名以区分不同接口的限制
return String.format("%s:%s:%s", ACCESS_LIMIT_LOCK_KEY, prefix, DigestUtil.md5Hex(String.format("%s-%s", className, method)));
}
}
- 调用示例实现
@GetMapping("/getUser")
@AccessLimit(times = 10, timeUnit = TimeUnit.SECONDS, maxCount = 5, preKey = "getUser", msg = "服务请求达到最大限制,请求被拒绝!")
public Result getUser() {
return Result.success("成功访问");
}
防重复提交
在一些业务场景中,重复提交同一个请求可能会导致数据的不一致,甚至严重影响业务逻辑的正确性。例如,在提交订单的场景中,重复提交可能会导致用户被多次扣款。为了避免这种情况,可以使用防重复提交技术,这对于保护数据一致性、避免资源浪费非常重要
- 自定义接口防重注解类
@RepeatSubmit
/**
* 自定义接口防重注解类
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 定义了两种防止重复提交的方式,PARAM 表示基于方法参数来防止重复,TOKEN 则可能涉及生成和验证token的机制
*/
enum Type { PARAM, TOKEN }
/**
* 设置默认的防重提交方式为基于方法参数。开发者可以不指定此参数,使用默认值。
* @return Type
*/
Type limitType() default Type.PARAM;
/**
* 允许设置加锁的过期时间,默认为5秒。这意味着在第一次请求之后的5秒内,相同的请求将被视为重复并被阻止
*/
long lockTime() default 5;
//提供了一个可选的服务ID参数,通过token时用作KEY计算
String serviceId() default "";
/**
* 提示语
*/
String msg() default "请求重复提交!";
}
- 利用
AOP
实现接口防重处理
/**
* 利用AOP实现接口防重处理
*/
@Aspect
@Slf4j
@RequiredArgsConstructor
@Component
public class RepeatSubmitAspect {
private final String REPEAT_SUBMIT_LOCK_KEY_PARAM = "REPEAT_SUBMIT_LOCK_KEY_PARAM";
private final String REPEAT_SUBMIT_LOCK_KEY_TOKEN = "REPEAT_SUBMIT_LOCK_KEY_TOKEN";
private final RedissonClient redissonClient;
private final RedisRepository redisRepository;
@Pointcut("@annotation(repeatSubmit)")
public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {
}
/**
* 环绕通知, 围绕着方法执行
* 两种方式
* 方式一:加锁 固定时间内不能重复提交
* 方式二:先请求获取token,再删除token,删除成功则是第一次提交
*/
@Around("pointCutNoRepeatSubmit(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//用于记录成功或者失败
boolean res = false;
//获取防重提交类型
String type = repeatSubmit.limitType().name();
if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
//方式一,参数形式防重提交
//通过 redissonClient 获取分布式锁,基于IP地址、类名、方法名生成唯一key
String ipAddr = IPUtil.getIpAddr(request);
String preKey = repeatSubmit.preKey();
String key = generateTokenRedisKey(joinPoint, ipAddr, preKey);
//获取注解中的锁时间
long lockTime = repeatSubmit.lockTime();
//获取注解中的时间单位
TimeUnit timeUnit = repeatSubmit.timeUnit();
//使用 tryLock 尝试获取锁,如果无法获取(即锁已被其他请求持有),则认为是重复提交,直接返回null
RLock lock = redissonClient.getLock(key);
//锁自动过期时间为 lockTime 秒,确保即使程序异常也不会永久锁定资源,尝试加锁,最多等待0秒,上锁以后 lockTime 秒自动解锁 [lockTime默认为5s, 可以自定义]
res = lock.tryLock(0, lockTime, timeUnit);
} else {
//方式二,令牌形式防重提交
//从请求头中获取 request-token,如果不存在,则抛出异常
String requestToken = request.getHeader("request-token");
if (StringUtils.isBlank(requestToken)) {
throw new LimitException("请求未包含令牌");
}
//使用 request-token 和 serviceId 构造Redis的key,尝试从Redis中删除这个键。如果删除成功,说明是首次提交;否则认为是重复提交
String key = String.format("%s:%s:%s", REPEAT_SUBMIT_LOCK_KEY_TOKEN, repeatSubmit.serviceId(), requestToken);
res = redisRepository.del(key);
}
if (!res) {
log.error("请求重复提交");
throw new LimitException(repeatSubmit.msg());
}
return joinPoint.proceed();
}
private String generateTokenRedisKey(ProceedingJoinPoint joinPoint, String ipAddr, String preKey) {
//根据ip地址、用户id、类名方法名、生成唯一的key
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String className = method.getDeclaringClass().getName();
String userId = "seven";
return String.format("%s:%s:%s", REPEAT_SUBMIT_LOCK_KEY_PARAM, preKey, DigestUtil.md5Hex(String.format("%s-%s-%s-%s", ipAddr, className, method, userId)));
}
}
- 调用示例
@PostMapping("/saveUser")
@RepeatSubmit(limitType = RepeatSubmit.Type.PARAM,lockTime = 5,timeUnit = TimeUnit.SECONDS,preKey = "saveUser",msg = "请求重复提交")
public Result saveUser() {
return Result.success("成功保存");
}
接口防抖
接口防抖是一种优化用户操作体验的技术,主要用于减少短时间内高频率触发的操作。例如,当用户快速点击按钮时,我们可以通过防抖机制,只处理最后一次触发的操作,而忽略前面短时间内的多次操作。防抖技术常用于输入框文本变化事件、按钮点击事件等场景,以提高系统的性能和用户体验。
后端接口防抖处理主要是为了避免在短时间内接收到大量相同的请求,特别是由于前端操作(如快速点击按钮)、网络重试或异常情况导致的重复请求。后端接口防抖通常涉及记录最近的请求信息,并在特定时间窗口内拒绝处理相同或相似的请求。
- 定义自定义注解
@AntiShake
// 该注解只能用于方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)// 运行时保留,这样才能在AOP中被检测到
public @interface AntiShake {
String preKey() default "";
// 默认防抖时间1秒
long value() default 1000L;
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
- 实现
AOP
切面处理防抖
@Aspect // 标记为切面类
@Component // 让Spring管理这个Bean
@RequiredArgsConstructor // 通过构造方法注入依赖
public class AntiShakeAspect {
private final String ANTI_SHAKE_LOCK_KEY = "ANTI_SHAKE_LOCK_KEY";
private final RedissonClient redissonClient;
@Around("@annotation(antiShake)") // 拦截所有标记了@AntiShake的方法
public Object aroundAdvice(ProceedingJoinPoint joinPoint, AntiShake antiShake) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
long currentTime = System.currentTimeMillis();
// 获取方法签名和参数作为 Redis 键
String ipAddr = IPUtil.getIpAddr(request);
String key = generateTokenRedisKey(joinPoint, ipAddr, antiShake.preKey());
RBucket bucket = redissonClient.getBucket(key);
Long lastTime = bucket.get();
if (lastTime != null && currentTime - lastTime < antiShake.value()) {
// 如果距离上次调用时间小于指定的防抖时间,则直接返回,不执行方法
return null; // 根据业务需要返回特定值
}
// 更新 Redis 中的时间戳
bucket.set(currentTime, antiShake.value(), antiShake.timeUnit());
return joinPoint.proceed(); // 执行原方法
}
private String generateTokenRedisKey(ProceedingJoinPoint joinPoint, String ipAddr, String preKey) {
//根据ip地址、用户id、类名方法名、生成唯一的key
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String className = method.getDeclaringClass().getName();
String userId = "seven";
return String.format("%s:%s:%s", ANTI_SHAKE_LOCK_KEY, preKey, DigestUtil.md5Hex(String.format("%s-%s-%s-%s", ipAddr, className, method, userId)));
}
}
- 调用示例代码
@PostMapping("/clickButton")
@AntiShake(value = 1000, timeUnit = TimeUnit.MILLISECONDS, preKey = "clickButton")
public Result clickButton() {
return Result.success("成功点击按钮");
}
接口防抖整体思路与防重复提交思路类似,防重复提交代码也可重用于接口防抖
关于作者
来自一线程序员Seven的探索与实践,持续学习迭代中~
本文已收录于我的个人博客:http://www.seven97.top
公众号:seven97,欢迎关注~
来源:juejin.cn/post/7408859165433364490
MySQL误删数据怎么办?
一、背景
某天,张三打算操作数据库,删除自己项目的无用数据,但是一不小心数据删多了。被误删的数据,如何恢复呢?本文将介绍相关方法,以及现成的一些工具。
例子:
有一个表
create table person
(
id bigint primary key auto_increment comment 'id',
name varchar(50) comment '名称'
) engine = innodb;
原本是要执行这条SQL语句:
delete from person where id > 500000;
不小心执行了这条SQL语句:
delete from person;
二、解决方案
处理这个问题的解决思路就是,基于binlog找回被删除的数据,将被删除的数据重新插入到数据库。
对于binlog文件来说,实际上保存的是对于数据库的正向操作。比如说,插入数据insert
,binlog中保存的也是insert
语句;删除数据delete
,binlog中保存的也是delete
语句。
因此,想要恢复被删除的数据,主要有两种方式:
描述 | 优点 | 缺点 |
---|---|---|
找到数据插入的位置,重新执行数据的插入操作 | 1. 比较方便,不需要生成逆向操作,直接执行sql脚本重新插入数据即可 2. 对binlog的模式没有限制,row模式、statement模式都能找到具体的数据 | 1. 如果数据插入之后还有更新操作,插入的数据不是最新的,会有问题 2. 如果被删除的数据比较多,插入的位置比较多,找到插入的位置比较困难 |
找到数据被删除的位置,生成逆向操作,重新执行插入操作 | 1. 只要找到数据被删除的位置即可找到所有被删除的数据,比较方便 | 1. 需要通过脚本生成逆向操作,才能将数据恢复 2. 需要保证binlog模式是row模式,才能找到被删除的数据。否则,statement模式不会找到具体的数据 |
下面就针对上面的两种方式,进行详细的讲解
1. 通用操作
首先介绍两种方式都需要使用到的一些通用的操作,主要用于设置binlog、找到binlog文件
1.1 确认binlog开启
1.1.1 查询开启状态
首先要保证binlog是开启的,不然数据肯定是没办法恢复回来的。
在MySQL中,可以通过执行以下SQL查询来检查是否已经开启了binlog:
SHOW VARIABLES LIKE 'log_bin';
这个查询将返回一个结果集,其中包含名为log_bin
的系统变量的值。如果log_bin
的值为ON
,则表示binlog已经开启;如果值为OFF
,则表示binlog没有开启。
mysql> SHOW VARIABLES LIKE 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin | ON |
+---------------+-------+
1 row in set (0.01 sec)
1.1.2 开启binlog
如果发现没有开启,可以通过修改MySQL配置文件(通常是my.cnf
或my.ini
,Linux下MySQL的配置文件目录一般是/etc/mysql
)中的[mysqld]
部分来开启binlog。如果在配置文件中找到了类似以下的设置,则表示binlog已经开启:
[mysqld]
log-bin=mysql-bin
server-id=1
- 修改配置启用了binlog之后,需要重启MySQL服务才能使更改生效
mysql-bin
表示binlog文件的前缀
- server-id 设置了MySQL服务器的唯一ID,必须设置ID,否则没办法开启binlog
1.2 binlog模式
刚刚提到,对于delete操作,只有row模式才能找到被删除数据的具体值,因此需要确认开启的binlog模式。
1.2.1 查询binlog模式
要查询MySQL的binlog模式,您可以使用以下SQL命令:
SHOW VARIABLES LIKE 'binlog_format';
这将返回一个结果集,其中包含当前的binlog格式。可能的值有:
ROW
:表示使用行模式(row-based replication),这是推荐的设置,因为它提供了更好的数据一致性。STATEMENT
:表示使用语句模式(statement-based replication),在这种模式下,可能会丢失一些数据,因为它仅记录执行的SQL语句。MIXED
:表示混合模式(mixed-based replication),在这种模式下,MySQL会根据需要自动切换行模式和语句模式。
1.2.2 配置binlog模式
可以通过修改MySQL配置文件(通常是my.cnf
或my.ini
,Linux下MySQL的配置文件目录一般是/etc/mysql
)中的[mysqld]
部分来修改binlog模式。
在[mysqld]
部分下,添加或修改以下行,将binlog_format
设置为想要的模式(ROW
、STATEMENT
或MIXED
):
[mysqld]
binlog_format=ROW
随后重启mysql服务使其生效
1.3 binlog信息查询
通过以下操作,我们可以找到binlog文件
1.3.1 查询当前使用的binlog文件
通过show master status;
可以找到当前正在使用的binlog文件
mysql> show master status\G
*************************** 1. row ***************************
File: mysql-bin.000217
Position: 668127868
Binlog_Do_DB:
Binlog_Ignore_DB:
Executed_Gtid_Set: 29dc2bf9-f657-11ee-b369-08c0eb829a3c:1-291852745,
744ca9cd-5f86-11ef-98d6-0c42a131d16f:1-5374311
1 row in set (0.00 sec)
1.3.2 找到所有binlog文件名
show master logs;
可以找到所有binlog文件名
mysql> show master logs;
+------------------+------------+
| Log_name | File_size |
+------------------+------------+
| mysql-bin.000200 | 1073818388 |
| mysql-bin.000201 | 1073757563 |
| mysql-bin.000202 | 1074635635 |
| mysql-bin.000203 | 1073801053 |
| mysql-bin.000204 | 1073856643 |
| mysql-bin.000205 | 1073910661 |
| mysql-bin.000206 | 1073742603 |
| mysql-bin.000207 | 1195256434 |
| mysql-bin.000208 | 1085915611 |
| mysql-bin.000209 | 1073990985 |
| mysql-bin.000210 | 1075942323 |
| mysql-bin.000211 | 1074716392 |
| mysql-bin.000212 | 1073763938 |
| mysql-bin.000213 | 1073780482 |
| mysql-bin.000214 | 1074029712 |
| mysql-bin.000215 | 1073832842 |
| mysql-bin.000216 | 1079999184 |
| mysql-bin.000217 | 668173793 |
+------------------+------------+
1.3.3 查询binlog保存位置
SHOW VARIABLES LIKE 'log_bin_basename';
可以找到binlog文件保存的目录位置。比如说/var/lib/mysql/mysql-bin
表示目录为/var/lib/mysql/
下的以mysql-bin
为前缀的文件。
我们通过文件的最后修改时间,可以看出binlog覆盖的时间范围。一般后缀的数字越大,表示越新。
mysql> SHOW VARIABLES LIKE 'log_bin_basename';
+------------------+--------------------------+
| Variable_name | Value |
+------------------+--------------------------+
| log_bin_basename | /var/lib/mysql/mysql-bin |
+------------------+--------------------------+
1 row in set (0.00 sec)
bash-4.2# ls /var/lib/mysql/mysql-bin* -alh
-rw-r----- 1 mysql mysql 1.1G Sep 9 02:28 /var/lib/mysql/mysql-bin.000200
-rw-r----- 1 mysql mysql 1.1G Sep 9 02:32 /var/lib/mysql/mysql-bin.000201
-rw-r----- 1 mysql mysql 1.1G Sep 9 02:39 /var/lib/mysql/mysql-bin.000202
-rw-r----- 1 mysql mysql 1.1G Sep 9 02:45 /var/lib/mysql/mysql-bin.000203
-rw-r----- 1 mysql mysql 1.1G Sep 9 07:52 /var/lib/mysql/mysql-bin.000204
-rw-r----- 1 mysql mysql 1.1G Sep 9 12:10 /var/lib/mysql/mysql-bin.000205
-rw-r----- 1 mysql mysql 1.1G Sep 10 04:40 /var/lib/mysql/mysql-bin.000206
-rw-r----- 1 mysql mysql 1.2G Sep 10 07:00 /var/lib/mysql/mysql-bin.000207
-rw-r----- 1 mysql mysql 1.1G Sep 11 07:54 /var/lib/mysql/mysql-bin.000208
-rw-r----- 1 mysql mysql 1.1G Sep 12 03:03 /var/lib/mysql/mysql-bin.000209
-rw-r--r-- 1 root root 24M Sep 11 09:06 /var/lib/mysql/mysql-bin.000209.event.log
-rw-r----- 1 mysql mysql 1.1G Sep 12 03:30 /var/lib/mysql/mysql-bin.000210
-rw-r----- 1 mysql mysql 1.1G Sep 12 08:33 /var/lib/mysql/mysql-bin.000211
-rw-r----- 1 mysql mysql 1.1G Sep 12 08:35 /var/lib/mysql/mysql-bin.000212
-rw-r----- 1 mysql mysql 1.1G Sep 12 22:00 /var/lib/mysql/mysql-bin.000213
-rw-r----- 1 mysql mysql 1.1G Sep 13 10:26 /var/lib/mysql/mysql-bin.000214
-rw-r----- 1 mysql mysql 1.1G Sep 13 10:29 /var/lib/mysql/mysql-bin.000215
-rw-r----- 1 mysql mysql 1.1G Sep 14 01:42 /var/lib/mysql/mysql-bin.000216
-rw-r----- 1 mysql mysql 637M Sep 14 06:11 /var/lib/mysql/mysql-bin.000217
-rw-r----- 1 mysql mysql 4.1K Sep 14 01:42 /var/lib/mysql/mysql-bin.index
2. 方案一:找到insert语句,重新插入
需要执行以下几个步骤:
- 确认
insert
插入数据的时间,找到对应的binlog文件 - 解析该binlog文件,指定时间点,在binlog文件中找到插入数据的位置
- 重新解析binlog文件,指定binlog位置。对解析出来的文件进行重放。
2.1 找到binlog文件
比如说,数据是在9月12日12:00插入的,那么我们看上方的所有binlog文件,可以看出插入语句应该保存在mysql-bin.000213文件中。
2.2 根据时间点解析binlog文件
通过mysqlbinlog
将binlog文件解析成可读的sql文件。
mysqlbinlog --base64-output=decode-rows -v --start-datetime="2024-09-12 11:59:00" --stop-datetime="2024-09-12 12:01:00" mysql-bin.000213 > binlog.sql
--base64-output=decode-rows
:将二进制日志中的行事件解码为 SQL 语句。
-v
或--verbose
:输出详细的事件信息。
--start-datetime="2024-09-12 11:59:00"
:从指定的日期和时间开始读取二进制日志。通过指定时间范围,可以减小解析出来的sql文件,避免太多无用信息使得查询位置比较困难。
--stop-datetime="2024-09-12 12:01:00"
:在指定的日期和时间停止读取二进制日志。
mysql-bin.000213
:要解析的二进制日志文件的路径和名称。
>
:将命令的输出重定向到指定的文件。
binlog.sql
:保存解码后的 SQL 语句的文件名。
2.2.1 statement模式确认binlog位置
我们可以找到insert int0 person values (1, 'first')
,并且分别在前后的BEGIN
和COMMIT
找到position。
BEGIN
往前找有一个position at 219
,COMMIT
往后找有一个position at 445
,这就是插入语句的实际binlog范围。
# at 219
#240914 17:14:26 server id 1 end_log_pos 300 CRC32 0xb8159bc1 Query thread_id=1267 exec_time=0 error_code=0
SET TIMESTAMP=1726305266/*!*/;
SET @@session.pseudo_thread_id=1267/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1436549120/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C latin1 *//*!*/;
SET @@session.character_set_client=8,@@session.collation_connection=8,@@session.collation_server=8/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
BEGIN
/*!*/;
# at 300
#240914 17:14:26 server id 1 end_log_pos 414 CRC32 0xb7e0263b Query thread_id=1267 exec_time=0 error_code=0
use `tests`/*!*/;
SET TIMESTAMP=1726305266/*!*/;
insert int0 person values (1, 'first')
/*!*/;
# at 414
#240914 17:14:26 server id 1 end_log_pos 445 CRC32 0x9345e6ca Xid = 30535
COMMIT/*!*/;
# at 445
2.2.2 row模式确认binlog位置
row模式下,与statement模式下的binlog格式有少许差别,但方法是一致的。
我们可以找到以 ###
开头的几行,包含INSERT INTO
语句。并且分别在前后的BEGIN
和COMMIT
找到position。
BEGIN
往前找有一个position at 219
,COMMIT
往后找有一个position at 426
,这就是插入语句的实际binlog范围。
# at 219
#240914 17:16:36 server id 1 end_log_pos 292 CRC32 0xe9082d52 Query thread_id=20 exec_time=0 error_code=0
SET TIMESTAMP=1726305396/*!*/;
SET @@session.pseudo_thread_id=20/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=1436549120/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C latin1 *//*!*/;
SET @@session.character_set_client=8,@@session.collation_connection=8,@@session.collation_server=8/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
BEGIN
/*!*/;
# at 292
#240914 17:16:36 server id 1 end_log_pos 345 CRC32 0x1832ced4 Table_map: `tests`.`person` mapped to number 111
# at 345
#240914 17:16:36 server id 1 end_log_pos 395 CRC32 0x32d6a21b Write_rows: table id 111 flags: STMT_END_F
### INSERT INTO `tests`.`person`
### SET
### @1=1
### @2='first'
# at 395
#240914 17:16:36 server id 1 end_log_pos 426 CRC32 0x07619928 Xid = 149
COMMIT/*!*/;
# at 426
2.3 根据binlog位置解析binlog文件
上一步,我们已经找到了具体的位置,现在我们可以重新解析binlog文件,指定binlog位置。内容和上方实际上没有太大差异。
mysqlbinlog --start-position=219 --stop-position=426 mysql-bin.000213 > binlog.sql
需要注意的是,这个解析语句,删掉了--base64-output=decode-rows -v 参数。因为这些参数是用于解码binlog的,是让开发人员更方便看到binlog被解析之后的格式。但是对mysql来说是没办法使用的。
2.4 重放数据
解析的这个文件就是一个sql脚本文件,通过往常的方式执行sql脚本即可
mysql -uroot -proot < binlog.sql
或者mysql客户端登陆之后,通过source命令执行
source binlog.sql;
3. 方案二:找到delete语句,生成逆向操作,重新insert
3.1 找到binlog文件
比如说,数据是在9月12日12:00删除的,那么我们看上方的所有binlog文件,可以看出插入语句应该保存在mysql-bin.000213文件中。
3.2 根据时间点解析binlog文件
操作和上方2.2的操作没有差异,我们主要来比较一下statement模式和row模式的差别。我们会发现statement模式下,没办法找到所有被删除的数据的具体数据,而row模式能找到。
3.2.1 statement模式
我们可以看到binlog只保存了一句 delete from person
。很遗憾,啥数据都没有,也没办法根据它生成逆向操作。
# at 445
#240914 17:15:13 server id 1 end_log_pos 510 CRC32 0x6a7a66e4 Anonymous_GTID last_committed=1 sequence_number=2 rbr_only=no
SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/;
# at 510
#240914 17:15:13 server id 1 end_log_pos 591 CRC32 0x55e4225b Query thread_id=1267 exec_time=0 error_code=0
SET TIMESTAMP=1726305313/*!*/;
BEGIN
/*!*/;
# at 591
#240914 17:15:13 server id 1 end_log_pos 685 CRC32 0x10938b9d Query thread_id=1267 exec_time=0 error_code=0
SET TIMESTAMP=1726305313/*!*/;
delete from person
/*!*/;
# at 685
#240914 17:15:13 server id 1 end_log_pos 716 CRC32 0x1ea4a681 Xid = 30610
COMMIT/*!*/;
# at 716
3.2.2 row模式
可以看到binlog以 ###
开头的几行,WHERE
之后,把被删除数据的每一个字段都作为条件嵌入到sql语句中了。条件的顺序,就是表结构的字段顺序。比如说@1
对应的就是id
,@2
对应的就是name
。
# at 1574
#240914 17:16:38 server id 1 end_log_pos 1642 CRC32 0x944b1b94 Query thread_id=20 exec_time=1260 error_code=0
SET TIMESTAMP=1726305398/*!*/;
BEGIN
/*!*/;
# at 1642
#240914 17:16:38 server id 1 end_log_pos 1695 CRC32 0x435282e2 Table_map: `tests`.`person` mapped to number 111
# at 1695
#240914 17:16:38 server id 1 end_log_pos 1745 CRC32 0x3063bf8c Delete_rows: table id 111 flags: STMT_END_F
### DELETE FROM `tests`.`person`
### WHERE
### @1=1
### @2='first'
# at 1745
#240914 17:16:38 server id 1 end_log_pos 1776 CRC32 0x086c2270 Xid = 3391
COMMIT/*!*/;
3.3 生成逆向操作
根据上面的binlog文件,我们可以通过脚本生成逆向操作。
insert int0 person values (1, 'first');
3.4 重放数据
与 2.4 一致
三、常见工具
目前有一些开源的工具,可以帮助我们解析binlog,并且自动生成binlog记录的操作的逆向操作。
1. binlog2mysql
binlog2sql由美团点评DBA团队(上海)出品,python脚本实现。主要原理是伪装成slave,向master获取binlog,并且根据binlog生成逆向操作。
下载地址:GitHub - danfengcao/binlog2sql: Parse MySQL binlog to SQL you want
在执行之前,需要确认mysql server已设置以下参数:
[mysqld]
server_id = 1
log_bin = /var/log/mysql/mysql-bin.log
max_binlog_size = 1G
binlog_format = row
binlog_row_image = full
获取正向操作:
> python binlog2sql.py -h127.0.0.1 -P13306 -uroot -p --start-file=mysql-bin.000002
Password:
INSERT INTO `tests`.`person`(`id`, `name`) VALUES (1, 'first'); #start 4 end 395 time 2024-09-14 17:16:36
DELETE FROM `tests`.`person` WHERE `id`=1 AND `name`='first' LIMIT 1; #start 426 end 667 time 2024-09-14 17:16:38
- 通过命令,输入用户名、密码、端口号、地址等,并且指定binlog文件
- 通过输出,可以看出所有正向操作,以及每个正向操作的时间、binlog位置
获取逆向操作:
> python binlog2sql.py -h127.0.0.1 -P13306 -uroot -p --start-file=mysql-bin.000002 --flashback
Password:
INSERT INTO `tests`.`person`(`id`, `name`) VALUES (1, 'first'); #start 426 end 667 time 2024-09-14 17:16:38
DELETE FROM `tests`.`person` WHERE `id`=1 AND `name`='first' LIMIT 1; #start 4 end 395 time 2024-09-14 17:16:36
- 命令中,新增一个参数 --flashback,用于指定回滚
- 通过输出,可以看出所有逆向操作。并且可以看出相对于正向操作来说,逆向操作的顺序是相反的,按时间从后往前排序
还有其他工具,比如说MyFlash等,大家可以自行研究
MyFlash:GitHub - Meituan-Dianping/MyFlash: flashback mysql data to any point
四、总结
- 我们可以通过binlog找回误删的数据,前提是开启了binlog。建议binlog模式为row模式,否则没办法根据正向操作生成逆向操作。
- 有一些开源工具可以自动解析binlog,并且生成逆向操作。
来源:juejin.cn/post/7416737238614589503
Spring Boot3,启动时间缩短 10 倍!
前面松哥写了一篇文章和大家聊了 Spring6 中引入的新玩意 AOT(见Spring Boot3 新玩法,AOT 优化!)。
文章发出来之后,有小伙伴问松哥有没有做性能比较,老实说,这个给落下了,所以今天再来一篇文章,和小伙伴们梳理比较小当我们利用 Native Image 的时候,Spring Boot 启动性能从参数上来说,到底提升了多少。
先告诉大家结论:启动速度提升 10 倍以上。
1. Native Image
1.1 GraalVM
不知道小伙伴们有没有注意到,现在当我们新建一个 Spring Boot 工程的时候,再添加依赖的时候有一个 GraalVM Native Support
,这个就是指提供了 GraalVM 的支持。
那么什么是 GraalVM 呢?
GraalVM 是一种高性能的通用虚拟机,它为 Java 应用提供 AOT 编译和二进制打包能力,基于 GraalVM 打出的二进制包可以实现快速启动、具有超高性能、无需预热时间、同时需要非常少的资源消耗,所以你把 GraalVM 当作 JVM 来用,是没有问题的。
在运行上,GraalVM 同时支持 JIT 和 AOT 两种模式:
- JIT 是即时编译(Just-In-Time Compilation)的缩写。它是一种在程序运行时将代码动态编译成机器码的技术。与传统的静态编译(Ahead-of-Time Compilation)不同,静态编译是在程序执行之前将代码编译成机器码,而 JIT 编译器在程序运行时根据需要将代码片段编译成机器码,然后再运行。所以 JIT 的启动会比较慢,因为编译需要占用运行时资源。我们平时使用 Oracle 提供的 Hotspot JVM 就属于这种。
- AOT 是预先编译(Ahead-of-Time Compilation)的缩写。它是一种在程序执行之前将代码静态编译成机器码的技术。与即时编译(JIT)不同,即时编译是在程序运行时动态地将代码编译成机器码。AOT 编译器在程序构建或安装阶段将代码转换为机器码,然后在运行时直接执行机器码,而无需再进行编译过程。这种静态编译的方式可以提高程序的启动速度和执行效率,但也会增加构建和安装的时间和复杂性。AOT 编译器通常用于静态语言的编译过程,如 C、C++ 等。
如果我们在 Java 应用程序中使用了 AOT 技术,那么我们的 Java 项目就会被直接编译为机器码可以脱离 JVM 运行,运行效率也会得到很大的提升。
那么什么又是 Native Image 呢?
1.2 Native Image
Native Image 则是 GraalVM 提供的一个非常具有特色的打包技术,这种打包方式可以将应用程序打包为一个可脱离 JVM 在本地操作系统上独立运行的二进制包,这样就省去了 JVM 加载和字节码运行期预热的时间,提升了程序的运行效率。
Native Image 具备以下特点:
- 即时启动:由于不需要 JVM 启动和类加载过程,Native Image 可以实现快速启动和即时执行。
- 减少内存占用:编译成本地代码后,应用程序通常会有更低的运行时内存占用,因为它们不需要 JVM 的额外内存开销。
- 静态分析:在构建 Native Image 时,GraalVM 使用静态分析来确定应用程序的哪些部分是必需的,并且只包含这些部分,这有助于减小最终可执行文件的大小。
- 即时性能:虽然 JVM 可以通过JIT(Just-In-Time)编译在运行时优化代码,但 Native Image 提供了即时的、预先优化的性能,这对于需要快速响应的应用程序特别有用。
- 跨平台兼容性:Native Image 可以为不同的操作系统构建特定的可执行文件,包括 Linux、macOS 和 Windows,即在 Mac 和 Linux 上自动生成系统可以执行的二进制文件,在 Windows 上则自动生成 exe 文件。
- 安全性:由于 Native Image 不依赖于 JVM,因此减少了 JVM 可能存在的安全漏洞的攻击面。
- 与 C 语言互操作:Native Image 可以与本地 C 语言库更容易地集成,因为它们都是在同一环境中运行的本地代码。
根据前面的介绍大家也能看到,GraalVM 所做的事情就是在程序运行之前,该编译的就编译好,这样当程序跑起来的时候,运行效率就会高,而这一切,就是利用 AOT 来实现的。
但是!对于一些涉及到动态访问的东西,GraalVM 似乎就有点力不从心了,原因很简单,GraalVM 在编译构建期间,会以 main 函数为入口,对我们的代码进行静态分析,静态分析的时候,一些无法触达的代码会被移除,而一些动态调用行为,例如反射、动态代理、动态属性、序列化、类延迟加载等,这些都需要程序真正跑起来才知道结果,这些就无法在编译构建期间被识别出来。
而反射、动态代理、序列化等恰恰是我们 Java 日常开发中最最重要的东西,不可能我们为了 Native Image 舍弃这些东西!因此,从 Spring6(Spring Boot3)开始支持 AOT Processing!AOT Processing 用来完成自动化的 Metadata 采集,这个采集主要就是解决反射、动态代理、动态属性、条件注解动态计算等问题,在编译构建期间自动采集相关的元数据信息并生成配置文件,然后将 Metadata 提供给 AOT 编译器使用。
道理搞明白之后,接下来通过一个案例来感受下 Native Image 的威力吧!
2. 准备工作
首先需要我们安装 GraalVM。
GraalVM 下载地址:
下载下来之后就是一个压缩文件,解压,然后配置一下环境变量就可以了,这个默认大家都会,我就不多说了。
GraalVM 配置好之后,还需要安装 Native Image 工具,命令如下:
gu install native-image
装好之后,可以通过如下命令检查安装结果:
另一方面,Native Image 在进行打包的时候,会用到一些 C/C++ 相关的工具,所以还需要在电脑上安装 Visual Studio 2022,这个我们安装社区版就行了(visualstudio.microsoft.com/zh-hans/dow…
下载后双击安装就行了,安装的时候选择 C++ 桌面应用开发。
如此之后,准备工作就算完成了。
3. 实践
接下来我们创建一个 Spring Boot 工程,并且引入如下两个依赖:
然后我们开发一个接口:
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@GetMapping("/hello")
public String hello() {
return helloService.sayHello();
}
}
@Service
public class HelloService {
public String sayHello() {
return "hello aot";
}
}
这是一个很简单的接口,接下来我们分别打包成传统的 jar 和 Native Image。
传统 jar 包就不用我多说了,大家执行 mvn package 即可:
mvn package
打包完成之后,我们看下耗时时间:
耗时不算很久,差不多 3.7s 左右,算是比较快了,最终打成的 jar 包大小是 18.9MB。
再来看打成原生包,执行如下命令:
mvn clean native:compile -Pnative
这个打包时间就比较久了,需要耐心等待一会:
可以看到,总共耗时 4 分 54 秒。
Native Image 打包的时候,如果我们是在 Windows 上,会自动打包成 exe 文件,如果是 Mac/Linux,则生成对应系统的可执行文件。
这里生成的 aot_demo.exe 文件大小是 82MB。
两种不同的打包方式,所耗费的时间完全不在一个量级。
再来看启动时间。
先看 jar 包启动时间:
耗时约 1.326s。
再来看 exe 文件的启动时间:
好家伙,只有 0.079s。
1.326/0.079=16.78
启动效率提升了 16.78 倍!
我画个表格对比一下这两种打包方式:
jar | Native Image | |
---|---|---|
包大小 | 18.9MB | 82MB |
编译时间 | 3.7s | 4分54s |
启动时间 | 1.326s | 0.079s |
从这张表格中我们可以看到,Native Image 在打包的时候比较费时间,但是一旦打包成功,项目运行效率是非常高的。Native Image 很好的解决了 Java 冷启动耗时长、Java 应用需要预热等问题。
最后大家可以自行查看打包成 Native Image 时候的编译结果,如下图:
看过松哥之前将的 Spring 源码分析的小伙伴,这块的代码应该都很好明白,这就是直接把 BeanDefinition 给解析出来了,不仅注册了当前 Bean,也把当前 Bean 所需要的依赖给注入了,将来 Spring 执行的时候就不用再去解析 BeanDefinition 了。
同时我们可以看到在 META-INF 中生成了 reflect、resource 等配置文件。这些是我们添加的 native-maven-plugin 插件所分析出来的反射以及资源等信息,也是 Spring AOT Processing 这个环节处理的结果。
来源:juejin.cn/post/7330071686489817128
2025年微信授权登录全方案
经常做小程序的小伙伴都曾遇到过这个问题,那就是小程序授权登录,官方文档经常更新,API不时更新迭代,对于基础库版本等等,都有着既定要求。
按照以往方式再去做,发现行不通,折腾一上午,整个人沉默了,原来是官方又进行了大改......
对于小程序登录的问题,借着2025开年之际,给宝子们做一期盘点,来一期《小程序授权登录复盘全攻略》,日常做需求时,大家尽管放心食用,话不多说,直接开始正题!
拜托拜托:点赞收藏是我更新的最大动力!!!
目前为止,微信小程序登录授权,有三种方式,第一种是无感登录,第二种是手机号登录,第三种是用户信息授权登录
比较经典的就是飞猪小程序和顺丰小程序,如果你不知道怎么做,可以去看看它们的样式,一目了然
比如下面这个登录首页面,就属于经典的手机号登陆
点击快速登录,小程序会迅速调用用户的手机号授权
再放一个案例,那就是用户信息授权登录,顾名思义,该登陆主要为了获取用户信息(姓名、性别、地址、昵称等等),用于给个人中心模块做铺垫,图中人名我这边和谐掉,因为也是我前两天刚做完的业务。
至于无感登录,这里就不演示了,因为无感无感,顾名思义就是没有感觉,用户是看不出来授权的,所以直接讲方法就好!
顺便放上小程序开发文档:微信小程序官方文档
1.无感登录
首先无感登录是最简单的,步骤只有两步,第一步是前端调用官方文档API——wx.login,拿到登陆凭证code,通过wx.request()发起网络请求,随即传给后端。
第二步,后端那边利用code + appid + appsecret这三个数值,调用微信的auth.code2Session接口,拿到用户唯一标识openid 和 会话密钥session_key,随即定义token,将之与openid和session_key关联,最后再返回给前端。
前端拿到token,就很简单了,按照正常操作即可,比如拿token设置请求头、存入vuex、pinia等等,顺理成章直接写即可,大家都能明白。
总结一点:无感登录,说白了,就是拿小程序token的过程,够直白了吧!
至于有人会问,appid和appsecret是什么?看下图即可!(都在你的小程序后台里)
微信小程序开放平台:官方传送门
具体讲一下方法,顺便把代码附在下面,大家可以直接移植!
首先要注意的是,调用wx.login的时候,你的小程序基础库版本,不能低于2.3.1
可以在你的原生小程序工具查看
其次再看看wx.login的传参+返回值,重点关注success和fail,一个是成功回调,一个是失败回调
返回值是code,有时效限制,这里要注意的是,前端的appId,要和后端的appId一致。
有的人拿不同的appId去调用接口,最后会导致500报错!
代码示例:(用uniapp的,将wx.login替换为uni.login即可),example.com/onLogin 这个是事例网址,需要替换为你们后端的接口,主要用来获取token。
wx.login({
success (res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'https://example.com/onLogin',
data: {
code: res.code
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
2.手机号登陆
需要注意的是,个人账号,无法使用手机号登录功能,并且该功能是收费的。
标准单价每次组件调用成功,收0.03元,每个小程序账号将有1000次体验额度,该1000次的体验额度为正式版、体验版和开发版小程序共用,超额后,体验版和开发版小程序调用同正式版小程序一样,均收费。
这一要说明一点的是,相信很多人在网上都看到类似encryptedData、iv获取手机号的方法,25年为止,微信又改版了,手机号登录的流程又得到了简化。(前提是使用付费服务)
流程为:调用bindgetphonenumber,返还code,这个code是限时+一次性的,服务器只需要拿着这个code去和微信换手机号就可以了
传送门一:官方手机组件
传送门二获取手机号最新方法
`
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"></button>
`
这里要注意一点,如果你用的是uniapp,那么bindgetphonenumber需要换为@getphonenumber
还是通过wx.login拿code,然后调用这个接口,具体要和后端商量,前端的工作并不多,调用而已
参数需要这几个
与此同时,返回值手机号就来了
所以手机号登录没那么复杂,重点是需要付费,不付费的话,让用户自行输入表单,也行,看具体业务实现方式
3.用户信息授权登录
对于用户授权登录的问题,那么就绕不过wx.getUserInfo和wx.getUserProfile的历史渊源了。
早期的小程序开发,大家都是通过wx.getUserInfo拿到用户头像昵称,结果2021年4月,微信社区改版,导致getUserInfo不再有授权流程,开发者只能获取到匿名信息。
比如名字,大家都叫做“微信用户”,而头像,接口返回的都是统一灰色头像。
可这样就带来一个问题,那就是不同用户,昵称头像都一样,完全不方面管理,所以wx.getUserProfile接口应运而生!
这一有一个行为,大家要注意,wx.getUserInfo获取用户信息,不会有底部弹窗,而wx.getUserProfile则会出现下方的底部弹窗(样式看开头),根据你的需求自行选择。
再到2022年10月,微信社区又改版了,就连wx.getUserProfile这个接口,也不给开发者权限了,用户名+头像,全部变成了统一的“微信用户”+灰色头像。
所以如果你实在想获取用户信息,那么利用组件,让用户自行填写,是不错的选择。
`
getUserProfile(e) {
wx.getUserProfile({
desc: '用于完善会员资料',
success: (res) => {
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
}
`
来源:juejin.cn/post/7457926197163687970
程序员如何避免出纰漏?
这两天我们开发团队不知道咋的,跟包饺子下锅似的接连出了不少纰漏,有的大有的小,其实开发能力都可以,不是那种能力差导致的问题,我从外部观察,总结了一些出纰漏的原因和解决方案。
先说一下有啥纰漏。
- 小程序代码分包的时候,影响到线上正在使用的业务,损失了大概 1 晚上的流量。
- 上了身-份-证、人脸认证功能,测试回归的时候,测了不需要实名和人脸的场景,没测只需要身-份-证的场景,结果线上跑的时候用这个场景,导致功能也出了问题,用户反馈过来才发现。
- 错把代码提交到了 dev 分支。
看起来研发该死,但恐怕不全是研发的锅,当然我不是故意找理由,这些纰漏也是研发扛下来了,我只是尝试分析从更具体的原因分析,而不是简单的说一句能力太差、或者水平不够这样没法定位也没法改进的原因。
这些出问题的场景,无一例外都是很紧急的需求,开发加班加点做出来的,代码写的时候很匆忙,测试也是加班加点测的。
常在河边走,哪有不湿鞋?我觉得快和稳之间,对开发来说很难平衡,有些需求强行要那个时间点,最后只能牺牲稳定性求效率。
那怎么避免这种事情发生?
需求可以 delay,代码不能出问题
如果工作量实在大,那就先花点时间列举工作量大的原因。大部分领导其实讲道理的,你能像他说明工作量的确大,事项的确做不完,领导会额外给时间。
我觉得这是比只闷头写代码更有难度的事,也是一种能力的体现,这需要调研充分、思维清晰、表达有条理、和领导沟通的心理等等各项挑战。
只知道埋头苦干,但干不多不一定就是好。
万一要是真的只知道埋头苦干,那也要掌控好自己的节奏,一定要保证代码的质量,平时加加班,周末也来加班,通过拉长时间线的方式多写点代码,而不是通过偷懒、减少代码逻辑的方式。
加班的时候冒冒泡,留点记录,这样即使需求 delay 了,至少自己的态度表达到位了,一般领导也不会责怪。
需求这东西,delay 两天没那么恐怖,反倒是着急出了纰漏,那才是更恐怖的。
慢慢写
写代码很费脑子,要考虑到所有可能的异常场景,还要从业务上闭环,一着急,就容易漏场景,出纰漏,不要着急,细水长流。
想好再写
尤其是后端,新业务的架构设计,一定是要多花时间思考的,要充分考虑到业务的扩展性、未来的维护性、和其他业务对接的兼容性。
比如我最近写的京东商户订单支付,我们已有一套支付中心的系统,而在对接京东的时候,他们的支付其实是通过京东的订单状态回调来做的,我们一开始准备写在支付中心,后来随着三方接口的对接,对京东业务有更深的理解,我们决定做一套新的商户订单支付系统,和原本的支付中心(支付宝、微信支付)做区分。
如果我们当时匆忙的直接嵌入到支付中心,整个系统架构就会很混乱,订单和支付裹在一起,后续既不好维护,也不好扩展。
这样虽然需求有 delay 风险,但整体技术侧的方案,是绝对没问题的。大不了我周末来加班,加班都干不完,那就得赶紧汇报领导了。
包括最开始的人脸也是的,没有调研清楚,光阿里就两套不同的人脸接口,结果先用的贵的一套,后面发现有便宜的,又强行接入便宜的一套。如果一开始能多想想,先调研清楚,可能最后的工时反而更短一些。
专一写代码不要跳
写代码的时候,最好不要来回跳需求写,看起来很牛逼感觉也很吊,实际上很容易出问题,精力消耗太快了,有些场景思考不深入,就有可能埋雷。
决策和执行分开
如果开发过程中又做决策又做执行,尤其是干需求的时候,有的决策问起来吧很耗时间,需求到期上不了线了,就自己做个决策,没有告知其他人。这种场景的雷我也踩了几个。
开发对业务的理解不如运营产品深入,有时候开发觉得的最优决策不是运营想要的,最好不要为了图省隐蔽这些问题。
甩锅技巧
这部分是语言的艺术,就是当纰漏下来了,判责归自己,怎么表达,才是比较得体的。
一直说自己的责任吧,领导会觉得我很菜,一直推脱责任吧,领导又会觉得我不负责。
最好是那种和自己有点关系,但是关系不是那么直接的描述。
或是用于日常沟通,为了避免别人误把锅扣到自己头上。
我总结了同事们常用的有如下技巧:
- 首先主体对象不要说自己,比如分包的问题、锁的问题、分支的问题、没有这样的场景等等,避免说成我打的包有问题、我代码写的有问题、我分支切的不对等等。
- 先说一些撇开自己责任的话术,比如这里的代码没动过;之前还是好好的;这里用的外部接口的数据/逻辑。
- 接到莫名其妙的锅第一时间弹回去,怎么弹看 2 中的话术。我以前懒得弹,结果头上的锅越来越多。
挺瞧不上这些东西的,也不想花心思想,但有时候职场、工作、社会就是这么贱,人越是老实,就越容易被欺负;越能干活的人,最后会有越来越多的活;希望大家不要重蹈我的覆辙。
每天抽点时间学习和反思,加油,共勉。
来源:juejin.cn/post/7453023172226334739
一个Kotlin版Demo带你入门JNI,NDK编程
Android 越往深处研究,必然离不开NDK,和JNI相关知识
一、前言
Android开发中,最重要的一项技能便是NDK开发,它涉及到JNI,C,C++等相关知识
我们常见的MMKV,音视频库FFmpeg等库的应用,都有相关这方面的知识,它是Android开发人员通往深水区的一张门票。
本文我们就简单介绍JNI,NDK的相关入门知识:
1. JNI方法注册(静态注册,动态注册)
2. JNI的基础数据类型
3. JNI引用数据类型
4. JNI函数签名信息
5. JNIEnv的介绍
6. JNI编译之Cmake
7. 示例:获取JNI返回字符串(静态注册)
8. 示例:调用JNI,JNI调用Java层无返回值方法(静态注册)
9. 示例:调用JNI,JNI调用Java层无返回值方法(带参数)(静态注册)
10. 示例:调用JNI去调用java方法(静态注册)
11. 示例:调用JNI去调用java 变量值(静态注册)
12. 示例:去调用JNI去调用Java带callback方法,带参数(静态注册)
13. 示例:动态注册
二、基础介绍
1. JNI是什么? JNI(Java Native Interface),它是提供一种Java字节码调用C/C++的解决方案,JNI描述的是一种技术。
2. NDK是什么? NDK(Native Development Kit)
Android NDK 是一组允许您将 C 或 C++(“原生代码”)嵌入到 Android 应用中的工具,NDK描述的是工具集。 能够在 Android 应用中使用原生代码对于想执行以下一项或多项操作的开发者特别有用:
- 在平台之间移植其应用。
- 重复使用现有库,或者提供其自己的库供重复使用。
- 在某些情况下提高性能,特别是像游戏这种计算密集型应用。
3. JNI方法静态注册:
JNI函数名格式(需将”.”改为”—”):
Java_ + 包名(com.example.auto.jnitest)+ 类名(MainActivity) + 函数名(stringFromJNI)
静态方法的缺点:
- 要求JNI函数的名字必须遵循JNI规范的命名格式;
- 名字冗长,容易出错;
- 初次调用会根据函数名去搜索JNI中对应的函数,会影响执行效率;
- 需要编译所有声明了native函数的Java类,每个所生成的class文件都要用javah工具生成一个头文件;
4. JNI方法动态注册:
Java与JNI通过JNINativeMethod的结构来建立函数映射表,它在jni.h头文件中定义,其结构内容如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
创建映射表后,调用RegisterNatives函数将映射表注册给JVM;
当Java层通过System.loadLibrary加载JNI库时,会在库中查JNI_OnLoad函数。可将JNI_OnLoad视为JNI库的入口函数,需要在这里完成所有函数映射和动态注册工作,及其他一些初始化工作。
5. JNI的基础数据类型对照表:
Java类型 | JNI类型 | 描述 |
---|---|---|
boolean(布尔型) | jboolean | 无符号8位 |
byte(字节型) | jbyte | 有符号8位 |
char(字符型) | jchar | 无符号16位 |
short(短整型) | jshort | 有符号16位 |
int(整型) | jint | 有符号32位 |
long(长整型) | jlong | 有符号64位 |
foat(浮点型) | jfloat | 32位 |
double(双精度浮点型) | jdouble | 64位 |
6. JNI引用数据类型对照表:
Java引用类型 | JNI类型 | Java引用类型 | JNI类型 |
---|---|---|---|
All objects | jobject | char[ ] | jcharArray |
java.lang.Class | jclass | short[ ] | jshortArray |
java.lang.String | jstring | int[] | jintArray |
java.lang.Throwable | jthrowable | long[ ] | jlongArray |
Object[ ] | jobjectArray | float[] | jfloatArray |
boolean[ ] | jbooleanArray | double[ ] | jdoubleArray |
byte[ ] | jbyteArray |
7. JNI函数签名信息
由于Java支持函数重载,因此仅仅根据函数名是没法找到对应的JNI函数。为了解决这个问题,JNI将参数类型和返回值类型作为函数的签名信息。
JNI规范定义的函数签名信息格式: (参数1类型字符…)返回值类型字符
函数签名例子:
JNI常用的数据类型及对应字符对照表:
Java类型 | 字符 |
---|---|
void | V |
boolean | Z (容易误写成B) |
int | I |
long | J (容易误写成L) |
double | D |
float | F |
byte | B |
char | C |
short | S |
int[ ] | [I (数组以"["开始) |
String | Ljava/lang/String; (引用类型格式为”L包名类名;”,要记得加";") |
Object[] | [Ljava/lang/object; |
8. JNIEnv的介绍
- JNIEnv概念 : JNIEnv是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境。通过JNIEnv可以调用到一系列JNI系统函数。
- JNIEnv线程相关性: 每个线程中都有一个 JNIEnv 指针。JNIEnv只在其所在线程有效, 它不能在线程之间进行传递。
注意:在C++创建的子线程中获取JNIEnv,要通过调用JavaVM的AttachCurrentThread函数获得。在子线程退出时,要调用JavaVM的DetachCurrentThread函数来释放对应的资源,否则会出错。
9. JNI编译之Cmake
CMake 则是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt)生成 对应 makefile 或 project 文件,然后再调用底层的编译, 在Android Studio 2.2 之后支持Cmake编译。
- add_library 指令
语法:add_library(libname [SHARED | STATIC | MODULE] [EXCLUDE_FROM_ALL] [source])
将一组源文件 source 编译出一个库文件,并保存为 libname.so (lib 前缀是生成文件时 CMake自动添加上去的)。其中有三种库文件类型,不写的话,默认为 STATIC;
- SHARED: 表示动态库,可以在(Java)代码中使用 System.loadLibrary(name) 动态调用;
- STATIC: 表示静态库,集成到代码中会在编译时调用;
- MODULE: 只有在使用 dyId 的系统有效,如果不支持 dyId,则被当作 SHARED 对待;
- EXCLUDE_FROM_ALL: 表示这个库不被默认构建,除非其他组件依赖或手工构建;
#将compress.c 编译成 libcompress.so 的共享库
add_library(compress SHARED compress.c)
- target_link_libraries 指令 语法:target_link_libraries(target library <debug | optimized> library2…) 这个指令可以用来为 target 添加需要的链接的共享库,同样也可以用于为自己编写的共享库添加共享库链接。如:
#指定 compress 工程需要用到 libjpeg 库和 log 库
target_link_libraries(compress libjpeg ${log-lib})
- find_library 指令 语法:find_library( name1 path1 path2 ...) VAR 变量表示找到的库全路径,包含库文件名 。例如:
find_library(libX X11 /usr/lib)
find_library(log-lib log) #路径为空,应该是查找系统环境变量路径
示例工程Cmake截图如下:
三、示例工程代码
示例工程截图:
示例MainActivity内需要加载SO:
companion object {
// Used to load the 'native_kt_demo' library on application startup.
init {
System.loadLibrary("native_kt_demo")
}
}
1. 示例:获取JNI返回字符串(静态注册)
Kotlin 代码
external fun stringFromJNI(): String
JNI层下代码
//extern "C" 避免编绎器按照C++的方式去编绎C函数
extern "C"
//JNIEXPORT :用来表示该函数是否可导出(即:方法的可见性
//1、宏 JNIEXPORT 代表的就是右侧的表达式: __attribute__ ((visibility ("default")))
//2、或者也可以说: JNIEXPORT 是右侧表达式的别名;
//3、宏可表达的内容很多,如:一个具体的数值、一个规则、一段逻辑代码等
JNIEXPORT
//jstring 代表方法返回类型为Java中的 String
jstring
//用来表示函数的调用规范(如:__stdcall)
JNICALL
Java_com_wx_nativex_kt_demo_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
2. 示例:调用JNI,JNI调用Java层无返回值方法(静态注册)
Kotlin代码:
external fun callJNI()
JNI层代码:
extern "C" JNIEXPORT void JNICALL
Java_com_wx_nativex_kt_demo_MainActivity_callJNI(JNIEnv *env, jobject thiz) {
LOGE("-----静态注册 , 无返回值方法 调用成功-----");
jclass js = env->GetObjectClass(thiz);
jmethodID jmethodId = env->GetMethodID(js, "toast", "(Ljava/lang/String;)V");
env->CallVoidMethod(thiz, jmethodId, env->NewStringUTF("静态注册 无返回值方法 调用成功"));
}
3. 示例:调用JNI,JNI调用Java层无返回值方法(带参数)(静态注册)
Kotlin代码:
external fun stringFromJNIwithParameter(str: String): String
JNI层代码:
extern "C" JNIEXPORT jstring JNICALL
Java_com_wx_nativex_kt_demo_MainActivity_stringFromJNIwithParameter(JNIEnv *env, jobject thiz, jstring str) {
const char *data = env->GetStringUTFChars(str, NULL);
LOGE("-----获取到Java 传来的数据:data %s-----", data);
env->ReleaseStringChars(str, reinterpret_cast<const jchar *>(data));
const char *src = "111---";
const int size = sizeof(data) + sizeof(src);
char datares[size] = "111---";
return env->NewStringUTF(strcat(datares, data));
}
4. 示例:调用JNI去调用java方法(静态注册)
Kotlin代码:
external fun callNativeCallJavaMethod()
JNI层代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_wx_nativex_kt_demo_MainActivity_callNativeCallJavaMethod(JNIEnv *env, jobject thiz) {
jclass js = env->GetObjectClass(thiz);
jmethodID jmethodId = env->GetMethodID(js, "toast", "(Ljava/lang/String;)V");
env->CallVoidMethod(thiz, jmethodId, env->NewStringUTF("jni 通过反射调用 java toast方法"));
}
5. 示例:调用JNI去调用java 变量值(静态注册)
Kotlin代码:
external fun callNativeCallJavaField()
JNI层代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_wx_nativex_kt_demo_MainActivity_callNativeCallJavaField(JNIEnv *env, jobject thiz) {
jclass js = env->GetObjectClass(thiz);
jfieldID jfieldId = env->GetFieldID(js, "androidData", "Ljava/lang/String;");
jstring newDataValue = env->NewStringUTF("四海一家");
// jclass js = env->GetObjectClass(thiz);
jmethodID jmethodId = env->GetMethodID(js, "toast", "(Ljava/lang/String;)V");
env->CallVoidMethod(thiz, jmethodId, newDataValue);
// env->SetObjectField(thiz, jfieldId, newDataValue);
}
6. 示例:去调用JNI去调用Java带callback方法,带参数(静态注册)
Kotlin代码:
external fun callNativeWithCallBack(callBack: NativeCallBack)
JNI层代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_wx_nativex_kt_demo_MainActivity_callNativeWithCallBack(JNIEnv *env, jobject thiz, jobject call_back) {
LOGE("-----静态注册 , callback 调用成功-----");
jclass js = env->GetObjectClass(call_back);
jmethodID jmethodId = env->GetMethodID(js, "nmd", "(Ljava/lang/String;)V");
env->CallVoidMethod(call_back, jmethodId, env->NewStringUTF("我是Jni Native层callBack回调回来的数据值"));
}
7. 示例:动态注册
Kotlin代码:
external fun dynamicRegisterCallBack(callBack: NativeCallBack)
JNI层代码:
void regist(JNIEnv *env, jobject thiz, jobject call_back) {
LOGD("--动态注册调用成功-->");
jclass js = env->GetObjectClass(call_back);
jmethodID jmethodId = env->GetMethodID(js, "nmd", "(Ljava/lang/String;)V");
env->CallVoidMethod(call_back, jmethodId, env->NewStringUTF("我是Jni Native层动态注册callBack回调回来的数据值"));
}
jint RegisterNatives(JNIEnv *env) {
jclass activityClass = env->FindClass("com/wx/nativex/kt/demo/MainActivity");
if (activityClass == NULL) {
return JNI_ERR;
}
JNINativeMethod methods_MainActivity[] = {
{
"dynamicRegisterCallBack",
"(Lcom/wx/nativex/kt/demo/NativeCallBack;)V",
(void *) regist
}
};
return env->
RegisterNatives(activityClass, methods_MainActivity,
sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]));
}
//JNI_OnLoad java
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
LOGE("-----JNI_OnLoad 方法调用了-----");
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jint result = RegisterNatives(env);
// 函数注册
return JNI_VERSION_1_6;
}
总结
本文简单介绍了NDK编程中JNI的基础:并写了相关示例Demo代码
- JNI方法注册(静态注册,动态注册)
- JNI的基础数据类型
- JNI引用数据类型
- JNI函数签名信息
- JNIEnv的介绍
- JNI编译之Cmake
- 示例:获取JNI返回字符串(静态注册)
- 示例:调用JNI,JNI调用Java层无返回值方法(静态注册)
- 示例:调用JNI,JNI调用Java层无返回值方法(带参数)(静态注册)
- 示例:调用JNI去调用java方法(静态注册)
- 示例:调用JNI去调用java 变量值(静态注册)
- 示例:去调用JNI去调用Java带callback方法,带参数(静态注册)
- 示例:动态注册
感谢阅读:
欢迎用你发财的小手: 点点赞、收藏收藏,或者 关注关注
这里你会学到不一样的东西
项目地址
来源:juejin.cn/post/7452181029996380171
不上班的这一年,我都做了些什么?
原文《2024年度总结,回顾迅排设计发展历程》,首发于公众号:品味前端
大家好,这里是茶无味的一天。
去年我突然决定裸辞,褪去天命打工人的身份,想拿出一两年时间来专注做一件自己想做的事情,开始了昼夜狂奔的工作模式。由于大部分时间都在搞开发,所以姑且就称自己为独立开发者吧。
时光荏苒,转眼间我就踉跄的踏入了 2025,无论成败,也必须对过去一年稍微做下总结了。
新年伊始,独立开发的道路即将走过第一个年头,这一年中我的故事围绕着「迅排设计」在展开,所以这篇文章就带大家回顾下这个项目的发展过程。
拉开序幕
三年前的一天,领导让我仿造一个稿定设计,这对于程序员来说实在太过陌生,我就说先去搜一下看看,然后傻眼了,当时只觉得这根本就是不可能完成的任务。
过了几天领导又来问,表示无论做成啥样都行,我说试试吧。于是就开始了全世界的统一操作:打开 Github,看看有没有现成的项目,拿来改改然后交差(何同学工作室点头表示这个我们熟)。还没来得及为自己的机智窃喜,我又傻眼了,当时搜遍全网后发现,开源的项目都是功能少 bug 多、界面还很抽象,能用的竟一个没有!
虽然我心底里在打退堂鼓,身体却很诚实,行动派的我还是决定做了再说,毕竟越难啃的骨头往往越有价值。而且我也意识到,这几年浮现的众多编辑器产品无一不在向我们证明:Web 前端可以做很多复杂且有深度的事情,这正是自我提升的机会。我这样想着,写下了项目的第一行代码。
从项目中搜索到的记录来看,保留至今最早的代码落款于 2021 年 7 月,彼时的我凌晨 2 点还在敲代码,而这样的夜晚至今也已数不清经历了多少个。
当然班还要继续上,平时仍是对接口、还原 UI 图的无味日常,然后挤时间出来做这个图片设计工具,连周末也在敲代码(我可不想在公司加班当工贼啊),一个多月后原型版勉强可用,当时就长这个样:
在这个过程中我收获不少满足感,最开始的自我怀疑便一扫而空,也默默担起了 Owner 的角色,决定独立撑起项目。
没有时间,我就跟组长沟通少排点活,不然在多个项目间来回横跳实在无法集中精力展开工作。
没有产品经理,我就把市面上所有能找到的在线设计器全都用了个遍,倒推关键的功能点,再逐一开发。
没有 UI 设计师,我就尽量把界面做得清爽。这好比人不会化妆和穿搭吧,那我凭什么想着艳压群芳呢,保持干净整洁给人的感觉也不会差的。
在经历三个月后,项目顺利上线。这便是迅排设计的前身了,如今回看当时功能界面都挺简单的,不过能看出来和上面那个丑到没边的是同一个项目吗:
然而上线后不久,项目就一直停摆。
由于公司业务长期陷入困境,加之大环境冲击业绩不断下滑,我们也开始掉进内卷的泥潭中挣扎。公司开始了严抓考勤、要求义务加班等操作,甚至取消下午茶、推迟发放工资,很快一批接一批的裁员,最后我也离开了公司。
2022 年中旬,我在掘金写了第一篇年中总结,也是第一次分享做这个项目的心路历程,有不少人开始在那时关注我,而我也是在那时候埋下了将项目开源的种子。
失业加上疫情,中间空白了很长一段时间什么都没做。大约一年后,我重启了这个项目,对代码进行了一些重构,并总结出海报设计的几大要素:图片、文字、创意和排版。
我们的编辑器旨在提供图片文字等素材的同时,更能帮助用户迅速地完成排版,因此取名“迅排设计”,在 2023 年 7 月正式以开源形式出道,项目英文名称:poster-design
项目开源两天后 Github Star 数破百,给阮一峰老师投稿被收录到其周刊(第 263 期):开源软件如何赚钱中,「迅排设计」开始被更多人看到。
此后开源中国等网站陆续收录了项目,HelloGitHub 收录为 2023 年度 JavaScript 热门项目(相关文章)。
2024 年 3 月还冲上过一次 GitHub Trending 全站热门,如今迅排已经被越来越多人所熟知....
未来计划
来聊聊开源的一些安排吧。
2024 下半年的重心基本是在商业版中,也就是迅排设计 Plus 的开发,其中做了不少差异化的更新。
其实迅排刚开源的时候,只是想着弄个在线网站才能更好地演示功能,后来我发现不少普通用户是真的在使用我们的在线版做图,于是萌生出一个想法,干脆就把C端 SaaS 平台慢慢做起来呗。
所以开源版渐渐独立,现在只是迅排设计的一部分,或者说是核心编辑器。
而在一开始做 Plus 商业版时,我也曾尝试完全基于开源仓库迭代,以保持两边同步更新,但发现行不通,而且也会被迫让往后的 PR 变得困难,后来便单独写了个服务端 Demo 让开源版脱机运行。
开源是自由的,我们欢迎并接纳任何代码提交和想法。
持续推动开源项目的发展也是今年的一项计划,我希望能让更多人参与进来,传递开源的价值。
另外今年也会更积极地与外部产品或开源项目建立合作关系,交换流量和增加曝光。2024 年几乎没有怎么推广过项目,发的文章集中在公众号和粉丝群,曝光度是比较低的。
Plus 版付费产品
为了满足企业数字化升级或转型的需要,我开始了商业版计划。由于这是一个前后台完整、大而全的项目,所以代号叫 Plus 版,这是过去半年多的工作重心。
商业化的核心是为客户提供价值或解决问题,在“能服务好客户”和“能给客户提供好服务”之间,我选择了后者。我认为前者谁都可以做到,所以后者更有价值。
考虑到迅排设计本身仍有很大的成长空间,所以确定了长期更新、不断完善这个主基调,为了能快速更新迭代,还专门建立了一套升级维护的系统体系。
在销售和服务了一些客户之后,我对自己的产品也有了更清晰的认识和规划。这期间版本和定价做了多次调整,现在已经明确就落地两套方案:
- Plus版:交付的是迅排设计的整套在线编辑器解决方案,提供一个让客户无需开发、迅速私有化搭建起和迅排一样强大的在线设计系统,包括提供技术服务(例如云升级和模板市场等)。
- Slim版:有些客户可能不需要大而全的项目,且更看重源码交付。那么我们提供另外一套灵活定制化方案,能够交付源码,但不会对项目精雕细琢,而是重点关注如何实现客户的核心目标。
当然客户最关心的通常是定价,而这也是一开始困扰我最大的问题。
普通产品的定价模型通常是成本 + 利润,但我们却无法这么做。因为软件开发本身是一项复杂的工程,且需要不断进行技术创新和产品研发,如果以传统的人力成本计算报价,可能会忽略市场和客户的需求,导致产品难以销售。
类似产品的大公司依靠人才供应链和多年品牌积累,维持住了自身的市场份额,他们的定价能一定程度反映市场需求,却很难直接拿来参考。
我们的商业化模式在市场的优势是什么呢?其实是自身技术和经验的积累。相比之下品牌价值和口碑还没做起来的时候,基本没有溢价资格,毕竟我们才刚起步。
事实上无论任何行业,基本都是从性价比开始、然后追求高品质、最后才能获得高溢价。十几年前外国对 "Made In China" 有着刻板印象,难道说是当时的中国企业不想做高端产品吗?凡事都需要一个过程。
所以我最终思考后决定完全抛弃利润,甚至让出成本,仅以技术支持定价,这样前期才好销售。
只要做好长期策略,并根据市场和客户反馈灵活调整,后面不断提升产品功能价值,提升差异化、服务满意度等方面来持续影响客户愿意支付的价格,慢慢还是有可能盈利的。
而现阶段迅排 Plus 企业服务的定价,实际上出售的也是信心。
随着近段时间产品功能的完善、以及初期客户的良好反馈,这就是我们不断建立信心的过程。
还有一些客户会提出更多样化的需求,很遗憾我做不了特殊定制化。能否把握客户预期也是非常重要的,相信所有小微创业者都一样,做预期不明确的事情无异于杀鸡取卵。
未来的路还很长,对于迅排自身发展的定位我基本锚定在两点:
- 不断优化现有产品,追求极致
- 挖掘细分领域需求,专注小众市场
SaaS 平台规划
我们的项目虽然是独立开发的,但其实不比公司做的产品差,可以满足大部分普通用户的日常做图需求。这半年来迅排设计官网一直在低调运营,并随着迅排 Plus 版的开发进程不断完善自身。
接下来迅排设计将致力于做免费在线创作平台,编辑器和平台的模板、素材图片、字体等均为用户免费使用!后续则是希望通过和 AI 等技术的结合,比如打造一些增值付费的智能化工具来盈利。
红海市场竞争激烈,做这个平台目的是想回馈忠实的粉丝。我希望能努力将「迅排设计」打造成小众领域中用户口碑最好的在线设计产品,所以也一直在刻意地放缓扩大市场。
目前平台有 3000+ 注册用户,我基本每周会抽空更新点模板,虽然不是设计师出身,但经手过上千张海报,多少也摸索出一点门道,目前还在不断学习中。至今共做了 400+ 模板与组件,预计今年网站可以完善到 2000+ 以上的模板。
随着平台使用人数增加,就要着手开发后台统计与数据可视化的功能了。目前的网站访问数据是用“不蒜子”这个插件统计的,可以看到去年官网总 uv 数 8w,总 pv 数 15w。
接下来为了平台更好地发展,将会在适当的时候进行一次数据清理,届时会在官网通知,所有用户的作品数据和上传文件可能被重置。
网站开销
最后来聊点别的吧,复盘一下我从三年前至今的网站开销一共是多少。
服务器 1 号购于 2021 年 7 月,配置 1h2g,带宽 2 M,是最早服务迅排的机器,当时购买三年花费是:¥247
服务器 2 号购于 2022 年 3 月,配置 1h2g,带宽 1 M,购买三年花费:¥177。当时打折随意买的,当测试机,好像没怎么用过。
以上两台机器均已“陨落”,没有选择续费(价格翻十几二十倍是常规操作)。所以后面又买了 5 年的 2h4g 3M 服务器,花费¥1080,这就是目前迅排设计服役中的机器。
域名最后一次续费花了 ¥79,当时用的还是 .com ,已经涨到 90 元(几年前买入时才 45 元),虽然钱不多但无法接受连年涨价,所以干脆换了 .cn,一口气买了五年 ¥181。(旧域名后来竟被菠菜网站买了去,法克!)
七牛云oss的流量费用,由于我没有上 CDN、且限制传输大文件,消耗比较小,算个 20 元让它有些参与感吧。
后面还买了阿里的那个 99 服务器,2h2g 带宽 3M,当个副机用。
247 + 177 + 1080 + 79 + 181 + 20 + 99
统计下来的总花费约为:¥1883
这就是我三年来的所有建站成本,其实正常的网站运维成本肯定是我的好多倍,只是我比较爱折腾,在网站的访问速度、图片字体压缩等方面也下了不少功夫,才勉强让迅排运行时表面上看起来毫不费力。
当然最主要还是穷,为爱发电靠的全是个人极致的“勤俭持家”~ 现在哪怕我不再花费一分钱,「迅排设计」也仍然能坚持运行至 2028 年 😂
不知到那时候我们能换上豪华顶配性能溢出的服务器吗?
来源:juejin.cn/post/7456641634506096651
Android 车载应用开发——「RecyclerView」
前言
实践是最好的学习方式,技术也如此。
一、简介
RecyclerView
是列表;- 好处:更高效率的列表控件;
- 用法:重点
RecycleerView.Adapter
的写法;可以通过LayoutManager
(布局管理器)来决定布局的样式,是线性的、网格列表还是瀑布流列表; RecyclerView
列表是如何实现显示的 ?
- 是将数据放到对应的位置上,根据数据内容的数量来显示(即告诉列表有多少个条目) ;
二、Adapter
- 是什么
- 适配器、连接器;
- 为什么要有
Adapter
?
- 列表中不只有一条数据,不像
TextView
、ImageView
一样,一个控件对应一条数据; - 列表形式的数据,如何将多个布局与多个数据连接起来?中间就通过
adapter
,将数据放到对应的控件的位置;
- 列表中不只有一条数据,不像
- Adapter 的分类
ArrayAdapter
:简单列表;SimpleAdapter
:图文列表;BaseAdapter
:图文复杂列表 ;
三、示例
1、背景
用 RecyclerView 列表显示各个城市天气数据
2、代码
FutureWeatherAdapter
是一个自定义的适配器类,它继承自 RecyclerView.Adapter 类;在泛型参数中,指定了一个内部类 WeatherViewHolder 作为适配器的视图持有者
- WeatherViewHolder 是用于在 RecyclerView 中显示每个天气数据的视图持有者类;
- 通常情况下,你会在适配器内部定义一个继承自 RecyclerView.ViewHolder 的内部类来表示列表项的视图结构和布局
onCreateViewHolder()
方法用于创建 ViewHolder,即创建用于显示单个天气条目的视图,并返回 ViewHolder 对象;使用布局填充器从 XML 布局文件中实例化视图,并将其传递给自定义的 ViewHolder 对象。
- 在创建新的 ViewHolder 实例时调用。当 RecyclerView 需要显示新的列表项时,会调用该方法来创建一个 ViewHolder 对象 ;
- onCreateViewHolder() 返回的 ViewHolder 对象会被 RecyclerView 用于显示列表项。当 RecyclerView 需要显示新的列表项时,它会调用 onCreateViewHolder() 方法来创建一个新的 ViewHolder 对象,并将其返回
onBindViewHolder()
方法用于将数据绑定到 ViewHolder 上,即将具体的天气数据填充到对应的视图控件中。在这个方法中,获取当前位置的天气数据对象,然后将其属性分别设置到 ViewHolder 中的各个 TextView 和 ImageView 中;
- 方法在 RecyclerView 需要将数据绑定到 ViewHolder 以显示新的列表项时被调用。当 RecyclerView 中的列表项需要更新或者需要显示新的列表项时,会调用该方法;
getItemCount()
方法用于获取数据集中的条目数,即天气数据列表的大小;
- getItemCount() 方法返回的数据会告诉 RecyclerView 有多少个列表项需要在屏幕上显示。当 RecyclerView 需要确定列表的大小时,它会调用 getItemCount() 方法
内部类 WeatherViewHolder
继承自 RecyclerView.ViewHolder,用于持有每个天气条目的视图控件的引用;在构造方法中,通过传入的视图参数找到并引用了各个视图控件;
public class FutureWeatherAdapter extends RecyclerView.Adapter<com.example.weatherapp.adapter.FutureWeatherAdapter.WeatherViewHolder> {
private Context mContext; // 上下文
private List<DayWeatherBean> mWeatherBeans; // 数据
public FutureWeatherAdapter(Context mContext, List<DayWeatherBean> mWeatherBeans) {
this.mContext = mContext;
this.mWeatherBeans = mWeatherBeans;
}
// 先创建ViewHolder再将数据绑定
@NonNull
@Override
public WeatherViewHolder onCreateViewHolder(@NonNull ViewGr0up parent, int viewType) {
// onCreateViewHolder()方法负责创建ViewHolder并将其返回给RecyclerView
View view = LayoutInflater.from(mContext).inflate(R.layout.weather_item_layout, parent, false); // 布局
WeatherViewHolder weatherViewHolder = new WeatherViewHolder(view);
return weatherViewHolder;
}
@Override
public void onBindViewHolder(@NonNull WeatherViewHolder holder, int position) {
// onBindViewHolder()方法负责将数据绑定到ViewHolder
// holder: 表示要绑定的ViewHolder对象,position: 表示ViewHolder在RecyclerView中的位置
// onBindViewHolder()方法负责将数据填充到ViewHolder的视图中
// 它会被调用多次,每次RecyclerView需要显示一个新的ViewHolder时都会调用
DayWeatherBean weatherBean = mWeatherBeans.get(position); // 拿到当前位置的JavaBean对象
holder.tvWeather.setText(weatherBean.getWea());
holder.tvTem.setText(weatherBean.getTeamDay());
holder.tvAir.setText(weatherBean.getWin_speed());
holder.tvWin.setText(weatherBean.getWin());
holder.tvTemLowHigh.setText(weatherBean.getTeamNight());
holder.ivWeather.setImageResource(getImgResOfWeather(weatherBean.getWeaImg()));
}
// 总共有多少个条目
@Override
public int getItemCount() {
return (mWeatherBeans == null) ? 0 : mWeatherBeans.size();
}
class WeatherViewHolder extends RecyclerView.ViewHolder {
TextView tvWeather, tvTem, tvTemLowHigh, tvWin, tvAir;
ImageView ivWeather;
public WeatherViewHolder(@NonNull View itemView) {
super(itemView);
tvWeather = itemView.findViewById(R.id.tv_weather);
tvAir = itemView.findViewById(R.id.air);
tvTem = itemView.findViewById(R.id.tv_tem);
tvTemLowHigh = itemView.findViewById(R.id.tv_tem_low_high);
tvWin = itemView.findViewById(R.id.tv_win);
ivWeather = itemView.findViewById(R.id.iv_weather);
}
}
来源:juejin.cn/post/7345379878240501771
CMS垃圾回收器的工作原理是什么?为什么它会被官方废弃?
你好,我是猿java。
1. 网上关于 CMS的文章很多,为什么要重复造车轮?
答:网上很多关于 CMS收集器的文章写得不够具体,有的甚至一知半解,更多的是不假思索的转载,想通过自己对 CMS的理解以及大量资料的佐证,提供更具体形象正确的分析。
2. CMS已经被弃用,为什么还要分析它?
答:首先,CMS收集器依然是面试中的一个高频问题;
其次,CMS作为垃圾收集器的一个里程碑,作为 Java程序员,不了解原理,于情于理说不过去;
3. JVM已经把垃圾回收自动化了,为什么还要讲解 CMS?
答:排查生产环境的各种内存溢出,内存泄漏,垃圾回收导致性能瓶颈等技术问题,如果不懂原理,如何排查和优化?
温馨提示:如果没有特殊说明,本文提及的虚拟机默认为 HotSpot虚拟机。
背景
首先,了解下 HotSpot虚拟机中 9款垃圾回收器的发布时间及其对应的 JDK版本,如下图:
接着,了解下 CMS垃圾回收器的生命线:
- 2002年9月,JDK 1.4.1 版本,CMS实验性引入;
- 2003年6月,JDK 1.4.2 版本,CMS正式投入使用;
- 2017年9月,JDK 9 版本,CMS被标记弃用;
- 2020年3月,JDK 14 版本,CMS从 JDK中移除;
效力 18年,一代花季回收器,从此退出历史舞台;
什么是垃圾
既然分析的是垃圾回收器,那么,我们首先需要知道:在 JVM 中,什么是“垃圾”?
这里的“垃圾”用了双引号,是因为它和我们生活中理解的垃圾不一样。在 JVM中,垃圾(Garbage)是指那些不再被应用程序使用的对象,也就是说这些对象不再可达,即对象已死。
如何判断对象不可达(已死)?
在 JVM中,通过一种可达性分析(Reachability Analysis)算法来判断对象是否可达。 该算法的基本思路是:通过 GC Roots 集合里的根对象作为起始点,一直追踪所有存在引用关系的对象(这条引用关系链路叫做引用链 Reference Chain), 如果某对象到 GC Roots之间没有引用链,那么该对象就是不可达。 如下图,obj4, obj5,obj6 尽管相互直接关联,但是没有 GC Root连接,所以是不可达,同理 obj7也不可达:
关于可达性分析,还有一种方法是引用技术算法,该方法的思路是:在对象中添加一个计数器,增加一次引用计数器 +1,减少一次引用计数器 -1,当计数器始终为 0时代表不被使用,这种方法一般是用于 Python的CPython 和微软的COM(Component Object Model)等技术中,JVM中使用的是可达性分析算法,这点需要特别注意。
哪些对象可以作为 GC Roots?
GC Roots 是 GC Root的集合,本质上是一组必须活跃的对象引用,主要包含以下几种类型:
虚拟机栈中的引用对象:每个线程的虚拟机栈中的局部变量表中的引用。这些引用可能是方法的参数、局部变量或临时状态。
方法区中的类静态属性引用对象:所有加载的类的静态字段。静态属性是类级别的,因此它们在整个Java虚拟机中是全局可访问的。
方法区中的常量引用对象:方法区中的常量池(例如字符串常量池)中的引用。
本地方法栈中的JNI引用:由 Java本地接口(JNI)代码创建的引用,例如,Java代码调用了本地 C/C++库。
活跃的 Java线程:每个执行中的Java线程本身也是一个GC Root。
同步锁(synchronized block)持有的对象:被线程同步持有的对象。
Java虚拟机内部的引用:比如基本数据类型对应的Class对象,一些常见的异常对象(如NullPointerException、OutOfMemoryError)的实例,系统类加载器。
反射引用的对象:通过反射API持有的对象。
临时状态:例如,从Java代码到本地代码的调用。
这里举个简单的例子来解释 GC Root 以及 GC Root可达对象,如下代码:
public class RootGcExample {
private static Object sObj = new Object(); // 静态字段 sObj是 Gc Root
private static void staticMethod() {
Object mObj = new Object(); // 方法局部变量 mObj是 Gc Root
// ...
}
public static void main(String[] args) {
Object obj = new Object(); // 局部变量obj 是 Gc Root
staticMethod();
}
}
上述例子中,sObj 是一个静态变量引用,指向了一个 Object对象,因此,sObj是一个 Gc Root, 在staticMethod静态方法中,mObj 是一个方法局部变量,它也是一个 Gc Root, 在 main方法中,obj也是一个Gc Root。堆中的 Object对象就是 GC Root可达对象,上述关系可以描绘成下图:
回收哪里的垃圾?
从 CMS 简介可以知道 CMS是用于老年代的垃圾回收,但是对于这种抽象的文字描述,很多小伙伴肯定还是没有体感, 因此,我们把视角放眼到整个 JVM运行时的内存结构上,从整体上看看垃圾回收器到底回收的是哪些区域的垃圾, CMS 又是回收哪里的垃圾,如下图:
垃圾在哪里?
在了解了“垃圾”在 JVM中是如何定义之后,我们不禁会问到:这些“垃圾”存放在哪里呢?
在回答这个问题之前,我们先来了解 JVM的内存结构,根据 Java虚拟机规范,JVM内存包含以下几个运行时区域,如下图:
为了更好地理解 JVM内存结构,这里对各个区域做一个详细的介绍:
- 堆空间(Heap):它是 JVM内存中最大的一块线程共享的区域,用于存放 Java应用创建的对象实例和数组。堆空间进一步细分为几个区域:
- 年轻代:Young Generation,大部分的对象都是在这里创建。年轻代又分为一个 Eden区和两个 Survivor区(S0和S1)。这里的大部分对象生命周期比较短,会被垃圾回收器快速回收。
- 老年代:Old Generation 或 Tenured Generation,在年轻代中经过多次垃圾回收仍然存活的对象会被移动到老年代,或者一些大对象会直接被分配到老年代,这里的对象一般存活时间较长,垃圾回收频率较低。
- 永久代:Permanent Generation,PermGen,Java 8之前版本的叫法,用于存放类信息、方法信息、常量等。在 Java 8及之后的版本,永久代被元空间(Metaspace)所替代。
- 元空间:Metaspace,Java 8及之后版本的叫法,用于存放类的元数据信息,它使用本地物理内存,不在 JVM堆内。
- 方法区(Method Area):方法区是堆的一个逻辑区域,它是线程共享的,用于存储已被 JVM加载的类结构信息,常量、静态变量、即时编译后的代码缓存等数据。为了和堆区分开来,它也被叫做“非堆(Non-Heap)”。这个区域的回收对象主要是常量池和类型的卸载,而且回收的效果比较差。
关于方法区有一个误区:JDK 8以前,HotSpot虚拟机为了像堆一样管理方法区的垃圾回收,就使用永久代来实现方法区,因此有人就把方法区直接叫做永久代,而其它虚拟机不存在永久代的概念,因此,方法区如何实现属于虚拟机内部的机制,不是 JVM统一规范。另外,HotSpot发现永久代实现方法区这种做法会导致内存溢出,因此从 JDK8开始,把永久代彻底废除,改用和 JRockit一样的元空间。方法区也改用本地内存实现。
- 程序计数器(Program Counter Register):这是一个较小的线程私有内存空间,用于存储当前线程执行的字节码的行号指示器。每个线程都有自己的程序计数器,但这部分内存通常不涉及垃圾回收。
- 虚拟机栈(Java Virtual Machine Stack):每个 Java方法执行时都会创建一个线程私有的栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口信息等。虚拟机栈在方法执行完毕后会自动清理,因此也不是垃圾回收的重点。
- 本地方法栈(Native Method Stack):用于支持本地方法的执行(即通过JNI调用的非Java代码),它是线程私有的。本地方法栈也会在方法执行完毕后自动清理。
通过上述 JVM内存区域的介绍,我们可以发现 JVM各个内存区域都可能产生垃圾,只是程序计算器,本地方法区,虚拟机栈 3个区域随线程而生,随线程而亡,垃圾被自动回收,方法区回收效果比较差,而堆中的“垃圾”才是回收器关注的重点,因此,垃圾收集器重点关注的是 JVM的堆,而 CMS回收的是堆中的老年代,如下图:
到这里为止,我们已经从 JVM内存结构视角上掌握了垃圾收集器回收的区域以及 CMS 负责的区域。
接下来,分析一下 GC回收常用的几个重要技术点:三色标记法(Tricolor Marking),卡表(Card Table),写屏障(Write Barrier),理解它们可以帮助我们更好地去理解 GC回收的原理。
几个重要技术点
三色标记法
在垃圾收集器中,主要采用三色标记算法来标记对象的可达性:
- 白色:表示对象尚未被访问。初始状态时,所有的对象都被标记为白色。
- 灰色:表示对象已经被标记为存活,但其引用的对象还没有全部被扫描。灰色对象可能会引用白色对象。
- 黑色:表示对象已经被标记为存活,并且该对象的所有引用都已经被扫描过。黑色对象不会引用任何白色对象。
三色标记算法的工作流程大致如下:
- 初始化时,所有对象都标记为白色。
- 将所有的 GC Roots 对象标记为灰色,并放入灰色集合。
- 从集合中选择一个灰色对象,将其标记为黑色,并将其引用的所有白色对象标记为灰色,然后放入灰色集合。
- 重复步骤3,直到灰色集合为空。
- 最后,所有黑色对象都是活跃的,白色对象都是垃圾。
卡表
对于分代垃圾回收器,势必存在一个跨代引用的问题,通常会使用一种名为记忆集(Remembered Set)的数据结构,它是一种用于记录从非收集区指向收集区的指针集合的数据结构。
而卡表就是最常用的一种记忆集,它是一个字节数组,用于记录堆内存的映射关系,下面是 HotSpot虚拟机默认的卡表标记逻辑:
// >> 9 代表右移 9位,即 2^9 = 512 字节
CARD_TABLE[this address >> 9] = 0;
每个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块叫做“卡页(Card Page)”。因为卡页代表的是一个区域,所以可能存在很多对象,只要有一个对象存在跨代引用,就把数组的值设为1,称该元素“变脏(Dirty)”,该卡页叫“脏页(Dirty Page)”,如下:
// >> 9 代表右移 9位,即2^9=512
CARD_TABLE[this address >> 9] = 1;
当垃圾回收时,只要筛选卡表中有变脏的元素,即数组值为 1,就能判断出其对应的内存区域存在对象跨代引用,卡表和卡页的关系如下图:
写屏障
在 HotSpot虚拟机中,写屏障本质上是引用字段被赋值这个事件的一个环绕切面(Around AOP),即一个引用字段被赋值的前后可以为程序提供额外的动作(比如更新卡表),写屏障分为:前置写屏障(Pre-Write-Barrier)和后置写屏障(Post-Write-Barrier)2种类型。
需要注意的是:这里的写屏障和多线程并发中的内存屏障不是一个概念。
分析完几个重要的技术点之后,接下来,我们正式分析 CMS回收器。
CMS 简介
CMS 是 Concurrent Mark Sweep 的简称,中文翻译为并发标记清除,它的目标是减少垃圾回收时应用线程的停顿时间,并且实现应用线程和 GC线程并发执行。
CMS 用于老年代的垃圾回收,使用的是标记-清除算法。通过 -XX:+UseConMarkSweepGC 参数即可启动 CMS回收器。
在 CMS之前的 4款回收器(Serial,Serial Old,ParNew,Parallel Scavenge) ,应用线程和 GC线程无法并发执行,必须 Stop The World(将应用线程全部挂起), 并且它们关注的是可控的吞吐量,而 CMS回收器,应用线程和 GC线程可以并发执行,目标是缩短回收时应用线程的停顿时间,这是 CMS和其它 4款回收器本质上的区别,也是它作为里程碑的一个标志。
CMS 回收过程
从整体上看,CMS 垃圾回收主要包含 5个步骤(网上很多 4,6,7个步骤的版本,其实都大差不差,没有本质上的差异):
- Initial Mark(初始标记):会Stop The World
- Concurrent Marking(并发标记)
- Remark(重复标记):会Stop The World
- Concurrent Sweep(并发清除)
- Resetting(重置)
整个过程可以抽象成下图:
在讲解回收过程之前,先分析三色标记法,这样可以帮助我们更好地去理解 GC的原理。
1. 初始标记
初始标记阶段会 Stop The World(STW),即所有的应用线程(也叫 mutator线程)被挂起。
该阶段主要任务是:枚举出 GC Roots以及标识出 GC Roots直接关联的存活对象,包括那些可能从年轻代可达的对象。
那么,GC Roots是如何被枚举的?GC Roots的直接关联对象是什么?为什么需要 STW?
GC Roots是如何被枚举的?
通过上文对 GC Roots的描述可知,作为 GC Roots的对象类型有很多种,遍及 JVM中的多个区域,对于现如今这种大内存的 VM,如果需要临时去扫描各区域来获取 GC Roots,那将是很大的一个工程量,因此,JVM采用了一种名为 OopMap(Object-Oriented Programming Map)的数据结构,它用于在垃圾收集期间快速地定位和更新堆中的对象引用(OOP,Object-Oriented Pointer)。
OopMap是在 JVM在编译期间生成的,主要作用是提供一个映射,通过这个映射垃圾收集器可以知道在特定的程序执行点(如safepoint)哪些位置(比如在栈或寄存器中)存放着指向堆中对象的引用,这样就可以快速定位 GC Roots。
使用OopMap的优点包括:
- 提高效率:OopMap使得垃圾收集器能够快速准确地找到和更新所有的对象引用,从而减少垃圾收集的时间。
- 减少错误:手动管理对象引用的位置容易出错,OopMap提供了一种自动化的方式来追踪这些信息。
- 便于优化:由于 OopMap是在编译时生成的,编译器可以进行优化,比如减少需要记录的引用数量,从而减少垃圾收集的开销。
在 HotSpot虚拟机中,OopMap是实现精确垃圾收集的关键组件之一。
什么是 GC Roots直接关联的对象?
所谓直接关联对象就是 GC Root直接引用的对象,下面以一个示例来说明,如下代码:
public class AssociatedObjectExample {
public static void main(String[] args) {
Associated obj = new Associated(); // Associated 是 GC Root obj 直接关联
((Associated) obj).bObj = new BigObject(); // BigObject是 GC Root obj 的间接关联的对象,BigObject是一个大对象,直接分配到老年代
}
static class Associated {
BigObject bObj; // 与Associated对象直接关联的对象
}
static class BigObject {
// 其它代码
}
}
上述例子中,obj是一个 GC Root,Associated对象就是它的直接关联对象,bObj是一个 GC Root,BigObject对象是它的直接关联对象,obj可以通过 Associated对象间接关联 到 BigObject对象,但 BigObject对象不是 obj的直接关联对象,而是间接关联对象。 整个关联关系可以描绘成下图:
为什么需要 STW?
为什么初始标记阶段需要 Stop The World?这里主要归纳成两个原因:
- 确定 Roots集合:初始标记阶段的主要任务是识别出所有的 GC Roots,这是后续并发标记阶段的起点。 在多线程运行的环境中,如果应用线程和垃圾回收线程同时运行,应用线程可能会改变对象引用关系,导致 Roots集合不准确。 因此,需要暂停应用线程,以确保 GC Roots的准确性和一致性。
- 避免并发问题:在初始标记阶段,垃圾回收器需要更新一些共享的数据结构,例如标记位图或者引用队列。 如果应用线程在此时运行,可能会引入并发修改的问题,导致数据不一致。STW可以避免这种情况的发生。
2.并发标记**
这里的并发是指应用线程和 GC线程可以并发执行。
在并发标记阶段主要完成 2个事情:
- 遍历对象图,标记从 GC Roots可以追踪到所有可达的存活对象;
- 处理并发修改
因为应用线程仍在继续工作,因此老年代的对象可能会发生以下几种变化:
- 新生代的对象晋升到老年代;
- 直接在老年代分配对象;
- 老年代对象的引用关系发生变更;
为了防止这些并发修改被遗漏,CMS 使用了后置写屏障(Write Barrier)机制,确保这些更改会被记录在“卡表(Card Table)”中,同时将相应的卡表条目标记为脏(dirty),以便后续处理。
如下图:从 GC Roots追溯哦所有可达对象,并将它们修改为已标记,即黑色。
当老年代中,D 到 E到引用被修改时,就会触发写屏障机制,最终 E就会被写进脏页,如下图:
并发标记会出现对象可达性误判问题,如下图:假如对象 D对象被标记成黑色,E对象被标记为灰色(图左半部分),这时,工作线程将 E对象修改成不再指向F,并将 D对象指向 F对象(图右半部分),按照三色标记算法,D对象为黑色,不会再往下追溯,所以 F对象就无法被标记从而变成垃圾,“存活”对象凭空消失了,这是很可怕的问题,那么 CMS是如何解决这种问题的呢?
解决这种问题,通常有两种方案:
- 增量更新(Incremental Update)
当新增黑色对象指向白色对象关系时(D->F),需要记录这次新增,等并发扫描结束后,将这些黑色的对象作为 GC Root,重新扫描一次,也就是把这些黑色对象看成灰色对象,它们指向的白色对象就可以被正常标记。CMS采取的就是这种方式。
- 原始快照(Snapshot At The Beginning,SATB)
当删除灰色对象指向白色对象关系时(E->F),需要记录这次删除,等并发扫描结束后,将这些灰色的对象作为 GC Root,按照删除 E对象指向 F对象前一刻的快照(也就是E->F 还是可达的)重新扫描一次,即不管关系删除与否,都会按照删除前那一刻快照的对象图来进行搜索标记。G1,Shenandoah采取的是这种方式。
3.重新标记
重复标记阶段也会 Stop The World,即挂起所有的应用程序线程,该阶段主要完成事情是:
- 并发预清理:在重新标记阶段之前,CMS可能会执行一个可选的并发预清理步骤,以尽量减少重新标记阶段的工作量。(该过程在很多文章中会单独成一个大步骤讲解)
- 修正标记结果:由于在并发标记阶段导致的并发修改,导致漏标,错标,因此需要暂停应用线程(STW),确保修正这些标记结果。
- 处理卡表:检查并发标记阶段修改的这些脏卡,并重新标记引用的对象,以确保所有可达对象都被正确识别。
- 处理最终可达对象:处理那些在并发标记阶段被识别出的“最终可达”(Finalizable)对象。这些对象需要执行它们的 finalize方法,finalize方法可能会使对象重新变为可达状态。
- 处理弱引用、软引用、幻象引用等:处理各种不同类型的引用,确保它们按照预期被处理。例如,弱引用在 GC后会被清除,软引用在内存不足时会被清除,而幻象引用则在对象被垃圾收集器回收时被放入引用队列。
4.并发清除
这里的并发也是指应用线程和 GC线程可以并发执行,并发清除阶段主要完成 2个事情:
- 清除并发标记阶段标记为死亡的对象;
- 并发清除结束后,CMS 会利用空闲列表(free-list)将未被标记的内存(即垃圾对象占据的内存)收集起来,组成一个空闲列表,用于新对象的内存分配;
5.重置
清理和重置 CMS回收器的内部数据结构,为下一次垃圾回收做准备。
到此,回收过程就分析完毕,接下来总结下 CMS的优点和缺点。
CMS 的优点
低停顿时间
相对 Serial,Serial Old,ParNew,Parallel Scavenge 4款回收器,CMS收集器的主要优势是减少垃圾收集时的停顿时间,特别是减少了Full GC的停顿时间,这对于延迟敏感的应用程序非常有利。
并发收集
CMS在回收过程中,应用线程和 GC线程可以并发执行,从而减少了垃圾收集对应用程序的影响。
适合多核处理器
由于CMS利用了并发执行,它能够更好地利用现代多核处理器的能力,将垃圾收集的工作分散到多个CPU核心。
CMS 的缺点
浮动垃圾
在并发清除阶段,因为应用线程可以并发工作,可能会产生垃圾,这些垃圾在当前 GC无法处理,需要到下一次 GC才能进行处理,因此,这些垃圾就叫做“浮动垃圾”。
Concurrent Mode Failure
JDK5 默认设置下,当老年代使用了68%的空间后就会被激活 CMS回收,从JDK 6开始,垃圾回收启动阈值默认提升至92%,我们可以通过 -XX:CMSInitiatingOccupancyFraction 参数自行调节。
如果阈值是 68%,可能导致空间没有完全利用,频繁产生 GC,如果是92%,又会更容易面临另一种风险,要是预留的内存无法满足程序分配新对象的需要,就会出现一次 Concurrent Mode Failure(并发失败),因此会引发 FullGC。
这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。
内存碎片
因为 CMS采用的是标记-清理算法,当清理之后就会产生很多不连续的内存空间,这就叫做内存碎片。如果老年代无法使用连续空间来分配对象,就会出发 Full GC。为了解决这个问题,CMS收集器提供了 -XX:+UseCMS-CompactAtFullCollection 参数进行碎片压缩整理,参数默认是开启的,不过 从JDK 9开始废弃。
总结
- 本文不仅讲解了 CMS回收器,更是铺垫了很多 GC相关的基础知识,比如 安全点,三色标记法,卡表,写屏障。
- CMS 是 Concurrent Mark Sweep 的简称,中文翻译为并发标记清除,它的目标是减少垃圾回收时应用线程的停顿时间,并且实现应用线程和 GC线程并发执行。
- CMS 用于老年代的垃圾回收,使用的是标记-清除算法。通过 -XX:+UseConMarkSweepGC 参数即可启动 CMS收集器。
- CMS 主要包含:初始标记,并发标记,重新标记,并发清除,重置 5个过程。
- CMS 收集器使用三色标记法来标记对象,采用写屏障,卡表和脏页的方式来防止并发标记中修改的引用被漏标。
- CMS 收集器有 3大缺点:浮动垃圾,并发失败以及内存碎片。
尽管 CMS收集器已经被官方废弃了,但是它这种优化思路值得我们日常开发中借鉴。
希望文章可以给你带来收获和思考,如果有任何疑问,欢迎评论区留言讨论。如果本文对你有帮助,请帮忙点个在看,点个赞,或者转发给更多的小伙伴,获取三色标记法相关资料,请关注公众号,回复:三色
网站推荐
[CiteSeerX](citeseerx.ist.psu.edu)是一个公开的学术文献数字图书馆和搜索引擎,主要集中在计算机和信息科学领域的文献。该网站允许用户搜索、查看和下载相关的学术论文和文献,包括论文、会议记录、技术报告等。
CiteSeerX的特点包括:
- 自动引文索引:CiteSeerX使用算法自动从文档中提取引文,并创建文献之间的引用链接。
- 自动元数据提取:它能自动识别文档的元数据,如标题、作者、出版年份等。
- 相关文档推荐:根据用户的搜索和查看历史,CiteSeerX可以推荐相关的文档。
- 文档更新:CiteSeerX会自动在网络上查找和索引新文档,以保持数据库的更新。
CiteSeerX由宾夕法尼亚州立大学的信息科学与技术学院维护和管理。该项目是科研人员和学生获取计算机科学和相关学科文献的重要资源之一。
参考
HotSpot Virtual Machine Garbage Collection Tuning Guide
Java Garbage Collection Basics
Why does CMS collector collect root references from young generation on Initial Mark phase?
Memory Management in the Java HotSpot Virtual Machine
A Generational Mostly-concurrent Garbage Collector
The JVM Write Barrier - Card Marking
原创好文
来源:juejin.cn/post/7445517512609447951
给 7 年前自己的一封信
7 年前,我被培训机构 8k 高薪的幌子,骗着带款了 2w 块钱,签完合同的后,我才觉察到自己被骗了。
但是 Java 好像挺有意思的,我学得不错,班里有 40 多个人,最后只有 3 个人找到了工作,我就是其中之一。
于是我入行了 Java,我的第一份工作在去哪儿,很感谢当年的领导给我面试机会,要知道当时我只有高中文凭,HR 反复确认要我来面试吗?我的领导说来试试吧。
为了那次面试,我推掉了所有其他面试,我对自己说一定要拿下这个 offer,在去面试的地铁上,我还在准备,我甚至准备了万一别人不要我,我可以做些什么来挽回。
后来面试通过了。
我珍惜这来之不易的机会,努力奋斗,晚上 10 点多下班是常态,有次冬天加到 12 点,我走在路上看着满大街的雪,反射出白光照向天空,亮亮的一点也不像晚上,我也一点都不冷。
那真是段美好的回忆,但是一年半后,压力太大,我主动提出了离职。
我跳槽到一家私企,工资翻了一倍,而且工作内容相比较之前,轻松的要死。除了基础代码开发,我顺手把服务器、Jenkins 部署、发布脚本、gitlab、redis、测试和线上环境全搭了。
但公司业务没做起来,部门解散了。
我接着找工作,年底通过了 thoughworks 的面试,和 HR 约定好了开年就发 offer 去上班,接到电话的时候我在图书馆,真是开心死了,现在回忆起来也不经咧开了嘴,爷也是能去外企的人了哈哈。
但是造化弄人,过年期间疫情来了,我的 offer 没了。
后来找到了一家创业公司,我和 boss 聊的很合拍,入职后工作了一年多,他和福禄建立了深度合作,把我带了过去。
截止至今,我在福禄工作了四年多,现在我面临着和 6 年前一样的问题,我对工作有了不一样的看法。
人们把工作当成一个赚钱的方式,出卖自己的时间换取经济价值。当然这没有问题,但我相信我的 boss,我感谢他知遇之恩,这些年我一直尽力多做一些事情。
只不过我的能力和认知,没有到那个境界,做的是很多,但也做错了很多,方法不对,成长也不够。
直到最近一次线上事故,我被击垮了。这是我五年来,第一次滋生离职的念头。
放下了对 boss “报恩” 的想法,我开始再次思考,工作的意义、生活的意义、人生的意义。
这不是我第一次思考,虽然我到现在也没找到答案。
曾经我的生活一眼望得到头,事业上,35 岁前努力工作存钱,35 后有了一定风险,但应该还能再工作几年,只要任劳任怨,万一真没公司要,再想别的出路,什么出路,我也不知道,生命总会找到出路。
生活中,20 多岁结婚,过几年生个娃,然后赚钱养家,天天为孩子奔波,把孩子养大,尽量给他好的环境。
身边的同事、朋友都是这样的,他们也劝我这样,不要想那些有的没的。
但是我忍不住会想,那我呢?我把时间给了工作、家庭、孩子,我在哪里?
有多少人真的了解自己?我不了解,我不知道自己喜欢什么,擅长什么,未来想做什么,想成为什么样的人。
我只是随大流,别人做什么,我也跟着做。买房、买车、结婚、生娃,这些人生重要节点,我做的那么随意。
痛苦让我成长,让我反思。如果回到 10 年前,我会对那时的自己这么说。
人生的重大决策,一定要仔细思考
买房
不要买房,不要背 30 年带款。这会把人压死,让人不敢尝试,不敢探索,失去勇气。
有个房贷压在头顶,那种窒息感和压力,无时无刻不在消耗自己。
做任何经济上有关的决策,都会忍不住想到我还有房贷呢。
会错失很多机会,也会让操作变形。
买车
车也是个消耗品,买车要钱,养车也要钱。停车费、过路费、油费、保养、车险,每年怎么也要大几千上万。
如果车带来的价值不如车的支出,我建议直接把房子租在公司附近,走路上下班,平时有事打车。
结婚
我之前从未思考过结婚意味着什么,也是随大流的和一个女孩子谈恋爱,谈了几年差不多了,就结婚了。
结婚意味着,和一个人共度余生。
这种影响未来几十年的决定,我甚至没认真思考过一天。
所以请一定要认真思考,可能即使我怎么说也不能理解,第一次结婚都没什么经验。
具体点说就是不要因为父母催婚去结婚;想清楚两个人之间的大方向上能不能统一;想清楚自己想从婚姻中获得什么,能提供什么价值;不要因为牛牛充血一时冲动;情绪化的选择,最终都会后悔,并付出惨重代价。
生娃
生娃那简直是比结婚更要命的存在。
以我现在的认知,我真的不理解为什么要生娃。
知乎上各种各样的答案,没一个能说服我,什么觉得生活没意思造个娃;夫妻生活不和谐靠娃调节等。
如果觉得生活没意思,夫妻没感情,那是自己的问题,生娃只是转移了问题,而不是解决问题。
所以我的态度是在没有想清楚为什么生娃之前,不生娃。
买车、买房、生娃,就算决定要做,我也建议晚点做,趁年轻先把事业打顺。
事业
刚进社会,懵懵懂懂啥都不知道,哪家公司给的钱多就去哪家。这是对的。
然后努力工作,不要躺平,多赚点钱,多存点钱。
工作了几年以后,兜里有点积蓄了,能覆盖两三年的支出,就要开始思考了。
这份工作有前途吗?我喜欢吗?对我个人有什么帮助?能学到什么知识?可不可以试试别的工作?
选择什么工作,入什么行业。这也是影响未来几十年的决策。
我觉得不要把工作看成出卖时间换取收入,那样会觉得自己在给别人打工,在给别人做事,心很累,做的事也不咋地。
应该把工作看成能力训练场、大型实验基地、资源交换中心;通过工作提升自己能力,让自己更值钱,通过公司验证个人的方法,学习经验,整理方法论。
把技能和经验学到一定程度,就可以结合手头积累的资源,自己创业了。
以学会赚钱为目的,为自己打工。
现在新能源和 AI 是公认的有前景的行业,往这两个方向靠。
现在我处在有点积蓄,准备尝试新的方向,去和钱比较近的岗位,学习赚钱能力,为以后创业赚钱打打基础。
寻找人生意义
我想有钱了再思考这个问题,被生活压的喘不过气的人,天天为生活奔波,哪有时间想这些呢?
在没有找到答案前,认真生活,对自己负责。
空闲时间刷抖音,打游戏,到处玩,偶尔放松可以理解,一直这样不行,这不叫认真生活。
当然这不怪当事人,我之前也是那样的,下班和周末看直播,刷短视频,搞学习什么,不存在的。
直到近期的变故,我深入思考这些问题,到处找课找人学习,认知有了提升,做这些事变得理所当然。
这种转变就像之前是强迫自己每天必须写篇文章,做复盘写总结,用意志力坚持,很痛苦,坚持不了多久。
现在是就是想写了,有感悟,想找个地方记录,主动的写,认真的写。
希望我的经历可以给各位参考,尽快提升认知,趁年轻,还有机会。
不要等有了车贷房贷,还有娃,但被裁了,或还没被裁但被当牛做马使劲压榨,而自身却没了任何反抗资本,才幡然醒悟,那样太残忍了些。
加油,共勉。
来源:juejin.cn/post/7458954918590988328
2024年,30岁前最后一次年度思考
没错!95年,还剩几个月就奔三了。2024年,注定是人生中意义非凡的一年,忐忑、裁员、出书、求职、转正这几个词贯穿了一整年。
忐忑
在上一家公司时,我从面试开始和到入职半年转正后,其实内心对于公司的状况一直保持一种忐忑不安的心情,这种感觉跟我老婆说过几次,我们一致认为应当有心理准备。原因在于薪资与公司的组织架构、基础建设、日常工作量安排和人员扩充速度都让人感到迷惑。
公司是在一个包括高层话事人不断更换,高层(副总裁)突然接受停止调查;技术部门仅仅作为辅助,技术氛围低沉,基建缺失,直属leader作用甚微;工作量与人员匹配失常,人多活少,尽管如此年初还在不断扩招中,泡沫感极强,伴随着薪酬发放日漂浮不定,每到月底像是在开盲盒,你永远不知道银彳亍卡何时会有一笔款到账。
裁员
一系列薪酬制度改革和薪酬拖欠不得不怀疑高层战略的正确性,直到四月某一天CTO私聊我,泡沫破裂,裁员尘埃落定。
我被归属于第一批裁员名单中,与CTO交谈中,似乎也流露一丝对高层决策的不满,但没有明说,给我的理由是当前工作任务都很简单,匹配不了我的能力,所以给了我一个名额。
这放在当时听上去有些许意外,但我接受了这种措辞,并不是因为CTO说了几句好听的话,更多是我作为一个技术人的直觉认为这个CTO靠谱。离职过程中对人事提出的补偿计算方式以及分期发放,我都拒绝了,最后经过与人事反复讨论之后拿到了补偿,少不了他的协助,所以内心表示感谢。从现在的视角看来,似乎是他已经意料到公司的发展趋势,以致于后来被裁员的人有很大一部分都没有赔偿。
出书
离职后我在家休息了一个月,期间也为了帮一个粉丝忙,接手了他工作的一部分任务,主要是做游戏业务的动画。期间有被一个后端恶心到,业务不熟悉,接口一直不通就算了,关键还理直气壮说是前端问题;我佩服那个粉丝能够忍气吞声这么久,换做其他人也很难不高血压,为此特意发圈宣泄。
由于后端提供的接口迟迟不通,需求没有预期上线,为此他们老板还大发雷霆,最后把锅推给了这个前端粉丝,声称把他给炒了。没过一个月,粉丝的这个公司被帽子叔叔查封,业务涉及到了灰产,老板和负责人进去了。员工的工资都没发,但我的报酬是因为签了合约,在deadline之前要求他们打款,对我没有影响,这是苦了这个粉丝。
在此之后我便全职写书,《NestJS全栈开发解析:快速上手与实践》 这本书临近结尾,我一鼓作气完成了并在5.1号劳动节那天交稿;写书的想法也有一部分是来源于CTO的启发,后面图书审阅也是找了CTO帮忙,熬夜帮我看完并给了这个评语,为此我很感谢他。
经过几个月的审批和改稿,图书在9月份正式发布了各大平台,这是一件值得高兴的事情。
而对于前司的后续,据说后面还搬到一个CBD进行办公,但当时员工已经欠薪几个月,以至于到年底,公司被迫全员原地解散,很遗憾这不是一个好结果。
求职
交稿完成后,花了一个月左右时间求职,拿到了3个offer,最后选择了去深圳的美图,这是凭借NestJS的图书写作获得的一个岗位。之后由于组织架构变化,我在转正前夕面临选择继续从事Node全栈还是Go语言开发,考虑一番后我选择了后者,顺利转到了后端架构组,负责go语言开发,这对我来说又是一个新的尝试和挑战,我选择了这种变化,与框架和语言无关,只不过是践行我的人生哲学:【不断变化】,让自己处于一种长期乐观、短期痛苦、当下快乐的舒适区边缘中。
觉醒
关于成长,过去我一直不喜欢看历史,或许归根于上学时代对于历史学科的厌倦,没看过基本历史文献。2024年底,我看了教员的《毛选》、《实践论》、《矛盾论》、《寻乌调查》,第一种感受是成功绝不是偶然,环环相扣的逻辑能力令人惊叹。我想这些书籍回答了我一直以来的问题:
如何成为一个独立、深度思考的人?
我们人生中做了一个坏的决定,在股市中选择了不争气的股票,最坏的结果无非是让自己从头再来。但革命不同,选择错了就有可能让整个民族处于被毁灭的境地中,每一步都步履蹒跚,这该有怎样的智慧与思维?
第二种感受是遗憾没有早点开悟,在临近30岁时才开始阅读这些书籍,当然也很庆幸没有太晚,一切都来得及!
特别的是,《寻乌调查》报告里面的细节,应该是我人生中读过的一本最详细的一本书籍,里面还记载了寻乌与我老家(兴宁)相关的历史宜了,没有一句多余的,都是干货。第一次感受原来伟人离我这么近。
教员做了这个调查报告之后,便留下一句千古格言:没有调查,就没有发言权!反观自身,何尝不是应该这样呢?
关于家庭,今年整个过程中家里的大大小小的事基本上都是我老婆操办,为我们的小家默默付出了很多,加上我去了深圳之后,我的衣食住大部分也是她来打理,一个人照顾小孩,现在甜筒一岁半了,如我们所愿健康成长,这隶属她的功劳。
一个家庭要想变好,靠一个人努力不行,需要“拉拢”有能力的人一起,话事人脑子要清醒,能够明辨是非,唯唯诺诺绝对是会出问题的。
一个家族要想变好,靠一两个人不行,得靠一两个家庭真正向好,大家庭才会有希望。
最后,没有Flag,年度总结中对未来进行遐想没有意义,沉浸于自己完成所有Todo List的那种兴奋是虚构的,而实践中那种痛苦、无助才是我们最真实的感受,人不能总活在无限遐想的递归当中
。
我看过那些在新年Flag列举诸多愿望,买了一堆书籍想要读完的,来年能真正落地完成的少之又少,毕竟我亦如此。
2025年,爱自己,爱家人,步步为营,不负将来!祝所有支持我的粉丝朋友们,一切如意,事业感情双丰收~
来源:juejin.cn/post/7455282891535302708
用 DeepSeek 打造你的超强代码助手
大家好,今天我想给你们介绍一个我最近发现的工具,叫 DeepSeek Engineer。它是一个专门为开发者打造的代码助手应用,可以帮你读文件、改文件,甚至生成代码。更厉害的是,它完全基于 DeepSeek API,能实时生成 JSON 格式的响应,让你的开发体验提升一个档次。
DeepSeek Engineer 是啥?
简单来说,DeepSeek Engineer 是一个基于命令行的智能助手。它能帮你完成这些事:
- 快速读文件内容:比如你有个配置文件,直接用命令把它加载进助手,后续所有操作都可以基于这个文件。
- 自动改文件:它不仅能提建议,还可以直接生成差异表(diff),甚至自动应用修改。
- 智能代码生成:比如你让它生成代码片段,它会按照指定格式和规则直接返回。
更重要的是,这一切都是通过 DeepSeek 的强大 API 来实现的。想象一下,你有个贴身助手,不仅能听懂你的代码需求,还能直接动手帮你写!
核心功能拆解
我们先来看 DeepSeek Engineer 的几个核心能力,让你更好地理解它的强大之处。
1. 自动配置 DeepSeek 客户端
启动这个工具时,你只需要准备一个 .env
文件,里面写上你的 API Key,比如:
DEEPSEEK_API_KEY=your_api_key_here
然后它会自动帮你连接到 DeepSeek 的服务器(地址通过环境变量配置)。接下来,所有的对话和操作都走这个 API,让你体验到类似 GPT 的流畅交互。
2. 数据模型:严格又灵活
DeepSeek Engineer 使用了 Pydantic 来定义和管理数据模型,这保证了所有操作都很安全且清晰。比如,它的模型包括以下几个部分:
- FileToCreate:描述新建或更新的文件。
- FileToEdit:定义某个文件里需要替换的代码片段。
- AssistantResponse:用来结构化处理助手返回的对话内容和文件操作。
具体来说,如果你想改文件内容,可以让它返回一个 JSON 格式的修改建议,类似这样:
{
"file": "example.py",
"changes": [
{
"original": "print('Hello')",
"replacement": "print('Hello, DeepSeek!')"
}
]
}
这种方式既直观又安全,你完全可以放心地应用这些修改。
3. 强大的系统 Prompt
DeepSeek Engineer 背后有一个设计得非常好的系统 Prompt,它会引导对话始终输出结构化的 JSON 数据,同时还能支持文件创建和编辑操作。
这个设计的好处是,开发者不用担心助手回复出错或格式混乱。所有的响应都像程序接口一样,清晰、标准。
4. 常用 Helper 函数
工具中还提供了一些实用的函数,专门用来操作文件和内容:
read_local_file
:快速读取本地文件内容,返回成字符串。create_file
:帮你新建或覆盖文件。show_diff_table
:生成一个漂亮的差异表,展示文件修改前后的对比。apply_diff_edit
:直接应用代码片段级别的修改。
比如,你想更新一个文件里的某段代码,只需输入以下命令:
/add path/to/file
DeepSeek 会把这个文件的内容加载进来,你可以继续对话,让它生成修改建议并直接应用到文件中。
5. 交互式会话
运行主程序(比如 python3 main.py
),你会进入一个交互式的命令行界面。这里你可以随时输入请求、加载文件,或者让助手生成代码。
完整操作流程可以是这样的:
- 启动工具:
python3 main.py
- 加载一个文件:
/add example.py
- 让助手修改内容:
请把函数 `foo` 改成返回值为整数。
- 查看生成的建议并确认应用。
是不是很贴心?
与其他工具的对比
市面上其实有不少类似的代码助手,比如 GitHub Copilot、TabNine 等。那么 DeepSeek Engineer 和它们相比有什么特别之处呢?我们通过下表来简单对比一下:
功能 | DeepSeek Engineer | GitHub Copilot | TabNine |
---|---|---|---|
文件内容读取 | ✅ 支持 | ❌ 不支持 | ❌ 不支持 |
文件修改和应用 | ✅ 支持 | ❌ 不支持 | ❌ 不支持 |
JSON 响应结构化 | ✅ 内置支持 | ❌ 不支持 | ❌ 不支持 |
离线使用 | ❌ 需要联网 | ❌ 需要联网 | ✅ 部分支持 |
灵活性和可定制性 | ✅ 可配置 Prompt | ❌ 不支持 | ❌ 不支持 |
可以看出,DeepSeek Engineer 更加注重文件操作和开发流程的实际需求,非常适合需要精确控制和定制化的场景。
如何快速上手?
最后,说点大家最关心的:怎么用?
- 准备环境
- 安装依赖:
pip install -r requirements.txt
- 配置 API Key:创建
.env
文件,写入你的 Key。
- 安装依赖:
- 启动工具
- 直接运行主程序:
python3 main.py
- 直接运行主程序:
- 体验功能
- 用
/add
命令加载文件:
/add your_file.py
- 提出需求,让助手生成代码或修改建议。
- 用
- 探索更多用法
- 修改配置,试试用不同的环境变量自定义连接方式。
来源:juejin.cn/post/7454888708588945443
升级到 Java 21 是值得的
升级到 Java 21 是值得的
又到了一年中的这个时候——New Relic 的年度“State of the Java Ecosystem”调查结果出来了,我一如既往地深入研究了它。虽然我认为该报告做得很好并且提出了很好的问题,但我对有多少 Java 开发人员正在使用低版本感到沮丧。
您使用的是 Java 21 吗?确实应该使用了。
在开始调查之前,作为一名 Java 爱好者,我想谈谈我最喜欢的关于 Java 21 的一些事情。
首先我要说的是,Spring Boot 3.x 是当前 Java 虚拟机 (JVM) 上最流行的服务器端技术栈,至少需要 Java 17。它不支持 Java 8,这是第二个版本。根据调查,最常用的版本。
我很高兴看到 Java 17 的采用进展相对较快,但您确实应该使用 Java 21。Java 21 比 Java 8 好得多。它在所有方面都在技术上优越。它更快、更安全、更易于操作、性能更高、内存效率更高。
道德上也很优越。当您的孩子发现您在生产中使用 Java 8 时,您不会喜欢他们眼中流露出羞愧和悲伤的表情。
做正确的事,成为你希望在世界上看到的改变:使用 Java 21。它充满了优点,基本上是自 Java 7 以来的一种全新语言:Lambdas,Multiline strings。Smart switch expressions。 var
。Pattern matching。Named tuples(在 Java 中称为 records
)。
当然,最重要的是虚拟线程。虚拟线程是一件大事。它们提供了与 async
/ await
或 suspensions 相同的优点,但没有其他语言中冗长代码。
是的,你明白我的意思了。与其他语言相比,Java 的虚拟线程提供了更好的解决方案,并且代码更少。
如果你不知道我在说什么,并且使用其他语言,那么你现在会很生气。java?比您最喜欢的语言更简洁?不可能的!但我并没有错。
为什么虚拟线程很重要
要了解virtual threads,您需要了解创建它们是为了解决的问题。如果您还没有体验过虚拟线程,那么它们有点难以描述。我会尽力。
Java 有阻塞操作——比如 Thread.sleep(long)
、 InputStream.read
和 OutputStream.write
。如果您调用其中一个方法,程序将不会前进到下一行,直到这些方法完成它们正在做的事情并返回。
大多数网络服务都是 I/O 密集的,这意味着它们将大部分时间花在输入和输出方法上,例如 InputStream.read
和 OutputStream.write
。
任务提交到线程池中却没有更多线程的服务是很常见的,但仍然无法返回响应,因为所有现有线程都在等待某些 I/O 操作发生,例如跨线程的 I/O HTTP 边界、数据库或消息队列的 I/O。
有多种方法可以解锁 I/O。您可以使用 java.nio
,它非常复杂,会引起焦虑。您可以使用reactive式编程,它的工作原理是范式的(paradigmatically),但它是对整个代码库的完整重构。
因此,我们的想法是:如果编译器知道您何时执行了可能会阻塞的操作(例如 InputStream.read
)并重新排序代码的执行,这不是很好吗?因此,当您执行阻塞操作时,等待代码将从当前执行线程移出,直到阻塞操作完成,然后在准备好恢复执行后将其放回另一个线程。
这样,您就可以继续使用阻塞语义。第一行在第二行之前执行。这提高了可调试性和可扩展性。您不再垄断线程只是为了在等待某些事情完成时浪费它们。这将是两全其美:非阻塞 I/O 的可扩展性与更简单的阻塞 I/O 的明显简单性、可调试性和可维护性。
许多其他语言,如 Rust、Python、C#、TypeScript 和 JavaScript,都支持 async
/ await
。 Kotlin 支持 suspend
。这些关键字提示运行时您将要做一些阻塞的事情,并且它应该重新排序执行。这是一个 JavaScript 示例:
async function getCustomer(){ /* call a database */ }
const result = await getCustomer();
问题症结在于要调用 async
函数,还必须位于 async
函数中:
async function getCustomer(){ /* call a database */ }
async function main(){
const result = await getCustomer();
}
async
关键字 是病毒式的。它蔓延开来。最终,你的代码会陷入 async
/ await
的泥潭——你为什么在任何可能的地方使用async/await
呢?因为,它比使用低级、非阻塞 I/O 或反应式编程要好,但也只是勉强好。
Java 提供了一种更好的方法。只需为您的线程使用不同的工厂方法即可。
如果您使用 ExecutorService
创建新线程,请使用创建虚拟线程的新版本。
var es = Executors.newVirtualThreadPerTaskExecutor();
// ^- this is different and you'll probably only do it once
// or twice in a typical application
var future = es.submit(() -> System.out.println("hello, virtual threads!"));
如果您直接在较低级别创建线程,则使用新的工厂方法:
// this is different
var thread = Thread.ofVirtual().start(() -> System.out.println("hello, virtual threads!"));
您的大部分代码保持完全不变,但现在您的可扩展性得到了显着提高。如果您创建数百万个线程,运行时不会喘息。我无法预测您的结果会是什么,但您很有可能不再需要运行给定服务的几乎同样多的实例来处理负载。
如果您使用的是 Spring Boot 3.2(您是,不是吗?),那么您甚至不需要执行任何操作。只需在 application.properties
中指定 spring.threads.virtual.enabled=true
,然后向管理层请求加薪,费用由大幅降低的云基础设施成本支付。
并非每个应用程序都可以在技术上实现跨越,但其中绝大多数可以而且应该。
使用情况报告分析
最后,这让我回到了 New Relic 报告。不要误会我的意思:它做得非常好,值得一读。就像莎士比亚悲剧一样,它写得很好,讲述了一个悲伤的故事。
有一个完整的部分证实了显而易见的事实:天空是蓝色的,云彩无处不在。在容器中部署工作负载似乎是主流模式,受访者表示 70% 的 Java 工作负载使用容器。坦白说,我很惊讶它这么低。
同样令人感兴趣的是从单核配置转向多核的趋势。根据调查,30% 的容器化应用程序正在使用 Java 9 的 -XX:MaxRAMPercentage
标志,该标志限制了 RAM 使用。 G1 是最流行的垃圾收集器。一切都很好。
当涉及到 Java 版本时,该报告发生了悲剧性的转变。超过一半的应用程序(56%)在生产中使用 Java 11,而 2022 年这一比例为 48%。Java 8(十年前的 2014 年发布)紧随其后,近 33% 的应用程序在生产中使用它。根据调查,三分之一的应用程序仍在使用 Java 版本,该版本在《Flappy Bird》游戏被下架、《冰桶挑战》横扫 Vine、《Ellen DeGeneres 奥斯卡》自拍照火爆的同一年推出。
多个用户使用 Amazon 的 OpenJDK 分发版。该报告表明,这是因为甲骨文暂时为其发行引入了更严格的许可。但我想知道其中有多少只是 Amazon Web Services(最多产的基础设施即服务 (IaaS) 供应商)上 Java 工作负载默认分布的函数。自几年前推出以来,该发行版已受到广泛欢迎。 2020年,它的市场份额为2.18%,现在则为31%。如果这么多人可以如此迅速地迁移到完全不同的发行版,那么他们应该能够使用同一发行版的新版本,不是吗?
我想,趋势中还是有一些希望的。 Java 17 用户采用率一年内增长了 430%。因此,也许我们会在 Java 21 中看到类似的数字——Java 21 已经全面发布近六个月了。
你还在等什么?
正如我在 Voxxed Days 的演讲中所说,我相信现在是成为 Java 和 Spring Boot 开发人员的最佳时机。 Java 和 Spring 开发人员拥有最好的玩具。我什至还没有提到 GraalVM 本机映像,它可以显着缩短给定 Java 应用程序的启动时间并减少内存占用。这已经与 Java 21 完美配合。
这些东西就在这里,它们太棒了。能否实现这一跳跃取决于我们。这并不难。试试看。
安装 SDKMan,运行 sdk install java 21.0.2-graalce
然后运行 sdk default java 21.0.2-graalce
。这将为您提供 Java 21 和 GraalVM 本机映像编译器。访问 Spring Initializr,这是我在网络上第二喜欢的地方(仅次于生产),网址为 start.spring.io。配置一个新项目。选择 Java 21(自然!)。添加 GraalVM Native Support
。添加 Web
。点击 Generate
按钮并将其加载到您的 IDE 中。在 application.properties
中指定 spring.threads.virtual.enabled=true
。创建一个简单的 HTTP 控制器:
@Controller
class Greetings {
@GetMapping("/hi")
String hello(){
return "hello, Java 21!";
}
}
将其编译为 GraalVM 本机映像: ./gradlew nativeCompile
。运行 build
文件夹中的二进制文件。
现在,您已经有了一个应用程序,该应用程序只占用非 GraalVM 本机映像所需 RAM 的一小部分,并且还能够扩展到每秒更多的请求。简单,而且令人惊奇。
原文地址:We CAN Have Nice Things: Upgrading to Java 21 Is Worth It - The New Stack
来源:juejin.cn/post/7345763454814765083
工作中 Spring Boot 五大实用小技巧,来看看你掌握了几个?
0. 引入
Spring Boot 以其简化配置、快速开发和微服务支持等特点,成为了 Java 开发的首选框架。本文将结合我在实际工作中遇到的问题,分享五个高效的 Spring Boot 的技巧。希望这些技巧能对你有所帮助。
1. Spring Boot 执行初始化逻辑
1.1 背景
项目的某次更新,数据库中的某张表新增了一个字段,且与业务有关联,需要对新建的字段根据对应的业务进行赋值操作。
一种解决方案就是,更新前手动写 SQL 更新字段的值,但这样做的效率太低,而且每给不同环境更新一次,就需要手动执行一次,容易出错且效率低。
另一种方案则是在项目启动时进行初始化操作,完成字段对应值的更新,这种方案效率更高且不容易出错。
1.2 实现
Spring Boot 提供了多种方案用于项目启动后执行初始化的逻辑。
- 实现
CommandLineRunner
接口,重写run方法。
@Slf4j
@Component
public class InitListen implements CommandLineRunner {
@Override
public void run(String... args) {
// 初始化相关逻辑...
}
}
- 实现
ApplicationRunner
接口,重写run方法。
@Slf4j
@Component
public class InitListen implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// 初始化相关逻辑...
}
}
- 实现
ApplicationListener
接口
@Slf4j
@Configuration
public class StartClientListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent arg0) {
// 初始化逻辑
}
}
针对于上述这个需求,如何实现仅更新一次字段的值?
可在数据库字典表中设置一个更新标识字段,每次执行初始化逻辑之前,校验判断下字典中的这个值,确认是否已经更新,如果已经更新,就不需要再执行更新操作了。
2. Spring Boot 动态控制数据源的加载
2.1 背景
期望通过在application.yml
文件中,添加一个开关来控制是否加载数据库。
2.2 实现
启动类上添加注解 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }),代表禁止 Spring Boot 自动注入数据源。
新建 DataSourceConfig
配置类,用于初始化数据源。
在DataSourceConfig配置类上添加条件注解 @ConditionalOnProperty(name = "spring.datasource.enabled", havingValue = "true",代表只有当 spring.datasource.enabled 为 true时,加载数据库,其余情况不加载数据库。
仓库类 XxxRepository 的注入,需要使用注解 @Autowired(required = false)
详细可见文章:
Spring Boot 如何动态配置数据库的加载
3. Spring Boot 根据不同环境加载配置文件
3.1 背景
实际开发工作中,针对同一个项目,可能会存在开发环境、测试环境、正式环境等,不同环境的配置内容可能会不一致,如:数据库、Redis等等。期望在项目在启动时能够针对不同的环境来加载不同的配置文件。
3.2 实现
Spring 提供 Profiles 特性,通过启动时设置指令-Dspring.profiles.active
指定加载的配置文件,同一个配置文件中不同的配置使用---
来区分。
启动 jar 包时执行命令:
java -jar test.jar -Dspring.profiles.active=dev
-Dspring.profiles.active=dev
代表激活 profiles 为 dev 的相关配置。
## 用---区分环境,不同环境获取不同配置
---
# 开发环境
spring:
profiles: dev
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 命名空间为默认,所以不需要写命名空间
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
extension-configs[0]:
data-id: database-base.yaml
group: DEFAULT_GR0UP
refresh: true
extension-configs[1]:
# 本地单机Redis
data-id: redis-base-auth.yaml
group: DEFAULT_GR0UP
refresh: true
extension-configs[2]:
data-id: master-base-auth.yaml
group: DEFAULT_GR0UP
refresh: true
---
#测试环境
spring:
profiles: test
cloud:
nacos:
discovery:
server-addr: 192.168.0.111:8904
# 测试环境注册的命名空间
namespace: b80b921d-cd74-4f22-8025-333d9b3d0e1d
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
extension-configs[0]:
data-id: database-base-test.yaml
group: DEFAULT_GR0UP
refresh: true
extension-configs[1]:
data-id: redis-base-test.yaml
group: DEFAULT_GR0UP
refresh: true
extension-configs[2]:
data-id: master-auth-test.yaml
group: DEFAULT_GR0UP
refresh: true
---
# 生产环境
spring:
profiles: prod
cloud:
nacos:
discovery:
server-addr: 192.168.0.112:8848
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
extension-configs[0]:
# 生产环境
data-id: database-auth.yaml
group: DEFAULT_GR0UP
refresh: true
extension-configs[1]:
# 生产环境
data-id: redis-base-auth.yaml
group: DEFAULT_GR0UP
refresh: true
extension-configs[2]:
data-id: master-base-auth.yaml
group: DEFAULT_GR0UP
refresh: true
也可以定义多个配置文件,如在application.yml中定义和环境无关的配置,而application-{profile}.yml
则根据环境做不同区分,如在 application-dev.yml 中定义开发环境相关配置、application-test.yml 中定义测试环境相关配置。
启动时指定环境命令同上,仍为:
java -jar test.jar -Dspring.profiles.active=dev
4. Spring Boot 配置文件加密
4.1 背景
配置文件中包含的敏感信息(如数据库密码)都会以明文的形式存储,这种情况可能会存在安全风险,期望通过加密配置文件,确保应用程序的安全。
4.2 实现
- pom.xml 文件中引入依赖。
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
如果遇到 Unresolved dependency: 'com.github.ulisesbocchio:jasypt-spring-boot-starter:jar:2.1.2' 的错误,可执行
mvn clean install -U
强制更新依赖。
- application.yml 文件中增加配置如下:
jasypt:
encryptor:
password: G0C3D17o2n6
algorithm: PBEWithMD5AndDES
- 执行测试用例,获取加密后的内容。
@RunWith(SpringRunner.class)
@SpringBootTest
public class DatabaseTest {
@Autowired
private StringEncryptor encryptor;
@Test
public void getPass() {
String url = encryptor.encrypt("jdbc:mysql://localhost:3306/demo");
String name = encryptor.encrypt("root");
String password = encryptor.encrypt("123456");
System.out.println("database url: " + url);
System.out.println("database name: " + name);
System.out.println("database password: " + password);
Assert.assertTrue(url.length() > 0);
Assert.assertTrue(name.length() > 0);
Assert.assertTrue(password.length() > 0);
}
}
根据测试用例获取的结果,将加密后的字符串替换明文。
- 启动程序,验证数据库能否正常连接。
为了防止 jasypt.encryptor.password 泄露,反解出密码,有两种方案:
- 将 jasypt.encryptor.password 设置为环境变量,如:
vim /etc/profile
export jasypt.encryptor.password=YOUR_SECRET_KEY
- 将 jasypt.encryptor.password 作为启动程序的参数,如:
java -jar xxx.jar -Djasypt.encryptor.password=YOUR_SECRET_KEY
5. Spring Boot对打包好的jar包瘦身
5.1 背景
Sprng Boot项目的 jar 包动辄几百MB,如果有小的需求更新或者是Bug修复,就需要重新打包部署,改了一行代码,却上传几百MB的文件,这样会很浪费时间。
期望通过给 jar 包瘦身,从而节省部署的时间。
5.2 实现
pom.xml
文件中添加如下配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
<layout>ZIP</layout>
<!--这里是填写需要包含进去的jar,
必须项目中的某些模块,会经常变动,那么就应该将其坐标写进来
如果没有则nothing ,表示不打包依赖 -->
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
</plugin>
<!--拷贝依赖到jar外面的lib目录-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!--指定的依赖路径-->
<outputDirectory>
${project.build.directory}/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- 执行
mvn clean package
得到 jar 包,在项目启动时,需要通过 -Dloader.path指定lib的路径,如:
java -Dloader.path=./lib -jar testProject-0.0.1-SNAPSHOT.jar
效果如下:
通过分析 jar 包的结构可以得知,jar 包的 “大” 实际上是因为在打包时,会将项目所依赖的 jar 包放在 lib 夹文件中。而这部分依赖在版本迭代稳定后,基本是不会变化的。
上述这种给 jar 包瘦身的方案,实际上是在打包的时候忽略 lib 文件夹中的这些依赖,将这部分不变的依赖提前放到服务器上,打出来的 jar 包就变小了,从而提升发版效率。
参考资料
zhuanlan.zhihu.com/p/646593227
来源:juejin.cn/post/7424906244215193636
花式封装:Kotlin+协程+Flow+Retrofit+OkHttp +Repository,倾囊相授,彻底减少模版代码进阶之路
前言 :众里寻它千百度, 蓦然回首,此种代码却在灯火阑珊处。
一、前言
- 本文介绍思路:
本文重点介绍思路:四种方式花式解决Repository
中模版式的代码,逐级递增
1.1 :涉及到Kotlin
、协程
、Flow、viewModel、Retrofit、Okhttp
相关用法
1.2 :涉及到注解
、反射
、泛型
、注解处理器
相关用法
1.3 :涉及到动态代理
,kotlin
中suspend
方法反射调用及反射中异常处理
1.4 :本示例4个项目如图: - 网络框架搭建的封装,到目前为止最为流行又很优雅的的是
Kotlin
+协程
+Flow
+Retrofit
+OkHttp
+Repository
- 先来看看中间各个类的职责:
- 从上图可以看出
单一职责:
NetApi:
负责网络接口配置,包括 请求地址,请求头,请求方式,参数等等所有配置Flow+Retrofit+Okhttp:
联合起来负责把NetApi
中的各种配置组装成网络请求行为,并且通过Flow 组装成流,通过它可以控制该行为的异步方式,异步开始结束等等一系列的流行为。Repository:
负责Flow+Retrofit+Okhttp
请求结果的数据流,进行加工处理成我们想要的数据,大多数不需要处理的,可以直接给到ViewModel
ViewModel:
负责调用Repository
,拿到想要的数据然后提供给UI方展示使用或者相关使用也可以看到 它的 持有链 从右向左 一条线性持有:
ViewModel
持有Repository
,Repository
持有Flow+Retrofit+Okhttp
,Flow+Retrofit+Okhttp
持有NetApi
- 最终我们可以得到:
5.1. 网络请求行为 会根据NetApi
写出模板式的代码,这块解决模版式的代码在Retrofit
中它通过动态代理,把所有模版式的代码统一成了一个
5.2. 同理:Repository
也是根据NetApi
配置的接口,写成模版式的代码转换成流
二、花式封装(一)
NetApi
的配置:
interface NetApi {
// 示例get 请求
@GET("https://www.wanandroid.com/article/list/0/json")
suspend fun getHomeList(): CommonResult
// 示例get 请求2
@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList(@Path("path") page: Int): CommonResult
@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList(@Path("path") page: Int, @Path("path") a: Int): CommonResult
@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList(@Path("path") page: Int, @Path("path") f: Float): CommonResult
// 示例get 请求2
@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList2222(@Path("path") page: Int): CommonResult
@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList3333(@Path("path") page: Int): CommonResult
@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList5555(@Path("path") page: Int, @Query("d") ss: String, @HeaderMap map: Map): CommonResult
@GET("https://www.wanandroid.com/article/list/{path}/json")
suspend fun getHomeList6666(
@Path("path") page: Int,
@Query("d") float: Float,
@Query("d") long: Long,
@Query("d") double: Double,
@Query("d") byte: Byte,
@Query("d") short: Short,
@Query("d") char: Char,
@Query("d") boolean: Boolean,
@Query("d") string: String,
@Body body: RequestBodyWrapper
): CommonResult
//示例post 请求
@FormUrlEncoded
@POST("https://www.wanandroid.com/user/register")
suspend fun register(
@Field("username") username: String,
@Field("password") password: String,
@Field("repassword") repassword: String
): String
/************************* 以下只 示例写法,接口调不通,因为找不到那么多 公开接口 全是 Retrofit的用法 来测试 *****************************************************/
// @FormUrlEncoded
@Headers("Content-Type: application/x-www-form-urlencoded") //todo 固定 header
@POST("https://xxxxxxx")
suspend fun post1(@Body body: RequestBody): String
// @FormUrlEncoded
@Headers("Content-Type: application/x-www-form-urlencoded")
@POST("https://xxxxxxx22222")
suspend fun post12(@Body body: RequestBody, @HeaderMap map: Map): String //todo HeaderMap 多个请求头部自己填写
suspend fun post1222(@Body body: RequestBody, @HeaderMap map: Map): String //todo HeaderMap 多个请求头部自己填写
}
2. NetRepository
中是 根据 NetApi
写出下面类似的全模版式的代码:都是返回 Flow
流
class NetRepository private constructor() {
val service by lazy { RetrofitUtils.instance.create(NetApi::class.java) }
companion object {
val instance by lazy { NetRepository() }
}
// 示例get 请求
fun getHomeList() = flow { emit(service.getHomeList()) }
// 示例get 请求2
fun getHomeList(page: Int) = flow { emit(service.getHomeList(page)) }
fun getHomeList(page: Int, a: Int) = flow { emit(service.getHomeList(page, a)) }
fun getHomeList(page: Int, f: Float) = flow { emit(service.getHomeList(page, f)) }
// 示例get 请求2
fun getHomeList2222(page: Int) = flow { emit(service.getHomeList2222(page)) }
fun getHomeList3333(page: Int) = flow { emit(service.getHomeList3333(page)) }
fun getHomeList5555(page: Int, ss: String, map: Map<String, String>) = flow { emit(service.getHomeList5555(page, ss, map)) }
fun getHomeList6666(
page: Int, float: Float, long: Long, double: Double, byte: Byte,
short: Short, char: Char, boolean: Boolean, string: String, body: RequestBodyWrapper
) = flow {
emit(service.getHomeList6666(page, float, long, double, byte, short, char, boolean, string, body))
}
fun register(username: String, password: String, repassword: String) = flow { emit(service.register(username, password, repassword)) }
//
// /************************* 以下只 示例写法,接口调不通,因为找不到那么多 公开接口 全是 Retrofit的用法 来测试 *****************************************************/
//
//
fun post1(body: RequestBody) = flow { emit(service.post1(body)) }
fun post12(body: RequestBody, map: Map<String, String>) = flow { emit(service.post12(body, map)) }
fun post1222(id: Long, asr: String) = flow {
val map = mutableMapOf()
map["id"] = id
map["asr"] = asr
val mapHeader = HashMap()
mapHeader["v"] = 1000
mapHeader["device_sn"] = "Avidfasfa1213"
emit(service.post1222(RequestBodyWrapper(Gson().toJson(map)), mapHeader))
}
}
3. viewModel
调用端:
class MainViewModel : BaseViewModel() {
private val repository by lazy { NetRepository.instance }
fun getHomeList(page: Int) {
flowAsyncWorkOnViewModelScopeLaunch {
repository.getHomeList(page).onEach {
android.util.Log.e("MainViewModel", "one 111 ${it.data?.datas!![0].title}")
}
}
}
}
—————————————————我是分割线君—————————————————
上面花式玩法(一):
此种写法被广泛称作最优雅的一套网络封装
框架,绝大多数中、大厂
基本也就封装到此为止了可能还有些人想着:你的
repository
中就返回了Flow
, 里面就全是简单的emit(xxx)
,我项目里面不是这样的,我的还封装了成功,失败,或者其他的,但总体还是全是模版式的,除了特殊的一些方法,需要在请求前 ,请求后做些处理,有规律有模版的还是占大多数吧,只要大多数都一样的规律模版,都是可以处理的,里面稍微修改下细节,思路都是一样的。哪还能有什么玩法?
可能会有人想到 借助
Hilt
,Dagger2
,Koin
来创建Retrofit
,和创建repository
,创建ViewModel
这里不是讨论依赖注入创建对象的事情哪还有什么玩法?
有,必须有的。
三、花式封装(二)
- 既然上面是
Repository
类中,所有写法都是固定模版式的代码,那么让其根据NetApi:
自动生成Repository
类,我们这里借用注解处理器。 - 具体怎么使用介绍,请参考:
注解处理器在架构,框架中实战应用:MVVM中数据源提供Repository类的自动生成 - 本项目中只需要编译
app_wx2
工程 - 在下图中找到
5. viewModel调用端
class MainViewModel : BaseViewModel() {
private val repository by lazy { RNetApiRepository() }
fun getHomeList(page: Int) {
flowAsyncWorkOnViewModelScopeLaunch {
val time = System.currentTimeMillis()
repository.getHomeList(page).onEach {
android.util.Log.e("MainViewModel", "two 222 ${it.data?.datas!![0].title}")
android.util.Log.e("MainViewModel", "耗时:${(System.currentTimeMillis() - time)} ms")
}
}
}
}
6. 如果 Repository
中某个接口方法需要特殊处理怎么办?比如下图,请求前处理一下,从 拿到数据后我需要再次转化处理之后再给到 viewModel
怎么办?
//我这个接口 ,请求前需要 判断处理一下,拿到数据后也需要再处理一下
fun post333(id: Long, asr: String, m: String, n: String, list: List<String>) = flow {
val map = mutableMapOf()
map["id"] = id
map["asr"] = asr
val mapHeader = HashMap()
mapHeader["v"] = 1000
mapHeader["device_sn"] = "Avidfasfa1213"
//接口调用前 根据 需要处理操作
list.forEach {
if (map.containsKey(id.toString())) {
///
}
}
val result = service.post1222(RequestBodyWrapper(Gson().toJson(map)), mapHeader)
// 拿到数据后需要处理操作
val result1 = result
emit(result1)
}.map {
//需要再转化一下
it
}.filter {
//过滤一下
it.length == 3
}
7. 可以在 接口 NetApi
中该方法上配置 @Filter
注解过滤 ,该方法需要自己特殊处理,不自动生成,如下
@Filter
@POST("https://xxxxxxx22222")
suspend fun post333(@Body body: RequestBody, @HeaderMap map: Map): String
- 如果想 post请求的
RequestBody
内部参数单独出来进入方法传参,可以加上 在NetApi
中方法加上@PostBody
:如下:
@PostBody("{"ID":"Long","name":"String"}")
@POST("https://www.wanandroid.com/user/register")
suspend fun testPostBody222(@Body body: RequestBody): String
这样 该方法生成出来的对应方法就是:
public suspend fun testPostBody222(ID: Long, name: java.lang.String): Flow =
kotlinx.coroutines.flow.flow {
val map = mutableMapOf()
map["ID"] = ID
map["name"] = name
val result = service.testPostBody222(com.wx.test.api.retrofit.RequestBodyCreate.toBody(com.google.gson.Gson().toJson(map)))
emit(result)
}
怎么特殊处理,单独手动建一个Repository,针对该方法,单独写,特殊就要特殊手动处理,但是大多数模版式的代码,都可以让其自动生成。
—————————————————我是分割线君—————————————————
到了这里,我们再想, NetApi
是一个接口类,
但是实际上没有写接口实现类啊, 它怎么实现的呢?
我们上面 花式玩法(二)
中虽然是自动生成的,但是还是有方法体,
可不可以再省略点?
可以,必须有!
四、花式玩法(三)
- 我们可以根据
NetApi
里面的配置,自动生成INetApiRepository
接口类, 接口名和参数 都和NetApi
保持一致,唯一区别就是返回的对象变成了Flow
了,
这样在Repository
中就把数据转变为flow
流了 - 配置让代码自动生成的类:
@AutoCreateRepositoryInterface(interfaceApi = "com.wx.test.api.net.NetApi")
class KaptInterface {
}
生成的接口类 INetApiRepository
代码如下:
public interface INetApiRepository {
public fun getHomeList(): Flow>
public fun getHomeList(page: Int): Flow>
public fun getHomeList(page: Int, f: Float): Flow>
public fun getHomeList(page: Int, a: Int): Flow>
public fun getHomeList2222(page: Int): Flow>
public fun getHomeList3333(page: Int): Flow>
public fun getHomeList5555(
page: Int,
ss: String,
map: Map<String, String>
): Flow>
public fun getHomeList6666(
page: Int,
float: Float,
long: Long,
double: Double,
byte: Byte,
short: Short,
char: Char,
boolean: Boolean,
string: String,
body: RequestBodyWrapper
): Flow>
public fun getHomeListA(page: Int): Flow>
public fun getHomeListB(page: Int): Flow
public fun post1(body: RequestBody): Flow
public fun post12(body: RequestBody, map: Map<String, String>): Flow
public fun post1222(body: RequestBody, map: Map<String, Any>): Flow
public fun register(
username: String,
password: String,
repassword: String
): Flow
public fun testPostBody222(ID: Long, name: java.lang.String): Flow
}
Repository
职责承担的调用端:用动态代理:
class RepositoryPoxy private constructor() : BaseRepositoryProxy() {
val service = NetApi::class.java
val api by lazy { RetrofitUtils.instance.create(service) }
companion object {
val instance by lazy { RepositoryPoxy() }
}
fun callApiMethod(serviceR: Class<R>): R {
return Proxy.newProxyInstance(serviceR.classLoader, arrayOf(serviceR)) { proxy, method, args ->
flow {
val funcds = findSuspendMethod(service, method.name, args)
if (args == null) {
emit(funcds?.callSuspend(api))
} else {
emit(funcds?.callSuspend(api, *args))
}
// emit((service.getMethod(method.name, *parameterTypes)?.invoke(api, *(args ?: emptyArray())) as Call).execute().body())
}.catch {
if (it is InvocationTargetException) {
throw Throwable(it.targetException)
} else {
it.printStackTrace()
throw it
}
}
} as R
}
}
BaseRepositoryProxy
中内容:
open class BaseRepositoryProxy {
private val map by lazy { mutableMapOf?>() }
private val sb by lazy { StringBuffer() }
@OptIn(ExperimentalStdlibApi::class)
fun findSuspendMethod(service: Class<T>, methodName: String, args: Array<out Any>): KFunction<*>? {
sb.delete(0, sb.length)
sb.append(service.name)
.append(methodName)
args.forEach {
sb.append(it.javaClass.typeName)
}
val key = sb.toString()
if (!map.containsKey(key)) {
val function = service.kotlin.memberFunctions.find { f ->
var isRight = 0
if (f.name == methodName && f.isSuspend) {
if (args.size == 0 && f.parameters.size == 1) {
isRight = 2
} else {
f.parameters.forEachIndexed { index, it ->
if (index > 0 && args.size > 0) {
if (args.size == 0) {
isRight = 2
return@forEachIndexed
}
if (it.type.javaType.typeName == javaClassTransform(args[index - 1].javaClass).typeName) {
isRight = 2
} else {
isRight = 1
return@forEachIndexed
}
}
}
}
}
//方法名一直 是挂起函数 方法参数个数一致, 参数类型一致
f.name == methodName && f.isSuspend && f.parameters.size - 1 == args.size && isRight == 2
}
map[key] = function
}
return map[key]
}
private fun javaClassTransform(clazz: Class<Any>) = when (clazz.typeName) {
"java.lang.Integer" -> Int::class.java
"java.lang.String" -> String::class.java
"java.lang.Float" -> Float::class.java
"java.lang.Long" -> Long::class.java
"java.lang.Boolean" -> Boolean::class.java
"java.lang.Double" -> Double::class.java
"java.lang.Byte" -> Byte::class.java
"java.lang.Short" -> Short::class.java
"java.lang.Character" -> Char::class.java
"SingletonMap" -> Map::class.java
"LinkedHashMap" -> MutableMap::class.java
"HashMap" -> HashMap::class.java
"Part" -> MultipartBody.Part::class.java
"RequestBody" -> RequestBody::class.java
else -> {
if ("RequestBody" == clazz.superclass.simpleName) {
RequestBody::class.java
} else {
Any::class.java
}
}
}
}
- ViewModel中调用端:
class MainViewModel : BaseViewModel() {
private val repository by lazy { RepositoryPoxy.instance }
fun getHomeList(page: Int) {
flowAsyncWorkOnViewModelScopeLaunch {
val time = System.currentTimeMillis()
repository.callApiMethod(INetApiRepository::class.java).getHomeList(page).onEach {
android.util.Log.e("MainViewModel", "three 333 ${it.data?.datas!![0].title}")
android.util.Log.e("MainViewModel", "耗时:${(System.currentTimeMillis() - time)} ms")
}
}
}
}
—————————————————我是分割线君—————————————————
- 上面生成的接口类
INetApiRepository
其实方法和NetApi
拥有相似的模版,唯一区别就是返回类型,一个是对象,一个是Flow 流的对象还能省略吗?
有,必须有
五、花式玩法(四)
- 直接修改
RepositoryPoxy
,作为Reposttory的职责 ,连上面的INetApiRepository
的接口类全部省略了, 如下:
class RepositoryPoxy private constructor() : BaseRepositoryProxy() {
val service = NetApi::class.java
val api by lazy { RetrofitUtils.instance.create(service) }
companion object {
val instance by lazy { RepositoryPoxy() }
}
fun callApiMethod(clazzR: Class<R>, methodName: String, vararg args: Any): Flow {
return flow {
val clssss = mutableListOfout Any>>()
args?.forEach {
clssss.add(javaClassTransform(it.javaClass))
}
val parameterTypes = clssss.toTypedArray()
val call = (service.getMethod(methodName, *parameterTypes)?.invoke(api, *(args ?: emptyArray())) as Call)
call?.execute()?.body()?.let {
emit(it as R)
}
}
}
@OptIn(ExperimentalStdlibApi::class)
fun callApiSuspendMethod(clazzR: Class<R>, methodName: String, vararg args: Any): Flow {
return flow {
val funcds = findSuspendMethod(service, methodName, args)
if (args == null) {
emit(funcds?.callSuspend(api) as R)
} else {
emit(funcds?.callSuspend(api, *args) as R)
}
}
}
}
2. ViewModel中调用入下:
class MainViewModel : BaseViewModel() {
private val repository by lazy { RepositoryPoxy.instance }
fun getHomeList(page: Int) {
flowAsyncWorkOnViewModelScopeLaunch {
val time = System.currentTimeMillis()
repository.callApiSuspendMethod(HomeData::class.java, "getHomeListB", page).onEach {
android.util.Log.e("MainViewModel", "four 444 ${it.data?.datas!![0].title}")
android.util.Log.e("MainViewModel", "耗时:${(System.currentTimeMillis() - time)} ms")
}
}
}
}
六、总结
通过上面4中花式玩法:
- 花式玩法1: 我们知道了最常见最优雅的写法,但是模版式
repository
代码太多,而且需要手动写 - 花式玩法2: 把花式玩法1中的模版式
repository
,让其自动生成,对于特殊的方法,单独手动再写个repository
,这样让大多数模版式代码全自动生成 - 花式玩法3:
NetApi
,可以根据配置,动态代理生成网络请求行为,该行为统一为动态代理实现,无需对接口类NetApi
单独实现,那么我们的repository
也可以 生成一个接口类INetApiRepository
,然后动态代理实现其内部 方法体逻辑 - 花式玩法4:我连花式玩法3中的接口类
INetApiRepository
都不需要了,直接反射搞定所有。 - 同时可以学习到,注解、反射、泛型、注解处理器、动态代理
项目地址
感谢阅读:
欢迎 点赞、收藏、关注
来源:juejin.cn/post/7417847546323042345
go的生态真的一言难尽
前言
标题党了,原生go很好用,只不过我习惯了java封装大法。最近在看golang,因为是javaer,所以突发奇想,不如开发一个类似于 Maven 或 Gradle 的构建工具来管理 Go 项目的依赖,众所周知,构建和发布是一个复杂的任务,但通过合理的设计和利用现有的工具与库,可以实现一个功能强大且灵活的工具。
正文分为两部分:项目本身和如何使用
一、项目本身
1. 项目需求分析
核心需求
- 依赖管理:
- 解析和下载 Go 项目的依赖。
- 支持依赖版本控制和冲突解决。
- 构建管理:
- 支持编译 Go 项目。
- 支持跨平台编译。
- 支持自定义构建选项。
- 发布管理:
- 打包构建结果。
- 支持发布到不同的平台(如 Docker Hub、GitHub Releases)。
- 任务管理:
- 支持定义和执行自定义任务(如运行测试、生成文档)。
- 插件系统:
- 支持扩展工具的功能。
可选需求
- 缓存机制:缓存依赖和构建结果以提升速度。
- 并行执行:支持并行下载和编译。
2. 技术选型
2.1 编程语言
- Go 语言:由于我们要构建的是 Go 项目的构建工具,选择 Go 语言本身作为开发语言是合理的。
2.2 依赖管理
- Go Modules:Go 自带的依赖管理工具已经很好地解决了依赖管理的问题,可以直接利用 Go Modules 来解析和管理依赖。
2.3 构建工具
- Go 标准库:Go 的标准库提供了强大的编译和构建功能(如
go build
,go install
等命令),可以通过调用这些命令或直接使用相关库来进行构建。
2.4 发布工具
- Docker:对于发布管理,可能需要集成 Docker 来构建和发布 Docker 镜像。
- upx:用于压缩可执行文件。
2.5 配置文件格式
- YAML 或 TOML:选择一种易于阅读和编写的配置文件格式,如 YAML 或 TOML。
3. 系统架构设计
3.1 模块划分
- 依赖管理模块:
- 负责解析和下载项目的依赖。
- 构建管理模块:
- 负责编译 Go 项目,支持跨平台编译和自定义构建选项。
- 发布管理模块:
- 负责将构建结果打包和发布到不同平台。
- 任务管理模块:
- 负责定义和执行自定义任务。
- 插件系统:
- 提供扩展点,允许用户编写插件来扩展工具的功能。
3.2 系统流程
- 初始化项目:读取配置文件,初始化项目环境。
- 依赖管理:解析依赖并下载。
- 构建项目:根据配置文件进行项目构建。
- 执行任务:执行用户定义的任务(如测试)。
- 发布项目:打包构建结果并发布到指定平台。
4. 模块详细设计与实现
4.1 依赖管理模块
4.1.1 设计
利用 Go Modules 现有的功能来管理依赖。可以通过 go list
命令来获取项目的依赖:
4.1.2 实现
package dependency
import (
"fmt"
"os/exec"
)
// ListDependencies 列出项目所有依赖
func ListDependencies() ([]byte, error) {
cmd := exec.Command("go", "list", "-m", "all")
return cmd.Output()
}
// DownloadDependencies 下载项目所有依赖
func DownloadDependencies() error {
cmd := exec.Command("go", "mod", "download")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("download failed: %s", output)
}
return nil
}
4.2 构建管理模块
4.2.1 设计
调用 Go 编译器进行构建,支持跨平台编译和自定义构建选项。
4.2.2 实现
package build
import (
"fmt"
"os/exec"
"runtime"
"path/filepath"
)
// BuildProject 构建项目
func BuildProject(outputDir string) error {
// 设置跨平台编译参数
var goos, goarch string
switch runtime.GOOS {
case "windows":
goos = "windows"
case "linux":
goos = "linux"
default:
goos = runtime.GOOS
}
goarch = "amd64"
output := filepath.Join(outputDir, "myapp")
cmd := exec.Command("go", "build", "-o", output, "-ldflags", "-X main.version=1.0.0")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("build failed: %s", output)
}
fmt.Println("Build successful")
return nil
}
4.3 发布管理模块
4.3.1 设计
打包构建结果并发布到不同平台。例如,构建 Docker 镜像并发布到 Docker Hub。
4.3.2 实现
package release
import (
"fmt"
"os/exec"
)
// BuildDockerImage 构建 Docker 镜像
func BuildDockerImage(tag string) error {
cmd := exec.Command("docker", "build", "-t", tag, ".")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("docker build failed: %s", output)
}
fmt.Println("Docker image built successfully")
return nil
}
// PushDockerImage 推送 Docker 镜像
func PushDockerImage(tag string) error {
cmd := exec.Command("docker", "push", tag)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("docker push failed: %s", output)
}
fmt.Println("Docker image pushed successfully")
return nil
}
5. 任务管理模块
允许用户定义和执行自定义任务:
package task
import (
"fmt"
"os/exec"
)
type Task func() error
func RunTask(name string, task Task) {
fmt.Println("Running task:", name)
err := task()
if err != nil {
fmt.Println("Task failed:", err)
return
}
fmt.Println("Task completed:", name)
}
func TestTask() error {
cmd := exec.Command("go", "test", "./...")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("tests failed: %s", output)
}
fmt.Println("Tests passed")
return nil
}
6. 插件系统
可以通过动态加载外部插件或使用 Go 插件机制来实现插件系统:
package plugin
import (
"fmt"
"plugin"
)
type Plugin interface {
Run() error
}
func LoadPlugin(path string) (Plugin, error) {
p, err := plugin.Open(path)
if err != nil {
return nil, err
}
symbol, err := p.Lookup("PluginImpl")
if err != nil {
return nil, err
}
impl, ok := symbol.(Plugin)
if !ok {
return nil, fmt.Errorf("unexpected type from module symbol")
}
return impl, nil
}
5. 示例配置文件
使用 YAML 作为配置文件格式,定义项目的构建和发布选项:
name: myapp
version: 1.0.0
build:
options:
- -ldflags
- "-X main.version=1.0.0"
release:
docker:
image: myapp:latest
tag: v1.0.0
tasks:
- name: test
command: go test ./...
6. 持续改进
后续我将持续改进工具的功能和性能,例如:
- 增加更多的构建和发布选项。
- 优化依赖管理和冲突解决算法。
- 提供更丰富的插件。
二、如何使用
1. 安装构建工具
我已经将构建工具发布到 GitHub 并提供了可执行文件,用户可以通过以下方式安装该工具。
1.1 使用安装脚本安装
我将提供一个简单的安装脚本,开发者可以通过 curl
或 wget
安装构建工具。
使用 curl
安装
curl -L https://github.com/yunmaoQu/GoForge/releases/download/v1.0.0/install.sh | bash
使用 wget
安装
wget -qO- https://github.com//yunmaoQu/GoForge/releases/download/v1.0.0/install.sh | bash
1.2 手动下载并安装
如果你不想使用自动安装脚本,可以直接从 GitHub Releases 页面手动下载适合你操作系统的二进制文件。
- 访问 GitHub Releases 页面。
- 下载适合你操作系统的二进制文件:
- Linux:
GoForge-linux-amd64
- macOS:
GoForge-darwin-amd64
- Windows:
GoForge-windows-amd64.exe
- Linux:
- 将下载的二进制文件移动到系统的 PATH 路径(如
/usr/local/bin/
),并确保文件有执行权限。
# 以 Linux 系统为例
mv GoForge-linux-amd64 /usr/local/bin/GoForge
chmod +x /usr/local/bin/GoForge
2. 创建 Go 项目并配置构建工具
2.1 初始化 Go 项目
假设你已经有一个 Go 项目或你想创建一个新的 Go 项目。首先,初始化 Go 模块:
mkdir my-go-project
cd my-go-project
go mod init github.com/myuser/my-go-project
2.2 创建 build.yaml
文件
在项目根目录下创建 build.yaml
文件,这个文件类似于 Maven 的 pom.xml
或 Gradle 的 build.gradle
,用于配置项目的依赖、构建任务和发布任务。
示例 build.yaml
project:
name: my-go-project
version: 1.0.0
dependencies:
- name: github.com/gin-gonic/gin
version: v1.7.7
- name: github.com/stretchr/testify
version: v1.7.0
build:
output: bin/my-go-app
commands:
- go build -o bin/my-go-app main.go
tasks:
clean:
command: rm -rf bin/
test:
command: go test ./...
build:
dependsOn:
- test
command: go build -o bin/my-go-app main.go
publish:
type: github
repo: myuser/my-go-project
token: $GITHUB_TOKEN
assets:
- bin/my-go-app
配置说明:
- project: 定义项目名称和版本。
- dependencies: 列出项目的依赖包及其版本号。
- build: 定义构建输出路径和构建命令。
- tasks: 用户可以定义自定义任务(如
clean
、test
、build
等),并可以配置任务依赖关系。 - publish: 定义发布到 GitHub 的配置,包括发布的仓库和需要发布的二进制文件。
3. 执行构建任务
构建工具允许你通过命令行执行各种任务,如构建、测试、清理、发布等。以下是一些常用的命令。
3.1 构建项目
执行以下命令来构建项目。该命令会根据 build.yaml
文件中定义的 build
任务进行构建,并生成二进制文件到指定的 output
目录。
GoForge build
构建过程会自动执行依赖任务(例如 test
任务),确保在构建之前所有测试通过。
3.2 运行测试
如果你想单独运行测试,可以使用以下命令:
GoForge test
这将执行 go test ./...
,并运行所有测试文件。
3.3 清理构建产物
如果你想删除构建生成的二进制文件等产物,可以运行 clean
任务:
GoForge clean
这会执行 rm -rf bin/
,清理 bin/
目录下的所有文件。
3.4 列出所有可用任务
如果你想查看所有可用的任务,可以运行:
GoForge tasks
这会列出 build.yaml
文件中定义的所有任务,并显示它们的依赖关系。
4. 依赖管理
构建工具会根据 build.yaml
中的 dependencies
部分来处理 Go 项目的依赖。
4.1 安装依赖
当执行构建任务时,工具会自动解析依赖并安装指定的第三方库(类似于 go mod tidy
)。
你也可以单独运行以下命令来手动处理依赖:
GoForge deps
4.2 更新依赖
如果你需要更新依赖版本,可以在 build.yaml
中手动更改依赖的版本号,然后运行 mybuild deps
来更新依赖。
5. 发布项目
构建工具提供了发布项目到 GitHub 等平台的功能。根据 build.yaml
中的 publish
配置,你可以将项目的构建产物发布到 GitHub Releases。
5.1 配置发布相关信息
确保你在 build.yaml
中正确配置了发布信息:
publish:
type: github
repo: myuser/my-go-project
token: $GITHUB_TOKEN
assets:
- bin/my-go-app
- type: 发布的目标平台(GitHub 等)。
- repo: GitHub 仓库路径。
- token: 需要设置环境变量
GITHUB_TOKEN
,用于认证 GitHub API。 - assets: 指定发布时需要上传的二进制文件。
5.2 发布项目
确保你已经完成构建,并且生成了二进制文件。然后,你可以执行以下命令来发布项目:
GoForge publish
这会将 bin/my-go-app
上传到 GitHub Releases,并生成一个新的发布版本。
5.3 测试发布(Dry Run)
如果你想在发布之前测试发布流程(不上传文件),可以使用 --dry-run
选项:
GoForge publish --dry-run
这会模拟发布过程,但不会实际上传文件。
6. 高级功能
6.1 增量构建
构建工具支持增量构建,如果你在 build.yaml
中启用了增量构建功能,工具会根据文件的修改时间戳或内容哈希来判断是否需要重新构建未被修改的部分。
build:
output: bin/my-go-app
incremental: true
commands:
- go build -o bin/my-go-app main.go
6.2 插件机制
你可以通过插件机制来扩展构建工具的功能。例如,你可以为工具增加自定义的任务逻辑,或在构建生命周期的不同阶段插入钩子。
在 build.yaml
中定义插件:
plugins:
- name: custom-task
path: plugins/custom-task.go
编写 custom-task.go
,并实现你需要的功能。
7. 调试和日志
如果你在使用时遇到了问题,可以通过以下方式启用调试模式,查看详细的日志输出:
GoForge --debug build
这会输出工具在执行任务时的每一步详细日志,帮助你定位问题。
总结
通过这个构建工具,你可以轻松管理 Go 项目的依赖、构建过程和发布任务。以下是使用步骤的简要总结:
- 安装构建工具:使用安装脚本或手动下载二进制文件。
- 配置项目:创建
build.yaml
文件,定义依赖、构建任务和发布任务。 - 执行任务:通过命令行执行构建、测试、清理等任务。
- 发布项目:将项目的构建产物发布到 GitHub 或其他平台。
来源:juejin.cn/post/7431545806085423158
不是,哥们,谁教你这样处理生产问题的?
你好呀,我是歪歪。
最近遇到一个生产问题,我负责的一个服务触发了内存使用率预警,收到预警的时候我去看了内存使用率已经到了 80%,看了一眼 GC 又发现还没有触发 FullGC,一次都没有。
基于这个现象,当时推测有两种可能,一种是内存溢出,一种是内存泄漏。
好,假设现在是面试,面试官目前就给了这点信息,他问你到底是溢出还是泄漏,你怎么回答?
在回答之前,我们得现明确啥是溢出,啥情况又是泄漏。
- 内存溢出(OutOfMemoryError):内存溢出指的是程序请求的内存超出了 JVM 当前允许的最大内存容量。当 JVM 试图为一个对象分配内存时,如果当前可用的堆内存不足以满足需求,就会抛出 java.lang.OutOfMemoryError 异常。这通常是因为堆空间太小或者由于某些原因导致堆空间被占满。
- 内存泄漏 (Memory Leak):内存泄漏是指不再使用的内存空间没有被释放,导致这部分内存无法再次被使用。虽然内存泄漏不会立即导致程序崩溃,但它会逐渐消耗可用内存,最终可能导致内存溢出。
虽然都与内存相关,但它们发生的时机和影响有所不同。内存溢出通常发生在程序运行时,当数据结构的大小超过预设限制时,常见的情况是你要分配一个大对象,比如一次从数据中查到了过多的数据。
而内存泄漏和“过多”关系不大,是一个细水长流的过程,一次内存泄漏的影响可能微乎其微,但随着时间推移,多次内存泄漏累积起来,最终可能导致内存溢出。
概念就是这个概念,这两个玩意经常被大家搞混,所以多嘴提一下。
概念明确了,回到最开始这个问题,你怎么回答?
你回答不了。
因为这些信息太不完整了,所以你回答不了。
面试的时候面试官就喜欢出这种全是错误选项的题目来迷惑你,摸摸你的底子到底怎么样。
首先,为什么不能判断,是因为前面说了:一次 FullGC 都没有。
虽然现在内存使用率已经到 80% 了,万一一次 FullGC 之后,内存使用率又下去了呢,说明程序没有任何问题。
如果没有下去,说明大概率是内存溢出了,需要去代码里面找哪里分配了大对象了。
那如果下去了,能说明一定没有内存泄漏吗?
也不能,因为前面又说了:内存泄漏是一个细水长流的过程。
关于内存溢出,如果监控手段齐全到位的话,你就记住左边这个走势图:
一个缓慢的持续上升的内存趋势图, 最后疯狂触发 GC,但是并没有内存被回收,最后程序直接崩掉。
内存泄漏,一眼定真假。
这个图来自我去年写的这篇文章:《虽然是我遇到的一个棘手的生产问题,但是我写出来之后,就是你的了。》
里面就是描述了一个内存泄漏的问题,通过分析 Dump 文件的方式,最终成功定位到泄漏点,修复代码。
一个不论多么复杂的内存泄漏问题,处理起来都是有方法论的。
不过就是 Dump 文件分析、工具的使用以及足够的耐心和些许的运气罢了。
所以我不打算赘述这些东西了,我想要分享的是我这次是怎么对应文章开始说的内存预警的。
我的处理方式就是:重启服务。
是的,常规来说都是会保留现场,然后重启服务。但是我的处理方式是:直接执行重启服务的预案。没有后续动作了。
我当时脑子里面的考虑大概是这样的。
首先,这个服务是一个边缘服务,它所承载的数据量不多,其业务已经超过一年多没有新增,存量数据正在慢慢的消亡。代码近一两年没啥改动,只有一些升级 jar 包,日志埋点这类的横向改造。
其次,我看了一下这个服务已经有超过四个月没有重启过了,这期间没有任何突发流量,每天处理的数据呈递减趋势,内存走势确实是一个缓慢上升的过程,我初步怀疑是有内存泄漏。
然后,这个服务是我从别的团队那边接手的一个服务,基于前一点,业务正在消亡这个因素,我也只是知道大概的功能,并不知道内部的细节,所以由于对系统的熟悉度不够,如果要定位问题,会较为困难。
最后,基于公司制度,虽然我知道应该怎么去排查问题,命令和工具我都会使用,但是我作为开发人员是没有权限使用运维人员的各类排查工具和排查命令的,所以如果要定位问题,我必须请求协调一个运维同事帮忙。
于是,在心里默默的盘算了一下投入产出比,我决定直接重启服务,不去定位问题。
按照目前的频率,程序正常运行四五个月后可能会触发内存预警,那么大不了就每隔三个月重启一次服务嘛,重启一次只需要 30s。一年按照重启 4 次算,也就是才 2 分钟。
这个业务我们就算它要五年后才彻底消亡,那么也就才 10 分钟而已。
如果我要去定位到底是不是内存泄露,到底在哪儿泄露的,结合我对于系统的熟悉程度和公司必须有的流程,这一波时间消耗,少说点,加起来得三五个工作日吧。
10 分钟和三五个工作日,这投入产出比,该选哪个,一目了然了吧?
我分享这个事情的目的,其实就是想说明我在这个事情上领悟到的一个点:在工作中,你遇到的问题,不是每一个都必须被解决的,也可以选择绕过问题,只要最终结果是好的就行。
如果我们抛开其他因素,只是从程序员的本职工作来看,那么遇到诸如内存泄漏的问题的时候,就是应该去定位问题、解决问题。
但是在职场中,其实还需要结合实际情况,进行分析。
什么是实际情况呢?
我前面列出来的那个“首先,其次,然后,最后”,就是我这个问题在技术之外的实际情况。
这些实际情况,让我决定不用去定位这个问题。
这也不是逃避问题,这是权衡利弊之后的最佳选择。
同样是一天的时间,我可以去定位这个“重启就能解决”的问题,也可以去做其他的更有价值事情,敲一些业务价值更大的代码。
这个是需要去权衡的,一个重要的衡量标准就是前面说的:投入产出比。
关于“不是所有的问题都必须被解决的,也可以选择绕过问题”这个事情,我再给你举一个我遇到的真实的例子。
几年前,我们团队遇到一个问题,我们使用的 RPC 框架是 Dubbo,有几个核心服务在投产期间滚动发布的时候,流量老是弄不干净,导致服务已经下线了,上游系统还在调用。
当时安排我去调研一下解决方案。
其实这就是一个优雅下线的问题,但是当时资历尚浅,我认真研究了一段时间,确实没研究出问题的根本解决方案。
后来我们给出的解决方案就是做一个容错机制,如果投产期间有因为流量不干净的问题导致请求处理失败的,我们把这些数据记录下来,然后等到投产完成后再进行重发。
没有解决根本问题,选择绕过了问题,但是从最终结果上看,问题是被解决了。
再后来,我们搭建了双中心。投产之前,A,B 中心都有流量,每次投产的时候,先把所有流量从 A 中心切到 B 中心去,在 A 中心没有任何流量的情况下,进行服务投产。B 中心反之。
这样,从投产流程上就规避了“流量老是弄不干净”的问题,因为投产的时候对应的服务已经没有在途流量了,不需要考虑优雅的问题了,从而规避了优雅下线的问题。
问题还是没有被解决,但是问题被彻底绕过。
最后,再举一个我在知乎上看到的一个回答,和我想要表达的观点,有异曲同工之妙:
http://www.zhihu.com/question/63…
这个回答下面的评论也很有意思,有兴趣的可以去翻一下,我截取两个我觉得有意思的:
在职场上,甚至在生活中,一个虽然没有解决方案但是可以被绕过的问题,我认为不是问题。
但是这个也得分情况,不是所有问题都能绕开的,假如是一个关键服务,那肯定不能置之不理,硬着头皮也得上。
关键是,我在职场上和生活中遇到过好多人,遇到问题的时候,似乎只会硬着头皮往上冲。
只会硬着头皮往上冲和知道什么时候应该硬着头皮往上冲,是两种截然不同的职场阶段。
所以有时候,遇到问题的时候,不要硬上,也让头皮休息一下,看看能不能绕过去。
来源:juejin.cn/post/7417842116506058771
慎重!小公司到底该不该自己封装组件库?
前端开发与组件库
注:全文所说的小公司特指:资源不足、技术能力不足的团队
在一些小公司的项目开发中,我们可能常常听到这样的想法:
- 我们公司的需求比较特殊,用现有的组件库不够灵活,不如自己封装一套!
- 现有的组件库样式不符合我们的产品需求,我们需要统一风格和功能,不如自己开发一套组件库吧!
以前我会很天真的支持这样的想法,现在,我会给提出者一个大嘴巴子
!看似高瞻远瞩,实则全是陷阱,甚至可能成为整个团队的噩梦。
一个loading组件引起的生产事故
我先讲一个我们公司因为组件库导致的财产损失生产事故!
之前,我们业务有实现过一个表格,由于接口非常快,我们并没有增加loading
样式。
代码实现也非常简单:
<template>
<section class="table-section">
<m-table :columns="columns" :data-source="tableData">
</m-table>
</section>
</template>
<script setup>
const tableData = ref([]);
const columns = []
const getTableData = async () => {
// ...接口调用逻辑
queryManageList()
};
// 获取表格数据
getTableData();
onMounted(() => {
// 动态设置表头数据
columns = []
});
</script>
m-table
是我们公司的内部表格组件,上面的代码在生产稳定运行。随着数据的增多,接口有些慢了,于是客户希望加个loading。
我们公司的Loading组件模仿自Elemnet Plus,api的调用也非常相似
参考文档,代码的更改也就非常容易
<template>
<section class="table-section">
<m-table :columns="columns" :data-source="tableData">
</m-table>
</section>
</template>
<script setup>
const tableData = ref([]);
const columns = []
const getTableData = async () => {
loadingInstance = Loading('.table-section');
// ...接口调用逻辑
await queryManageList()
loadingInstance.destroy
};
// 获取表格数据
getTableData();
onMounted(() => {
// 动态设置表头数据
columns = []
});
</script>
代码看着严丝合缝,十分完美,然而,部署生产后,发现columns直接没了!
经过线上排查,发现loadingInstance = Loading('.table-section')
这段代码直接替换了section
标签内部的所有dom元素,造成表格的vue实例出现异常,onMounted
钩子根本没有执行!
反观Element PLUS,人家直接是在section标签下生成的遮罩层,就不会存在这个问题!
小公司开发的组件,由于开发者技术参差不齐,很容易出现线上问题啊!这种问题在我们的日常开发中非常常见,害人啊!
为什么小公司不要轻易封装组件库
通过上面的案例,可以看出:小公司的开发人员技术参差不齐,组件库的质量也就无法得到保证。
当然,技术还不是主要原因,毕竟技术是可以提升的,但下面的几个问题才是真要命的!
资源不足:人力和时间的双重消耗
封装组件库并非单纯的开发任务,它需要大量的人力和时间投入。对小公司而言,团队往往规模有限,开发资源紧张。
- 开发人员:为了封装一个组件库,原本负责业务开发的人员必须抽出精力进行组件封装工作,业务开发的进度被迫拖延。
- 时间成本:开发一个组件库不仅仅是写几个按钮或者表单,还涉及到设计体系、文档编写、单元测试、性能优化和浏览器兼容性处理等,这是一项长期工程。
就拿我们公司举例,我们一遍要写业务代码,一遍要维护组件,非常消耗时间!
就这,公司还不断地给我们加任务,把我们当牛马,直接开启996
!
加班费没有我们就忍了,996一次不够,还梅开二度
!
业务开发都没时间,还维护组件库,这不是自己坑自己么? 小公司没钱没实力,再别开发组件库了,来来回回坑自己人!
维护成本高:一时造轮子,一世修轮子
自己封装组件库容易,但长期维护它却很困难。随着项目的迭代和需求的变化,组件库也需要不断更新和优化。
- 需求增加: 业务需求多样化导致组件库功能膨胀,原本简单的组件变得复杂不堪。
- Bug 修复: 自己封装的组件库缺乏大规模使用的验证,隐藏的 Bug 往往在上线后爆发,修复工作耗费大量时间。
- 兼容性问题: 浏览器兼容、新技术支持(如 Vue 3、React 18)的适配工作更是让人头疼。
我们的组件库更新非常频繁,因为随着产品的迭代,要增加很多新功能。有时候,为了使用组件的新功能或者样式,我们不得不升级组件版本。然而,有一次升级后,组件库内部存在严重bug,导致我们原有的许多界面崩溃,造成了重大生产事故!
这种组件升级导致的页面问题时常发生,加了一个功能,导致一个隐藏bug,后续为了解决这个隐藏bug,又引入其他bug,最终一个小功能要发好几个组件版本才能解决。我已经无力吐槽,害人啊!
而且,由于组件的功能不完善,经常要花费非常多的沟通成本
技术负债:短期便利,长期拖累
自建组件库在开发初期可能感觉很“顺手”,但随着项目规模扩大,组件库的缺陷会逐渐显现,成为团队的技术负债。
- 缺乏标准化: 自建组件库的规范不够完善,不同开发者在实现同一功能时可能写出风格完全不同的代码。
- 文档不足: 由于时间和人力限制,自建组件库的文档往往不完善,后期新成员加入时难以上手。
- 升级困难: 自建组件库的每次升级都可能影响到现有业务,增加维护和测试成本。
员工离职风险:组件库成孤岛
小公司人员流动较为频繁。
如果负责组件库开发的员工离职,组件库很可能会变成“孤岛”,无人维护,直接影响到项目的可持续性。
经济形式不好,我们公司近几年也裁了不少员,导致一些组件直接没人维护了,直接影响项目进度。
所以,资金、时间不充足,咱小厂还是别学大厂维护组件库了,要钱没钱,要时间没时间,来来会会坑的都是自己!
总结
封装组件库对小公司来说是一个高风险、高成本、低收益的选择。本人建议如下:
- 优先选择成熟的开源组件库: 如 Ant Design、Element Plus 等,它们功能完善且生态丰富,能够快速适配业务需求!
- 定制而非重造: 如果开源组件库无法完全满足需求,可以在其基础上进行二次封装(各位leader注意看,组件库基本都支持样式定制的!) ,而不是从零开始构建。
- 聚焦业务: 小公司开发团队的首要任务是满足业务需求,组件库的开发应该是锦上添花,而非拖慢进度的负担。
各位Leader注意,技术的最终目的是为业务服务!别为了凸显自己牛逼,强行开发维护一个组件库,最终只会害人害,搬起石头砸自己的脚!
小公司不要让组件库拖住脚步,把资源投入到更有价值的地方,才是发展的正确道路。
注:以上所有言论只针对公司的产品、项目而言!对于个人,你怎么玩都行,玩的越花越好,毕竟以后出去都是面试的资本!
啥也不是,散会!
来源:juejin.cn/post/7440850542585266227
我为什么选择成为程序员?
前言:
我选择成为程序员不是兴趣所在,也不是为了职业发展,全是生活所迫!
第一章:那年,我双手插兜,对外面的世界一无所知
时间回到2009年,时间过得真快啊,一下就是15年前的事情了,15年前我刚小学毕业,在此之前我从未接触过电脑,电脑对于我来说是个遥远的“物件”,从未用过电脑,也从来没想过电脑会给我的生活带来什么改变,还记得初中第一次去电脑室上电脑课的时候,我连电脑如何开机都不知道,班上条件好的同学,或者那些“不乖”的同学会去网吧玩电脑游戏,可能了解一点电脑,对于我们这种家里条件不好的,相对也比较“乖”的同学来说几乎没去过网吧,电脑可能也就是出现在书本里的知识,第一次去上电脑课的那种兴奋以及到了电脑室以后连怎么开机都不会的尴尬,现在都还历历在目!
我对上网没什么兴趣,主要是老师和父母反对,自己也没什么钱,唯一能接触到电脑的时候就是上电脑课的时候学一下怎么打字,我学会玩的第一款游戏叫做“蜘蛛纸牌”。
在我的印象里,那时候的电脑还是用的window xp系统,那是我对电脑的最初印象。
老家的紫外线强的让人想死,我的父母每天顶着烈日劳作,皮肤被晒的黝黑,三十岁的他们却苍老得像五十岁一样,但是一年下来却挣不到几个钱,开学前是最难熬的,因为父母经常会为了凑足我的费用而不断争吵,那时候的梦想很纯粹,能正常上学,能多有一点零花钱,能吃饱饭,以后长大了不用再下地干活(我真的很讨厌晒太阳),电脑这种遥远的东西,我真的没有想过我能拥有它!介于当时的生长环境,怎么也想不到以后的我会和程序员这个行当有任何的交集。
第二章:命运的齿轮开始转动
整个初中能接触到电脑的机会不多,到了初二和初三电脑课也被停了,老师给的解释是“这东西不考试,我们要把精力花在刀刃上!”
所以我也全身心投入到文化课中,这时候对我来说也没什么影响,因为我本身对电脑也"不感兴趣",中考结束以后我的成绩还算理想,考上了市里一所还算可以的高中,学校的配套设施很完善,老师讲课都使用电脑,课间休息的时候经常使用,电脑看篮球比赛,放假的时候别人都回家了,我们就几个同学留在学校,一起用电脑看电影,有什么问题就会用电脑去网上搜一下,我们的数学老师天天用电脑玩"三国杀"这款卡牌游戏。
我们也会经常用电脑下载音乐,电影之类的东西!我离电脑的距离也越来越近了,但是那时候电脑对于我来说只是一个娱乐工具,并不知道他能给我带来任何的经济价值,那时候的我打字还是用一根手指,这个习惯一直保留到我上大学接触到英雄联盟这款游戏!这里先按下不表,后面我再说这个事。
第三章:歪打正着之面向薪资选专业
不要和穷人谈理想,因为穷人已经尝了很多生活的苦,不习惯理想这么甜的东西,会让他们水土不服!
时间来到2014年的七月,那时的我刚经历了一场失败的高考,感觉人生非常的暗淡,我想到复读,可是父母没有什么文化,他们听了很多,“本来考上了,复读一年没考上”的故事,死活不让我复读,因为他们觉得我已经考上一本了,已经很好了,复读反对,再加上自己家里如此贫困,我也只能选择去报志愿,我选志愿的策论很简单,就两个:
1.那些学校是一本学校且招的人数多
2.哪些专业毕业以后收入高!
上网查了一圈后很多人都说通信工程和网络工程毕业以后薪酬待遇相对较高,我也不知道是真是假,我直接把所有学校的所有拍在全面的专业都写成了通信工程和网络工程!我上大学了,并且上了通信工程专业,这是我新的开始,从此以后我将会经常以电脑为伴!
第四章:划水的大学生涯
大学下学期我开始接触编程,我接触了第一门编程《C语言程序设计》,
这时候我也有了自己的个人电脑,大部分时间电脑还是被用在娱乐上,偶尔也会用电脑做写实验课的作业,外加一些电脑的基础课是需要考试的,这时候我几乎每天都要花时间去接触电脑,很多的电脑软件安装了卸载,卸载了又重新安装,也有一些电脑软件虽然安装了但是一直没用过。“差生文具多”,这句话绝对有他的道理!
不管你承不承认,农村的孩子和城市的孩子始终是有差距的,比如在对电脑这一点就很有差距,城里的孩子接触的早,所以长大了学习电脑方面的知识也更快,农村的孩子起步比较晚,所以我到了大学学编程就会感到”水土不服“!学习编程我感觉是很痛苦的,很多时候我学不懂,导致我更没什么兴趣了,所以我的大学生活学习基本上都是在划水,基本上都是60分飘过,或者给老师发短信,求老师让我过,编程课基本都是靠背,硬是把编程学成了语文,操啊!这种痛估计只有学渣才能理解得了!
但是这个时候我入坑了英雄联盟,这款游戏好玩,但是也很气人,我经常会在游戏中和别人对喷,每次和别人对喷我都因为自己一个手指打字,被别人骂的气死,为了和别人对骂不落于下风,我改掉了自己一个手指打字的习惯,慢慢的,我打字速度也变快了,从此别人要是骂我,我就骂死他,成了真正的键盘侠!(当时我一般不会主动骂人,也不会在网络上和别人对线,只在游戏里和别人对线)
第五章:放弃吧,少年
可是我也知道,日子不能这样过呀,像我这样的孩子,我能靠谁呢,反正编程是学不进去了,那就想别的法子吧,于是我就尝试别的出路,我去做过很多兼职,去图书馆当图书管理员,去发传单,去来着三轮车给别人送水,也自己做过一些小生意,当学生开学的时候我们室友就会去进货,搞一些新生用品来卖,赚点生活费,同时也是为了锻炼自己的能力,印象最深的是,大二的暑假,那个暑假我没有回家,我做了一份有挑战的兼职,你可以理解为销售,个人对商家销售一些产品,然后我们再从公司赚取佣金,没有底薪,没有提成,只有佣金,我搞了一个假期,没卖出一份产品,每顿都吃一块三一碗的热干面,我真的吃吐了,后面我看到热干面我都干呕!
第六章:少年,回头是岸
这个暑假的经历,让我不得不思考,我是否真的适合做销售这份工作,编程虽然难,但是学好了确是一辈子的手艺,古人云:“天旱饿不死手艺人”,于是我又硬着头皮开始学,可是学什么呢?那时候Java比较流行,而我又刚好有一门课就是Java,刚考完没多久,还有一些印象,于是我就每天学习,误打误撞的终于在毕业的时候找到了一份软件开发的工作我,我也顺利成为了一名Java程序员!
终章:回望过去,全是无奈
我不是一个有梦想的人,也不是一个很有规划的人,成为一名程序员既不是我的兴趣爱好,也是不起球为了职责发展,只是在贫瘠的环境中,让我时刻需要为了自己生计发愁,为明天的饱和饥而担忧,如果可以的话,我也愿意一双拖鞋,一条短裤,几串钥匙,每逢月底收收租,躺在沙发上,喝着绿茶,摇着蒲扇,好不快活!然后告诉我的朋友们,真羡慕你们能够出门闯荡,长见识,我在家可太无聊了,所以我喝了好多绿茶!
来源:juejin.cn/post/7356485240804606006
人类发财指北
·人永远不可能通过出卖时间来致富,不管你是医生还是律师,一个小时几百美金还是几千人民币,这是一个简单的算术问题,人一天只有24小时,你拿计算器把下半辈子所有可以卖的时间都算上,依然达不到「富」。
·出卖时间有很多种不同变体,比如个体户/自由职业者虽然摆脱了为雇佣者上供剩余价值的命运,但是自身承担市场风险与接案的波动,工作流无法自动化,本质上还是出卖个人时间,时间从哪里来,牺牲健康牺牲睡眠。
·不管你是什么title、在什么有名的地方工作、福利有多少、西装有多贵,只要还有别人来告诉你你需要在什么时间出现在什么地点,你就还是一个奴隶,如果你已经是大老板,每天必须参加数个会议,那你就成了自身事业的奴隶。
·开会毫无意义
·钱其实就是大风刮来的,对于大部分自立的成功人士来说,「变富」过程本身就是几年光景,只是事先耕耘了许多年,但是刮到哪也不会刮到上班族那里,因为就算刮到了,上班族也没有时间弯腰捡钱,因为上班族的时间已经为了每月的固定收入,已经卖光了。对一个佃农来说,天上掉钱你也没法弯腰捡,因为你在弯腰刨地。
·人类脱离金本位已经很久了,「钱」只是一个符号,今天的钱拿回原始人社会毫无价值,这个符号只在共同相信的人群中有意义,不存在独立于「人类社会网络」的钱,钱是一种人与人关系的总合,财富是个人对社会其他个体所「做功」的标记。
·人类整个经济在走向虚拟化,并且从很早以前就开始了,曾经人们买DVD不是为了那张光碟本身,是为了上面的内容,而现在随着互联网的发展,「内容」的发行投放已经变得无比方便和正常。
·任何从零开始发家致富的过程总结下来都是「为社会其他成员提供想要但一直没能得到的产品」的过程,这总是和人的欲望高度相关,所有受欢迎的产品都是某种欲望的载体。你满足他人的欲望,让他人感觉良好,他人就会回馈你。
·世界上几乎没有任何事情是线性的,在所有的领域都存在「效应放大器」,在某一个阶段/时机/领域做一倍的工作,会有百倍的回报,真正重要的东西总是很少的。如果还没有找到某一个「效应放大器」,有一个,每个人都有,那就是「把时间放在重要的事上」,刷短视频十分钟和运动十分钟同样是十分钟,但效用是完全不一样的。
·赛博时代会逆转工业时代所留下的很多印记,比如8小时工作制,集体劳动,打卡,一日三餐,企业雇佣制,以及养老金。工业时代需要抹杀个体的特色和想法,投入集体劳动,积累剩余,赛博时代则会解放个体特色,或者说个体特色本身将会是盈利的关键。人很难被标准化的冷冰冰工业品感动,但人会被另一个人类感动。
·人们会需要工作更多年,因为人类的寿命延长了,青年、中年、老年每一个阶段都延长了,可能性也延长了,这一代的人类大概要活到110岁,职业的转换也会变得更频繁。
·赛博时代的赛道是无限的,因为「内容」和「产品」的界限模糊了,而内容可以是无限的,只要加一点新花样,就又是新的了。
·财富不是「零和游戏」,意即如果你需要赚一笔钱,那必须坑害他人这一笔钱,这是金本位农耕时代生产力上限固定后的思维。
·人口其实是影响发财最大的要素,因为财富本身其实等同于其他社会个体的一种投票行为,财富的创造和成立都只是一个在人与人之间成立的游戏,而人口本身不复存在,就意味着音乐没有听众、电影没有观众、明星没有粉丝、产品没有消费者、培训没有学生,没有了人本身市场就不复存在了,一切将自然凋零。
·地球上的一切资源都来自太阳,植物、动物、煤炭都是太阳能的不同固定形式,太阳一天对地球的照射相当于5085亿吨煤完全充分燃烧的能量,这些能量可以供全球70多亿人口使用24年。
·人类的财富总量在增加的最好证据是:肥胖人数的增加。因为人体的脂肪也是一种太阳能,能量从太阳到动植物,再到人体内。肥胖人口增加说明出现了总体剩余。
·「经济上行」和「经济下行」都不是一种客观的现实,它只是一种集体幻觉,因为经济运作依赖于「预期」,而「预期」其实只是人的一种心理感受。从这个角度来说,其实就是悲观的人越多,越下行。乐观的人越多,越上行。一切只是一种心情而已。
一切只是一种心情而已。
来源:juejin.cn/post/7362078214381600806
写给我前端同事,从事一年多应该要怎么成长的路线
写给我前端同事,从事一年多前端应该要怎么成长的路线
我知道在很多中大型公司,其实有好多领导前辈、以及师傅会给那些校招生
,以及应届生
规划一定的学习成长路线,可能还会有定期的大佬分享。有这么一个领路人、环境在其实成长是飞速的。
我入职了一家新单位,这家单位的没有太多规范,没有太多的组件封装积累,还会考核每周的代码量
,我发现有些阶段代码量(测试阶段、需求阶段等)不够的时候大家都是往项目中塞没用的代码,还有些同学会复制公共组件的代码进自己的模块充代码,技术栈使用的是vue3 + js + ant-design-vue
,一大部分人做的都是项目
。
苏洋同学
(化名)工作了2年左右,换过2家公司,第一家是个小公司也是在写一些后台管理的功能,这是他的第二家公司入职这家公司也有一年左右时间了。他逻辑,工作态度都没问题,也算是个积极上进的零零后好青年。他当前项目中的代码开发没问题,项目中多是一些业务表单,工作流之类的东西,有用户权限控制。有的时候他会请教我一些问题我俩也就从问题聊了聊技术,并且我把我的博客分享给了他,他说他有空会看看学习下。
我跟他从我的博客文章中聊了下他可能需要怎么做来获得成长,当然现在这个环境下我不能跟他说你多学一点可能工资
就会高一些,假如在3年前我一定会告诉他,你学完这些其实可以换个公司去试试一定会比你现在的要高一点。可能学习完假如被迫换工作(裁员)了,机会会大点吧
大佬请绕路,我可能给的建议并不是最好的,但是我觉得对他来说现阶段是最使用的
我知道在很多中大型公司,其实有好多领导前辈、以及师傅会给那些
校招生
,以及应届生
规划一定的学习成长路线,可能还会有定期的大佬分享。有这么一个领路人、环境在其实成长是飞速的。
我入职了一家新单位,这家单位的没有太多规范,没有太多的组件封装积累,还会考核每周的代码量
,我发现有些阶段代码量(测试阶段、需求阶段等)不够的时候大家都是往项目中塞没用的代码,还有些同学会复制公共组件的代码进自己的模块充代码,技术栈使用的是vue3 + js + ant-design-vue
,一大部分人做的都是项目
。
苏洋同学
(化名)工作了2年左右,换过2家公司,第一家是个小公司也是在写一些后台管理的功能,这是他的第二家公司入职这家公司也有一年左右时间了。他逻辑,工作态度都没问题,也算是个积极上进的零零后好青年。他当前项目中的代码开发没问题,项目中多是一些业务表单,工作流之类的东西,有用户权限控制。有的时候他会请教我一些问题我俩也就从问题聊了聊技术,并且我把我的博客分享给了他,他说他有空会看看学习下。
我跟他从我的博客文章中聊了下他可能需要怎么做来获得成长,当然现在这个环境下我不能跟他说你多学一点可能工资
就会高一些,假如在3年前我一定会告诉他,你学完这些其实可以换个公司去试试一定会比你现在的要高一点。可能学习完假如被迫换工作(裁员)了,机会会大点吧
大佬请绕路,我可能给的建议并不是最好的,但是我觉得对他来说现阶段是最使用的
针对他的成长经历规划
他的js
基础可能没那么好,像一些数据处理上是有些问题,那么我建议他:
- 重新学习下
js
针对数组
,字符串
等API,像字符串的cancat、includes、indexOf、lastIndexOf、endsWith、startsWith
等等,像数组的forEach、map、filter、reduce、find、findIndex、some、every
等等,他说他有些好像没有使用过。 学习了解用法,并且写一样的源码。例如:
'123'.endsWith('3'); // true
export const _endsWith = (source: string, target: string) => {
const len = target.length;
const substrValue = source.substr(source.length - len, len);
return substrValue === target;
};
_endsWith('123456', '56'); // true
- 对
堆
和栈
要有一定的理解,对深拷贝、浅拷贝有一定的理解。 - 对
宏任务
和微任务
以及事件执行的理解。 - 对
防抖
和节流
有一定的理解 - 对
this
有一定的理解并写出apply
、call
、bind
的实现。 - 对类型判断
instanceof
、typeof
、Object.prototype.toString.call
等方法有理解。 - 对对象方法的使用
Object.keys、Object.values、Object.entries、Object.assign
等等
- 去看下
lodash
的源码,例如:throttle、debounce、cloneDeep、groupBy、get、difference
等等一些常用的方法函数要会写源码,最好自己写一遍。
- 对正则表达式能对有一定的理解,并且写出一些常用的正则。
CSS
中对主题适配能有一定的理解,例如使用 less
和 Scss
变量做主题适配,以及使用2套样式,或者使用css全局变量做主题适配。能区分出这几种的不同点
如果能把js
的以上都能掌握了,那么你的基础算是扎实的了,差的可能就是工作经验以及深入了解一些框架的源码了。
- 这个时候可以学习下代码规范了,其实
vue
的话可以看看element ui
组件代码的规范,组件的设计以及源码具体。至少能实现message
组件以及按钮组件
- 学习下设计模式,例如:
单例模式
、策略模式
、代理模式
、发布订阅模式
等等。 - 可以多看看怎么写防御式编程,让你的代码更加健壮(这也就是为啥项目中bug多的问题,代码写的还不够严谨)
- 可以去学习下
TS
,可能不用去特别做类型体操,基本的泛型能用,例如:Array
、Record
、Partial
、Pick
、Omit
、Exclude
、Extract
等等。 - 如果你对
vue
和react
想深入研究,可以对比着使用看看它们之前的差异,有自己的认识。 webpack
的配置,对打包优化有理解,对loader和plugin有理解,对打包工具有使用过。- 了解下
npm
,对npm 发布等命令有一定的理解,并且尝试自己发布一个包。 - 对
git
提交规范有理解,并且使用。可以深入了解下git规范定义以及拦截。 - 对
nginx
有一定的了解,并且使用。因为现在好多项目都是多页应用了,nginx就显得很重要了。 echarts
是图表库,可以学习下他的那些简单图表怎么使用canvas
画出来的。
恭喜,假如你上面都能学会,我觉得你很了不起了,至少算是中级前端工程师
。
- 制定公司代码规范
eslint
, git
提交规范等等 git CI
制定工作流是很重要的,可以学习下。- ...
- ...
- ...
他的
js
基础可能没那么好,像一些数据处理上是有些问题,那么我建议他:
- 重新学习下
js
针对数组
,字符串
等API,像字符串的cancat、includes、indexOf、lastIndexOf、endsWith、startsWith
等等,像数组的forEach、map、filter、reduce、find、findIndex、some、every
等等,他说他有些好像没有使用过。 学习了解用法,并且写一样的源码。例如:
'123'.endsWith('3'); // true
export const _endsWith = (source: string, target: string) => {
const len = target.length;
const substrValue = source.substr(source.length - len, len);
return substrValue === target;
};
_endsWith('123456', '56'); // true
堆
和栈
要有一定的理解,对深拷贝、浅拷贝有一定的理解。宏任务
和微任务
以及事件执行的理解。防抖
和节流
有一定的理解this
有一定的理解并写出apply
、call
、bind
的实现。instanceof
、typeof
、Object.prototype.toString.call
等方法有理解。Object.keys、Object.values、Object.entries、Object.assign
等等lodash
的源码,例如:throttle、debounce、cloneDeep、groupBy、get、difference
等等一些常用的方法函数要会写源码,最好自己写一遍。CSS
中对主题适配能有一定的理解,例如使用 less
和 Scss
变量做主题适配,以及使用2套样式,或者使用css全局变量做主题适配。能区分出这几种的不同点如果能把js
的以上都能掌握了,那么你的基础算是扎实的了,差的可能就是工作经验以及深入了解一些框架的源码了。
vue
的话可以看看element ui
组件代码的规范,组件的设计以及源码具体。至少能实现message
组件以及按钮组件
单例模式
、策略模式
、代理模式
、发布订阅模式
等等。TS
,可能不用去特别做类型体操,基本的泛型能用,例如:Array
、Record
、Partial
、Pick
、Omit
、Exclude
、Extract
等等。vue
和react
想深入研究,可以对比着使用看看它们之前的差异,有自己的认识。webpack
的配置,对打包优化有理解,对loader和plugin有理解,对打包工具有使用过。npm
,对npm 发布等命令有一定的理解,并且尝试自己发布一个包。git
提交规范有理解,并且使用。可以深入了解下git规范定义以及拦截。nginx
有一定的了解,并且使用。因为现在好多项目都是多页应用了,nginx就显得很重要了。echarts
是图表库,可以学习下他的那些简单图表怎么使用canvas
画出来的。恭喜,假如你上面都能学会,我觉得你很了不起了,至少算是中级前端工程师
。
eslint
, git
提交规范等等git CI
制定工作流是很重要的,可以学习下。结语
其实如果从事这个行业,可以把上面当作一个学习清单,然后按照顺序学习,这些都是必须且要学会的。然后把学习到的东西都记录下来。多总结,多思考,
作者:三原
来源:juejin.cn/post/7448899248475684899
其实如果从事这个行业,可以把上面当作一个学习清单,然后按照顺序学习,这些都是必须且要学会的。然后把学习到的东西都记录下来。多总结,多思考,
来源:juejin.cn/post/7448899248475684899
DeepMind天才科学家疑抑郁自杀!41岁SuperGLUE之父英年早逝,AI圈悲痛不已
就在刚刚,一个令人悲伤的消息传来。
谷歌 DeepMind 研究科学家 Felix Hill,于 2024 年 12 月 5 日英年早逝,年仅 41 岁。
自 2023 年初以来,他一直在与严重的精神疾病作斗争。期间,他表现出了重度抑郁和严重的自杀倾向,终于还是没有扛过去。
Felix 是一位学术成果颇丰的 AI 学者,谷歌总引用量为 19680,参与创建了自然语言理解基准 GLUE 和 SuperGLUE。
Felix Hill 本科在牛津大学学习数学,随后在剑桥大学拿到了语言学硕士学位,和计算语言学博士学位。
毕业后,他曾有 8 个月在高中担任数学老师,随后进入谷歌 DeepMind,当了将近 8 年的 AI 研究者。
消息传来,他的 AI 圈好友无不表示悲痛和难过。
1 月 11 日,Felix 的葬礼将于伦敦北部举行,届时将进行现场直播。
许多认识他的人悲痛地留言说:Felix 是一个很特别的人,超前于这个时代。
左右滑动查看
最令人心碎的博客
是什么样的原因,让这样一位成果丰硕的 AI 学者的生命逝去?
他生前的一篇博客,揭露了许多细节。
他详细描述了 2023 年母亲离世后,正在精神病院接受治疗的自己症状更严重了。接下来的 12 个月内,他更是陷入极度焦虑和自杀的抑郁状态。
他也写道,自己当初投身 AI 研究并不是为了赚钱,但 AI 大爆发后,自己仿佛被迫进入「战争」状态,写论文、搞研究、创业,都令人压力重重,找不到出路。
即使积累了大量财富,自己也依然出了问题。
英伟达高级研究者 Jim Fan 读完悲痛地表示:这是我读过最令人心碎的博客,因为它如此真实,如此贴近内心。
Jim Fan 表示,AI 不应该是 200B 权重的压力和痛苦。
曾经,这是一个充满咖啡因带来的灵光乍现的地方,是令人兴奋的深夜 arxiv 探索之旅,是能让研究者脸上露出笑容的绝妙想法。但所有涌入的资本和关注,似乎正在迫使每个人竞相逐底。
黄仁勋经常对员工们强调,不要用「打败这个,碾压那个」的措辞。因为大家的目的是为了提升整个生态,而不是让任何人陷入深渊。AI 学者的工作是做大蛋糕,越大越好,然后再分配。
AI 不是零和博弈,事实上,它可能是人类有史以来拥有的最正和的博弈。大家应该做的,是向竞争对手传递爱意。
Jim Fan 写道,虽然并未有幸在现实生活中认识 Felix,但自己很喜欢他的研究品味,为他的每一篇新论文都设置了 Google Scholar 提醒。他在 AI 智能体和 VLM 方面的工作也对自己影响很深。
「他本应该成为一个很好的朋友。我想认识他,但现在已经不可能了。」
「安息吧,Felix。愿来世你不需要去战斗。」
2000 亿权重的责任:现代 AI 工作中的压力
下面这篇博客中,Felix 详细回顾了 AI 爆火的几年,给自己的生活带来的剧变——
过去两年,AI 领域发生了不可逆转的变化。
ChatGPT 的月活数接近 2 亿人。Gemini 在 2024 年 5 月的访问量接近 3.2 亿次,AI 爱好者现在可以使用 AI 微波炉、AI 牙刷,甚至 AI 足球。
然而,对于我们这些在 AI 领域工作的人来说,这种大众兴趣的激增既是福也是祸。
诚然,薪资水平上涨了,股价和市值也随之提高。但另一方面,这种变化也带来了独特的压力。
这篇博客讲述的是现代 AI 领域的压力。它面向所有在 AI 领域工作的人(按保守估计,现在全球人口中大约有 87% 的人在从事 AI 相关工作),特别是那些从事 AI 研究的人。
最终,我希望通过讨论 AI 研究中的压力源,让我们这些有幸在该领域工作的人生活得更快乐一些。
因为,尽管目前一片混乱,这仍然是一个美妙而充实的职业——它有潜力解决科学、哲学乃至人类本身的诸多重大问题。
无处可逃
几个月前,我参加了一个朋友的 40 岁生日派对。在那些我不太熟悉的人中,我注意到一个奇怪的现象。
尽管我那时身体不适,而且明显不太想讲话,但我周围还是形成了一个包围圈,原因仅仅是,大家知道我在 DeepMind 上班。
而且,他们想聊的不是足球或 80 年代音乐,恰恰是我最想避免思考的主题——AI。
虽然很感激这么多人对我的工作感兴趣,但这也提醒我过去两年发生了多大的变化。
银行家、律师、医生和管理顾问都想听听我对 ChatGPT 的看法;虽然很少有人声称在工作中直接使用了 LLM,但他们确信,AI 领域正在发生一些他们应该了解的事情。
作为一名研究人员,我相信你也能体会到这种在社交场合无法放松的感觉。
但情况更糟。就连在自己家里,我也找不到安宁。
我早已不再看新闻,因为害怕引发焦虑。但即使是在看足球、VH1、蒙塔尔巴诺探长,或者那部出色的《那不勒斯四部曲》改编剧时,广告中也充斥着与 AI 相关的内容。
在这段时间,我时常幻想着收拾行李,跨越大洲去加入一个隐居群体。但很可能连内观禅修(Vipassana)也被 AI 渗透了,这不会让我惊讶。
无形的竞争
几家大公司竞相开发最大、最好的 LLM,这一事实本身就令人压力重重——无论你为哪家公司工作。
现在从事 AI 研究,感觉就像参与一场战争。希特勒和达奇 · 舒尔茨的例子告诉我们,参战可能导致精神病态、离婚和自杀等严重后果。
当然,我并不是要把参与 AI 研究等同于参与真实战争,但我的亲身经历却表明,这种类比是很真实的。
关乎底线的工作
通常,业界的研究人员并不习惯于自己的工作对雇主的底线产生直接且即时的影响。
当然,许多研究人员都梦想能够产生这样的影响。只是以前,这种机会可能是十年难遇。
如今,对 LLM 基础研究的结果,往往只会对模型性能产生微小、短期的波动。然而,由于公司估值与 LLM 性能(难以分割地?)挂钩,这些波动可能导致股价出现数十亿美元的起伏。
这种动态令人倍感压力,而且这也不是 AI 研究人员在研究生阶段、博士后期间,甚至在 2022 年之前的工作中所能预料到的。
**钱,**钱,还是钱
大多数 AI 研究人员,尤其是我们这些超过某个年龄的人,当初投身研究并不是为了赚钱。
做自己热爱的工作还能获得丰厚报酬听起来是个完美方案,但这也可能引发强烈的焦虑。特别是当推动你收入增长的外部因素不在你的控制范围内,且 / 或者这些因素让你对工作的热爱程度大不如前时。
无论 AI 是否与此有关,突然积累财富可能会导致各种问题,看看那些经过多年努力终于成名的演员或歌手就知道了。成瘾、感情破裂、友谊破碎,甚至自杀只是一些较为常见的症状。
这些症状,我确实都感同身受。
科学家角色缺失
LLM 的规模、简单性和有效性使得做出「相关」的「科学研究」变得困难,这里的相关指的是能立即改进 LLM。
领先的 LLM 研究人员已经开始认同 Rich Sutton 的「苦涩教训」:除了规模之外,几乎不需要任何创新。
而且,即使理论上可能存在实质性创新,实现它往往需要在不同条件下反复训练最大规模的 LLM。这甚至连最大的公司都负担不起。
对于一个「普通」的研究科学家来说,这感觉简直令人绝望。
对于习惯于在 5 至 10 人的小团队中工作的工业界科学家来说,这些已经很艰难。但学术界的人所遭遇的无疑更加严峻,比如那些博士生、博士后和 AI/CS / 机器学习领域的教职人员。
发表论文
虽然学术界的人可以(也应该)继续发表从 LLM 实验中获得的见解,但对于工业界的科学家来说,发表论文的问题就没那么明确了。
发表论文一直是科学过程的内在组成部分,也一直是 AI 研究的核心原则。我接触过的大多数 AI 研究人员,特别是研究科学家,都同意我的观点:发表论文是我们职业生涯的关键。
但是,至少在工业界,过去 2 年来,研究成果是否能够发表的问题变得越来越不明确。能够改进 LLM 的小技巧可能等同于 LLM 之战中的关键武器。将这些秘密公开是否对资助研究的组织有利,这始终是一个需要深思熟虑的问题。
这一切都意味着,研究人员经常对自己想法的前途毫无把握,至少,这会对我造成巨大的压力。
创业公司
当然,摆脱这些困扰的一个可行出路,就是规划科研方向,筹集资金并成立创业公司。事实上,目前 AI 创业公司(无论大小)的激增表明有多少科学家选择了这条路。
但成为创始人并不能必然地规避相关压力。
众所周知,创业的压力也很大;即使在当前投资者热情高涨的情况下,许多资金充足的 AI 创业公司仍然失败了。
我知道,成为创始人是一段特别孤独的旅程。这无疑是当下雄心勃勃的科学家们的一个可行选择,但这既不会让做研究变得容易,也不会减轻压力。
为什么要写关于压力的博客
过去两年在 AI 领域可谓混乱而疯狂,而对我个人而言,这更是一段特别动荡的时期。
2023 年 4 月,我的母亲在与阿尔茨海默症长期抗争后离世。那时的我正在精神病院接受治疗,因为出现了严重的精神症状,其中压力很可能是重要诱因。
在接下来的 12 个月里,表面上我是在康复中,但实际上却陷入了极度焦虑和自杀倾向的抑郁状态。
所幸在这期间,我遇到了非常理解我的处境(以及认可我对公司的价值)的雇主,他们一直为我提供治疗和精神上的支持。
经过另外 6 个月的重度抑郁之后,我的状况开始好转,最近也感觉自己有能力写下这些经历。
我深刻地认识到压力和焦虑是密不可分的;事实上,它们本质上可能是同一件事。诚然,像任何适应性特征一样,焦虑有时也能带来积极影响(比如提高生产力),但一旦焦虑变得失控,后果可能相当严重。
正是在尝试重新学习如何成为一名 AI 研究员的过程中,回顾 AI 领域这两年的发展,让我获得了这篇博客中所分享的见解。
诚然,仅仅分享这些见解并不能从根本上解决问题,但在最艰难的时期,能给我带来希望的少数事情之一就是意识到我并不是一个人在战斗。
如果你现在也在经历类似的困扰,请记住——你并不孤单。
社交焦虑
我已经讨论了当前从事 AI 研究的人可能遭受的诸多压力或焦虑的诱因。
然而,还有一种压力我尚未提及,这是因为我很幸运从未亲身经历过。我对它的了解,完全来自于与朋友和同事的深入交谈。
这种压力就是社交焦虑。
据朋友们反映,那些有社交焦虑的人往往会觉得群体互动充满挑战。在现代 AI 领域,这是一个格外严峻的挑战,因为大型项目团队和大规模的(通常是跨洲际的)协作已成为必需。
目前行业中的高流动率更是雪上加霜,因为已建立的团队(通常作为社交「安全网」)可能在一夜之间解散瓦解。
人员流动还可能引发信任危机,因为曾经可靠的伙伴可能会转投竞争对手的研究团队。
值得欣慰的是,社交焦虑和我此前讨论过的所有焦虑或压力表现一样,都是可以克服的。克服的第一步是培养以家人和「非 AI 圈」朋友为主的自然支持系统。
而关键的第二步,则是我们所有从事 AI 工作的人,都要开始并持续保持关于压力的坦诚对话。
因此,诚挚邀请你在社交媒体上分享自己的经历和感受。让我们携手努力,不仅将 AI 研究打造成一个充满活力和智力挑战的领域,更要使其成为一个充满同理心和善意的温暖家园。
AI 大佬发长文缅怀
Contextual AI 首席执行官 Douwe Kiela 发文表示,我真的很难过,我亲爱的朋友 Felix Hill 离开了我们。他在世界各地有很多朋友和同事。为了让更多人知道,他的家人希望我们分享这个网页,一起纪念他的一生:
EPFL 教授,前 DeepMind 研究科学家 Caglar Gulcehre 表示,听到 Felix 离开我们的消息,真是令人心碎!
他回忆道,「我第一次见到 Felix 是在蒙特利尔读博二的时候。那时候,我正经历抑郁症,头两年的生活很难熬。我搬到一个新国家,再加上冰天雪地的天气,让我感到特别不适应。
Felix 总是充满活力,乐观开朗。但有时候,你很难知道别人生活中正在经历什么。所以,对他人多一份理解,不要轻易下结论很重要。或许他们可能正经历着不为人知的困境。可惜的是,很多人仍然低估了心理健康的重要性。
寻求帮助并不是软弱的表现。刚到 DeepMind 工作的头两年,我的父亲突发心脏病,后来还失明了;我的姐姐也被诊断出癌症。那时候,我又搬到了另一个国家。如果不是寻求了专业帮助,我不知道自己该如何应对这一切。
无论何时需要帮助,都要勇敢去寻求。我很感激身边有许多支持我的人,在困难时期给予了我很大的帮助。如果心理状态不佳,很难在工作上取得成功。向 Felix 家人致以深切的慰问」。
DeepMind 研究科学家 Andrew Lampinen 发文怀念其这位曾经指导过自己的导师——
Felix Hill 是一位非常出色的导师——偶尔也是我的冬泳伙伴。我能加入 DeepMind,以及形成如今做研究的方式,很大程度上都要归功于他。都过去一个月了,我还是觉得难以相信他已经离开了。
Felix 在选择研究方向上有着超强的眼光,而且直觉特别准。每当遇到新想法,他总会表现出孩子般的热情和好奇心,还特别幽默,能够与他一起工作真的让人备受启发。
他也特别看重跟他共事的每个人。在我刚到 DeepMind 的时候,他和 Jane Wang 一起把一群超棒的人聚在了一起,比如 Stephanie Chan、Aaditya Singh、Allison Tam,还有其他很多朋友和合作伙伴。
不过他也经历过一段艰难时期,尤其是最近这几年。我最后一次和他聊天是在他离世前大概一个月,那时他跟我分享了一个雄心勃勃又有点疯狂的项目想法,让我仿佛看到了他当年的影子。但那会儿我太忙了,一直没有再跟进,直到现在为时已晚,至今这件事让我特别后悔。
纽约大学计算机科学和数据科学教授 Kyunghyun Cho 更是写了一篇长文缅怀逝者——「再见了,Felix」。
文章地址:kyunghyuncho.me/bye-felix/
这段文字写于 2024 年 12 月 9 日,但当时我既不愿意也无法接受所发生的事实,所以一直没有勇气发布。即便到现在,每当想起这件事,我的心仍然隐隐作痛。我选择在 2024 年的最后一天发布这段文字,以此缅怀 Felix。
时间回溯到 2014 年初夏。我当时在蒙特利尔大学担任 Yoshua Bengio 教授的博士后研究员,而 Felix 是一位刚刚抵达蒙特利尔的访问学生。
那时,我正在致力于开发一个能够处理长句子的神经机器翻译系统(Neural Machine Translation,NMT),为此我尝试了所有能想到的方法(注意力机制当时并不在其中,直到同年夏天 Dima Bahdanau 作为实习生来到蒙特利尔才有了突破)。
在这些探索性尝试中,我构思了使用门控卷积编码器(Gated Convolutional Encoder)来替代基于循环神经网络(RNN)的编码器。通过对门控机制施加适当的约束,我成功训练出了这个模型,并使其具备了一定的可解释性。
当 Felix 来到我的办公桌前,以语言学家和计算机科学家的身份做自我介绍时,我很兴奋地想要向他展示这个新模型所揭示的可解释结构。
于是我向他展示了:
Felix 以极其笃定的语气对我说,「Kyunghyun,语法并不重要」。这大概是我听过非韩国人中最标准的名字发音。
那一刻我就预感到,Felix 一定会成为我最好的朋友之一——事实证明,从那时起直到现在确实如此)。而他说的这句话,也在往后几年里频频出现在我的学术演讲 slides 中。
在我们共度的那些愉快时光中,除了进行深入却不失趣味的哲学探讨,我们也开展了一些研究合作。
除了共同发现的诸多有趣成果外,我们最具标志性的「贡献」反而是一个相当特别的现象:从 2016 年开始,我们无意中引领了一股持续 3-5 年的潮流,就是人人都仿佛着了魔似的要在论文中塞入一个装满海量数字的「超级大表格」(The Really Enormous Table)。
当时我们在写「Learning distributed representations of sentences from unlabelled data」这篇论文时,完全没想那么多,但最终还是放入了两个巨大的数据表格:
2018 年初,Felix 在 ICLR 的论文集中发现了好几篇包含「超级大表格」的论文,这让我们不禁哑然失笑。
即使在严谨的学术研究中,我们依然保持着这种愉快的合作方式。
时光飞逝,将近十年后,Felix 作为 2023 年拉美人工智能会议 Khipu 的组织者之一,邀请我担任演讲嘉宾。他兴致勃勃地向我描绘我们将要进行的活动:观看足球比赛,游览布宜诺斯艾利斯等等。
然而,当我在 2023 年 3 月抵达蒙得维的亚参加 Khipu 时,却发现 Felix 并未到场。其他组织者告诉我,他因为健康问题无法前来。那时的我还不曾想到,这竟是我最后一次有机会与他相见。
Felix 在 2023 年第二届 Khipu 上未领取的参会正件
2024 年 6 月,经过漫长岁月,我终于有机会造访伦敦,便给 Felix 发消息约他共进午餐。尽管我清楚见面的可能性渺茫,但我真的非常期待能与他相聚、畅谈,一起消磨时光。
我们上次见面还是在疫情之前,之后就只能靠偶尔的远程视频联系。我也暗自期待能看到他康复的喜人变化。
几个月后(2024 年 8 月),Felix 回复了消息,并为迟复致歉,这实在让人心疼。他还附上了一张我们的合影(不是 AI 生成的,但是 Felix「生成」的),唤起了我们上次在伦敦相聚的美好回忆。
左图:这是一张由 Felix 本人「生成」的与我的合影;右图:这是一张真实拍摄的 Felix 与我的合影
上周五,我收到了 Douwe 发来的 WhatsApp 消息。正是由于 Felix 在 2014 年的引荐,我才认识了 Douwe,并与他建立了深厚的友谊。我立即尝试联系 Felix,但不管是 WhatsApp 还是手机,都已经无法接通。
Felix,愿你现在已不再痛苦,在天国与母亲团聚。
参考资料:
http://www.paperlesspost.com/go/7BbrzXXh…
来源:juejin.cn/post/7455518848273498124
2024:踏平坎坷成大道,斗罢艰险又出发!
一、开篇
12月今年最后一个月了,相逢的人已走散,Q4的OKR已经定型了,很平淡无味、闲的无聊,提前写个年终总结吧。25年,再过一个月就35岁了,一个人来北京也已经11年了。年近末尾,思绪良多。回顾过去来看,这一年还真的经历了很多的事情,我的生活也发生了翻天覆地的变化!时光走笔,岁月成章,书写一本名为《我》的彩色童话,刻画属于自己的千种情绪、万般色彩。如果用一句话概括下过去的一年,那就是:力学笃行倍道而进,从不缺少挑战的勇气;脚步坚实步伐坚定,从未停止奔跑的脚步。
- 历经苦难,方知生命可贵!
- 承受困难,方懂世事艰辛!
- 无惧苦难,方能勇往直前!
二、历历在目,回首成长之路
2.1 天地风尘三尺剑,江湖岁月一诗篇
这个世界,人有万算,但却不如老天一算,做人要坦坦荡荡,做事要问心无愧。
回顾2024年,经历了非常多的大事情,有幸这一年全家人平平安安,在稳步前进。算是折腾的一年,刚开年之初,就喜得医院三日游,还记得那晚独自一人站在窗口,望着远处的万家灯火。依然不记得当时在想什么,只记得呆呆的站了一晚。也许过去的23年命里跟小人犯冲,又喜得公司裁员大礼包。原本想新年之后在重新找工作的,可麻绳专挑细处断,厄运只找苦命人,3月又被迫到医院“营业”,好在“苍天有眼见可怜,善恶有报分两岸”,还是顺利从手术台上走下来。医院是离生死最近的地方,再次面对时,本觉得可以洒脱些,然而,不然!真的真的是再也不想去医院了!
这一年,有过迷茫和无助,更多的是家里人带来的幸福和开心,有家人们的支持,也有了新的目标。也许是大病一场后,很多东西彻底的看清了、放下了。最大的问题是,今年颓废了,丢掉了自己的早起习惯,开始习惯性的熬夜娱乐,总是晚起床。有时候,我也会羡慕当初年少的自己,那时的自己,敢爱敢恨、敢打敢杀,更加不顾后果。而现在的自己呢,懂得控制自己的情绪,学会了顾全大局,也是能屈能伸。但这样的自己,也有弊端,隐藏了太多情绪,同时也控制了太多感情,活的也不如当初那般潇洒了。可没办法,成长,是人的必经之路,尤其是男人。成熟的男人,不能一味追求潇洒。他可以不已天下人的安危为己任,但至少要保障身边人的安危。为求生计赴他乡,今朝重温儿时梦,却已青丝染白霜。沧海桑田,世事变迁,回不去了,终究还是回不去啊……
我单枪匹马的走到现在,任何人都不是我的靠山。苦,我吃了;委屈,我咽了……伤痕累累走到现在,流言蜚语又能奈我何!再穷,我也没有骗过朋友;再苦,我也没有坑过身边人;再难,我也没有算计过谁……其实,我更喜欢好多年前的自己,他比我有胆量,比我遗憾少,比我懂得少,比我相信的很多……
2.2 书海遨游,陶冶情操
读书百遍,其义自见。 ——《三国志》
如今,手机已成为我们生活和工作的必需品,仿佛离开了它,就失去了飞翔的翅膀。我们依赖手机获取信息、沟通联络、娱乐消遣,几乎每一刻都离不开这个小小的屏幕。然而,这份过度依赖,却让我们的心灵逐渐陷入了一片荒芜之地。风沙漫天,孤寂如影随形,我们急需一股神秘而强大的力量,在这片荒芜中播撒希望的种子,使之绿意盎然,生机勃勃。这股力量,便是阅读。
在这个快节奏的时代,我们或许会因为忙碌而忽略阅读的价值。但请铭记,无论生活多么喧嚣,都要为自己的心灵保留一片净土,用来播种那些能够滋养我们灵魂的书籍。我每天抽出半小时的时间,远离手机、电视等电子产品的干扰,静下心来读一本书。当沉浸在书海中时,我发现那些曾经困扰我们的烦恼逐渐消散,内心变得宁静而充实。让我暂时忘却现实的痛苦,在无形中给予我力量,教会我如何在困境中寻找出路,如何在绝望中看到希望的曙光。
那啥,虽然 24 年阅读的书籍着实不多,大部分是闲书,惭愧惭愧,25 年要加油了,
三、我的程序人生
3.1 缘起性空,归来不少年
有所选择,有所放弃,做到“尽人事,听天命”就好了。
岁月匆匆流逝,回顾这一年最大的变化就是心态也有所起伏,整体有点躺平了,研究技术的动力也消失了,职业发展上感觉有点停滞不前了,也不太能跟生机勃勃的年轻人一起卷了。还记得从医院刚回来的那几周,整天在家里拉着窗帘,除了吃饭就是躺在床上刷手机,让我尽可能分散注意力,减少内心的痛苦。但是这样的状态也不是事儿啊,那只能去找个工作先干着了。活下来,是我目前的首要任务。于是在网上海投了一遍,结果惨不忍睹,根本没几家公司招人,前前后后两个月,真正靠谱的面试就那么几家。好在等到7月底顺利拿了offer,重新踏上“牛马”的大道。薪资也没有原来的多,但是拿到offer那一刻我依然有些激动,我感觉我活下来了,不管怎样,现在能喘口气了。
现在上班已经四个多月了,新公司挺好,不加班,基本上7点前就都走了。每天就是按部就班上下班,完成老板给的任务,其他的事情也不用自己操心,终于又做起自己熟悉且擅长的事情。如今逐渐适应了新公司的节奏,也算成功融入了团队,同时和同事相处的也十分的融洽。现在所在公司很少有加班的情况,回想上家公司总是天天加班到12点,劳心劳力而最后却卸磨杀驴。经过上半年的与天搏命、下半年奋发图强,关于工作我只有几点反思:
- 长时间处于过度忙碌的状态会导致效率下降,影响工作和生活的质量,甚至可能会成为不去做很多事情的借口。过度忙碌会遏制个人成长,让你没有时间接触新东西,没有时间总结和沉淀,没有时间去做未来的规划,最终陷入成长的死循环。
- 我们之前接受到的教育基本都是从自身找问题,但我这么多年的经历其实看到,很多时候问题根本就不是自身的问题。很多事不是单靠自己就能解决的,可能你本身所处的环境就有问题,你得到的输入就是不够多,你能获取到的资源就是不够多,你就是没有得到足够的指导和支持,别人给的建议就是不适合你…… 这种情况下重点不只是把锅甩给别人,而是应该去关注外部的改变,而不是自身的不足,适时寻求外部力量的帮助。
- 和优秀的同事共事是一种幸运。这一年,确实新接触了很多的同事,有合作的很愉快的,有希望能向他学习的,也有一些不太能理解的。但总的来说同事们的职业素养还是很高的, 相处的也十分的融洽。
3.2 坚定持续,攻破重重难关
人生不可能一帆风顺,就像大海不可能风平浪静一样。在生活中还是会遇到很多的困难与挑战,但庆幸的是我都扛下来了~
春耕夏耘,秋收冬藏。回望来时路,满心皆澎湃。我个人感觉今年几乎没有太多成长,并没有对于某项技术有非常深刻的理解或者突破,连博客的阅读量和涨粉量也是靠着之前的老博文来的。今年最重要的事情之一就是在新工作上稳定下来,当然这也是工作以来最不容易的一年。在这个飞速变化的技术环境中,在程序员的世界里,写代码、修Bug、加班常常成为了日常,作为程序员,技术更新的速度是无法忽视的。
虽然已经34岁了,不能熬夜写代码了,但是我还是喜欢偶尔敲敲代码,还是期待代码运行起来的效果,排除bug之后还是会高兴,这大概就是程序人生的乐趣!前些天翻了翻前端历史,突然发现,前端的屎事💩的确特别多。前端折腾来折腾去,好在现在才略微回到正轨上。作为后端 Java 程序员的我,并不反对前端折腾,对一些清晰的方向,我们还是应该全力以赴去折腾的。单一技能已经不足以满足现代项目需求,尤其是在公司需要「全栈开发」的趋势下,拥有一定的跨领域能力是你脱颖而出的关键。而且编程领域的变化是快速且持续的,持续学习是程序员生涯中的重要部分。可以通过参加技术分享会、研讨会,阅读最新的技术书籍和博客,或者加入技术社区来保持与行业趋势同步。此外,还可以通过在线课程或认证来提升自己的技能,为未来职业生涯积累资本。
四、心怀远方,踏上逐梦之旅
既已达成目标,也绝不懈怠,做好规划,奔向下一个目标⭐
4.1 厚积薄发,突破自我
学如不及,犹恐失之。——《论语·泰伯》
程序员的职业发展并非一帆风顺,许多人会在某个阶段遇到瓶颈,感到技术不再进步或工作内容重复。如果感觉工作内容乏味且没有新的挑战,可以尝试向上级申请更多复杂的项目或转向其他技术领域发展。接受新的挑战不仅能让你重新激发兴趣,还能快速提升技能。或者,也可以考虑参与开源项目,这不仅是学习新技能的绝佳方式,还能与其他开发者互动,团队协作、沟通能力等软技能更进一步。
我自认为是一个爱学习的人,从事互联网行业以来,一直都是从事 Java 的相关工作,除此还系统学过React、JS等,浅尝即止的就不说了,每一项都投入了较多时间,在这些技术的学习高峰,估计能达到中级开发者的开发效率吧。但是随着时间的流逝,其中大部分都忘的差不多了,如果面试官问我相关问题,我大概率是答不出来的,重新去做相关事情也需要去温习才能想起,很多人可能也有类似的感觉。去年的未来展望是打脸了,不少原先的计划没有完成,还因为突发的安排打乱了原先自己的部署,不过失之东隅、收之桑榆,收获也不少,那么我对 25 年的展望如下(写委婉点怕打脸太狠):
- 「学习 Python」:未雨绸缪,只盯着一个方向很容易触及天花板,真正有能力的人应博学多才,触类旁通,我差得远呢。
- 「保持阅读」:之前买的很多书籍、专栏、小册、视频什么的不能浪费了,要看起来了(收藏从未停止,学习从未开始 🤪)。
- 「锻炼身体」:经历那几年的疫情,大家也明白 🐶 命的重要,锻炼身体要坚持下来,行远自迩/
- 「保险」:在考虑是不是要给自己上一个保险,虽然还年轻。
- 「圆满」:
4.2 向上破圈,遇见无限可能
也是因着有这样的经历,我也去尝试了很多之前完全没有尝试过的事情,真正感受到了什么叫做【脱离舒适圈】
25 年的我 35 岁了,算是半只脚已经迈过而立之年的门槛,但距离成家立业还隔着老远,父母将老未老,知己零落四散。说实话,我现在知道了,山的那边还是山,我不知道什么时候才能看到海,甚至我可能一辈子都看不到海了。确实事情有些多且似乎都很重要,罗曼·罗兰的一句话怎么说来着 「有些人二十岁就死了,等到八十岁才被埋葬」,但至少到目前为止,生活还没把我扼杀不是嘛哈哈~。
在这个飞速发展的时代,我们生活在一个个无形的圈子中。这些圈子可能是由我们的职业、兴趣、社交圈子或其他因素所定义的。每个人都有自己的舒适区,舒适是堕落的开始,而开拓则是成长的基石。只有从旧圈子进入更高的圈子,你才会进步。敢于挑战自己,才能经历成长和蜕变,领略不一样的风景。摧毁那些限制你发展的信念,敢于跳出舒适区,勇往直前,闯出属于自己的天地!
五、新的一年,一起加油
长路漫漫,奋斗不息。学会和自己和解,学会接受自己的平庸,但是依然要努力,毕竟在这个阴雨连天的环境下,没有伞的孩子只能努力奔跑。我们的目标不仅局限于眼前的山川,我们的征途是星辰大海。那是一片辽阔无垠的天地,充满了未知与挑战。乾坤未定,每个人都有成为黑马的潜力。胜负尚未分明,未来充满无限可能。
- 愿你展翅高飞,化作飞鸟翱翔,自由随风。
- 愿你扎根大地,长成参天大树,傲骨铮铮。
- 愿你扬帆起航,翻飞万丈理想,意气昂扬。
- 愿你手握星火,燃尽塞途荆棘,前路坦荡。
- 祝你每次前行都能通往心之所向,每刻驻足都能遍赏锦绣河山。行己所爱,爱己所行,所有遇见皆是最好的安排。
来源:juejin.cn/post/7453611125453864996
三行五行的 SQL 只存在于教科书和培训班
教科书中 SQL 例句通常都很简单易懂,甚至可以当英语来读,这就给人造成 SQL 简单易学的印象。
但实际上,这种三行五行的 SQL 只存在于教科书和培训班,我们在现实业务中写的 SQL 不会论行,而是以 K 计的,一条 SQL 几百行 N 层嵌套,写出 3K5K 是常事,这种 SQL,完全谈不上简单易学,对专业程序员都是恶梦。
以 K 计本身倒不是大问题,需求真地复杂时,也只能写得长,Python/Java 代码可能会更长。但 SQL 的长和其它语言的长不一样,SQL 的长常常会意味着难写难懂,而且这个难写难懂和任务复杂度不成比例。除了一些最简单情况外,稍复杂些的任务,SQL 的难度就会陡增,对程序员的智商要求很高,所以经常用作应聘考题。
这是为什么呢?
其中一个原因是我们之前讲过的,SQL 像英语而缺乏过程性,要把很多动作搅合在一句中,凭空地增大思维难度。
但是我们会发现,即使 SQL 增加了步骤化的 CTE 语法,面对稍复杂的任务时,仍然会写的非常难懂。
这是因为,SQL 的描述能力还有不少重要的缺失,这导致程序员不能按自然思维写代码,要换着方法绕。
我们通过一个简单的例子来看一下。
简化的销售业绩表 T 有三个字段:sales 销售员,product 产品,amount 销售额。我们想知道空调和电视销售额都在前 10 名的销售员名单。
这个问题并不难,可以很自然地设计出计算过程:
1.按空调销售额排序,找出前 10 名;
2.按电视销售额排序,找出前 10 名;
3.对 1、2 的结果取交集,得到我们想要的
用 CTE 语法后 SQL 可以写成这样:
with A as (select top 10 sales from T where product='AC' order by amount desc),
B as (select top 10 sales from T where product='TV' order by amount desc)
select * from A intersect B
这个句子不太短,但思路还是清晰的。
现在,我们把问题复杂化一点,改为计算所有产品销售额都在前 10 名的销售员,延用上述的思路很容易想到:
1. 列出所有产品;
2. 算出每种产品销售额的前 10 名,分别保存;
3. 针对这些前 10 名取交集;
遗憾开始出现,CTE 语法只能写出确定个数的中间结果。而我们事先不知道总共有多个产品,也就是说 WITH 子句的个数是不确定的,这就写不出来了。
好吧,换一种思路:
1.将数据按产品分组,将每组排序,计算出每组前 10 名;
2.针对这些前 10 名取交集;
这需要把第一步的分组结果保存起来,而这个中间结果是一个表,其中有个字段要存储对应的分组成员的前 10 名,也就是字段的取值将是个集合,SQL 不支持这种数据类型,还是写不出来。
我们可以再转换思路。按产品分组后,计算每个销售员在所有分组的前 10 名中出现的次数,若与产品总数相同,则表示该销售员在所有产品销售额中均在前 10 名内。
select sales from (
select sales from (
select sales, rank() over (partition by product order by amount desc ) ranking
from T ) where ranking <=10 )
group by sales having count(*)=(select count(distinct product) from T)
在窗口函数支持下,终于能写出来了。但是,这样的思路,绕不绕呢,有多少人想到并写出来呢?
前两种简单的思路无法用 SQL 实现,只能采用第三种迂回的思路。这里的原因在于 SQL 的一个重要缺失:集合化不彻底。
SQL 有集合概念,但并未把集合作为一种基础数据类型提供,不允许字段取值是集合,除了表之外也没有其它集合形式的数据类型,这使得大量集合运算在思维和书写时都非常绕。
我们刚才用了关键字 top,事实上关系代数理论中没有这个东西,这不是 SQL 的标准写法。
没有 top 如何找前 10 名呢?
大体思路是这样:找出比自己大的成员个数作为是名次,然后取出名次不超过 10 的成员
select sales from (
select A.sales sales, A.product product,
(select count(*)+1 from T
where A.product=product and A.amount<=amount) ranking
from T A )where product='AC' and ranking<=10
注意,这里的子查询没办法用 CTE 语法分步写,因为它用到了主查询中的信息作为参数。
或可以用连接来写,这样子查询倒是可以用 CTE 语法分步了:
select sales from (
select A.sales sales, A.product product, count(*)+1 ranking from T A, T B
where A.sales=B.sales and A.product=B.product and A.amount<=B.amount
gr0up by A.sales,A.product )
where product='AC' and ranking<=10
无论如何,这种东西都太绕了,专业程序员也要想一阵子,仅仅是计算了一个前 10 名。
造成这个现象的原因就是 SQL 的另一个缺失:缺乏有序支持。SQL 继承了数学上的无序集合,与次序有关的计算相当困难,而可想而知,与次序有关的计算会有多么普遍(诸如比上月、比去年同期、前 20%、排名等)。
SQL2003 标准中增加的窗口函数提供了一些与次序有关的计算能力,这在一定程度上缓解 SQL 有序计算的困难,前 10 名可以这样写:
select sales from (
select sales, rank() over (partition by product order by amount desc ) ranking
from T )
where ranking <=10
还是要用子查询。
窗口函数并没有根本改变 SQL 无序集合的基础,还是会有许多有序运算难以解决。比如我们经常用来举例的,计算一支股票最长连续上涨了多少天:
select max(ContinuousDays) from (
select count(*) ContinuousDays from (
select sum(UpDownTag) over (order by TradeDate) NoRisingDays from (
select TradeDate,case when Price>lag(price) over ( order by TradeDate) then 0 else 1 end UpDownTag from Stock ))
group by NoRisingDays )
自然思维是这样,按日期排序后开始计数,碰到涨了就加 1,跌了就清 0,看计数器最大计到几。但这个思路写不出 SQL,只能绕成这样多层嵌套的。
这个问题真地是当作应聘考题的,通过率不到 20%。
这么一个简单的例子就能暴露出 SQL 缺失的能力,SQL 缺失的内容还有更多,限于篇幅,这里就不再深入讨论了。
反正结果就是,SQL 实现查询时无法应用自然思路,经常需要绕路迂回,写得又长又难懂。
现实任务要远远比这些例子复杂,过程中会面临诸多大大小小的困难。这个问题绕一下,那个问题多几行,一个稍复杂的任务写出几百行多层嵌套的 SQL 也就不奇怪了,过两月自己也看不懂也不奇怪了。
事实上 SQL 一点也不容易。
下面是广告时间。
SQL 很难写怎么办?用 esProc SPL!
esProc SPL 是个 Java 写的开源软件,在这里github.com/SPLWare/esP…
SPL 在 SQL 已有的集合化基础上增加了离散性,从而获得了彻底的集合化和有序能力,上面的例子就 SPL 就可以延用自然思路写出来:
所有产品销售额都在前 10 名的销售员,按产品分组,取每个组的前 10 名再算交集;
T.group(product).(~.top(10;-amount)).isect()
SPL 支持集合的集合,top 也只是常规的聚合计算,有了这些基础,实现自然思路很容易。
一支股票最长连续上涨了多少天,只要按自然思路写就行了
cnt=0
Stock.sort(TradeDate).max(cnt=if(Price>Price[-1],cnt+1,0))
SPL 有强大的有序计算能力,即使实现和上面 SQL 同样的逻辑也非常轻松:
Stock.sort(TradeDate).group@i(Price<Price[-1]).max(~.len())
来源:juejin.cn/post/7441756894094491689
年,是团圆
大概是我刚上小学时候,村子里流行起来出门打工,每个家庭里面的壮年男人都会去湖北挖桩,组团去。他们过完年出发,先走路下山,坐车再坐船。
出门一段时间后,他们会寄钱回家,那时寄钱的方式我不清楚也还从没有问过,只记得家里女人们听到对门肉嗓子大喊“xxx,来拿汇票”时,她们是很开心的。
快过年,男人们从外面回家,背着或挑着大件的行李,行李中是破旧的棉穗,小孩的新衣与不常见的水果糖。
挖桩需要两个人配合,一人在井里往下挖,一人在上面接桶转移土石,隔一段时间换人。有想法的男人认为他可以一直待在井里,女人提桶倒土是完全没有问题的,如果与自己的女人组团,工钱不用与别人分,挣钱的速度便会快上一倍。
于是,到我上初中时候,出门打工的规模更加扩大,家中的男人女人一起出门,小孩交给父母带着。
男人女人外出打工的时间是一样的,过完年出门,快过年时回家。
男人女人年龄渐长,他们的小孩长成大人,成人都外出打工,过完年出门,快过年时回家。
男人女人与小孩的目的地并不相同。于是过年,成为一家团圆的日子。
家家张灯结彩,人人喜笑颜开。
初一的早餐,是可以很简单的,汤圆或是包面;也可以是复杂的,蒸米饭,炸鱼、牛肉干、猪耳朵、凉拌鸡脚等许多凉菜,昨天的猪脚炖鸡汤,再配几个新鲜炒菜,满满一桌。复杂与简单的差异,来自于当天行程的安排,马上要走亲戚就简单些,家中要来客人便复杂点。
午饭,必然是丰盛的,一锅必不可少的鲜炖的猪脚炖鸡汤(是的,猪脚与鸡年三十只炖四分之一,留着客人来后能炖新的猪脚汤),一盆小孩儿最是欢迎的洋芋片,一锅人人喜爱的猪头汤清炖萝卜,两盘肥腻多汁男人们热衷的辣椒炒三线儿,一盘香肠、两碗扣肉,再来一盘炸豆腐……凉菜有猪肝、猪耳朵、鸡脚、炸鱼、牛肉干、萝卜干儿、折耳根……炒一个青菜,或是再加一碗青菜猪血汤。
男人们大都是会喝点酒的,他们互相劝着,你敬我一口酒,我回你两句话:“这酒好甜啊!”女人们偶尔也喝些酒,但更多是和小孩一起喝饮料。主妇是最后上桌吃饭的,大家吃饭时,她为桌上换热汤,为客人盛热饭。
吃过午饭,晒晒太阳烤烤火,嗑嗑瓜子吹吹牛。客人准备回家,临出发告诫主人:“你们明天一定要来啊,都要来。”
初二初三初四初五初六,是初一的重复。
隔得近,午饭的重复便是晚饭。吃过晚饭,今日不用再做饭,男人女人们围着炉子说过去说现在,说去年说当下,那“哈哈”大笑声,是时常能传到地坝的。
地坝里,孩子们的游戏一个接一个的切换。老鹰抓小鸡、三个字(一个人追众人跑,快被抓到时喊出随便的三个字,被抓者定住等待救援,抓人者切换目标;待所有人都被定住,便问“桃花开了没?”)、跳绳、打沙包……四岁五岁的,大多数游戏还玩不太明白,但他们的参与感该是最强的,因为哥哥姐姐叔叔阿姨都护着他们。
地坝里的叽叽喳喳,盖过屋子里哈哈哈哈。
年,是团圆!
来源:juejin.cn/post/7337957655190847522
看完前端各种风骚操作,我眼睛被亮瞎了!
一、实现一个快速评分组件
const getRate = rate => "★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate);
console.log(getRate(0)); // ☆☆☆☆☆
console.log(getRate(1)); // ★☆☆☆☆
console.log(getRate(2)); // ★★☆☆☆
console.log(getRate(3)); // ★★★☆☆
console.log(getRate(4)); // ★★★★☆
console.log(getRate(5)); // ★★★★★
这个都不用多解释了,简直写的太妙了!
二、巧用位运算
用位运算可以实现很多功能,比如乘2、除2(或者2的倍数),向下取整这些计算操作,而且性能很高!
let num = 3.14159;
console.log(~~ num); // 向下取整,输出3
console.log(2 >> 1); // >>表示右移运算符,除2,输出1
console.log(2 << 1); // <<表示左3移运算符,乘2,输出4
并且,利用~符
,即按位取反运算符(NOT operator)
,还可以和字符串的indeOf
方法配合使用。
const str = 'acdafadfa'
if (~str.indexOf('ac')) {
console.log('包含')
}
其实原理很简单,举几个例子大家就明白了:
~-1
的结果是0
。~0
的结果是-1
。~1
的结果是-2
,~2
的结果是-3
。
三、漂亮随机码
const str = Math.random().toString(36).substring(2, 10)
console.log(str); // 随机输出8位随机码
这个在要为每个用户生成一个随机码的时候特别好用,具体随机码多少位可以自己控制,如果要的随机码位数特别长,可以把这个函数多调用一次,然后把结果进行字符串拼接。
四、史上最NB的报错处理
try {
const str = '';
str.map(); // Uncaught TypeError: str.map is not a function
} catch(e) {
window.open("https://stackoverflow.com/search?q=js+" + e.message);
}
这应该是史上最NB的报错处理了,一般来说,抛出错误时应该打印日志并上报,这里直接带着报错信息给人家重定向到stackoverflow
去了,顺便stackoverflow
搜索了下这个报错,直接搜一波错误解决方案,而且这个网站是全英文的,顺便还能学一波英语,对于开发者来说,这简直太妙了!不过记得上线的时候,记得在跳转前加一个if(process.env.NODE_ENV === 'development')
,不然上到线上一旦报错可就惨了!
五、倒序排序的简写
const arr = [1, 2, 3, 4, 5];
for (let i = arr.length - 1; i >= 0; i--) {
console.log(arr[i]);
}
可简写为:
const arr = [1, 2, 3, 4, 5];
for(let i = arr.length; i--;) {
console.log(arr[i]);
}
代码解释:
先来回顾下for循环的书写结构,即for (初始化表达式; 条件表达式; 递增表达式)
,初始化表达式只会执行一次,而条件表达式和递增表达式在每次循环时都会执行一次,而正好这个倒序循环的终止执行条件为i==0
,所以就可以把条件表达式
和递增表达式
合而为一了,主打的就是一个简洁。
六、在控制台输出一个键盘图形
console.log((_=>[..."`1234567890-=~~QWERTYUIOP[]\\~ASDFGHJKL;'~~ZXCVBNM,./~"].map(x=>(o+=`/${b='_'.repeat(w=x<y?2:' 667699'[x=["BS","TAB","CAPS","ENTER"][p++]||'SHIFT',p])}\\|`,m+=y+(x+' ').slice(0,w)+y+y,n+=y+b+y+y,l+=' __'+b)[73]&&(k.push(l,m,n,o),l='',m=n=o=y),m=n=o=y='|',p=l=k=[])&&k.join`
`)())
这段代码会在浏览器控制台中打印出一个键盘图形,不得不说写出这段代码的人真的太有才了!
以上就是我总结的一些前端代码的风骚操作,大家有没有更风骚的操作呢?欢迎大家留言分享!
来源:juejin.cn/post/7453414571563925542
研发工作中的感悟
前两天工作上出现了纰漏,感兴趣的可以往前翻翻,大致情况是拼命干,结果干得越多错得越多,出了纰漏背了不少黑锅。
最近一直在调整工作状态,复盘以前错误的工作方式。
多和团队沟通解决方案
以前我都是一个人单打独斗,功能点从设计到逻辑到实现,都是我一个人。
这种情况首先一个人力量有限,有时候设计的不那么完美;其次是大家一起讨论出来的东西,做出来就算不好,也有人帮忙说话,避免了墙倒众人推的局面。
主动暴露自己遇到的问题
以前我也是闷头干,很多过程中的困难和问题,我都自己默默解决了。这就搞得别人以为这事没难度,还嫌我进度慢。
现在不管问题能不能解决,遇到卡点我都吱一声,能不能解决再说,能解决是我牛逼,不能解决就往上报。
不要被上线时间卡死
我们公司有个非常不好的风气,技术还不知道功能点,运营就定好了上线时间,一开始搞得很焦虑,匆匆忙忙赶着上线,结果出纰漏,开会一顿挨叼。
有些很大的需求,两三个人一两个月那种,即使前期做了评估,做着做着也会被临时需求插入呀、技术方案调整呀、三方调用阻碍等事拖慢进度。
现在我对这事看淡了,要做就做好,技术层面设计搞得清清楚楚,按规范的来,工时正常评估,需求 delay?无所谓的,直接报个直属领导,让领导之间去互撕。我周末来加天班表示下态度,其实一个人没人打扰专注写代码挺爽的。
配置化的东西让运营操作
我以前有个特别不好的习惯,配置的东西图快图简单,配在 redis 里或acos,每次运营跟我说我去改。
千万不要这么搞,不出问题还好,但也浪费开发时间,出了问题那更是开发背锅。
所以不如一开始就花点时间写个页面给运营自己配,别看一开始费了点时间,从此没了后患。
主要是看到论坛一个哥们自己配置的利息少了个 0 导致涨了 10 倍,事后自己背锅还是去修数据,也是惨的一批。
只做事不做决策
还是以前责任心太强了,到我这的事我必须完结掉,中间遇到的各种困难险阻我都干掉了。
现在中间遇到什么问题,尤其是需要做决策的地方,研发的就 @ 直属领导,业务的就 @ 运营和产品,让他们出解决方案,确定好了我再动工。
比如最近在接入京东小程序,用户注册的地方要不要对三方手机号做虚拟号拦截?我想个屁直接丢群里面问业务。比如一单一品下单缺商铺各种配置,我也是一个 @ 给到运营。商户订单转支付总觉得怪怪的,那就拉领导反复多次确定和讨论,果然架构都要改掉。
自从不做决策后,我感觉一身轻。首先决策本身很消耗精力,要想清楚不同决策的影响点,也需要一定的经验支持;其次决策做了是要担责的,万一做错了呢?锅这不就来了吗?况且我也没到那个位置,做那些有点越位了,不合适。
有意识的过滤杂事
晚上八点多的时候,群里反馈有人收不到登录验证码,运营疯狂 @ 我。
我试了下 5s 就收到了验证码,就这么杂碎的事,自己点一下系统就能判断的,也要 @ 我?用户手机欠费我还得去给他充点话费是吧?
纯粹是我以前太好欺负,来我这的什么脏活累活,我都没吭声的干完了,是开发的,不是开发的,反正到我这都能闭环,这么好用的人,不往死里用?
现在像这种 SB 场景,我看都不看,还把飞书提醒都关了,就留个加急消息通知。
还有些人老喜欢问我 xx 场景下 xx 逻辑是怎样的。我也是一脸黑人问号,这玩意不去问产品来找研发几个意思?
这些问题我也没怎么管了,已读不回就完事,结果大部分情况,没回也没再追问了。
脸皮厚起来
这话咋理解呢?就是细节的地方,要决策的地方,不要觉得这么简单的事也要抛出来,厚着脸问出来。
我以前就有这种不好的思想,总觉得事情很简单,决策影响面也不大,自己 hold 得住。结果就是解决得好没好处,解决不好就吊,出岔子还背锅。
现在我非常不要脸的把各种细节处的问题都抛出来,在群里面说的清清楚楚。
一来是逻辑大家都同步下,二来是决策相关的给领导。
就像这次京东小程序,需求评审会的时候运营、产品都说实名信息京东有接口,讨论都不让讨论直接过。结果对接的时候,京东直接懵逼了,说我没接口啊,我也傻眼了,信誓旦旦的接口没了,开发一半到这节点又要重新调整方案。
就这运营还私我让我搞清楚需求别问尴尬问题,我 TM 反手一个质疑三连,合着你给我搞个京东实名信息接口呗,我不问京东问你吗?
总结今日份把杂事脏活全甩了,不该背的锅全反弹,内心舒畅,心情大好。特意奖励自己鸭脖一根、鸭头一个、鸭肠一捆、薯片一袋、浪味仙一包、快乐水一瓶。
拒绝内耗,勇敢甩锅,快乐自己,不顾他人,从我做起,共勉。
来源:juejin.cn/post/7452665094193020939
30岁的程序媛,升值加薪与我无缘
前言
上篇讲述了一位老哥的10年搬砖历程《不容易,35岁的我还在小公司苟且偷生》,有位小姐姐看了之后比较有感触,希望我能将她的故事也讲讲,看看能否有共鸣的朋友。
程序媛的前半生
我今年30岁,无房无贷孑然一身。
出生在95年的沿海小镇(隶属八山一水一分田的省份),我四岁那年父母终于如愿以偿地迎来了弟弟,从此以后弟弟就是家里的中心。
高考填报自愿的时候,想到远点的地方上大学,最终上了四川的一所院校。坐了将近三十个小时的列车,也就是那会儿才真真体会到了书上说的:"我国地势西高东低,幅员辽阔"。
专业学的信息工程,大学那会班里男多女少,大家都挺照顾我的,性格也变得开朗了许多,谈了个男朋友,我们相约考个985院校的研究生。
那年春天,考研成绩出来,我没过线,他低空掠过,接受调剂到另一个省读研,然而正当我打算二战的时候,被分手了...
因为考研错过了秋招,春招也只剩了尾巴,也不打算二战了,家里人让我回去找工作,不过我还不太想回去,先找工作再说。幸好平时跟着班里的大牛们一起做过项目,项目虽小,但知其然也知其所以然,花了2个月终于找到一份前端的工作。
2018年入职成都北部郊县的一家小创业公司,公司主营业务是硬件,互联网软件是辅助,前端+后端也就6个人,没想到前端除了我还有另一个妹子。
那会工资比较低,自己也知道本身有几斤几两,为了节省开支,每天都带饭,当然每天也是按时下班,下班后先把第二天要带的饭菜准备好之后,再花一小时提升自己的技术(主要是看别人的轮子),最后边做面膜边刷美剧打发时间,直到窗外的喧闹声逐渐隐去,我也入睡啦。
平淡的日子真的淡如水,印象比较深的是有个晚上需要紧急修复Bug,要去公司对齐方案,尴尬的是,最终查出来不是我的问题。回家的路上,看着昏暗路灯下长长的影子,莫名的感觉害怕,越怕走的越快,越快影子越长,终于看到小区门口有个抽烟保安叔叔,赶紧飞奔过去。当进入小区的那刻,回过头来,却怎么也找不到影子了。从那时起,暗下决定,再也不加班了。
2020年7月,刚好在公司待了两年,周围的同事都换了一半的新面孔,不少同事去了南边,在小聚的时候他们说起了南边的机会比这多多了,让我考虑考虑。我也仔细思考了自己的处境,软件在这家并不是主营业务,想找一家纯互联网的公司,最主要的是这两年没有涨薪,新来的小伙伴工资都比我高,于是也暗中投递了简历。
运气不错,一个月内面了4、5家,最终选定了一家互联网公司,工资是上家的两倍,当时暗自感叹同一个城市差距咋那么明显。
新的公司,单单是前端的研发都有30+人,而这还只是成都分部的。刚开始很是珍惜这份机会,任劳任怨,偶尔也会加班表现一下。后面逐渐和同事们混熟了,才发现自己的工资就是垫底的存在,瞬间加班的动力跌至冰点。
在这公司接触了更多的业务,学习了很多技术知识,甚至知道有些同事大佬自己创造轮子。也许是我比较菜,或是我工资比较低,领导给我的任务都不会太难,我也没出过什么幺蛾子。每年都有涨薪的机会,当然每年都没我的份,在这块我是有心理预期的,因此也不会失落(或许是已经麻木了?)。
23年底开始,陆续有同事拿到了大礼包,甚至有一次我刚从厕所出来,对面的同事的工位就空了。问了才知道,谈妥了,立马走。
到了24年初,小组内陆续走了一半的同事,直属领导找我谈话问我想法,我说我比较懵,明明我在组内最菜,为啥走的不是我。领导说,相比而言你的性价比较高,我暗想:直说我薪资低呗~。
到了24年底,还暗自庆幸自己能挺过最艰难的一年,没曾想拿到了大礼包,没有任何挣扎,签协议走人。
休息了一个月,尝试投递简历,直到现在才有两次面试邀约,神奇的是这两家都会问我同一个问题:结婚了没,打算多久结?我:???
我决定了,以后进行自我介绍的时候先发制人,本人未婚未孕未育。
面试结果最终当然是不了了之了。
直到成为自由人才深刻体会到今年寒气之重,都结冰了。
还好申请了失业保险金,聊以慰藉吧。
经过了用人市场的洗礼,在三月份之前都不打算投递简历了,反正都是石沉大海。
刚好有时间多休息,多去看看以前没去的地方(虽然大部分是穷游),节后再做打算吧。
30岁的我,在人们眼中是个剩女。
30岁的我,阴差阳错学了技术。
30岁的我,没造过一个技术轮子。
30岁的我,远离家乡几千里。
30岁的我,在领失业保险金。
30岁的我,也许会离开这座城市。
30岁的我,祝朋友们所愿皆所得。
来源:juejin.cn/post/7461587929447776275
迟来的2024年终总结
这是一篇迟来的 2024年终总结,从来没正式的为自己做过总结。
一、工作
2023年底,其实当时经过了几面之后,本来已经拿到了 Gitee(开源中国) 的 Offer,然后因为杭州有朋友说有项目,于是思考许久之后,还是决定来了杭州。
选择杭州放弃了深圳,我觉得应该还是这几个原因:
- 杭州有朋友在,工作安排好之后,住宿的问题也一并解决了,当时到杭州之后基本就是直接拎包入住的。而且公司离住的地方走路也就十分钟。
- (因为老婆孩子在区县,市区家里目前也是空置的状态),如果杭州回老家,可以选择飞机、高铁,而且高铁能直达重庆家里,家里到火车站也就五分钟车程,来去方便。深圳的话,就只能飞机到重庆,再转动车到家里。我又是很怕麻烦的一个人。
- Gitee 的 Offer 是产品经理,纠结了一下之后,觉得如果转了的话,估计以后自己写代码会更少了
- 还没来过杭州。
参与的工作和项目
1.1 老系统的维护和迭代
本身有一套基于 PHP 的灵工财税系统在生产上跑,需要进行日常的维护、迭代一些新功能。
系统周边还有一套 支付宝小程序 也在线上运行着。
1.2 新系统的设计与开发
基于老系统的业务需求,重新架构设计和开发了一套新系统:
- 使用 Java17 / SpringBoot3 / MySQL8 / JPA / Redis / RocketMQ 等后端技术栈对后台服务做支持
- 使用 Vue3 / Vite / TypeScript / ElementPlus 等前端技术栈对前端页面做支持
新系统前前后后开发和测试花了大概三个月的时间,技术团队人员 2 个全栈,两个产品,两个测试。
1.3 MCN机构主播平台
设计开发了一个 MCN 机构的主播社区,技术栈和上面新系统基本一致,主要实现了一个后台服务、一个 Web 端的管理系统、一个基于 uniapp 的 App,上架了 App Store,Android 端倒是没有直接上商店,提供的是 H5 官网直接下载 APK.
1.4 一些小工具
也做了一些公司内部很多小工具的开发,例如基于 小爱同学 的业务语音通知服务、Web 叫号服务(类似在页面上输入信息,指定公司内各个部门的小爱同学进行通知的功能)
也不停折腾了公司的一些 VPN 网络架构 局域网服务器架构 等工作,例如基于 vmware vsphere
vcenter
vsan
的超融合架构等。
用大模型搭了一些好玩的服务,比如 ts.hamm.cn java.hamm.cn 等
1.5 其他项目
也客串了一个前端,参与了公司其他小组的社交类产品的管理后台开发。
因公司有一个 AI 出海项目需求,预研了一个 AI智能体项目,主要是一些角色扮演的场景服务 (此处有狗头)
。
二、开源
今年做了一些开源小项目,当然比去年的积极性要低了很多:
2.1 SPMS 智能生产管理
S-PMS (Smart Production Management System) 智能生产管理系统 ,是一个集成化、智能化的企业级应用软件,它集成了多个核心的生产管理模块,包括 制造执行系统 (MES)、仓库管理系统 (WMS)、企业资源计划系统 (ERP)、质量管理系统 (QMS) 以及 物联网管理系统 (IoTS) 等。
技术栈使用的也是和 1.1.2 中提到的一样。
其中完成了两个端的开发:
这个项目其实从 2013年底 就已经开始了,目前还在迭代中。
2.2 OllamaK
因为觉得其他的 Ollama iOS 客户端都不好用,然后自己花了几天时间写了个简单的 Ollama iOS 客户端。
Github: github.com/HammCn/Olla…
基于 Swift + SwiftUI 设计。
2.3 AirPower4T
AirPower4T 是一个基于 Vue3 TypeScript Element Plus Vite 的开发基础库,使用面向对象、装饰器、Hooks等开发模式,内置了数据模型转换、表格表单装饰器配置、加解密和编码解码、网络请求、权限管理等常见后台功能以及页面组件,助力后台类系统的前端开发效率,同时保障了优雅的代码质量。
Github: github.com/HammCn/AirP…
2.4 AirPower4J
AirPower4J是一个基于 Java17、SpringBoot3.x、JPA&MySQL 的后端开发脚手架,其中包含了一些 RBAC、请求验证、CURD封装、异常处理、多租户SaaS、加解密与安全、WebSocket等模块,以满足日常开发的快捷、稳健、标准化等要求。
Github: github.com/HammCn/AirP…
2.5 一些SDK包
2.5.1 WeComSDK
企业微信的 Java SDK
。目前是开发中,对 企业微信 的一些 OpenAPI
进行了封装。
Github: github.com/HammCn/WeCo…
2.5.2 WeComRobotSDK
一个很好用的企业微信机器人开发工具包SDK。也是发布到了 maven
仓库。
Github: github.com/HammCn/WeCo…
2.5.3 AirPowerJavaSdk
AirPower Java SDK 是基于 Java8 下用于快速对接 AirPower4J 项目中的开放应用的开发工具包,实现了与 AirPower4J 匹配的 AES / RSA 出入参加解密、参数签名、防止重返攻击、数据自动转换等功能,针对基于 AirPower4J 下的 Web 项目提供快速支持开放能力。
Github: github.com/HammCn/AirP…
三、写作
这一年免不了在掘金和其他社区摸了不少鱼。
3.1 掘金专栏
开了三个掘金的专栏:
3.1.1 《用TypeScript写前端》
本篇专栏主要讲解作者是如何大胆放肆的使用 TypeScript 面向对象思维来写前端的。
截止目前,共收录了 32篇 文章,订阅用户 500 人,希望能真正的帮到这 500 个订阅的朋友。
3.1.2 《来杯Java压压惊》
主要是分享一些用Java写后端的心得体会。
截止目前,共收录了 14篇 文章,订阅用户 6 人,因为是 11月 才创建的专栏,数据有些许惨淡。
3.1.3 《你好,全干工程师》
网络?运维?架构?产品?设计?可能这个专栏都有涉及到。
截止目前,共收录了 47篇 文章,订阅用户 21 人,数据也不是那么好看。
3.2 粉丝数据
截止目前:
3.2.1 掘金粉丝:800
3.2.2 Github粉丝:211 (@HammCn)
3.2.3 Gitee粉丝:887
3.2.4 公众号粉丝:3401 (@imHamm)
公众号的粉丝也不是很垂直,现在几乎不在公众号发布什么内容了。
3.2.5 微博粉丝:5000
(不垂直,已经不打算经营了,不再公开了)
3.3 阅读数据
截止目前,掘金阅读数据:
四、生活
杭州的生活很糟糕。特别是美食。
4.1 饮食问题
刚来的时候,还能维持每周两三次在家做饭炒菜,这几个月几乎没怎么在家做了,都选择了外卖或者在外面吃。
美团上拉黑了很多个商家了,实在是难吃。
4.2 日常出行
因为几个朋友都在一起,所以日常也基本都是在一块。一般也只在家、公司、附近商场、机场、火车站 这些地方。
日常没有什么出行的需求,但给老婆换掉了之前 我开的油车,换了 另外一辆油车。。。
(给家里添置了第二辆林肯了,蛮喜欢这个品牌的)
唯二在杭州较远的两三次出行:
4.2.1 灵隐寺
去过一次就不再想去第二次了。
4.2.2 乌镇
和重庆的磁器口差不多,没什么意思。
五、家庭
家庭是最重要的部分,所以选择放到最后说了。
5.1 儿子
儿子今年六月份三岁了,也上了幼儿园小班。
小子从小就聪明,情商也高。就是在学校不爱吃饭,还回家说学校的饭菜不好吃。
现在几乎能用英文从 1-100 读出来,一些颜色、水果、物体 也都能简单的表达了。
数学方面的话,10以内的加法没问题了,减法还不太会的样子。
5.2 老婆
家里最漂亮的女人。带孩子、上班都是她。
5.3 亲人
爸妈,岳父岳母依然是围着儿子在转,也慢慢的有了一些岁月老去的痕迹了。
依然是身体健康,这也就是最大的幸福。
5.4 离开了两个亲人
我这边的爷爷和外婆相继在今年离开了我们。希望他们在那边没有烦恼,快乐生活。
六、总结
这一年经历了太多,本文也是流水账的方式做了个年终的总结。
对2025年的期望,目前也还很迷茫。
先祝福吧:
希望儿子能健康快乐的成长,能学习到很多好玩的东西。
希望老婆依然是貌美如花,别被儿子整天的调皮折腾。
希望爸妈,岳父岳母,爷爷奶奶们身体健康,生活没有烦恼。
至于我自己,现在还没想好,但希望2025年工作上能有一些新的突破。
就这样,也祝所有幸福的人们,2025的愿望也都能实现。
来源:juejin.cn/post/7461207850456842303
✨Try-Catch✨竟然会影响性能
前言
一朋友问我Try-Catch写多了会不会让程序变慢,我不加思索的回答肯定不会,毕竟曾经研究过Java异常相关的字节码指令,只要被Try-Catch的代码不抛出异常,那么代码执行链路是不会加深的。
可事后我反复思考这个看似简单实则也不复杂的问题,我觉得顺着这个问题往下,还有一些东西可以思考,如果你感兴趣,那就跟随本文的视角一起来看下吧。
正文
首先郑重声明,单纯的针对一段代码添加Try-Catch,是 不会
影响性能的,我们可以通过下面的示例代码并结合字节码指令来看下。
示例代码如下所示。
public class TryCatchPerformance {
public Response execute(String state) {
return innerHandle(state);
}
public Response innerHandle(String state) {
// todo 暂无逻辑
return null;
}
public static class Response {
private int state;
public Response(int state) {
this.state = state;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
}
我们依次执行如下语句为上述代码生成字节码指令。
# 编译Java文件
javac .\TryCatchPerformance.java
# 反汇编字节码文件
javap -c .\TryCatchPerformance.class
可以得到execute() 方法的字节码指令如下。
public com.lee.learn.exception.TryCatchPerformance$Response execute(java.lang.String);
Code:
0: aload_0
1: aload_1
2: invokevirtual #2 // Method innerHandle:(Ljava/lang/String;)Lcom/lee/learn/exception/TryCatchPerformance$Response;
5: areturn
现在对execute() 方法添加Try-Catch,如下所示。
public class TryCatchPerformance {
public Response execute(String state) {
try {
return innerHandle(state);
} catch (Exception e) {
return new Response(500);
}
}
public Response innerHandle(String state) {
// todo 暂无逻辑
return null;
}
public static class Response {
private int state;
public Response(int state) {
this.state = state;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
}
查看execute() 方法的字节码指令如下所示。
public com.lee.learn.exception.TryCatchPerformance$Response execute(java.lang.String);
Code:
0: aload_0
1: aload_1
2: invokevirtual #2 // Method innerHandle:(Ljava/lang/String;)Lcom/lee/learn/exception/TryCatchPerformance$Response;
5: areturn
6: astore_2
7: new #4 // class com/lee/learn/exception/TryCatchPerformance$Response
10: dup
11: sipush 500
14: invokespecial #5 // Method com/lee/learn/exception/TryCatchPerformance$Response."":(I)V
17: areturn
Exception table:
from to target type
0 5 6 Class java/lang/Exception
虽然添加Try-Catch后,字节码指令增加了很多条,但是通过Exception table(异常表)我们可知,只有指令0到5在执行过程中抛出了Exception,才会跳转到指令6开始执行,换言之只要不抛出异常,那么在执行完指令5后方法就结束了,此时和没添加Try-Catch时的代码执行链路是一样的,也就是不抛出异常时,Try-Catch不会影响程序性能。
我们添加Try-Catch,其实就是为了做异常处理,也就是我们天然的认为被Try-Catch的代码就是会抛出异常的,而异常一旦发生,此时程序性能就会受到一定程度的影响,表现在如下两个方面。
- 异常对象创建有性能开销。具体表现在异常对象创建时会去爬栈得到方法调用链路信息;
- Try-Catch捕获到异常后会让代码执行链路变深。
由此可见Try-Catch其实不会影响程序性能,但是异常的出现的的确确会影响,无论是JVM创建的异常,还是我们在代码中new出来的异常,都是会影响性能的。
所以现在我们来看看如下代码有什么可以优化的地方。
public class TryCatchPerformance {
public Response execute(String state) {
try {
return innerHandle(state);
} catch (Exception e) {
return new Response(500);
}
}
public Response innerHandle(String state) {
if (state == null || state.isEmpty()) {
// 通过异常中断执行
throw new IllegalStateException();
} else if ("success".equals(state)) {
return new Response(200);
} else {
return new Response(400);
}
}
public static class Response {
private int state;
public Response(int state) {
this.state = state;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
}
上述代码的问题出现在innerHandle() ,仗着调用方有Try-Catch做异常处理,就在入参非法时通过创建异常来中断执行,我相信在实际的工程开发中,很多时候大家都是这么干的,因为有统一异常处理,那么通过抛出异常来中断执行并在统一异常处理的地方返回响应,是一件再平常不过的事情了,但是通过前面的分析我们知道,创建异常有性能开销,捕获异常并处理也有性能开销,这些性能开销其实是可以避免的,例如下面这样。
public class TryCatchPerformance {
public Response execute(String state) {
try {
return innerHandle(state);
} catch (Exception e) {
return new Response(500);
}
}
public Response innerHandle(String state) {
if (state == null || state.isEmpty()) {
// 通过提前返回响应的方式中断执行
return new Response(500);
} else if ("success".equals(state)) {
return new Response(200);
} else {
return new Response(400);
}
}
public static class Response {
private int state;
public Response(int state) {
this.state = state;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
}
}
如果当某个分支执行到了,我们也确切的知道该分支下的响应是什么,此时直接返回响应,相较于抛出异常后在统一异常处理那里返回响应,性能会更好。
总结
Try-Catch其实不会影响程序性能,因为在没有异常发生时,代码执行链路不会加深。
但是如果出现异常,那么程序性能就会受到影响,表现在如下两个方面。
- 异常对象创建有性能开销。具体表现在异常对象创建时会去爬栈得到方法调用链路信息;
- Try-Catch捕获到异常后会让代码执行链路变深。
因此在日常开发中,可以适当增加防御性编程来防止JVM抛出异常,也建议尽量将主动的异常抛出替换为提前返回响应,总之就是尽量减少非必要的异常出现。
来源:juejin.cn/post/7458929387784077349
再见2024,秋枫春樱夏海与钱
也许一切都有个期限。
人生的前 7 年是童年,接着是 17 年的学生生涯(1 年复读)。在大学四年里,除了应付本身的学习外,一半的时间鼓捣音乐,一半的时间在炒股,对结束学生的生活充满了期待。
进入工作以后,来到今年,也17年了。
上班和上学真的很像的。上学时,小学中学大学一路打怪升级,上班时,小厂中厂再到大厂,也是一路打拼。每天两点一线,年复一年,日复一日。
因父亲是国企员工,自小对大厂的集体生活就有恐惧感。然而经过奋斗再进入大厂,享受到更好的福利待遇时,才发现大厂的生活更适合大众。
但是,经过前面几年的折腾,已经消耗过多的精力,进大厂之后缺乏了向前进的动力。在小厂里,你 3-5 年就可以成为小领导。在大厂里,3 年才刚刚对公司各方面有一些了解,才能协调资源去完成一个小项目。大厂里需要消耗更多的时间精力处理本身工作之外的事情。
在大厂时间一天天流逝,发现自己并没有得到真正的提升。
于是,凑巧也是 17 年,终于迎来了结束的一天。
因为给自己打了个 C,喜获炒鱿鱼一份,也正式离开了大部分人走的轨道。
大学的时候无所事事,那四年的确是个人最舒服的时间。现在也无所事事了,挺好。
旅行
没事就找点事吧。除了省内走走逛逛之外,也出省游了。
春天,到武汉赏樱花;秋天,到南京赏枫林。夏天则走了一趟海南环岛游。春夏之间,还去了宁波,与深圳同样的海滨城市。
中国的城市化的确很好,在任何城市生活都不会有很大的差别,只是各处的风景、人文又略有不同。
武汉,赏的是代表着中日友好的樱花,也是它的悠久历史。平原一望无际,楼都比山高,黄鹤楼与长江大桥夹在高楼间,并不显突兀。这里有春秋战国的遗物,有 800 年的楚国文化历史,也有与深圳一样新的光谷。
南京,除了心心念念的红枫林,还有六朝古都的风韵。枫叶看了,南朝四百八十寺看了,再去看看博物馆。作为在文化荒漠长大的广东人,博物馆看到第三天,脑里存储的历史知识已经用完。最后亦不能忘,要去中山陵园瞻仰近代伟人。
宁波像矮版的深圳,高楼大厦少了些,但是不缺发达的城市商业,不缺鲜甜可口的海鲜,也不缺舒适慵懒的海边。这里还有中国最大的私人藏书阁,这里也是徐霞客旅行的起点。
至于海南,熟悉又陌生。的确远离了大陆,除了冼夫人、苏东坡的痕迹外,没有太多先人的故事。在这里躺平也是一个选择。多少明白了全国各地的人都喜欢去海南的原因。
我心中是没游遍全国的计划的。作为一个中年人,开始向内求,其实不太需要外界刺激来放松自己,或者来达到什么样的目的。
明年会不会继续出发?当然是看钱行事。
钱
钱的问题才是主要的问题,尤其对一个失去了工资收入的人。
投资上,今年预判失败,提早清仓了纳斯达克指数 ETF,然后在 9 月底,参与了大 A 股的闪电牛市,但是没有全身而退。在股市上只能说小盈利。离职后,更是忘了“财不入急门”,跑去炒期货,最后在期货上亏了不少。
赢利比最高的反而是虚拟货币。这笔资金来源于参与了新发行的币。卖出后,交易了几笔,账户翻了倍。然而只有可怜的80刀。
所以整体下来,是亏损了。
终于发觉,自己作为 20 年老股民,一直没有很好地对待自己的投资。回想起来,自己有四点没有做好的:
- 在自己收入最好的时候,没有存到钱,没养成好的储蓄习惯。
- 买房时机还可以,但是没有尽早还房贷。
- 大部分钱都用于买房,只用很少的钱参与投资。
- 有自己的投资风格,却没有完善自己的风格,并不断地执行让雪球滚起来。
如果有时光机,我一定回去对自己说,“存钱,存钱,存钱!”
技术
今年,对规范、标准有了进一步了解。
在前端里,react 之前在高阶函数、服务端组件中的探索,成就了无头 UI,成就了shacn/ui;shacn/ui 丰富的下游组件、模板成就了 AI 编程工具 v0.dev。目前 v0.dev 还在继续接入 next.js 的模板。
另外,如果用了 RESTful 写的接口,使用 AI 编码可能 1 句话就解决了,如果不是,则需要对增删改查接口,每个都写不同的对话。
可以明确的是,越是标准、规范的技术/项目,越是容易接入 AI。
规范、标准是逐渐形成的,某些技术变成标准的时候,它后续的发展,比其他的技术都要顺利、要迅猛。
像现在国内的手机厂商,都在适配 iPhone 生态一样,即使 iPhone 是封闭的生态,但是其本身有非常丰富的开放的接口,这些接口就是 iPhone 定的标准,一直在稳定逐步推进,一步步地把用户锁在自己的生态中,你不加入永远就分不到蛋糕。(当然,小米也搭建了类似的生态,但影响力仅限国内)
我们在争论标准的时候,从未想过标准背后是什么,从未想过执行标准之后会带来的巨大好处。
国内 SAAS 还在垮台中,他们服务的一家家小公司,甚至是大公司,还在为一个流程要经过谁的审批争得死去活来。外国公司已经完成了阶段的飞跃,他们的数据早已为接入 AI 做好了准备。此时,国内还有些公司想着跳过信息化,直接进入 AI 时代……
说回 AI,如果 2023 是 AI 编程元年,今年就已经有初步成型的产品了,类似于 cursor 之类的编程 IDE。
用了 AI 编程再也回不去了,至少我是这样的。每月的 cursor 的费用并不低,但是它给我带来的收益是远大于此。从写demo,到逐步完成更复杂的内容,我把更多的事情交给了 AI。
总之,拥抱 AI, 依然是这几年的正确选择!
结束语
年底听了李楠在《脑放电波》的访谈,把近几十年关于去中心化的思潮与变革联系起来了。
世界的割裂正在进行,也许回不去那种互助互爱的地球村了,但也回不到联合国与大国控制的世界了。
在去中心化的所有技术都已经突破,成为现实。人们也开始接受虚拟货币,愿意把其变成一份份真的钱在社会上流通。社交媒体上,数字游民满世界的跑,宣扬着这一切的到来。
未来世界,也许真的如其所说,会由一个个的社区组成。你可以成为一个超级个体,社区根据你在链上的贡献给你奖励对应的数字货币,你通过数字货币获取自己所需的商品。
这一切不是不可能。
也许,未来已来!
新的一年继续努力吧,至少,继续为自己努力吧!
来源:juejin.cn/post/7454332481933295631
程序员焦虑症之「没用过」和「不知道」,码农的「拧螺丝」之道
许久没扯淡,今天就写点没营养的内容。
前几天和朋友聊天,其中一个话题很有意思,那就是「没用过」和「不知道」是他日常焦虑症的来源,因为他在一家传统企业做开发,技术栈一直很保守,很多框架代码可能一两年都不会升级改动,许多新东西都没用过,所以每次看到别人聊新技术的时候,都会觉得很焦虑,想“了解”却又“用不上”,想“上进”却又“学不进”,最后干脆选择“眼不见为净” 。
其实我过去也有类似的经历,每次接触到“新东西”时,内心潜移默化就会开始着急,仿佛再不跟进就要“挨打”,而“新东西”又层出不穷,结果就是东拼一脚西凑一下,最终像个无头苍蝇一样四处碰壁,不得而终。
为什么你懂这么多?相信这种情况对于「老 Androider 」多多少少应该都经历过,毕竟十年前的 Android 开发,「技术选型」可以说是日新月异,「你用过xxxx吗」和「你还在用xxxx」可以说是圈内话题的主旋律,举亿个例子:
- 你还在用 Eclipse ,现在都用 Android Studio 了
- 你还在本地 jar 包,现在都用 Gradle 远程依赖了
- xUtils 和 Afinal 听说过没?开发神器啊
- 你怎么还在用 ImageLoader ,知道 Picasso 吗? 用过 Fresco 没有?现在都推荐 Glide 了
- 你知道 GreenDao 吗?现在谷歌都推荐 Room 了,你用过 Realm 吗?
- 你还在用 ButterKnife?现在都用 DataBinding、ViewBinding 了
- 你怎么还用 Apache HttpClient,试试 Volley 呗?
- 现在都是 OKhttp 了,那你知道 Retrofit 吗?
- 你用过 gPRC 和 GraphQL 吗?
- 你还在用 MVC ,你知道 MVP 吗?我都用 MVVM、MVI 了
- 你用过 dynamic-load-apk、VirtualAPK、DroidPlugin、RePlugin、tinker 吗?
- 你知道 Dagger 吗?现在都 Dagger2 了
- 你还在用 Dagger ? 现在已经是 Hilt 了
- 你用过 EventBus 吗?
- 你知道 LeakCanary 吗?听过 BlockCanary 吗?
- 你还在用 Java 的 Dagger 啊,Kotlin 都用 Koin 了
- 你知道 Rxjava 吗?已经 Rxjava2 了
- 你用过 Couroutines 和 Flow 吗?
- 你知道 LiveData 吗?
- 用过 jadx 和 apktool 吗?
- 怎么还在用 Java ?Kotlin 都烂大街了
- 你知道 Jetpack 吗?用过 Lifecycle、Navigation、CameraX、Paging、Glance、Slice、Startup、Viewpager2、DateStore、WorkManager 吗?
- 你做过小程序吗?uni-app 听过吗?React Native 知道吗?Flutter 、KMP、Compose 了解不?
- 鸿蒙 Next 你适配了没?ArkTS 和 ArkUI 学了没?
- ····
有没有很熟悉的既视感?这还只是 Android 圈子的一角,如果你还做前端:那还有:
jQuery、AngularJS、Angular、Vue、React、Ember、Node、Express、Svelte、Nest、Nuxt、Deno、Solid····
就光说 React 更新带来的 JSX、Portals、Hook、Fiber、Concurrent、Suspense、Server Components、Transitions 就够一直玩下去····
但是在焦虑追新的同时,其实这里面一直有一个问题:会用这些框架,真的就是技术吗 ?你追的究竟是什么 ?
回答这个问题,刚好可以用到最近看到的一张图,因为我们大概率不是一个软件工程师:
- 高情商:蓝领科技者
- 低情商:工厂螺丝工
我们的工作就是使用别人「制作好的工具」,所以我们热衷追逐「新工具」,但是,大多数时候,我们又不了解工具是如何工作的,我们只是一直在“反复”的学会如何使用它们,并且焦虑于,我们还没全都学会。
我认为「拧螺丝」确实是一个很不错的比喻,各类框架就像是适配不同型号的螺丝刀,而市面上的螺丝头形态各异,我们就是不停的在学会「如何把某款型号的螺丝刀插入到对应螺丝头」,然后开始心满意足的拧一颗「新型号螺丝」。
拧熟了,大概还能解锁了多种姿势,拧起来更快更省力,不同型号的螺丝刀,对准的难度和发力的安全范围可能也会有些不同,不过没关系,多拧几次就熟悉了。
那么我们比的是谁认识的螺丝头多吗?
所以,拧螺丝是门槛吗?大概率不是的,因为学会拿螺丝刀就行。那问题又来了,既然都是拧螺丝,换个姿势,换个型号,你就不会拧了吗?不应该啊对吧 ?
回归到各式各样的框架上,有文档和有社区的情况下,换种语言,换种框架,难道就拧不动了?这又扯出另外一个问题:你的能力依赖于框架,是否又能超脱出框架?
所以我们在「追新」的时候追的是什么?是 1 - 2 - 3 这样的变化:
- 从 1 到 2 用户拧螺母需要准备的扳手数量减少了
- 从 2 到 3 扳手变得更加帅气有力,并且附带的“力道”也有所上升
那么开发的鄙视链就来了:
- 因为我用的是自动挡扳手 2 ,所以我看不起手动挡的扳手 1
- 因为我用的是全新工艺的扳手 3 ,所以比老工艺的扳手 2 牛逼
我想着这大概率是「追新」带来的「错觉」,牛逼的是扳手的制造者,而作为使用者,我们都是踩着别人的肩膀混口饭吃的工人。
回过头来看,在这种情况下,随着技术的发展,新生框架和技术会让开发变成更便捷,同时降低开发的门槛,从而方便后来者入坑,所以本质上就像开车,开「自动档」并没有比「手动挡」牛逼多少 。
就算是自动挡,也分很多换挡方式,那么你会用「怀挡」,真的就比用「直排挡」在技术上 NB 吗?难道比的不应该是,谁对「变速箱」的理解更深刻?尽管大部人其实都不会懂得如何造车,但在用车上,大家真正的差距在于:
- 车坏了你会不会修?
- 如何调节提升车的动力和操控
那么回过头来,所以作为老 Android 开发,在经历了开发项目需要准备“一堆扳手”的手动挡时代,如今在这个只要一个“扳手”就能干活的自动挡时代,怎么可能会拧不动螺母?
更多时候问题不在于我们学得不够多,知道的不够新,而是我们不知道框架实际上是如何工作,我们只是学会了使用一个叫做“React”的工具。
不是为了读源码而读源码,而是通过源码我知道了 CVT 和双离合的区别,知道了它们的实现原理和优劣,那么挡把是怀挡还是旋钮重要吗?
在不理解运作的原理,没有基础知识铺垫,当任务变成修理一个发动机时,当任务变成提高框架的性能瓶颈时,就会无从下手,就会回归到前面所说的:You are not a software engineer 。
我们不可能全知全能,也不可能认识所有螺丝头,也没机会熟练操控所有档位型号,但是我们的目的也不是为了认识所有汽车档把,我们的目的只是为了开车,然后进一步能力就是修车和调教,这对于大多数人来说,就很难能可贵了。
大多数人学习精力有限,但是理解能力和开车经验是可以精进的,比如最近看各个大厂的大佬们把 KMP 和 Compose 适配到鸿蒙上的分享,就能感受到老司机们的车技之滑溜。
而回到我们在焦虑「没用过」和「不知道」的时候,更多还是对于自身能力的不自信,就像是我们的尊严只能苟存于框架之下,多年的工作经验,只能寄希望于 XXX 不要凉。
所以才会对于 XXX 要火,XXX 要凉如此敏感,本质还是我们迷失在了工具上。
就像一些多年耕耘的老开发会告诉你,许多上层开发者「跟进」的基本都是:“ keeping up with all the bullshit that Google / Android dev community throws at you” ,远离所谓的 “architecture influencers” ,你需要的是自己的思维方式和选择能力,而不是臣服在框架之下,被社区推来推去:
当前,其实这也和当前很多工作岗位的设定有关系,公司当前只是需要一个螺丝钉,公司不需要你理解螺丝刀为什么十字和一字的区别,你拧就是了,标签化和细分化确实更好管理,而有人也愿意带着标签,这无可厚非。
而大佬们就不同,在和社区大佬的沟通中,我发现基本上他们的涉猎范围都很广泛,而且都很有深度,或者说:语言和框架都只是为了解决问题的工具。
总有人走在你前面,只是你是否还在路上。
好了,其实道理大家都懂,只是如何知行合一,那就见仁见智了,许多没写这种没营养的内容,毕竟纸上得来总觉浅,真要总结,大概就是:人和人体质不同不能一概而论。
来源:juejin.cn/post/7451964967165231104
又整新活,新版 IntelliJ IDEA 2024.1 有点东西!
就在上周,Jetbrains 又迎来了一波大版本更新,这也是 JetBrains 2024首个大动作!
JetBrains 为其多款 IDE 发布了 2024 年度首个大版本更新 (2024.1)。
作为旗下重要的产品之一,IntelliJ IDEA当然也不例外。这不,现如今 IntelliJ IDEA 也来到了 2024.1 大版本了!
据官方介绍,这次 2024.1 新版本进行了数十项改进。
下面就针对本次新版 IntelliJ IDEA 的一些主要更新和特性做一个梳理和介绍,希望能对大家有所帮助。
全行代码补全
IntelliJ IDEA Ultimate 2024.1 带有针对 Java 和 Kotlin 的全行代码补全。
该项功能由无缝集成到 IDE 中的高级深度学习模型来提供支持。它可以基于上下文分析预测和建议整行代码,以助于提高编码效率。
对 Java 22 的支持
IntelliJ IDEA 2024.1 提供了对 2024 年 3 月刚发布的 JDK 22 中的功能集的支持。
支持覆盖未命名变量与模式的最终迭代、字符串模板与隐式声明的类的第二个预览版,以及实例main方法。 此外,这次更新还引入了对super(...)
之前预览状态下的 new 语句支持。
新终端加持
IntelliJ IDEA 2024.1推出了重构后的新终端,具有可视化和功能增强,有助于简化命令行任务。
此更新为既有工具带来了全新的外观,命令被分为不同的块,扩展的功能集包括块间丝滑导航、命令补全和命令历史记录的轻松访问等。
编辑器中的粘性行
此次新版本更新在编辑器中引入了粘性行,旨在简化大文件的处理和新代码库的探索。滚动时,此功能会将类或方法的开头等关键结构元素固定到编辑器顶部。
这样一来作用域将始终保持在视野中,用户可以点击固定的行快速浏览代码。
AI Assistant 改进
在本次新版中,AI Assistant 获得了多项有价值的更新,包括改进的测试生成和云代码补全、提交消息的自定义提示语、从代码段创建文件的功能,以及更新的编辑器内代码生成。
不过需要注意的事,在这次 2024.1 版中,AI Assistant 已解绑,现在作为独立插件提供。这一改动是为了在使用 AI 赋能的技术方面提供更多的决策灵活度,让用户能够在工作环境中更好地控制偏好设置和要求。
索引编制期间 IDE 功能对 Java 和 Kotlin 的可用
这次新版本中,代码高亮显示和补全等基本 IDE 功能可在项目索引编制期间用于 Java 和 Kotlin,这将会增强用户项目的启动体验。
此外,用户可以在项目仍在加载时即使用 Go to class(转到类)和 Go to symbol(转到符号)来浏览代码。
更新的 New Project(新建项目)向导
为了减轻用户在配置新项目时的认知负担,新版微调了 New Project(新建项目)向导的布局。语言列表现在位于左上角,使最常用的选项更加醒目。
用于缩小整个 IDE 的选项
新版支持可以将 IDE 缩小到 90%、80% 或 70%,从而可以灵活地调整 IDE 元素的大小。
对Java支持的更新
- 字符串模板中的语言注入
IntelliJ IDEA 2024.1 引入了将语言注入字符串模板的功能。
用户既可以使用注解(注解会自动选择所需语言),也可以使用 Inject language or reference(注入语言或引用)来从列表中手动选择语言。
- 改进的日志工作流
由于日志记录是日常开发的重要环节,新版本引入了一系列更新来增强 IntelliJ IDEA 在日志方面的用户体验。
比如现在用户可以从控制台中的日志消息中轻松导航到生成它们的代码。
此外,IDE会在有需要的位置建议添加记录器,并简化插入记录器语句的操作,即便记录器实例不在作用域内。
- 新检查与快速修复
新版本为 Java 实现了新的检查和快速修复,帮助用户保持代码整洁无误。
比如,IDE 现在会检测可被替换为对 Long.hashCode() 或 Double.hashCode() 方法的调用的按位操作。
此外,新的快速修复也可以根据代码库的要求简化隐式和显式类声明之间的切换。
另一项新检查为匹配代码段建议使用现有 static 方法,使代码可以轻松重用,而无需引入额外 API。此外,IDE现在可以检测并报告永远不会执行的无法访问的代码。
- 重构的 Conflicts Detected(检测到冲突)对话框
这次版本 2024.1 重构了 Conflicts Detected(检测到冲突)对话框以提高可读性。
现在,对话框中的代码反映了编辑器中的内容,使用户可以更清楚地了解冲突,并且 IDE 会自动保存窗口大小调整以供将来使用。
另外,这次还更新了按钮及其行为以简化重构工作流,对话框现在可以完全通过键盘访问,用户可以使用快捷键和箭头键进行无缝交互。
- Rename(重命名)重构嵌入提示
为了使重命名流程更简单、更直观,新版推出了一个新的嵌入提示,在更改的代码元素上显示。要将代码库中的所有引用更新为新版本,点击此提示并确认更改即可。
版本控制系统改进
- 编辑器内的代码审查
IntelliJ IDEA 2024.1 为 GitHub 和 GitLab 用户引入了增强的代码审查体验。
该功能与编辑器集成,以促进作者与审查者直接互动。在检查拉取/合并请求分支时,审查模式会自动激活,并在装订区域中显示粉色标记,表明代码更改可供审查。
点击这些标记会弹出一个显示原始代码的弹出窗口,这样用户就能快速识别哪些代码已被更改。
装订区域图标可以帮助用户迅速发起新讨论,以及查看和隐藏现有讨论。另外这些图标还可以让用户更方便地访问评论,从而更轻松地完成查看、回复等功能。
- Log(日志)标签页中显示审查分支更改的选项
新版通过提供分支相关更改的集中视图来简化了代码审查工作流。
对于 GitHub、GitLab 和 Space,用户现在可以在 Git 工具窗口中的单独 Log(日志)标签页中查看具体分支中的更改。用户可以点击 Pull Requests(拉取请求)工具窗口中的分支名称,然后从菜单中选择 Show in Git Log(在 Git 日志中显示)。
- 对代码审查评论回应的支持
新版开始支持对 GitHub 拉取请求和 GitLab 合并请求的审查评论发表回复,目前已有一组表情符号可供选择。
- 从推送通知创建拉取/合并请求
成功将更改推送到版本控制系统后,新版IDE将会发布一条通知,提醒用户已成功推送并建议创建拉取/合并请求的操作。
- 防止大文件提交到仓库
为了帮助用户避免由于文件过大而导致版本控制拒绝,新版IDE现在包含预提交检查,以防止用户提交此类文件并通知用户该限制。
构建工具改进
- 针对 Maven 项目的打开速度提升
新版 IDEA 现在通过解析 pom.xml 文件构建项目模型。这使得有效项目结构可以在几秒钟内获得,具有所有依赖项的完整项目模型则同时在后台构建,这样一来用户就无需等待完全同步即可开始处理项目。
- 从快速文档弹出窗口直接访问源文件
快速文档弹出窗口现在提供了一种下载源代码的简单方式。
现在当用户需要查看库或依赖项的文档并需要访问其源代码时,按 F1 即可。
更新后的弹出窗口将提供一个直接链接,用户可以使用它来下载所需的源文件,以简化工作流。
- Maven 工具窗口中的 Maven 仓库
Maven 仓库列表及其索引编制状态现在直接显示在 Maven 工具窗口中,而不是以前 Maven 设置中的位置。
- Gradle 版本支持更新
从这个新版本开始,IntelliJ IDEA 将不再支持使用低于 Gradle 版本 4.5 的项目,并且 IDE 不会对带有不支持的 Gradle 版本的项目执行 Gradle 同步。
运行/调试更新
- 多语句的内联断点
新版IDEA为在包含 lambda 函数或 return 语句的行中的断点设置提供了更方便的工作流。
点击装订区域设置断点后,IDE会自动显示可在其中设置额外断点的内联标记。每个断点都可以独立配置,释放高级调试功能。
- 条件语句覆盖
2024.1 新版使 IntelliJ IDEA 距离实现全面测试覆盖又近了一步。该项更新的重点是确定测试未完全覆盖代码中的哪些条件语句。
现在,IntelliJ IDEA 既显示哪一行具有未覆盖的条件,还会指定未覆盖的条件分支或变量值。 这项功能默认启用。
框架和技术
- 针对 Spring 的改进 Bean 补全和自动装配
IntelliJ IDEA Ultimate 现在为应用程序上下文中的所有 Bean 提供自动补全,并自动装配 Bean。
如果 Bean 通过构造函数自动装配依赖项,则相关字段也会通过构造函数自动装配。 同样,如果依赖项是通过字段或 Lombok 的 @RequiredArgsConstructor 注解注入,则新 Bean 会自动通过字段装配。
- 增强的 Spring 图表
新版的 Spring 模型图表更易访问。用户可以使用 Bean 行标记或对 Spring 类使用意图操作 (⌥⏎) 进行调用。
同时新版为 Spring 图表引入了新的图标,增强了 Spring 原型(如组件、控制器、仓库和配置 Bean)的可视化。 此外,用户现在可以方便地切换库中 Bean 的可见性(默认隐藏)。
除此之外,其他包括像数据库工具、其他框架、语言和技术的支持等方面的更新和说明,大家也可参阅jetbrains.com/zh-cn/idea/whatsnew。
注:本文在GitHub开源仓库「编程之路」 github.com/rd2coding/R… 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。
来源:juejin.cn/post/7355389990531907636
没想到学会这个canvas库,竟能做这么多项目
大家好,我是一名前端工程师,也是开源图片编辑器vue-fabric-editor项目的作者,2024年5月从北京辞职,我便开始了自己的轻创业之路,接触了不同的客户和业务场景,回顾这半年,没想到学会fabric.js
这个Canvas
库,竟能做这么多项目。
如果你打算学习一个Canvas
库或者做图片设计、定制设计相关的工具,我建议你学习一下fabric.js
这个库,它非常强大,可以做出很多有意思的项目,希望我的项目经历能给你的技术选型做一些参考。
项目经历
从北京回老家邯郸后,我陆续做了很多项目,包括正件照设计、锦旗/铭牌定制工具、Shopify定制插件、批量生成图片、手机版图片设计工具、服装设计、电商工具等,这些项目都离不开fabric.js
这个库。回顾这段经历,让我深刻体会到它的强大和广泛应用。
图片设计
图片设计是我接触的第一个主要应用领域。项目最初源于一个小红书成语卡片设计工具的构想,随后逐步扩展到更广泛的设计场景,包括小红书封面、公众号头图、营销海报以及电商图片等多种自媒体内容制作。
这类应用的核心功能在于自定义画布尺寸和元素排版,得益于fabric.js的原生支持,实现起来相对简单。我们主要工作是开发直观的属性编辑面板,使用户能够便捷地调整所选元素的文字和图片属性。
当然如果做的完善一些,还需要历史记录
、标尺
、辅助线对齐
、快捷键
等,这些功能fabric.js
并没有包含,需要我们自己实现,这些功能可以参考vue-fabric-editor 项目,它已经实现了这些功能。
还有很多细节的功能,比如组合保存、字体特效、图层拖拽、图片滤镜等,这些功能我们做的比较完善了。
定制设计工具
图片设计的场景相对通用,没有太多定制化的需求。而定制类的设计工具则需要针对特定场景深度开发,比如正件照、锦旗/铭牌设计、相册设计等,每个场景有不同的定制功能。
正件照设计工具的核心在于自动化的处理。主要工作量集中在尺寸的匹配,确保图片能自动调整到最佳大小。同时,需要提供人物图片的裁剪功能,让用户能便捷地进行换装、切换正件尺寸、更换背景等操作。
锦旗与铭牌设计则更注重文字内容的自动排版。系统需要根据用户输入的抬头、落款、赠言等内容,自动计算最优的文字间距和整体布局,确保作品的美观性。特别是铭牌设计,还需要实现曲线文字功能,让文字能够优雅地沿着弧形排布。
相册设计工具的重点是提供灵活的画布裁剪功能。用户可以使用各种预设的形状模板来裁剪图片,需要确保裁剪后的图片既美观又协调,最终生成精美的画册作品,交互上方便用户拖拽图片快速放入裁剪区域。
电商工具
电商场景比图片设计更垂直,除了普通的平面设计,例如店铺装修、商品主图、详情图的设计,另外还需要对商品进行换尺寸、抠图、换背景、去水印、涂抹消除、超清放大等操作,这些对图片处理的要求更高一些。
批量生成
批量算是一个比较刚需的功能,比如电商的主图,很多需要根据不同产品到图片和价格来批量加边框和文字,以及节庆价格折扣等,来生成商品主图,结合图片和表格可以快速生成,减少设计师的重复工作量。
另一部分是偏打印的场景,比如批量制作一些商品的二维码条形码,用在超市价签、电子价签、一物一码、服装标签等场景,根据数据表格来批量生成。
这种项目主要的工作量在交互上,如何将画布中的文字和图片元素与表格中的数据一一对应,并批量生成,另外会有一些细节,比如条形码的尺寸、图片的尺寸如何与画布中的尺寸比例进行匹配,这些细节需要我们自己实现。
上边的方式是通过表格来批量生成图片,还有一种是根据 API来批量生成图片,很多场景其实没有编辑页面,只希望能够通过一个 API,传入模板和数据,直接生成图片,fabric.js 支持在nodejs 中使用,我们要做的就是根据模板和数据拼接 JSON,然后通过fabric.js 在后端生成图片,然后返回给前端,性能很好,实际测试 2 核 2G 的机器,每张图片在 100ms 左右。
很多营销内容和知识卡片、证书、奖状也可以通过批量生成图片API来实现。
当然,还有一些更复杂的场景,比如不同的数据匹配不同的模板,不同的组件展示不同的形式等,包括错别字检测、翻译等,我们也为客户做了很多定制化的匹配规则。
服装/商品定制
服装/商品定制是让用户在设计平台上上传图片,然后将图片贴图到对应的商品模板上,实现让用户快速预览设计效果的需求。
这种场景一般会分为 2 类,一类是是针对 C 端用户,需要的是简单、直观,能够让用户上传一张图片,简单调整一下位置就能确认效果快速下单。
我在这篇文章里做了详细介绍:《fabric.js 实现服装/商品定制预览效果》。
另一类是针对小 B 端的用户,他们对设计细节有更高的要求,比如领子、口袋、袖子等,不同的位置进行不同的元素贴图,最后将这些元素组合成一个完整的服装效果图,最后需要生成预览图片,在电商平台售卖,完成设计后,还要将不同区域的图片进行存储,提供给生产厂家,厂家快速进行生产。
比如抱枕、手机壳、T恤、卫衣、帽子、鞋子、包包等,都可以通过类似服装设计的功能来实现。
很多开发者会提出疑问,是否需要介入 3D 的开发呢?
我们也和很多客户沟通过,从业务的角度看,他回答是:3D 的运营成本太高。他们做的都是小商品,SKU 很多很杂,如果每上一个商品就要进行 3D 建模,周期长并且成本高,他们更希望的是通过 2D 的图片来实现,而且 2D 完全能够满足让用户快速预览确认效果的需求,所以 2D 的服装设计工具就成为了他们的首选。
包装设计
包装设计是让用户在设计平台上,上传自己的图片,然后将图片贴图都包装模板上,主要的场景是生成定制场景,比如纸箱、纸袋、纸盒、纸杯、纸质包装等,这些场景需要根据不同的尺寸、形状、材质、颜色等进行定制化设计,最后生成预览图片。
因为设计到不同的形状和切面,而且大部分是大批量定制生产,所以对细节比较谨慎,另外包装规格相对比较固定,所有用3D模型来实现就比较符合。
另外,在确定设计效果后,需要导出刀版图,提供给生产厂家,厂家根据刀版图进行生产,所以需要将设计图导出为刀版图,这个功能 fabric.js 也支持,可以导出为 SVG 格式直接生产使用。
AI结合
在AI 大火的阶段,就不得不提 AI 的场景了,无论在自媒体内容、电商、商品、服装设计的场景,都有 AI 介入的影子,举个例子,通过 AI生成内容来批量生成营销内容图片,通过 AI 来对电商图片进行换背景和图片翻译,通过 AI 生成印花图案来制作服装,通过 AI 来生成纹理图来生成纸盒包装,太多太多的 AI 的应用场景,也是客户真金白银定制开发的功能。
展望2025
从图片设计的场景来看,我们的产品已经很成熟了,也算是主力产品了,未来会持续迭代和优化,让体验更好,功能更强大,把细节做的更完善,例如支持打印、视频生成等功能。
从定制设计工具的场景来看,我们积累了不同商品定制设计的经验,从技术和产品到角度看,我们还可以抽象出更好的解决方案,让客户能够更高效、低成本的接入,提供给他们的客户使用,快速实现设计生产的打通。
2024 到 2025 ,从在家办公一个人轻创业,搬到了我们的办公室,期待未来越来创造更多价值。
总结
半年的时间,这些项目的需求fabric.js
都帮我们实现了,所以如果你对Canvas
感兴趣,我的亲身经历告诉你,学习fabric.js
是一个不错的选择。
另外,对我来说更重要的是,客户教会了我们很多业务知识,这些才是宝贵的业务知识和行业经验,一定要心存敬畏,保持空杯,只有这样我们才能做好在线设计工具解决方案。
这篇文章也算是我从 2024年离职出来到现在的一个年终总结了,希望我们踩过的坑和积累的经验都变成有价值的服务,作为基石在2025年服务更多客户,文章内容供大家一些参考,期待你的批评指正,一起成长,祝大家 2025年大展宏图。
给我们的开源项目一个Star吧:github.com/ikuaitu/vue… 😄。
来源:juejin.cn/post/7459286862839054373
《真还传》续集来了,罗永浩J1助手能否创造“锤子”奇迹?
罗永浩备受期待的AI应用——J1 Assistant终于上线了,这款应用标志着他“最后一次创业”的正式启动。曾因锤子手机与SmartisanOS而广受关注的罗永浩,在经历过硬件领域的沉浮后,终于转向了AI技术的蓝海。J1 Assistant的上线,成为了他在AI领域的新起点,而这款产品也标志着罗永浩的“真还传”之路的回归。
目前,J1 Assistant已在Android平台推出Beta版,但只有三星Galaxy和谷歌Pixel的最新三代机型支持,且仅提供英文版,显然其首要市场定为海外而非国内。这一策略也印证了罗永浩的新目标——跨越国界,将AI技术推向全球。
与此同时,罗永浩的另一款 AI 硬件新品——JARVIS ONE也在路上,官网已有预告,预计将参加即将举行的CES 2025消费电子展。
请在此添加图片描述
锤子味的AI助手:重回初心,旧貌换新颜
从产品的设计来看,J1 Assistant显然延续了“锤子味”,无论是UI设计还是功能整合,都能看到过去SmartisanOS的影子。这款应用不仅仅是一个普通的AI助手,它可以视作待办清单、便签、AI聊天、即时通讯、搜索等多种功能的集合体。对于曾经使用过锤子手机的用户来说,这些熟悉的设计元素不仅带来了一种情感上的回归,也让人感受到一种怀旧的力量。
J1 Assistant通过五个Tab区分了五大核心功能:To Do(待办清单)、Notes(笔记)、AI Assistant(助手)、J1 Message(聊天)和Search(搜索) 。其中,Notes的设计延续了锤子便签的风格,但功能上略显简陋,缺乏排版工具和图片分享功能;To Do则是一个基础版待办清单,操作简便,却略显单一。
请在此添加图片描述
AI与信息管理的双重打击:技术与实用的结合
与传统的待办清单和笔记功能相比,J1 Assistant还融入了更多创新性功能——J1 Message和Search。J1 Message的设计灵感来源于已停运的“子弹短信”,用户通过注册后可进行即时聊天,然而在如今的即时通讯市场竞争如此激烈的情况下,这一功能能否得到用户的广泛接受仍有待观察。而Search功能,则类似于TNT的“发牌手”,支持多来源的搜索,用户可以根据自己的需求自定义最多五个来源进行查询。
在语音交互方面,J1 Assistant也做了突破,采用了“Ripple Touch(波纹触摸)”设计,用户按住语音图标进行语音输入时,可以灵活选择不同的操作方式(如保存为笔记、待办清单或直接发送给他人),这无疑增加了操作的便捷性和实用性。
请在此添加图片描述
AI价值的延伸:效率与便捷并重
J1 Assistant的最大亮点,不仅仅是传统的AI对话功能,而是它如何巧妙地将AI与信息管理结合,创造多重价值转化。举个例子,用户可以要求AI将它的回答直接保存为待办清单或笔记,从而将信息的价值最大化。如果你在CES 2025期间,想要记录重要的展会活动,AI助手就能帮助你快速整理信息,自动生成待办清单和笔记,从而大大提升工作效率。
尽管目前J1 Assistant还处于Beta版本,存在一些问题和bug,比如部分AI回答保存不完整,或者待办清单内容丢失等,但这些问题在更新后有望得到解决。正如任何一款刚上线的产品一样,J1 Assistant的潜力还有待发掘。
定位问题:这款APP真的能满足我们需求吗?
虽然J1 Assistant融合了多个创新功能,但它的市场定位仍显模糊。与其他成熟的AI助手相比,J1 Assistant的功能整合显得有些“乱炖”,缺乏独特的亮点和清晰的市场定位。尤其是在待办清单和笔记功能这一块,它是否能够真正吸引到那些有强烈需求的用户,仍然是一个未知数。
罗永浩的AI之路,还能走多远?
请在此添加图片描述
J1 Assistant是罗永浩进入AI领域的第一步,但要想在竞争激烈的市场中脱颖而出,它仍面临不小的挑战。从目前来看,J1 Assistant确实有着不少创新之处,尤其是在信息管理和AI对话的结合方面,但它能否真正解决用户的痛点,还需更多时间的验证。罗永浩的“真还传”之路,依然充满变数,他的AI应用能否走得更远,或许最终取决于它是否能够精准满足用户的实际需求,带来真正的价值。
从硬件到软件,罗永浩的创业之路仿佛进入了一个全新的阶段。这一次,他的“AI助手”会带给我们什么样的惊喜,值得每个人期待。
来源:juejin.cn/post/7457567841556693032
前支付宝工程师带你复盘支付宝P0故障
前支付宝工程师带你复盘支付宝P0故障
事故介绍
首先叠个甲,所有数据来自互联网公开数据,没有泄露任何老东家数据。
大家可能都听说了,但是我还是介绍一下。2025-1-16 14:40开始,一部分用户发现在支付宝内进行支付时,发现订单被优惠减免了20%的金额。意味着原本你买一个手抓饼可能100块,用支付宝直接省20。而且这不仅限于支付订单,转账订单也可以享受这个优惠(不知道有没有人开两个小号,反复转账薅羊毛的)。
支付宝方面很快反应,在14:45时完成了故障修复,并且在2025-1-17 1:00发出声明,称不会对享受到这个优惠的用户追回资金。(敢做敢认,点赞)
产生原因分析
在支付宝的通告里,我们看到,产生这个事故的原因是"某个常规营销活动后台配错了营模板",这句话我给大家解释一下。一般一个新的活动功能的上线,可能需要程序员开发新的功能,然后将功能里需要使用的规则,做成配置,配置在营销中心的管理后台上。当然对于一些比较成熟的活动,也可以直接复用以前的代码,只需增加配置即可。
但是在通告里,我们没办法判断这是一个新开发的活动还是复用以前开发出的老活动。针对需要开发的新活动和可以直接使用配置复用的老活动,这两者我们单独分析。
需要开发的新活动
一般新活动开发完毕后,正常是程序员会提前告诉运营在配置中心建好规则,并且将灰度人群置为0。然后程序员发布新代码,发布中因为没有人群命中活动规则,所以活动不生效。为了验证代码有效,会在发布的早期,让运营在营销中心配置一些测试用户,来测试看能否命中规则,并且验证活动在后续的收单,结算流程里是否正常。
举一个实际的例子,程序员小薰在服务demo order service里开发了新功能,需要发布上线,demo order service假设有1000台机器,小薰联系运营小丽在运营中心配置几个灰度账号,用来验证在线上发布后功能是否正常。在灰度发布阶段,会先选择几台机器做灰度,一般不超过10台,比如
- 第一批次 3台
- 第二批次 7台
- 第三批次 5%
- 第四批次 10%
- 第五批次 20%
- 第六批次 35%
- 第七批次 全量发布剩下所有的机器
根据支付宝解决故障只花了5分钟,我们可以推测出,应该是在第一批次时就发现了问题,并且采取了止血措施。那么收到影响的流量就是0.3%-1%,大家可能好奇为什么能确定说肯定是在第一批次就收到的影响,因为实际一次服务重启很耗时,一般都不止5min,而且服务发布后会有10-30min的观察期,再结合发生事故到解决事故总共没花费5min,我们可以推测出,第二批次应该还没发布。
但是流量并不代表受到影响的支付单量,因为要考虑有多少人命中了这个规则,这就取决于运营的配置了,如果运营只是把一些不应该开灰的用户加在了白名单里,影响还好,只会有一些固定的人员受影响。但如果运营是直接100%用户全量灰度,那就糟糕了。
我们得出在这种情况下,受到影响的单量范围为(0, 1%)
亏了多少钱
相信大家最感兴趣的一定是这次事故支付宝到底亏了多少钱,要回答这个问题我们首先要知道支付宝一天的交易流水是多少。当然这种数据官方一般是不会放出来的。但是我们可以大概算一下
23年移动支付555万亿,增速为11%。我们假设还是按照这个增速来预测24年的移动支付,当然还要考虑支付宝和微信在移动支付交易市场的份额,大概6/4开的比例。
那么24年的移动支付交易额
#以下单位亿元
5550000 * 1.11 = 6160500
#那么24年的平均日交易额
6160500/365 = 16878.08
#考虑到昨天事故发生的时间已经是25年,我们直接用24年的平均日支付交易额不合适,我们假设移动支付的日交易流水增长是线性的,我们再乘以一个增长速率得到12月的移动支付日交易额
16878.08 * 1.22 = 20561.26
#再乘以支付宝再移动支付市场的份额
20561.26 * 0.6 = 12336.76
那么我们假设支付宝一天的交易金额12336.76亿元,那么结合我们预估的影响流量范围(0, 1%),以及每一单20%的优惠力度,影响的时间5min,得出在事故事件内受影响的订单数量
# 单位亿元
12336.76 * 0.01 * 0.2 * 5 / 60 / 24 = 0.0857
即亏损金额不超过857W,实际上考虑到这种带有优惠的活动可能会有用户薅羊毛,重复下单,我们可以再把它影响范围乘以一个放大系数,比如1.5,即
#单位万元
857 * 1.5 = 1286
可以看到金额顶天也不会超过1286W,而且在实际的营销活动里配置的时候,一个营销活动的预算金额是会有上限的我们称为资金池,超过这个上限即使参加活动也不能享受到优惠,这个金额我们假设为2000W(一般活动不会这么多,一般几百万就算多了)。如果资金池小于1286W,比如资金池只有500W,资损的上限就只有500W了。
总的来说,我们还是认为本次事故的资损对于支付宝来说并不算多,大概1286W。
那些人会背锅
大家第二感兴趣的肯定就是那些人会背锅了,我们可以从整个流程上看那些人参与了这次事故。
- 运营 作为直接引发本次事故的责任人,没有认真检查配置,就上线,肯定要背大锅,主要责任跑不掉。(考虑到蚂蚁最近的降本增效,不知道这位运营是不是本部的也说不准)
- 开发功能的程序员 开发功能的程序员负责上线服务,并且把配置设计好交给运营人员去配置,虽然不是程序员配的,但是作为服务的owner,理论上应该要再发布前再去确认一下这个配置正不正确,所以开发跑不了(是不是感觉pua太凶了,放心吧,实际老板找你北固时说辞肯定比这更严重)
- 运营的老板,以及审核了运营配置的老板 所有配置都需要老板审核才能生效的,虽然老板一般不细看,但是不出事都好,出了事,背锅吧。
- 测试 测试老实说,责任不大,一般这种时候就是拉出来做替罪羊,和开发一样要带点连带责任。
- 程序员的老板 老板这种时候就还是连带责任
- 两位老板的顶头上司 这种级别的老板一般就是P9/P10了,分团队。责任大小也看事故的影响面,影响不大的话,处分大概就是罚酒三杯,大的话,被一撸到底边缘化也有可能
不需要开发的老活动
如果是不需要开发的老活动,运营直接在运营中心改配置即可让活动生效,这种情况下受到影响的流量范围就取决于运营的操作了,(0,100%]都有可能
亏了多少钱
这种情况下资损的计算方式还是类似上面,只不过在流量的影响范围变大了,我们可以直接用上面计算出的亏损金额乘以流量的影响倍率
#单位万元
1286 * 100 = 128600
看起来好像很夸张128600,以下干到13Y了,但是还是像我们上面说的,资损不会超过资金池配额,我们假设的资金池配额5000W,所以其实还好。实际的资金池配额我想会远远低于这个数。
那些人会背锅
- 运营
- 运营的老板
- 运营的老板的老板
背锅原因同上,不细说了
事故总结
从这次的事故,我们可以复盘一下再新功能上线时需要面临哪些问题以及对应的解决思路。当然支付宝内部也都有这些手段的成熟解决方案,但是实际落在执行上却是稀巴烂。
- 功能开发时要做好监控,能够监控出功能异常的流量并及时报警(本次事故的表现里得满分)
- 做好发布前的配置检查,配置上线一定要有审批,开发要和实际的配置操作人确认,这里的确认不仅仅是口头确认,要自己心里有数(本次事故得0分)
- 发布前要做好降级预案,必须要保证当功能出现异常时能降级(本次事故表现满分)
来源:juejin.cn/post/7460781036075761673
2024年终总结——未来该走向何处?
生活
年初,在老家办了答谢宴,也算是完成了父母的心愿,他们总说着等我结婚了,他们就可以退休了,虽然没有挣到钱,买的那个社保也领不了多少钱。但想到他们马上60了,人生难道就是打一辈子工吗?我支持他们回老家,无论怎样,有钱多花点,没钱少花点。
5.1 去了抚仙湖,昆明,抚仙湖之前去过一次,这次换了个位置,找了个山上的民宿,自由风还是很舒服。
现在想想,跳龙门,我是不是应该从上面翻过去,哈哈哈哈😂!!!!
7月份暑假期间,又去了一次三亚,住了一次海景房,贵的确实有道理。这次6个小伙伴一起,租了一辆车,沿着海南的东线,从三亚自驾到海口,第一次知道海南的高速不收费。
8月份去了贵州,赤水瀑布,赤水瀑布有充电桩,进去游玩的时间正好充电,算是第一次自驾游吧(海南算租车游😂);
当时,出去游玩全是大妈大爷,好羡慕他们呀,想原地退休!
去看了草原,和我想象的不一样,我一直以为草原就是公园的放大版,还是我见识少了,去了六盘水是真的凉快。
8月底,我老婆怀孕了,也是在5.1 开始备孕的,本来想的是到8月再怀不上,就等明年了,相当于我们在他不到一个月的时候,就带他去自驾游了,也是心大。
国庆节,因为怀孕,就没出远门,我自己回了趟老家,然后去定了月子中心,老婆结婚那会儿就说一定要住月子中心。知道怀孕后,就是产检,但基本都约的周内,我没有去,丈母娘陪着去的。
月子中心提供了孕期瑜伽,我还陪着她去参加了两次双人瑜伽。
12.22号去看了四维,有了他的第一张照片,也开始焦虑,该怎么教育小孩,都是全新的体验。期待着他的到来...
自媒体
技术方面,已经算是停止输出了。
今年尝试了很多小红书账号,我原本做苔藓的账号,靠ai生成的图,涨了几千粉,但没找到变现的路子,然后停了。
前端的账号,无意中接了一个面试招人的单子,挣了2000。然后觉得前端已死,技术方面不想做了,就注销了账号。
陆陆续续做了ai 壁纸,自媒体方面的,还找了一个做项目的人,花了300块钱,但是后面我也没做起来。12月份去考了普通话,我小学教资笔试面试都过了,就等普通话二甲,就可以领证了。然后就自己做了个普通话学习助手,又在小红书做起了,普通话赛道。
http://47.109.182.55/
对了,母婴赛道也起了个账号,现在主要都在记录一些孕期日常,看看后面小孩出生有没有什么可做的选题吧。
今天自媒体账号真的做了好多,肯定不少于6个。
还有抖音,也搬运了一些视频,没什么好的点子,算了吧。
前端&工作
还记得,当年刚入行的时候,写的第一篇文章《从学校到实习直至毕业,前端——我一直在路上》,还想着老了来回味,哈哈哈,感觉我的职业生涯也走到了末期了!!
因为我已经不看好做技术了,就业形势艰难,我的群里,今年失业的好多,还有人被裁了两三次,工资一降再降,都还是要干。也有认识的大学生,毕业了一直找不到工作,特别是双非大学生,马上过完年,新一批的大学生又要出来了,哎...
我也停止了在技术上进一步深耕,一直都在尝试自媒体,其他的实体创业也不敢辞职去干。
工作上呢,在这个公司快两年了,也没有做出什么成功的产品,我对公司的方法论也不认可了,一句话在这行疲了 —— 巨轮难掉头!
今年的精力大多花在了自媒体上,技术方面没学什么东西,公司今年新项目又用上了nextjs,现在我们是vue3, react, nextjs, nuxtjs 并行,好几个项目同时在做,感觉就技术来说,公司领导想要啥就用啥呗。
而且我们都用上了cursor,ai的发展让我更焦虑了,在ai的时代,我们应该扮演什么样的角色。首先我也认同ai不可能取代所有的程序员,但它提高了生产效率的同时,必然就用不了那么多人了。我之前看到一句话:“不要和ai比智力,不要和机器人比体力”,我很认同,有了cursor,我的工作变得更简单了,更快捷了。我也不觉得,我们再去学什么技术栈,能比他更快,特别是工作过几年的同学,语言都是相通的,有了ai的加持,切换技术栈就像喝水一样,可能有点夸张,但门槛已经很低了。
上面的普通话助手,我就是让cursor写的,我主要提供一下数据,做一些微调,我觉得它真的挺不错的,当然它就是不能背锅!😂
前端,我可能不能一直在路上了,对不起,我明年还得大力尝试做其他的突破,寻找下一个方向。
最后
各位,不破不立,愿新的一年,万事顺心!!!!
来源:juejin.cn/post/7451819556030758947
Timesheet.js - 轻松打造炫酷时间表
Timesheet.js - 轻松打造炫酷时间表
前言
在现代网页设计中,时间表是一个常见的元素,用于展示项目进度、历史事件、个人经历等信息。
然而,创建一个既美观又功能强大的时间表并非易事。
幸运的是,Timesheet.js
这款神奇的 JavaScript
开源时间表库为我们提供了一个简洁而强大的解决方案。
本文将详细介绍 Timesheet.js
的特点、使用方法,并通过一个真实的使用案例来展示其强大功能。
介绍
Timesheet.js
是一个轻量级的 JavaScript
库,专门用于创建基于 HTML5
和 CSS3
的时间表。
它无需依赖任何外部框架,如 jQuery
或 Angular.js
,即可快速生成美观的时间表布局。
Timesheet.js
的优势在于其简洁性和用户友好性,仅需几行 JavaScript
代码即可实现功能,同时提供了丰富的自定义选项,允许开发者根据需求进行样式调整。
核心特性
无依赖:不依赖任何外部 JavaScript
框架,减少了项目复杂性和加载时间。
易于使用:通过简单的 JavaScript
代码即可创建时间表,易于上手。
高度可定制:提供了丰富的 CSS
类,方便开发者自定义时间表的外观。
响应式设计:支持移动设备,确保在不同屏幕尺寸上都能良好显示。
官方资源
官网:sbstjn.github.io/timesheet.j…
GitHub 仓库:github.com/sbstjn/time…
使用案例
假设我们要为一个在线教育平台创建一个展示学生学习历程的时间表。
这个时间表将展示学生从入学到毕业的各个阶段,包括参加的课程、获得的证书等信息。
步骤 1:引入库文件
首先,在 HTML
文件中引入 Timesheet.js
的 CSS
和 JavaScript
文件。
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/timesheet.js/dist/timesheet.min.css" />
<div id="student-timeline">div>
<script src="https://cdn.jsdelivr.net/npm/timesheet.js/dist/timesheet.min.js">script>
步骤 2:准备数据
接下来,准备时间表所需的数据。
在这个案例中,我们将展示一个学生从 2018 年入学到 2022 年毕业的学习历程。
const studentTimelineData = [
['09/2018', '06/2019', '入学 & 基础课程学习', 'default'],
['09/2019', '06/2020', '专业课程学习', 'ipsum'],
['07/2020', '01/2021', '暑期实习', 'dolor'],
['09/2020', '06/2021', '高级课程学习', 'lorem'],
['07/2021', '01/2022', '毕业设计', 'default'],
['06/2022', '09/2022', '毕业 & 就业', 'ipsum']
];
步骤 3:初始化 Timesheet.js
最后,使用 Timesheet.js
初始化时间表,并传入准备好的数据。
完整代码
将上述代码整合到一个 HTML
文件中,即可创建出一个展示学生学习历程的时间表。
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生学习历程时间表title>
<link rel="stylesheet" href="./timesheet.js/dist/timesheet.min.css" />
head>
<body>
<div id="student-timeline">div>
<script src="./timesheet.js/dist/timesheet.min.js">script>
<script>
const studentTimelineData = [
['09/2018', '06/2019', '入学 & 基础课程学习', 'default'],
['09/2019', '06/2020', '专业课程学习', 'ipsum'],
['07/2020', '01/2021', '暑期实习', 'dolor'],
['09/2020', '06/2021', '高级课程学习', 'lorem'],
['07/2021', '01/2022', '毕业设计', 'default'],
['06/2022', '09/2022', '毕业 & 就业', 'ipsum']
];
const timesheet = new Timesheet('student-timeline', 2018, 2022, studentTimelineData);
script>
body>
html>
效果如下
总结
Timesheet.js
是一个非常实用的 JavaScript
时间表库,它以简洁的代码和强大的功能为开发者提供了一个创建时间表的便捷工具。
通过本文的介绍和使用案例,相信你已经对 Timesheet.js
有了基础的了解。
无论是在个人项目还是企业应用中,Timesheet.js
都能帮助你快速创建出美观且功能强大的时间表,提升用户体验。
如果你对 Timesheet.js
感兴趣,不妨尝试在自己的项目中使用它,探索更多可能。
来源:juejin.cn/post/7461233603431890980
身份认证的尽头竟然是无密码 ?
概述
几乎所有的系统都会面临安全认证相关的问题,但是安全相关的问题是一个很麻烦的事情。因为它不产生直接的业务价值,而且处理起来复杂繁琐,所以很多时都容易被忽视。很多后期造成重大的安全隐患,往往都是前期的不重视造成的。但庆幸的是安全问题是普遍存在的,而且大家面临的问题几乎相同,所以可以制定行业标准来规范处理,甚至是可以抽出专门的基础设施(例如:AD、LDAP 等)来专门解决这类共性的问题。总之,关于安全问题非常复杂而且麻烦,对于大多数 99% 的系统来说,不要想着在安全问题领域上搞发明和创新,容易踩坑。而且行业的标准解决方案已经非常成熟了。经过长时间的检验。所以在安全领域,踏踏实实的遵循规范和标准就是最好的安全设计。
HTTP 认证
HTTP 认证协议的最初是在 HTTP/1.1标准中定义的,后续由 IETF 在 RFC 7235 中进行完善。HTTP 协议的主要涉及两种的认证机制。
基本认证
常见的叫法是 HTTP Basic,是一种对于安全性不高,以演示为目的的简单的认证机制(例如你家路由器的登录界面),客户端用户名和密码进行 Base64 编码(注意是编码,不是加密)后,放入 HTTP 请求的头中。服务器在接收到请求后,解码这个字段来验证用户的身份。示例:
GET /some-protected-resource HTTP/1.1
Host: example.com
Authorization: Basic dXNlcjpwYXNzd29yZA==
虽然这种方式简单,但并不安全,因为 base64
编码很容易被解码。建议仅在 HTTPS 协议下使用,以确保安全性。
摘要认证
主要是为了解决 HTTP Basic 的安全问题,但是相对也更复杂一些,摘要认证使用 MD5 哈希函数对用户的密码进行加密,并结合一些盐值(可选)生成一个摘要值,然后将这个值放入请求头中。即使在传输过程中被截获,攻击者也无法直接从摘要中还原出用户的密码。示例:
GET /dir/index.html HTTP/1.1
Host: example.com
Authorization: Digest username="user", realm="example.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41"
**补充:**另在 RFC 7235 规范中还定义当用户没有认证访问服务资源时应返回 401 Unauthorized
状态码,示例:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Restricted Area"
这一规范目前应用在所有的身份认证流程中,并且沿用至今。
Web 认证
表单认证
虽然 HTTP 有标准的认证协议,但目前实际场景中大多应用都还是基于表单认证实现,具体步骤是:
- 前端通过表单收集用户的账号和密码
- 通过协商的方式发送服务端进行验证的方式。
常见的表单认证页面通常如下:
html>
<html>
<head>
<title>Login Pagetitle>
head>
<body>
<h2>Login Formh2>
<form action="/perform_login" method="post">
<div class="container">
<label for="username"><b>Usernameb>label>
<input type="text" placeholder="Enter Username" name="username" required>
<label for="password"><b>Passwordb>label>
<input type="password" placeholder="Enter Password" name="password" required>
<button type="submit">Loginbutton>
div>
form>
body>
html>
为什么表单认证会成为主流 ?主要有以下几点原因:
- 界面美化:开发者可以创建定制化的登录界面,可以与应用的整体设计风格保持一致。而 HTTP 认证通常会弹出一个很丑的模态对话框让用户输入凭证。
- 灵活性:可以在表单里面自定义更多的逻辑和流程,比如多因素认证、密码重置、记住我功能等。这些功能对于提高应用的安全性和便利性非常重要。
- 安全性:表单认证可以更容易地结合现代的安全实践,背后也有 OAuth 2 、Spring Security 等框架的主持。
表单认证传输内容和格式基本都是自定义本没啥规范可言。但是在 2019 年之后 web 认证开始发布标准的认证协议。
WebAuthn
WebAuthn 是一种彻底抛弃传统密码的认证,完全基于生物识别技术和实体密钥作为身份识别的凭证(有兴趣的小伙伴可以在 github 开启 Webauhtn 的 2FA 认证体验一下)。在 2019 年 3 月,W3C 正式发布了 WebAuthn 的第一版规范。
相比于传统的密码,WebAuthn 具有以下优势:
- 减少密码泄露:传统的用户名和密码登录容易受到钓鱼攻击和数据泄露的影响。WebAuthn,不依赖于密码,不存在密码丢失风险。
- 提高用户体验:用户不需要记住复杂的密码,通过使用生物识别等方式可以更快捷、更方便地登录。
- 多因素认证:WebAuthn 可以作为多因素认证过程中的一部分,进一步增强安全性。使用生物识别加上硬件密钥的方式进行认证,比短信验证码更安全。
总的来说,WebAuthn 是未来的身份认证方式,通过提供一个更安全、更方便的认证方式,目的是替代传统的基于密码的登录方法,从而解决了网络安全中的一些长期问题。WebAuthn 目前已经得到流程的浏览器厂商(Chrome、Firefox、Edge、Safari)、操作系统(WIndows、macOS、Linux)的广泛支持。
实现效果
当你的应用接入 WebAuthn 后,用户便可以通过生物识别设备进行认证,效果如下:
实现原理
WebAuthn 实现较为复杂,这里不做详细描述,具体可参看权威的官方文档,大概交互过程可以参考以下时序图:
登录流程大致可以分为以下步骤:
- 用户访问登录页面,填入用户名后即可点击登录按钮。
- 服务器返回随机字符串 Challenge、用户 UserID。
- 浏览器将 Challenge 和 UserID 转发给验证器。
- 验证器提示用户进行认证操作。
- 服务端接收到浏览器转发来的被私钥加密的 Challenge,以此前注册时存储的公钥进行解密,如果解密成功则宣告登录成功。
WebAuthn 采用非对称加密的公钥、私钥替代传统的密码,这是非常理想的认证方案,私钥是保密的,只有验证器需要知道它,连用户本人都不需要知道,也就没有人为泄漏的可能;
备注:你可以通过访问 webauthn.me 了解到更多消息的信息
文章不适合加入过多的演示代码,想要手上体验的可以参考 okta 官方给出基于 Java 17 和 Maven 构建的 webauthn 示例程序,如下:
来源:juejin.cn/post/7354632375446061083
前端同时联调多个后端
前言
最近公司项目有时需要和多个后端同时对接(😔小公司一对N),于是对公司项目基础配置文件做了些修改,最终达到能对于多个后端同时启动多个前端服务,而不是每次都需要和A同学对接完,代理地址再换成B同学然后重新启动项目
个人经验0.5年,菜鸡前端一枚,第一次写文章,只是对个人工作简要记录😂!!!
公司项目有vue脚手架搭建的也有vite搭建的,下面让我们分两种方式来修改配置文件
vue-cli方式【webpack】
1. 个人习惯把proxy单独抽离出来放到.env.development
# 启动端口号
VUE_PORT = 8000
# 代理配置
# A同学
VUE_PROXY_A = [["/api","http://localhost:3001"]]
# B同学
VUE_PROXY_B = [["/api","http://localhost:3002"]]
2. 使用cross-env来加载不同的代理
npm i -D cross-env
重新编写下script
3. 读取环境变量
vueCli内部dotenv已经加载到process.env,我们再做一层包裹,之前配置的proxy,这种其实是字符串,需要处理
const { VUE_PROXY, VUE_PORT } = require("./constant.js")
// Read all environment variable configuration files to process.env
function wrapperEnv(envConf) {
const ret = {}
const SERVER_NAME = process.env.NODE_ENV_PROXY || VUE_PROXY
for (const envName of Object.keys(envConf)) {
if (!envName.startsWith('VUE')) {
continue
}
let realName = envConf[envName].replace(/\\n/g, '\n')
realName = realName === 'true' ? true : realName === 'false' ? false : realName
if (envName === VUE_PORT) {
realName = Number(realName)
}
if (envName === SERVER_NAME && realName) {
try {
realName = JSON.parse(realName.replace(/'/g, '"'))
} catch (error) {
realName = ''
}
}
ret[envName === SERVER_NAME ? VUE_PROXY : envName] = realName
if (typeof realName === 'string') {
process.env[envName] = realName
} else if (typeof realName === 'object') {
process.env[envName] = JSON.stringify(realName)
}
}
return ret
}
module.exports = {
wrapperEnv
}
这样我们就可以拿到所有的环境变量,并且proxy是数组,而不是字符串
4. 生成proxy
/**
* Used to parse the .env.development proxy configuration
*/
const httpsRE = /^https:\/\//
/**
* Generate proxy
* @param list
*/
function createProxy(list = []) {
const ret = {}
for (const [prefix, target] of list) {
const isHttps = httpsRE.test(target)
// https://webpack.docschina.org/configuration/dev-server/#devserverproxy
ret[prefix] = {
target: target,
changeOrigin: true,
ws: true,
pathRewrite: { [`^${prefix}`]: '' },
// https is require secure=false
...(isHttps ? { secure: false } : {}),
}
}
return ret
}
module.exports = {
createProxy,
}
5. 修改vue.config.js
const { defineConfig } = require('@vue/cli-service')
const { wrapperEnv } = require('build/vue/util')
const { createProxy } = require('./build/vue/proxy')
const {
VUE_PORT,
VUE_PROXY
} = wrapperEnv(process.env)
module.exports = defineConfig({
transpileDependencies: true,
// webpack-dev-server 相关配置
devServer: {
host: '0.0.0.0',
port: VUE_PORT,
open: false,
overlay: {
logging: 'info',
errors: true,
warnings: true
},
proxy: createProxy(VUE_PROXY),
disableHostCheck: true
},
})
6. 使用mock模拟两个后端服务
A同学使用3001端口
B同学使用3002端口
7. 测试是否达到效果
同样我们前端也起两个8000和8001
接下来看下8000端口请求
再看下8001请求
vite
结语
以上只写了webpack不过vite和这也差不多!!!
来源:juejin.cn/post/7456266020379541531
我在国企当合同工的那段日子
心血来潮
25号考完了,非常不理想,果然700页的东西不是一个月能搞完的。不对,我今儿写日志是为了纪念一下我的第一家公司,咋扯到别的了......言归正传,我在第一家公司待了仨年,可能是年纪到了(26岁咋还不退休啊),也可能是留了点感情在,离开前有些百感交集,思来想去还是写一个懒人日志吧,纪念一下我打工的三年光阴吧。
(:з」∠)
初说公司
先说一下俺的第一家公司,咱从学校出来就来这儿报道了,公司是国企控股,领导层全是国企员工,其他进公司的员工就是合同工,或者说是国企合同工,能吃公司东西,不是人力外包。
(:з」∠)
成都这边的开发都是围绕着云服务的,包括云操作系统、云桌面系统、云运维系统以及多云系统(我个人喜欢把他称为多个云集成系统),当然全是定制化项目。对,忘说了,公司主要业务是轨道交通行业,做云相关的产品是将轨道行业的运维放在云上面,算是相应国家的两化融合(信息化和工业化)。
对了,得说一下公司待遇,公司给的工资都在平均水平以下,尤其是对应届生而言,社保基数是工资八折(试用期)交的,公积金是12%,没有餐补但自带食堂以及饭卡补助,有些节假日有礼品,至少基础福利还好。
项目与业务
我所在的项目组就是多云系统,也算是我认为公司能拿得出手的项目。虽然是集成项目,但它只能集成。好像说了跟没说一样,那说具体点吧,比如说业主那边需要云,但怕私有云厂商垄断坐地起价,所以说一般配额划分为“7/3”、“4/3/3”、“6/4”,这样就有两套云系统,为了用起来顺心就需要一个集成系统,所以说我这个项目组的业务来源就是这样,至于你说的我们集成系统会不会垄断坐地起价,拜托,我们系统只会集成,没有底层设备控制权,坐地起价就直接禁用就行了,就不用这个系统呗,反正资源在另外的云操作系统中。
好了,话题回来,说说项目组开发相关的吧,项目开发受阻有三:与三方厂商沟通、项目代码老旧、随时随地变更的需求。
先说第一点吧,集成系统最大的麻烦就是跟三方厂商沟通,当然测试环境、测试数据获取这类的细节也算三方厂商沟通。因为地铁行业算是智能中国建设的一部分,所以说不光是我,连三方厂商的软件都必须是定制的。开发时候就要等着厂商环境稳定了,有数据了再联调,联调有bug了,再走一轮上面的流程,极大地增加了沟通成本以及开发成本。
在沟通,再沟通
其二就是和很多工业软件公司一样,软件项目时间跨度很大,里面东西不知道转手了好多次,缝缝补补式的开发,开发要考虑很多兼容性问题以及自己想办法写补丁。比如说node@6.x.x
不支持Object.entries
,你就要手动在webpack.base.conf.js
写的兼容,问我为啥不配置babel
呢,上次改babel
配置都是2016年的事儿了。代码要写兼容,久而久之就会忘记什么事封装、抽象,全部遗失在兼容的漩涡中。
我就改了一点点怎么崩了
其三就是随时随地变更的需求,这里我叠个甲,这个我不是甩锅给产品,虽然是产品改的需求,但产品不是想改就改,一定是业主/客户/上级/领导指示要改的。有需求变动谁都不会安逸,谁都烦,但请把炮火对准,不要误伤友军。频繁调整的需求会不断地消磨激情和热情,模糊项目方向,当然还有临时变卦导致的加班。
一直在变的需求
心态变化
三年工作时间虽然很短,但足以改变心态。原来有些迷茫到彻底迷茫;原来想要搞出一番事业到慢慢得过且过;原来想努力改变世界走到只想躺平加速世界毁灭。
公司的缝缝补补,工作的缝缝补补,项目的缝缝补补,这样的缝缝补补渐渐地缝补在人身伤,人心里。原来就算只有940的显卡也要努力熬夜玩游戏,现在用上3060ti后却只想打开直播看看,就只看看,重新上手玩太耗精力了。至于脱单嘛,自己都这么累了,为啥带着另一个一起累呢?
尾声
本来6月3号说写完的,忙着离职交接以及新公司入职,再加上拖延症又犯了,所以说一直到20号才写完,不过至少咱写完了,能发。
这篇算是自己里程记录,同时也是发牢骚,大家就当笑话看看吧。
来源:juejin.cn/post/7382121357608321059
太惨了,凌晨4 点替别人修复bug……
差点翻车
前两个月的某天凌晨,我司全新的一个营销工具,在全国如期上线。然而整个发布过程并非一帆风顺,在线上环境全量发布后,有同事观测到他所负责模块的监控曲线有异常!监控曲线在发布的时刻近乎于直线下跌。
经过初步排查,故障影响是:一部分新用户无法使用营销优惠~ 影响面非常大,所幸在凌晨的业务低峰期,实际影响有限,但是需要快速修复!不然等天亮用户请求量上来了,故障影响和定级就更大了!
目前接近凌晨4 点,时间很紧张!虽然这部分内容并非我负责,但我是当天的现场值班人,必须上!肝!
屎海无涯
我喝了一口红牛,打开电脑就扎进了陌生代码的汪洋大海中……
看着看着,我察觉到味道不对劲。我觉得这部分代码不是汪洋大海,而是一片屎海…… 代码堆砌如屎山,单个方法竟超过500行;嵌套的if else结构深不可测;日志更是完全缺失;职责不但不单一,反而极度混乱。总之,整个代码简直如同一团乱麻,排查难度极大。
四五个同事一起在排查代码,虽然他们负责过这部分代码,但是大家都十分挠头,找不到 bug 在哪。
当局者迷,旁观者清。经过了30分钟的细致分析,终于,我率先找到了 bug 原因。激动地心颤抖的手,我开了 5 分钟的 bug 发布会,通报了 bug 根因和修复方案。
破案了!
确定 bug 根因后,其他人默默去休息了……
接下来我负责修 bug、测试、打包、发版、验证…… 不知不觉,天空破晓,一直搞到早上 8 点多…… 在线上完成验证,监控曲线恢复正常!bug 修复完成!
bug根因
由于公司代码保密,所以我使用伪代码解释。
业务逻辑是遍历所有的优惠活动,若任意一个优惠活动需要限制新用户使用,那么就需要去查询当前用户是否新用户。
bug 代码如下! (实际的屎山代码,比这部分代码要复杂得多!)
boolean newUserCheckEnabled = false;
for ( Activity activity : activityList ) {
newUserCheckEnabled = activity.isLimitNewUser();
}
想必大家一眼就能看出问题所在!这样写代码, newUserCheckEnabled 等于最后一个活动的值,如果最后一个活动不限制新用户使用,那么 newUserCheckEnabled 就是 false,然而中间的活动可能需要限制新用户,于是 bug 产生了!
老板亲自指导写代码
正确的代码应该这样写,我按照如下方式修复了 bug,但是老板对代码不满意!
boolean newUserCheckEnabled = false;
for ( Activity activity : activityList ) {
if (activity.isLimitNewUser()) {
newUserCheckEnabled = true;
}
}
”一行代码就能解决的事,不需要使用 if “ ,老板看完我的代码后,说道。
他给出的代码示例如下,使用 || 表达式
boolean newUserCheckEnabled = false;
for ( Activity activity : activityList ) {
newUserCheckEnabled = newUserCheckEnabled || activity.isLimitNewUser();
}
if 代码被替换如下!
newUserCheckEnabled = newUserCheckEnabled || activity.isLimitNewUser();
"这能行吗”? 我的大脑飞速运转…… 这两段代码等价吗?似乎等价,但不是十分确定……
老板面前,不能暴露自己没跟上节奏,否则暴露智商。
我假装立刻明白,于是吹了一句,“卧槽,牛逼,这样写确实更加简洁吖!👍🏻”。(大家觉得应该怎么拍马屁,更合适?)
私底下,我还在心里嘀咕,两者真的等价吗?
现在我可以肯定:确实是等价的!
来源:juejin.cn/post/7425875126527918130
我以残躯入2025-以身入局胜天半子
我以残躯入2025-以身入局胜天半子
今天是 2025 年的第二天,我写下这篇文章,讲述我在 2024 年的经历和挑战,以及我如何克服困难,最终成功走出困境。本来是想昨天元旦第一天写的,但不凑巧的是元旦第一天我得甲流了,发烧了一天,所以今天才写这篇文章。
回顾2024
2024 年可以说是我经历的一个不平凡的一年,经历了股票亏损、事业不顺、年底被裁员、元旦得了甲流等诸多困难。
股票投资
我在 A 股其实也算一个老手了,虽然挣的少,但前 5 年基本上都是挣的,但比较悲催的是 24 年证券主席换了之后,市场行情波动极大。
先是我买的业绩不错的成长股持续下跌,后面换仓重仓买了国资背景的通信行业股票,想着上涨趋势但没想到 5 月直接 ST
了,直接导致了我今年股票大幅度下跌,最多时亏损达 50%+
。
同时也买了港股,港股也被踢出港深股通
了,股价至今还跌幅超过 80%
,基本上就是把我本金吃掉了。
因为股票跌幅比较大,每个交易日对我来说,都是折磨啊,我也很无奈。越追越跌、不追继续套牢,这种情况到 24 年 9 月才有一定的缓解。
事业不顺
23 年下半年,我手底下的俩个前端小伙伴被裁员了,导致从 23 年底到 24 年都非常的忙,被压的闯不过气来,也萌生了跳槽换个新的工作环境的想法,也出去找了工作,但一直找不到合适的工作,都要求降薪,那我就不能接受了。
然后,公司 9 月份搬家,我通勤时间增加了 20 分钟,每天来回就是 40 分钟,接近每天快 3 小时的通勤时间,然后公司还要求我们免费加班到 8 点,目前还在执行这个政策中,即晚 8 点才能下班,8 点之后才算加班调休,这点让我很头疼。
年底被裁员
到了年底 11 月公司开启了我入职以来的第三波裁员,这次的主题是降本增效
,在 11 月我们小部门裁掉了一个 BI(我们薪资最高的)、一个测试、产品、还给我们后端 JAVA 薪资最高的同学来了个降薪 3.5K
,据说我也在裁员名单上,我的薪资其实蛮高的,后来的开发基本上都比我低,但 11 月我没有收到对我裁员或降薪的通知,以为躲过了一次。
但到了 12 月上旬,有一天 CTO 让我晚点留下给我讲了合同到期不续的事情,至此我被正式裁员了,但因为提前一个月通知的,只给我赔偿 n,同时也不给我年终奖,前俩年我的年终奖其实挺高的,都是 3.5+ 的评分,有 2 个月。
虽然被裁了,但我也没有啥不开心的,因为公司的工作环境已经极度恶化了,年终下调、裁员、降薪、加班、同事内卷,已经不是我愿意为之付出努力的公司了。
元旦甲流
以为就这样熬过了 24 年,但危险悄然而至,12 月 31 号晚上,我开始发高烧,我的儿子从 30 号就开始高烧不退了,最后在元旦那天去医院确诊了甲流,也算是一个悲剧的开年,只能看着别人去逛商场跨年。
这也让我认识到身体健康其实是第一位的,其他都是其次。
走出困境
在投资方面,虽然目前我还是亏损的,但我有信心在 25 年收益回正,这一年的大幅下跌和波动,让我对如何研究股票和人性有了更深的体会,目前已经从亏损的 50% 回到了 20%,也让我有了更多的经历。
事业方面,年中开始我积极调整自己的个人状态,已经找回初心,开始了新的工作状态:积极写博客记录学习状态、持续进行技术分享、参加社区活动、积极的找新工作。通过持续的沉淀和学习,我逐渐找到了自己的兴趣和方向,那就是未来朝独立开发者的道路走,未来我可能是个销售、运营、产品、项目经理,但也会持续的进行产品开发,通过技术造福大众。
学习方面,我持续写了一些系列,大约有 60 多篇文章,也希望和大家一起学习,一起进步成长。
身体健康方面,后面我还是要减减肥、增强免疫力,让身体保持健康的状态。
祝福大家
写到这里就没啥写的了,祝福大家身体健康万事如意,以身入局胜天半子。
来源:juejin.cn/post/7454974103259824155
我的 CEO 觉得任何技术经理都是多余的
原文 QUESTIONABLE ADVICE: “MY BOSS SAYS WE DON’T NEED ANY ENGINEERING MANAGERS. IS HE RIGHT?”
我最近加入了一家初创公司,负责管理一个约 40 名工程师的团队,担任技术副总裁。然而,我与 CEO(之前是工程师)在是否需要雇佣专职技术经理的问题上产生了很大的冲突。目前,工程师们被分成了 3-4 人的小团队,每个团队有一个工程师头头,负责领导团队,但他们的主要职责仍然是编写代码和交付产品。
我有 HC 在未来一年雇佣更多的工程师,但没有经理的 HC。老板认为我们是初创公司,负担不起这种奢侈品。在我看来,我们显然需要技术经理,但在他看来,经理只是多余的开销,在我们的阶段所有人都应该全力编写代码。
我不知道该如何论证。在我看来这很显然,但实际上我很难用言语表达为什么我们需要技术经理。你能帮帮我吗?
—— 真的是多余的开销吗(?!)
这里有很多问题需要解答。
你的首席执行官不理解为什么需要经理,这并不奇怪,因为他似乎不明白为什么需要组织结构。🙈 他为什么要对你如何组织团队或你可以雇佣哪些角色进行微管理?他雇用了你来做这份工作,却不让你完成。他甚至不能解释为什么不让你做。这不是个好兆头。
但这个问题确实值得思考。我们假设他不是故意要刁难你。😒
我能想到两种论证雇用技术经理的方式:一种是相当复杂的,从第一性原理 (First Principle) 出发,另一种非常简单,但可能不太令人满意。
我个人对权威有一种强烈的反感;我讨厌被告知该做什么。直到最近,我才通过系统理论的视角,找到了一种对层级制度既健康又实用的理解。
为什么组织中存在层级制度?
层级制度确实带有很多负面包袱。我们许多人都有过在层级制度下与经理或整个组织打交道的不幸经历。在这些地方,层级制度被用作压迫的工具,人们通过垄断信息和玩弄权力游戏来提升地位,决策则是通过权力压制来做出。
在那种地方工作真的是一种折磨。谁愿意将自己的创造力和生命力投入到一个感觉像《呆伯特》漫画的地方,明知道自己的价值被极少认可或回报,而且这些价值会慢慢地但确实被压制掉?
但层级制度本质上并非是专制的。层级制度并不是人类为控制和支配彼此而发明的一种政治结构,它实际上是自组织系统的一种属性,是为了子系统的有效运作而出现的。事实上,层级制度对复杂系统的适应性、弹性和可扩展性至关重要。
让我们从一些关于系统的基本事实开始,为可能不熟悉的人介绍一下。
层级是自组织系统的一种属性
一个系统是「由相互依赖的组件组成的网络,这些组件共同工作以实现一个共同目标」(W. Edward Deming)。一堆沙子不是一个系统,但一辆车是一个系统;如果你把油箱取下来,车就无法运作。
子系统是一个在更大系统内有较小目标的元素集合。在一个系统中可以有很多层次的子系统,它们相互依存地运行。子系统总是为了支持更大系统的需求而工作;如果子系统只为自己的最佳利益优化,整个系统可能会挂掉(这就是「次优」(suboptimal)这个术语的由来 😄)。
如果一个系统能够通过多样化、适应和改进自身使自己变得更加复杂,那么它就是自组织的。随着系统自组织并增加其复杂性,它们往往会生成层级 —— 即系统和子系统的排列。在一个稳定、有弹性和高效的系统中,子系统在很大程度上可以自我管理、自我调节,并为更大系统的需求服务,而更大系统则负责协调子系统之间的关系并帮助它们更好地发挥作用。
层级最小化了协调成本,减少了系统中任何部分需要跟踪的信息量,防止信息过载。子系统内部的信息传递和关系比子系统之间的信息传递或关系要密集得多,延迟也少得多。
(对于任何软件工程师来说,这些应该都很熟悉。模块化,对吧?😍)
按照这个定义,我们可以说,经理的工作就是在团队之间进行协调并帮助他们的团队表现得更好。
对社会技术系统的二分是伪命题
你可能听过这个谬论:「工程师搞技术,经理搞人。」我讨厌这种说法。😊 我认为这完全误解了社会技术系统的本质。社会技术系统中的「社会」和「技术」并不是截然分开的,而是相互交织、相互依存的。事实上,很少有纯粹的技术工作或纯粹的人际工作;有大量涉及两种技能的粘合工作。
看看任何一个有效运作的工程组织除了编写代码之外还要做的一部分任务:
- 招聘、建立人脉、面试、培训面试官、汇总反馈、撰写职位描述和职业发展路径
- 每个项目或承诺的项目管理、优先级排序、管理利益相关者和解决冲突、估算规模和范围、进行回顾会议
- 召开团队会议、进行一对一交流、提供持续的成长反馈、撰写评审、代表团队的需求 架构设计、代码审查、重构;捕获 DORA 和生产力指标、管理警报量以防止倦怠
许多工作可以由工程师完成,而且通常也是如此。每家公司对这些任务的分配方式有所不同。这是一件好事!你不希望这些工作仅由经理来做。你希望个人贡献者共同创造组织,并参与其运行方式。几乎所有这些工作由有工程背景的人完成会更有效。
所以,你可以理解为什么有人会犹豫是否要把宝贵的人员编制花在技术经理上。为什么不希望技术部门的每个人的主要工作都是编写和交付代码呢?这不是从定义上说最大化生产力的最佳方式吗?
额……😉
技术经理是一层有用的抽象
理论上,你可以列出所有需要完成的协调任务,并让不同的人来负责每一项。但实际上,这是不切实际的,因为这样每个人都需要了解所有事情。记住,层级制度的主要好处之一是减少信息过载。团队内部的沟通应该是高效和快速的,而团队之间的沟通则可以少一些。
随着公司的扩展,你不能期望每个人都认识其他所有人;我们需要抽象的概念才能运作。经理是他们团队的联络点和代表,充当重要信息的路由器。
有时我把经理想象成公司的神经系统,将信息从一个部门传递到另一个部门,以协调行动。将许多或大部分功能集中到一个人身上,可以利用专业化的优势,因为经理会不断建立关系和背景知识,并在他们的角色中不断改进,这大大减少了其他人的上下文切换。
管理者 (Manager) 日程与创造者 (Maker) 日程
技术工作需要集中和专注。上下文切换的成本很高,过多的中断是挺要命的。而管理工作则是每小时左右进行一次上下文切换,并且一整天都要应对各种打断。这是两种完全不同的工作模式、思维方式和日程安排,无法很好地共存。
通常,你希望团队成员能够把大部分时间花在直接为他们负责的成果做出贡献的事情上。工程师只能做有限的粘合工作,否则他们的日程安排就会变得支离破碎,从而无法履行他们的承诺。而管理者的日程安排本身已经是支离破碎的,因此让他们承担更多的粘合工作通常不会带来太大干扰。
虽然并不是所有粘合工作都应该由管理者来完成,但管理者的职责是确保所有工作都能完成。管理者的职责是尽量让每个工程师都能从事有趣且具有挑战性的工作,但不能让他们感到过于负担重,还要确保不愉快的工作能公平分配。管理者还要确保,如果我们要求某人完成一项工作,就必须为其配备成功完成这项工作所需的资源,包括专注的时间。
管理是问责的工具
当你是工程师时,你对自己开发、部署和维护的软件负责。而作为经理,你则对团队和整个组织负责。
管理是一种让人们对特定结果(如构建具备正确技能、关系和流程的团队,以做出正确的决策并为公司创造价值)负责的方式,并为他们提供实现这些结果所需的资源(预算、工具和人员编制)。如果你不把组织建设作为某人的首要任务,那么这就不会成为任何人的首要任务,这意味着它可能不会得到很好地执行。那么,这该由谁负责呢,CEO 先生?
你对技术负责人、工程师或任何负责交付软件的人在「业余时间」能完成的任务有一个合理的上限。如果你试图让技术负责人负责构建健康的工程团队、工具和流程,那么你就是在要求他们在同一个日历里做两份时间不兼容的工作。最可能的情况是,他们会专注于自己觉得舒适的成果(技术成果),而在后台堆积组织债务。
在自然层级中,我们向上看是为了目标,向下看是为了功能。简而言之,这就是我们需要技术经理的复杂原因。
选择无趣的技术文化
更简单的论点是:大多数工程组织都有技术经理。这是默认设置。多年来,许多比你或我更聪明的人花了大量时间思考和调整组织结构,这就是我们得到的结果。
正如丹-麦金利(Dan McKinley)的名言,我们应该「选择无趣的技术」。无趣并不意味着不好,而是意味着它的能力和失败条件是众所周知的。你只能获得少数的创新点数,因此你应该明智地将这些点数用在能够成就或毁掉你业务的核心差异点上。文化也是如此。你真的想把你的点数用在组织结构上吗?为什么?
无论好坏,层级组织结构是众所周知的。市场上有很多人擅长管理或与管理者合作,你可以雇佣他们。你可以接受培训、指导,或者阅读大量的自助书籍。有各种各样的管理哲学可以围绕它们来凝聚团队或用来排除其他人。另一方面,我所知道的无经理实验(例如 Medium 和 GitHub 的全员自治,或 Linden Lab 的「选择你的工作」)都被悄然放弃或被颠覆了。在我的经验中,这并不是因为领导者疯狂追求权力,而是由于混乱、缺乏重点和执行不力。
当没有明确的结构或层级时,结果不是自由和平等,而是「非正式的、不被承认的和不负责任的领导」,正如《无结构的暴政》中详细描述的那样。事实上,这些团队往往是混乱、脆弱和令人沮丧的。我知道!我也很生气!😭
这个论点并不一定能证明你的 CEO 是错的,但我认为他的证明标准比你的要高得多。「我不想让我的任何工程师停止写代码」并不是一个有效的论点。但我也觉得我还没有完全解决生产力的核心问题,所以我们再来讨论一下这个问题。
更多代码行数 ≠ 更高生产力
简要回顾一下:我们在讨论一个有约 40 名工程师的组织,分成 10 个小组,每组有 3-4 名工程师,每组都有一个技术负责人。你的 CEO 认为,如果有人停止全职编程,这个减速将是你们无法承受的。
也许吧。但根据我的经验,由经验丰富的技术经理领导的几个较大团队,将远远优于这些小团队。这差距很明显。而且,他们可以以更高效、可持续和人性化的方式完成工作,而不是这种拼命的死命赶工。
系统思维告诉我们原因!更少的团队,但规模更大,你会有更少的整体管理开销,且大大减少了团队内慢且昂贵的协调。你可以在团队内部实现丰富、密集的知识传递,从而实现更大面积的共享。每组有7-9名工程师,你可以建立一个真正的值班轮换,这意味着更少的英雄主义和更少的倦怠。你需要进行的协调可以更具战略性,减少战术性,更具前瞻性。
五个大团队是否能比十个小团队编写更多的代码行数,即使有五名工程师成为经理并停止编写代码?可能会,但谁在乎呢?你的客户根本不关心你写了多少代码行数。他们关心的是你是否在构建正确的东西,是否在解决对他们重要的问题。关键是推动业务前进,而不是单纯地编写代码。不要忘记,单纯地编写代码会产生额外的成本和负面效应。
决定你速度的是你是否把时间花在了正确的事情上。学会正确决定构建什么是每个组织都必须自己解决的问题,而且这是一项持续不断的工作。技术经理不会做所有的工作或做出所有的决策,但根据我的经验,他们对于确保工作顺利进行并且做得很好,绝对至关重要。正如我在上篇文章中写到的,技术经理是系统用来学习和改进的反馈循环的载体。
管理人员是否会成为不必要的开销?
当然有可能。管理的核心是协调团队之间的工作并提升团队的运作效率,所以任何减少协调需求的方式也会减少对管理的需求。如果你是一家小公司,或者你的团队成员都是非常资深且习惯合作的,那么你就不需要太多的协调。另一个重要因素是变化的速度;如果你的公司在快速增长或者人员流动频繁,或者面临很多时间压力或频繁的战略调整,你对管理人员的需求就会增加。但也有许多较小的组织在没有太多正式管理的情况下运作得很好。
我不喜欢「开销」这个词,因为 a) 这有点粗鲁,b) 称管理人员为「开销」的人通常是不尊重或不重视管理这门技艺的人。
但管理实际上确实是开销😅。许多其他的粘合工作也是如此!这些工作很重要,但它们本身并不能推动业务向前发展;我们应该尽量只做那些绝对必要的工作。粘合工作的天然属性使得它很容易扩散,吞噬所有可用的时间和资源(甚至更多)。
限制是好的。感觉资源不足是好的,这应该成为常态。管理很容易变得臃肿,管理人员可能非常不愿意承认这一点,因为他们从来没有感到压力或紧张减少。(事实上,情况可能恰恰相反;臃肿的管理层可能会为管理人员带来更多工作,而精简的组织结构可能会让他们反而感到压力更小。官僚主义往往会自我发育。特别是当管理层过于关注晋升和自我时。这也是确保管理不应仅为升职或统治的又一个充分理由)
管理也很像运营工作,当它做得好的时候,是看不见的。评估管理人员的工作可能非常困难,尤其是在短期内,而决定何时创建或偿还组织债务是一个完全不同的复杂问题,远远超出了这篇文章的讨论范围。
但是,是的,管理人员绝对可以成为不必要的开销。
然而,如果你有 40 个工程师都向一个副总裁汇报,而没有其他人专门负责人员、团队和组织相关的工作,那么我可以相当肯定地说,这对你来说目前不是一个问题。
<3
💡 更多资讯,请关注 Bytebase 公号:Bytebase
来源:juejin.cn/post/7373226679730536458
2年前的今天,我决定了躺平退休
两年前的这个时候,突然觉得说话特别费劲,舌头不太听使唤,左手突然不听话,就像李雪健老师表演那个帕金森老头喝酒一样。
我心里一慌,请假去了医院,验血,CT,超声。然后医生给我列了长长一篇诊断书:高血脂,高血压,糖尿病,冠心病,还有最可怕的脑出血,还好只是渗血,虽然并不是很严重,但是位置不太好,影响了身体感官和左手。
平时身体非常好,也经常运动,为什么会突然得这么多病呢。毫无征兆的左手就不听使唤了。而且听力在这一段时间也非常差。通过大夫诊断,一部分是遗传因素,另一个是和我常年酗酒,熬夜有关,每天几乎只睡3-4小时。
是的,,,,,,我喜欢在家喝着啤酒写代码,甚至有时候在单位加班的时候也是喝啤酒写代码。和别人不太一样,别人喝酒爱睡觉,我喝啤酒失眠。因为接了很多项目,上班之余都是晚上和周末熬夜写代码做自己的项目。
其实听到这个消息我很失望,失望的并不是因为身体垮了,钱还没赚够,而是我还没有完成我的目标就是打造一个自己主导的产品。
那天从医院回家,我并没有去坐地铁,而是从中日友好医院徒步走回天通苑的出租屋。在路上,我反复的想,今后的路该如何走。
继续在互联网行业工作肯定是不行的,病情会进一步加重,到时候就真的成一个废人了,反而会拖累整个家庭。如果不继续“卷”那我也就无法实现自己来北京的目标了。不过好在经过这么多年的积累,已经存够足够养老的资本,并不需要为妻儿老小的生存发愁,但是也没有到财富自由的程度。
躺平,躺到儿子回老家上学就回老家退休
。这是一个并不那么困难的决定。但是却是一个非常无奈的决定,躺平就意味着自己来北京定下的目标没有完成,意味着北漂失败。
做好这个决定以后,我就开始彻底躺平,把手里的几个项目草草收尾,赔了大几十万。等于这一年白忙活。好在还有一份工作收入。同时也拒掉了2个新的Offer。在疫情最困难的时候,还能拿到两个涨薪offer。我还是蛮佩服我自己的。但是为了不影响我的额外收入,加上现在工作不是很喜欢,也就一直犹豫不决。但是这次生病彻底让我下定了决定 ---- 算了。
其实,经历这么多年,什么都看的很清楚,但是我的性格并不适合这个行业,我这个人最大的特点就是腰杆子硬,不喜欢向上管理,经常有人说我那么圆滑,肯定是老油条,而实际上,我整整18年的工作经历,只对领导说过一次违心的话,变相的夸了老板定制的开发模式,老板看着我笑了笑,也不知道他是不是听出来我这话是讽刺还是撒谎。
而其余都是和老板对着干,只有2任老板是我比较钦佩的,也是配合最舒服的。而且共同特点都是百度出身,我特别喜欢百度系的老板。特别务实,认认真真做业务。不搞虚头巴脑的事情,更不在工作中弄虚作假。一个是滴滴的梁老板,另一个就是在途家时候的黄老板。
当然,在我整个职业生涯有很多厉害的老板,有的在人脉厉害,有的人个人管理能力,有的在技术。但是由于我性格原因,我就是跟他们合不来,所以要么你把我开了,要么等我找好下家主动离开。
所以我的职业生涯很不稳定,就比如我见过的一个我认为在技术能力上最厉害的老板,也是我唯一在技术上佩服的人,就是在36kr期间认识的海波老师,听他讲的系统架构分享和一些技术方案,真的是豁然开朗,在Saas和Paas的方方面面,架构演化,架构升级所可能遇到的各种问题及面对产品高速迭代所需要解决的问题及方案都门清,而且他本身也是自己带头写代码,实际编码能力也是非常的牛,并不是那种“口嗨”型领导。但就是我跟他的性格合不来,最后我把他那套架构方案摸透了以后就跑路了,而从他那里学的那套技术方案,在我日后在lowcode和Paas以及活动运营平台的技术方案设计上帮助颇多。而他不久之后也离开了。据说去了字节。
混迹于形形色色的老板手底下,遇到过的事情非常多,也让我认清了一点,那就是,牛人是需要平台去成就的,平台提供了锻炼你的机会和让你成长的机会。所以你学到了,你就成了牛人。而不是你自己手头那点沾沾自喜的觉得别人没你了解的深入的技术点。所以平台非常重要,绝大多数情况下都是如此。
所以我这种人就不适合,因为我不喜欢违心。我顶多就是不说出来,不参与,不直接反对就已经是对老板最大的尊重了
。所以我能看透很多事情,但是也知道我不讨老板喜欢,而我的性格也不可能为了让老板喜欢而卑躬屈膝,所以,我早早就提前做好准备,就是拉项目,注意这不是做私活
。拉项目就是承包项目,然后找几个做私活的人给他们开发。这项收入有时候甚至一年下来比我的工资还要高。风险也是有的,那就是可能赔钱,十几万十几万的赔。所以也是一个风险与收益共存的事情。做项目的好处是,你可以不断的接触新的甲方,扩张自己的人脉,也就不断的有项目。
但是由于这次生病,我手头的3个项目都没有做好,都被清场了。所以为了弥补朋友的损失,我一个人扛下了所有。也同时意味着后面也就没项目可接了。身体不允许了。
躺平以后,为了等孩子回老家上学,本职工作上,也开始混,我最后一年多的时间里,写代码,都不运行直接就提测。是的。没错。。。。。。就是这样。但是功能是都好用的,基本的职业操守是要有的。虽然也会有更多的bug。但是一周我只干半天就可以完成一周的工作。这可能就是经验和业务理解的重要性。所以,我一直不太理解很多互联网企业精简人员的时候为什么精简的是一线开发人员,而留下的是那些只会指挥的小组长。这也是为什么各大互联网企业都在去肥增瘦,结果肥的一点也没减下去。
不是有那么一句话,P8找P7分一个需求,然后P7找P6喊来P5开发。互联网就是这样子,一群不了解实际业务和实际代码的人,在那里高谈阔论,聊方案,聊架构,聊产品,聊业务,聊客户,聊趋势,然后让那些一脸“懵逼”的人去开发。最后的结果可想而知,最后很多需求都是一地鸡毛,但是责任却都要一线执行去承担,而为了证明需求的正向收益,那就在指标口径上“合理”的动动手脚,所以我在我整个职业生涯说出了那么一次,也是唯一一次违心的恭维话。
所以我特别佩服一个网红叫“大圣老师”,是一个卖课的,虽然我看不上他做的割韭菜的事情,但是我很佩服他这个人,他也是很刚的人,就是看不惯老板pua和无意义的加班,人家就是不干了。成功开辟了第二职业曲线,而且也很不错。
另一个网红就是“神光”,虽然我也看不上他,但是我很佩服他,佩服他追求自我的勇气。
而反观那些在职场唯唯诺诺卑躬屈膝的人,现在过的如何呢?人啊。还是要有点个性。没个性的人下场都挺惨的。
峰回路转,人那,这一辈子就是命,有时候把,真的是你也不知道结果会是什么样,23年在我百无聊赖,闲的五脊六兽的时候,一周的工作基本上半天就干完了,所以一个机缘巧合,遇见了一群有意思的人。当时大模型正在风口浪尖。好多人都在大模型里面摸金,而有这么一群人,一群大学生,在海外对我们进行大模型技术封锁的时候,为了自己的初衷,建立了在问这个网站。
而作为起步比别人要晚,产品做的还很粗糙如何跟市场上的竞品竞争呢?而且不收费,更不打广告,完全靠赞助存活。但是这一切都是为了在国外封锁我国大模型技术背景下的那句话“让知识无界,智能触手可及”。站长原文
所以在同类起步更早,产品做的更精细的很多产品逐渐倒下去以后,zaiwen还活着。所以我觉得特别有意思,这种产品活下来的概率是非常低的,除非整个团队都是为爱发电,于是我也加入到这个团队。
事实上也确实这样,整个团队是有极少部分社会工作者和大部分在校大学生组成,而大家聚一起做这件事的初衷就是为了让知识无国界,让国内用户可以更方便的体验最先进的海外大模型技术。而目标用户,也都是学生,老师和科研工作者。
就这样在这里,我重新找回了自己的目标,虽然,由于资金问题,资源问题,以及我个人身体限制能做的事情很少,但是却发现,大家都做的非常有动力,产品也在不断的迭代不断的发展,并且还活的很好。团队的人在这里也干的很开心。
今天,正是两年前我诊断出脑出血的那天,心里没有低落,也没有失望,更没有懊悔,有的只是新的体验。人生啊,来这一世,就是来体验的,别难为自己。顺势而为,就像张朝阳说的那句话“年轻人挺不容易的,建议年轻人不要过度努力,太过拼搏的话(对身体)是有伤害的,年轻人得面对现实,这个世界是不公平的”
来源:juejin.cn/post/7416168750364540940
2024 年: 落考、车祸、失业, 没了!!!
引言
2024
没啥成长的, 净剩下焦虑、内耗、失意了! 随便写写总结, 诸君随便看看吧...
一、落考(软考)
如题, 今年报考了软考(高项), 选择题和论文没过 😅😅😅 明年继续吧!!
去年在掘金读了好多篇年终总结, 偶然了解到了杭州 E
类人才, 杭州 E
类人才无疑对我这等普通人来说是可以触及到的, 而且优待 福利较高
的一类人才了。
而对于我等普通人来说, 通过 软考 + 专利/软件著作
是最便捷可行的一种。这其中唯一有难度的其实就是 软考
了
软考是一种简称, 其全名是计算机技术与软件专业技术资格(水平)考试, 大家也称之为计算机软考、计算机软件资格考试等。软考又分为初级、中级、高级, 申请 E
类人才则需要高级资格证书, 这里直接报考高级就行(不需要从初级考起)。
如上图每个等级都有很多门专业, 今年我报考的是 信息系统项目管理师
简称 高项
, 因为据说这门比较简单, 都是介绍项目管理上的一些知识, 背的比较多, 对专业要求没那么高。
当然如题, 今年没准备好, 有两门挂了 😭😭😭, 总共要考三门分别是选择题、案例分析、论文, 每门总分都是 75
分, 需要每门都考及格(45
分), 才算通过考试。
经验教训:
- 一定要提前做好计划, 严格按照计划安排时间学习。 我自己就是中间有段时间公司活比较多、加上自己懈怠了, 后面就严重影响了进度
- 一定要多刷题, 特别是选择题, 以刷题为主。我这次基本没刷选择题 🤦
- 论文提前开始! 备好模版就开始写吧
二、车祸
是的, 不幸的是在 6
月底, 一天阴雨绵绵的傍晚, 在下班回家路上(家门口)骑着小毛驴的我和尊贵的宝马车主相撞了!!
被撞倒瞬间, 还是很刺激的! 整个大脑嗡嗡的, 思考 🤔 几秒, 原地蹦了起来!! 后面就是报警, 开具交通责任认定书(对方全责)。交警到场时看了下伤势, 说应该没事大事的。问我要不要去医院做个检查可以自己打车或者让司机送我去, 纠结了下还是打算去医院瞅瞅(后面回想起来也是后怕, 幸亏去了, 要不然可能半条命就没了...)!
到医院一通检查, 很不幸... 走不了了, 寰椎骨折得住院了 😱。医生看到片子后那一脸严肃的表情, 可把我吓坏了! 医生语气都变了, 让我赶紧坐下, 脖子不要乱动, 叫来了工作人员给我整了个颈托! 然后让我办理住院....
后面了解了下, 寰椎骨位于脑瓜子和脖子中间, 用于支持脖子的一个环形骨头, 骨头中间镂空的, 人的所有神经都是从这个环中间穿过到达身体各个地方, 所以这个位置骨折, 处理不好就可能会压迫到神经! 运气好的是我伤的位置比较好, 在脖子前侧, 如果是脖子后面可能就得开刀了!! 更庆幸的是, 来了医院做检查, 否则这条命可能就得交代了!!
后面住院 8
天, 过上了早 7
晚 10
的作息了, 每天就是看看电视、玩玩手机, 然后抽空就过道里溜达溜达, 提前过上了养老生活!!
出院后, 就开始居家办公咯! 中间还抽空搬了家, 然后还给自己整出了荨麻疹, 我也是醉了...
同时我的交通事故是发生在下班路上, 并且是对方全责, 所以还属于工伤! 故在受伤居家期间还申请了工伤。感慨下, 工伤流程也太麻烦了吧... 到目前为止还没整完 😤
最后友情提示, 出事不管怎样一定一定要报警(定责)、去医院做好检查...
三、祸不单行(失业)
9
月眼瞅着脖子马上要好了, 终于可以结束居家办公了! 接踵而来的是, 公司经营不善, 大规模裁员的消息!!! 所以自然的, 我又再次失业了!! 人生第二次失业了, 也没啥感觉, 该赔偿赔偿, 该滚蛋滚蛋!!
失业了, 三无人士, 一点也不带慌的, 该吃吃该喝喝! 这期间顺便处理了交通赔偿事宜, 虽然没多少! 伤好后, 去大西北溜达了一趟!
一下子没反应过来就到年底了... 只能怪今年太早过年了, 就这样办, 一切明年再议...
四、生活
今年重新找了住处, 顶楼, 再也不会被楼上邻居吵到了, 少了很多内耗, 就是夏天有点子热!! 同时房子也更大点了! 当然租金肯定也更贵了!
今年下厨的次数也明显多了起来, 出去吃大餐的次数少了挺多的! 上班也开始自己带饭咯, 当然省钱好像并没有省到, 自己煮饭量不好掌握, 一煮一大锅, 一吃一个不吱声! 同时大鱼大肉的, 一顿饭价格也不贵, 好在比外面吃健康点!
今年好像也没赞下什么大钱, 没有养成记账习惯, 所以一切靠感觉! 反正赞是攒了, 但是肯定没达到预期, 明年一定要养成记账的习惯
今年的韭菜长大了一点点, 感谢债基、感谢纳斯达克(明年能回本吗? 😵💫)
回顾下, 今年倒是去了不少地方, 一月迪士尼、三月苏州、六月昆明、十月武威张掖、十一月邯郸、十二月滑雪
家中新增一员, 名唤 二狗子
! 刚入门就命运多舛, 感冒、流鼻涕、咳嗽不断.... 但也不影响她可可爱爱!
五、卷? 不卷?
今年的代码全部贡献给了 昆仑虚, 但实际上也没干啥, 就将 昆仑虚 迁移到 NextJS
并引入 TS
, 代码量和去年比少得可伶!
内容创作这块也没啥成绩, 掘金输出 22
篇文章, 公众号「昆仑虚F2E」日常更新原创文章, 然鹅仅仅新增了 30
关注😓😓😓(这里求个关注)
内容创作收入: 掘金金石 +604
、公众号收入 +13
卷吗? 今年一顿摆烂, 和去年给自己定的目标差太多咯.....
对了, 今年还读了 5+
本书...
六、展望 2025
计划以及有了, 重点就三件事: 攒钱、卷、减肥。 更细节的就不列了, 直接看去年的吧, 基本差不多 !! 🤣🤣🤣🤣
来源:juejin.cn/post/7454508125772218395
旧Android手机改为个人服务器,不需要root
一、前言
随着手机更新换代的加速,每个人都有一些功能正常,但是闲置的手机,其实现在的手机都是ARM架构的,大多数手机内存还不小,相对于现在各大厂商提供的云服务器来讲,配置已经很不错了,所以这么好的资源能利用起来还是非常不错的~
二、工具介绍
目前能用的工具有很多,比如BusyBox、Linux Deploy、juice ssh、termux,但是很多都是需要手机能够root的,但是root并不是所有手机都能够简单获取到的,所以我这里选取Termux进行操作。
三、什么是Termux
Termux 是一款运行于 Android 系统的开源终端模拟器。提供了 Linux 环境,即使设备不具备 root 权限也可使用。通过自带的包管理器(Pacman、 APT),Termux 可以安装许多现代化的开发和系统维护工具,例如 zsh、Python、Ruby、NodeJS、MySQL 等软件。
四、开始改造
4.1 Termux安装
Termux下载:github.com/termux/term…
安装完成后,可以执行以下命令更新一下各软件包:
pkg update && pkg upgrade
4.2 安装openSSH
成功安装Termux之后,虽然手机是可以像服务器一样执行一些操作,但是毕竟手机管理配置起来没有PC方便,所以可以安装SSH服务,方便PC来远程操作。
# 安装openssh
pkg install openssh
# 默认端口为8022,修改端口
sshd -p 8888
# 启动ssh服务
sshd
4.3 远程连接SSH
要远程连接可以使用终端或者SSH客户端(如:PuTTY、Termius、XShell、MobaXterm等),使用以下命令连接到Termux服务。
ssh -p 8022 <username>@<device_ip>
username
在Android手机上使用Termux搭建服务器,并通过SSH让PC进行登录和操作时,**默认的用户名通常是u0_aXXX
,**可以通过以下方式获取到你的用户名是什么:
# 查询termux服务用户名
whoami
device_ip
通过以下命令获取手机的IP,这里的IP是局域网IP。
# 获取设备IP
ifconfig wlan0
连接时需要密码,由于termux服务默认密码为空,所以需要设置一个密码,具体方式如下:
# 切换管理员账户(如果有)
su
# 设置密码
passwd
五、注意点
5.1 保持服务在线
由于Termux是直接运行到Android手机上的,也是一个APP程序,所以需要注意Termux程序不要退出了。
5.2 内网服务
虽然经过上述方式已经实现了服务器的常规基础配置和操作功能,但是毕竟是在手机上的一个服务,也是受到网络环境限制的,因此如果要保证服务可用,需要保证手机和使用端在同意局域网内。
六、扩展
如果对手机作为网站服务器以及移动无线硬盘相关的内容,欢迎关注,后续会尽快分享相关方法。
来源:juejin.cn/post/7459816593230397494
老弟想自己做个微信,被我一个问题劝退了。。
大家好,我是程序员鱼皮。最近老弟小阿巴放暑假,想找点事情做,于是就来问我:老鲏,我想做个练手项目,有没有什么好的建议?
我说:练手项目的话,就做个自己感兴趣的呗,想加什么功能就加什么,做起来会更舒服~
小阿巴:Emm,我感兴趣的太多了,有没有推荐啊?
我说:那就想想自己经常使用的网站或 APP,选个对业务流程相对熟悉的。
小阿巴思考片刻,一拍脑袋:对啊,我天天用微信,那我就做个微信吧!说不定之后大家都在用我做的软件聊天呢?
我一听,不禁暗自惊叹,没想到小伙子年纪轻轻,野心很大啊!
我说:想法不错,但想做个微信这样的 IM(即时通讯)项目,可没有那么简单,你有什么实现思路么?说来听听?
小阿巴:微信的核心功能是收发消息,我可以把用户 A 发送的消息保存到数据库中,用户 B 进入聊天界面时,从数据库查询出发给他的消息就行。
我一听这个回答,就知道以小阿巴目前的水平,想做出微信是不太可能了。。。
我问:Emm,暂且不考虑用户体验和性能,我们就先实现基础功能吧,你会怎么让用户查看自己的历史消息呢?
小阿巴思考片刻,然后嘴角微微上扬,露出狡黠的笑容:你是不是以为我会说一次性把所有历史消息全部查出来?可惜啊老鲏,你把我想的太天真了,用户可能有成百上千条历史消息,全量加载会很慢,所以我必然会使用 分页
来查询!
我说:行,那你打算怎么分页呢?
小阿巴:这还真难不倒我,这几年我苦练增删改查,分页写得很溜的!纸笔呈上来,看我给你手写 SQL:
select * from message
where user = '鱼皮'
limit 0, 20;
我说:Emm,老弟啊,听我一句劝,咱先别想着做微信了,先实现一个消息管理系统吧。
小阿巴:怎么说?吾 SQL 不亦精乎?
其实这也是一道经典的场景题:即时通讯项目中怎么实现历史消息的下拉分页加载?
下面鱼皮给大家讲解一下。
如何实现下拉分页加载?
业务场景
一般在即时通讯项目(比如聊天室)中,我们会采用下拉分页的方式让用户加载历史消息记录。
区别于标准分页每次只展示当前页面的数据,下拉分页加载是 增量加载 的模式,每次下拉时会请求加载一小部分新数据,并放到已加载的数据列表中,从而形成无限滚动的效果,确保用户体验流畅。
比如用户有 10 条消息记录,以 5 条为单位进行分页,刚进入房间时只会加载最新的 5 条消息:
下拉后,会加载历史的第 6 - 10 条消息:
理解了业务场景后,再看下实现方案,为什么不建议使用传统分页实现下拉加载。
传统分页的问题
在传统分页中,数据通常是 基于页码或偏移量 进行加载的。如果数据在分页过程发生了变化,比如插入新数据、删除老数据,用户看到的分页数据可能会出现不一致,导致用户错过或重复某些数据。
举个例子,对于即时通讯项目,用户可能会持续收到新的消息。如果按照传统分页基于偏移量加载,第一页已经加载了第 1 - 5 行的数据,本来要查询的第二页数据是第 6 - 10 行(对应的 SQL 语句为 limit 5, 5),数据库记录如下:
结果在查询第二页前,突然用户又收到了 5 条新消息,数据库记录就变成了下面这样。原本的第一页,变成了当前的第二页!
这样就导致查询出的第二页数据,正好是之前已经查询出的第一页的数据,造成了消息重复加载。所以不建议采用这种方法。
推荐方案 - 游标分页
为了解决这种问题,可以使用游标分页。使用一个游标来跟踪分页位置,而不是基于页码,每次请求从上一次请求的游标开始加载数据。
一般我们会选择数据记录的唯一标识符(主键)、时间戳、或者具有排序能力的字段作为游标。比如即时通讯系统中的每个消息,通常都有一个唯一自增的 id,就可以作为游标。每次查询完当前页面的数据后,可以将最后一条消息记录的 id 作为游标值传递给前端(客户端)。
当要加载下一页时,前端携带游标值发起查询,后端操作数据库从 id 小于当前游标值的数据开始查询,这样查询结果就不会受到新增数据的影响。
对应的 SQL 语句为:
SELECT * FROM messages
WHERE id < :cursorId
ORDER BY id DESC
LIMIT 5;
扩展知识
其实游标分页是一种经典方案,它的应用场景很多,特别适用于增量数据加载、大数据量的高性能查询和处理。除了 IM 系统获取历史消息记录之外,常见场景还有社交媒体信息流、内容推荐系统、数据迁移备份等等。
最后
小阿巴听完,长叹道:唉,没想到光是这么一个小功能,就把我难住了。
我说:你可别这么想。。。难住你的,可不止这一个小功能啊!想做一个成熟的 IM 系统,除了最基础的消息发送和获取功能外,你得去学习 WebSocket 实时通讯、得考虑到消息收发的性能、得考虑到消息的顺序和一致性、得考虑到消息的存储成本和安全,等等等等。可没那么容易。
小阿巴:得,那我先去做消息管理系统了!🐶
来源:juejin.cn/post/7402517513932931122