注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

SpringBoot获取不到用户真实IP怎么办

今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地...
继续阅读 »

今天周六,Binvin来总结一下上周开发过程中遇到的一个小问题,项目部署后发现服务端无法获取到客户端真实的IP地址,这是怎么回事呢?给我都整懵逼了,经过短暂的思考,我发现了问题的真凶,那就是我们使用了Nginx作的请求转发,这才导致了获取不到客户端真实的IP地址,害,看看我是怎么解决的吧!


a00e8833034583c6895e1582c899a2f3.png


问题原因


客户端请求数据时走的是Nginx反向代理,默认情况下客户端的真实IP地址会被其过滤,使得SpringBoot程序无法直接获得真实的客户端IP地址,获取到的都是Nginx的IP地址。


解决方案:


通过更改Nginx配置文件将客户端真实的IP地址加到请求头中,这样就能正常获取到客户端的IP地址了,下面我一步步带你看看如何配置和获取。


修改Nginx配置文件


在需要做请求转发的配置里添加下面的配置


#这个参数设置了HTTP请求头的Host字段,host表示请求的Host头,也就是请求的域名。通过这个设置,Nginx会将请求的Host头信息传递给后端服务。
proxy_set_header Host $host;
#这个参数设置了HTTP请求头的X−Real−IP字段,remote_addr表示客户端的IP地址。通过这个设置,Nginx会将客户端的真实IP地址传递给后端服务
proxy_set_header X-Real-IP $remote_addr;
#这个参数设置了HTTP请求头的 X-Forwarded-For字段,"X-Forwarded-For"是一个标准的HTTP请求头,用于表示HTTP请求经过的代理服务器链路信息,proxy_add_x_forwarded_for表示添加额外的服务器链路信息。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

修改后我的nginx.conf中的server如下所示


server {
listen 443 ssl;
server_name xxx.com;

ssl_certificate "ssl证书pem文件";
ssl_certificate_key "ssl证书key文件";
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

location / {
root 前端html文件目录;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# 关键在下面这个配置,上面的配置自己根据情况而定就行
location /hello{
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

SpringBoot代码实现


第一种方式:在代码中直接通过X-Forwarded-For获取到真实IP地址


@Slf4j
public class CommonUtil {
/**
* <p> 获取当前请求客户端的IP地址 </p>
*
* @param request 请求信息
* @return ip地址
**/

public static String getIp(HttpServletRequest request) {
if (request == null) {
return null;
}
String unknown = "unknown";
// 使用X-Forwarded-For就能获取到客户端真实IP地址
String ip = request.getHeader("X-Forwarded-For");
log.info("X-Forwarded-For:" + ip);
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
log.info("Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
log.info("WL-Proxy-Client-IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
log.info("HTTP_X_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED");
log.info("HTTP_X_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_CLUSTER_CLIENT_IP");
log.info("HTTP_X_CLUSTER_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
log.info("HTTP_CLIENT_IP:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED_FOR");
log.info("HTTP_FORWARDED_FOR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_FORWARDED");
log.info("HTTP_FORWARDED:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_VIA");
log.info("HTTP_VIA:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getHeader("REMOTE_ADDR");
log.info("REMOTE_ADDR:" + ip);
}
if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("getRemoteAddr:" + ip);
}
return ip;
}

第二种方式:在application.yml文件中加以下配置,直接通过request.getRemoteAddr()并可以获取到真实IP


server:
port: 8090
tomcat:
#Nginx转发 获取客户端真实IP配置
remoteip:
remote-ip-header: X-Real-IP
protocol-header: X-Forwarded-Proto

作者:rollswang
来源:juejin.cn/post/7266040474321027124
收起阅读 »

小知识分享:控制层尽量别暴露这样的接口,避免横向越权。

前言 谈不上是多么厉害的知识,但可能确实有人不清楚或没见过。 我还是分享一下,就当一个小知识点。 如果知道的,就随便逛逛,不知道的,Get到了记得顺手点个赞哈。 正文 1、接口别随便暴露 当一个项目的维护周期拉长的时候,不断有新增的需求,如果经手的人也越...
继续阅读 »

前言



谈不上是多么厉害的知识,但可能确实有人不清楚或没见过。


我还是分享一下,就当一个小知识点。


如果知道的,就随便逛逛,不知道的,Get到了记得顺手点个赞哈。



正文


1、接口别随便暴露



当一个项目的维护周期拉长的时候,不断有新增的需求,如果经手的人也越来越多,接口是会肉眼可见增多的。




此时,如果一个团队没有良好的规范和代码审查机制,就会导致许多不安全的接口被暴露出来。




比如下面这种接口:



/**
* 根据ID查询患者信息
*/

@GetMapping("/{id}")
public AjaxResult getById(@PathVariable("id") Long id) {
PersonInfo personInfo = personInfoService.selectPersonInfoById(id);
return AjaxResult.success(personInfo);
}



这种接口是我们部门以前审查出来的其中一个,类似这样的接口还有很多。




这些接口都是不同的同事在紧凑的工作任务中写的,慢慢就积累出了一堆。




还有些是为了方便,直接通过代码生成器生成的,而代码生成器是把常用的CRUD接口都给你生成出来,如果研发人员没有责任心,可能就直接不管了,想着以后哪一天也许会用上呢。




别以为这种想法的人少啊,你整个职业生涯很可能就会遇见。




这就导致了,一堆用不上又不安全的接口出现了。




服务过政务机构、企事业单位、医疗等行业的工程师应该就知道,这些单位对于安全性的要求其实挺高的,尤其是这些年,会找专门的信息安全公司做攻防演练。




最近两年,很多省市甚至会自发组织全市的信息安全攻防演练,在当前大环境下这也是符合国情的。




而攻防演练的目的之一就是找系统安全漏洞,这里面就会有一个我本章要讲的典型漏洞,接口的横向越权。



2、什么是横向越权



广义的解释就是,该越权行为允许用户获取他们正常情况下无权访问的信息或执行操作。




如果纯粹从理论上理解,是很抽象的,所以我才把这个案例捞出来,让你一次就懂。




我们再回过头看看上面我贴出来的那段很正常的代码,就是根据id获取用户信息,你一定曾经在一些项目中见过这种接口,提供给前端直接调用,比如用户详情、订单详情,只要是和详情有关的,很可能前端会需要这么一个接口。




那么,问题在这里,我们的id是不是有规则的呢?比如下面这样:



1.jpg



可以看出来,id是自增的,增量是2。其实很多中小企业现在用MySQL都喜欢这样设置自增id,有些会设置增量,有些干脆就默认。




试想一下,我如果知道了id=865的用户信息,我也知道大部分中小企业喜欢用自增id,是不是就等于知道了1-1000000的用户信息,而用户信息可能包含身-份-证、手机号、详细住址等非常敏感的内容。




这就是典型的横向越权之一,我明明只应该拿到id=865这个用户的信息,但是通过非正常的方式,我暴力获取了其他100万个用户信息。




一旦真的发生这样的事故,不管最终结果如何,这家公司基本上就进黑名单了,从此在行业中消失。



3、权限控制不了吗



一定会有人产生疑惑,SpringBoot接口怎么可能直接放出来,一定都是有权限控制的,没有权限是根本不可能访问到的。




我打个比方,如果是后台管理这种,他是有登录的,登录后会产生token,token中是可以包含角色权限的,那么这种是没有问题的。




但如果没有登录操作呢,比如小程序这种,你打开就直接是首页各种信息,前端调接口很可能传递的只有网关层的token,又该如何呢。




尤其是小程序雨后春笋一般涌现的那几年,我曾经打开过很多小程序,都是没什么权限校验的,就是直接点来点去。




直到近几年,这种现象才慢慢消失,很多小程序打开后,会提示你授权登录,比如微信小程序,你一定遇到过打开小程序后让你授权登录的场景,如果不授权登录,你绝对做不了很多操作,这是很多互联网企业的安全意识都加强的结果。




我所在的公司早年刚进入医疗行业就经历过这种事情,为了占坑拿下了很多项目,但缺乏安全意识和管理规范,程序员也是来来走走,你写两个我写两个,导致不少接口都存在安全隐患。




直到被攻防演练攻破,甲方下发整改通知,还要我们写事故报告、原因、解决方案等等一大堆,我们才慌了。




连夜开会讨论出一套基本的安全整改思路,然后开始加班加点做安全改造。




我印象最深的就是其中这个接口横向越权,只传递了网关层的token,而没有细化到个人的权限控制,导致被信息安全公司通过抓包等一些我不了解的技术把token拿到了,然后直接横向获取到了很多用户敏感信息。




当时这个事情闹得很厉害,考虑到只是攻防演练,同时客户方对公司还保留信任,才只要求我们限期整改,否则就直接替换了。




所以,记得以后写接口的时候别只考虑业务逻辑,安全性也是考量之一。



4、如何防范



防范的方式,我归纳了这么几点:


1、不用的接口尽量删掉,这样也避免了多余接口埋下的安全隐患;


2、团队要有安全规范,比如敏感字段加密,引入代码审查机制,缩小安全隐患出现的范围;


3、带登录的终端,除了网关层校验,要精确控制登录用户的角色及权限;


4、不带登录的终端,除了网关层校验,要根据用户的唯一信息,来做授权登录,授权不成功不允许做其他操作,这也是现在比较流行的方式。


我个人理解,第4点和第3点本质一样,因为不带登录,所以要想办法制造登录,而目前比较友好的方式还是一键授权登录,不管是根据openid、手机号等等,总之要找到一个规则,这样省去了用户手动操作登录的时间。




总之,一定要控制用户只能看到属于自己的内容,避免横向越权。



总结



如果写的不好,还望大家原谅,只是分享了曾经工作中发生过的和安全改造有关的事情。


现在的程序员其实了解和接收的知识技术是挺多的,许多人其实都知道这些。


希望不知道的人,能够因为我的文章得到一点点帮助。


最后,大家其实可以去试一试,打开微信小程序,搜索下你们所在城市的某某中心医院,看看这样的医疗小程序打开后是什么样的,是不是有授权登录,或者其他方式来控制权限,搞不好一部分人能遇到有意思的事情。


作者:程序员济癫
来源:juejin.cn/post/7276467933235642405
收起阅读 »

分分钟带你实现视频消息的在线播放和本地播放|干货教程

发送视频消息是即时通讯应用中很常见的功能,现在的视频播放场景五花八门,眼瞅快下班,接到产品需求前提条件:实现方法:1、调用下载,下载成功后进行播放处理;EMMessage msg = EMClient.getInstance().chatManager().g...
继续阅读 »

有种需求叫"下班前实现"

发送视频消息是即时通讯应用中很常见的功能,现在的视频播放场景五花八门,眼瞅快下班,接到产品需求



如何快速实现这个需求,好准点下班回家抢显卡 ,好提升自己的工作效率,你只要熟读本篇文章,分分钟带你实现!

前提条件:

  • 完成环信IM SDK的初始化
    (没完成的参考文档:
    SDK初始化

  • 实现发送视频消息和接收视频消息
    (没实现的参考文档:
    发送和接收消息

实现方法:

一、本地播放实现方法

1、调用

EMChatManager#downloadAttachment

下载,下载成功后进行播放处理;

EMMessage msg = EMClient.getInstance().chatManager().getMessage(msgId);
EMCallBack callback = new EMCallBack() {
public void onSuccess() {
EMLog.e(TAG, "onSuccess" )
//下载成功,进行播放处理
}

public void onError(final int error, String message) {
EMLog.e(TAG, "offline file transfer error:" + message);
}

public void onProgress(final int progress, String status) {
EMLog.d(TAG, "Progress: " + progress);
}
};
msg.setMessageStatusCallback(callback);
EMClient.getInstance().chatManager().downloadAttachment(msg);

2、如果对本地存储的路径有特殊要求:

1)可以先通过

EMFileMessageBody#setlocalUrl

去修改路径;

2)再调用

EMChatManager#downloadAttachment

下载(下载操作可以参考上面);


二.在线播放实现方法

1、在接收消息监听onMessageReceived里,接收到视频消息;

EMMessageListener msgListener = new EMMessageListener() {
// 收到消息,遍历消息队列,解析和显示。
@Override
public void onMessageReceived(List<EMMessage> messages) {

}
};
// 注册消息监听
EMClient.getInstance().chatManager().addMessageListener(msgListener);
// 解注册消息监听
EMClient.getInstance().chatManager().removeMessageListener(msgListener);

2、拿到

EMVideoMessageBody#getRemoteUrl

拿到消息的远程服务器存储地址

// 从服务器端获取视频文件。
String imgRemoteUrl = ((EMVideoMessageBody) body).getRemoteUrl();

3、用第三方或者是VideoView进行播放视频(例子里使用的VideoView);

vidw = (VideoView) findViewById(R.id.viewview);
vidw.setVideoPath(imgRemoteUrl+"?em-redirect=true");
vidw.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
return true;
}
});
vidw.start();

按照上面的步骤测试,发现还是不能在线播放的朋友,就需要看下,你使用的Appkey需要联系环信商务经理免费开通在线播放功能

另需注意:imgRemoteUrl 需要拼接 ?em-redirect=true,否则也会出现播放不了的情况;

大功告成!快把代码推上去,回家抢显卡!

此教程适用于Android端,其他端兄弟们报一丝,下次一定!

相关文档:

收起阅读 »

很容易中招的一种索引失效场景,一定要小心

快过年,我的线上发布出现故障 “五哥,你在上线吗?”,旁边有一个声音传来。 “啊,怎么了?”。真是要命,在上线发布时候,我最讨厌别人叫我的名字 。我慌忙站起来,看向身后,原来是 建哥在问我。我慌忙的问,怎么回事。 “DBA 刚才在群里说,Task数据库 cpu...
继续阅读 »

快过年,我的线上发布出现故障


“五哥,你在上线吗?”,旁边有一个声音传来。


“啊,怎么了?”。真是要命,在上线发布时候,我最讨厌别人叫我的名字 。我慌忙站起来,看向身后,原来是 建哥在问我。我慌忙的问,怎么回事。


“DBA 刚才在群里说,Task数据库 cpu 负载增加!有大量慢查询”,建哥来我身边,跟我说。


慢慢的,我身边聚集着越来越多的人


image.png


“你在上线Task服务吗?改动什么内容了,看看要不要立即回滚?”旁边传来声音。此时,我的心开始怦怦乱跳,手心发痒,紧张不已。


我检查着线上机器的日志,试图证明报警的原因不是出在我这里。


我对着电脑,微微颤抖地回答大家:“我只是升级了基础架构的Jar包,其他内容没有改动啊。”此时我已分不清是谁在跟我说话,只能对着电脑作答……


这时DBA在群里发送了一条SQL,他说这条SQL导致了大量的慢查询。


我突然记起来了,我转过头问林哥:“林哥,你上线了什么内容?”这次林哥有代码的变更,跟我一起上线。我觉得可能是他那边有问题。


果然,林哥看着代码发呆。他嘟囔道:“我添加了索引啊,怎么会有慢查询呢?”原来慢查询的SQL是林哥刚刚添加的,这一刻我心里的石头放下了,问题并不在我,我轻松了许多。


“那我先回滚吧”,幸好我们刚发布了一半,现在回滚还来得及,我尝试回滚机器。此刻我的紧张情绪稍稍平静下来,手也不再发抖。


既然不是我的问题,我可以以吃瓜的心态,暗中观察事态的发展。我心想:真是吓死我了,幸好不是我的错。


然而我也有一些小抱怨:为什么非要和我一起搭车上线,出了事故,还得把我拖进来。


故障发生前的半小时


2年前除夕前的一周,我正准备着过年前的最后一次线上发布,这时候我刚入职两个月,自然而然会被分配一些简单的小活。这次上线的内容是将基础架构的Jar包升级到新版本。一般情况下,这种配套升级工作不会出问题,只需要按部就班上线就行。


“五哥,你是要上线 Task服务吗?”,工位旁的林哥问我,当时我正做着上线前的准备工作。


“对啊,马上要发布,怎么了?”,我转身回复他。


“我这有一个代码变更,跟你搭车一起上线吧,改动内容不太多。已经测试验证过了”,林哥说着,把代码变更内容发给我,简单和我说了下代码变更的内容。我看着改动内容确实不太多,新增了一个SQL查询,于是便答应下来。我重新打包,准备发布上线。


半小时以后,便出现了文章开头的情景。新增加的SQL 导致大量慢查询,数据库险些被打挂。


为什么加了索引,还会出现慢查询呢?


”加了索引,为什么还有慢查询?“,这是大家共同的疑问。


事后分析故障的原因,通过 mysql explain 命令,查看该SQL 确实没有命中索引,从而导致慢查询。


这个SQL 大概长这个样子!我去掉了业务相关的部分。


select * from order_discount_detail where orderId = 1123;


order_discount_detailorderId 这一列上确实加了索引,不应该出现慢查询,乍一看,没有什么问题。我本能的想到了索引失效的几种场景。难道是类型不匹配,导致索引失效?


果不其然, orderId 在数据库中的类型 是 varchar 类型,而传参是按照 long 类型传的。


复习一下: 类型转换导致索引失效


类型转换导致索引失效,是很容易犯的错误


因为在某些特殊场景下要对接外部订单,存在订单Id为字符串的情况,所以 orderId被设计成 varchar 字符串类型。然而出问题的场景比较明确,订单id 就是long类型,不可能是字符串类型。


所以林哥,他在使用Mybatis 时,直接使用 long 类型的 orderId字段传参,并且没有意识到两者数据类型不对。


因为测试环境数据量比较小,即使没有命中索引,也不会有很严重的慢查询,并且测试环境请求量比较低,该慢查询SQL 执行次数较少,所以对数据库压力不大,测试阶段一直没有发现性能问题。


直到代码发布到线上环境————数据量和访问量都非常高的环境,差点把数据库打挂。


mybatis 能避免 “类型转换导致索引失效” 的问题吗?


mybatis能自动识别数据库和Java类型不一致的情况吗?如果发现java类型和数据库类型不一致,自动把java 类型转换为数据库类型,就能避免索引失效的情况!


答案是不能。我没找到 mybatis 有这个能力。


mybatis 使用 #{} 占位符,会自动根据 参数的 Java 类型填充到 SQL中,同时可以避免SQL注入问题。


例如刚才的SQL 在 mybatis中这样写。


select * from order_discount_detail where orderId = #{orderId};


orderId 是 String 类型,SQL就变为


select * from order_discount_detail where orderId = ‘1123’;


mybatis 完全根据 传参的java类型,构建SQL,所以不要认为 mybatis帮你处理好java和数据库的类型差异问题,你需要自己关注这个问题!


再次提醒,"类型转换导致索引失效"的问题,非常容易踩坑。并且很难在测试环境发现性能问题,等到线上再发现问题就晚了,大家一定要小心!小心!


险些背锅


可能有朋友疑问,为什么发布一半时出现慢查询,单机发布阶段不能发现这个问题吗?


之所以没发现这个问题,是因为 新增SQL在 Kafka消费逻辑中,由于单机发布机器启动时没有争抢到 kafka 分片,所以没有走到新代码逻辑。


此外也没有遵循降级上线的代码规范,如果上线默认是降级状态,上线过程中就不会有问题。放量阶段可以通过降级开关快速止损,避免回滚机器过程缓慢而导致的长时间故障。


不是我的问题,为什么我也背了锅


因为我在发布阶段没有遵循规范,按照规定的流程应该在单机发布完成后进行引流压测。引流压测是指修改机器的Rpc权重,将Rpc请求集中到新发布的单机上,这样就能提前发现线上问题。


然而由于我偷懒,跳过了单机引流压测。由于发布的第一台机器没有抢占到Kafka分片,因此无法执行新代码逻辑。即使进行了单机引流压测,也无法提前发现故障。虽然如此,但我确实没有遵循发布规范,错在我。


如果上线时没有出现故障,这种不规范的上线流程可能不会受到责备。但如果出现问题,那只能怪我倒霉。在复盘过程中,我的领导抓住了这件事,给予了重点批评。作为刚入职的新人,被指责确实让我感到不舒服。


快要过年了,就因为搭车上线,自己也要承担别人犯错的后果,让我很难受。但是自己确实也有错,当时我的心情复杂而沉重。


两年前的事了,说出来让大家吃个瓜,乐呵一下。如果这瓜还行,东东发财的小手点个赞


作者:五阳
来源:juejin.cn/post/7305572311812636683
收起阅读 »

什么?你设计接口什么都不考虑?

后端接口设计 如果让你设计一个接口,你会考虑哪些问题? 1.接口参数校验 接口的入参和返回值都需要进行校验。 入参是否不能为空,入参的长度限制是多少,入参的格式限制,如邮箱格式限制 返回值是否为空,如果为空的时候是否返回默认值,这个默认值需要和前端协商 ...
继续阅读 »

后端接口设计


如果让你设计一个接口,你会考虑哪些问题?


image.png


1.接口参数校验


接口的入参和返回值都需要进行校验。



  • 入参是否不能为空,入参的长度限制是多少,入参的格式限制,如邮箱格式限制

  • 返回值是否为空,如果为空的时候是否返回默认值,这个默认值需要和前端协商


2.接口扩展性


举个例子,比如用户在进行某些操作之后,后端需要进行消息推送,那么是直接针对这个业务流程来开发一个专门为这个业务流程服务的消息推送功能呢?还是说将消息推送整合为一个通用的接口,其他流程都可以进行调用,并非针对特定业务。


这个场景可能光靠说不是很能理解,大家想想策略工厂设计模式,是不是可以根据不同的策略,来选择不同的实现方式呢?再结合上面的这个例子,是否对扩展性有了进一步的理解呢?


3.接口幂等设计


什么是幂等呢?幂等是指多次调用接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致


举个例子,在购物商场里面你用手机下单,要买某个商品,你需要去支付,然后你点击了支付,但是因为网速问题,始终没有跳转到


支付界面,于是你又连点了几次支付,那在没有做接口幂等的时候,是不是你点击了多少次支付,我们就需要执行多少次支付操作?


所以接口幂等到的是什么?防止用户多次调用同一个接口



  • 对于查询和删除类型的接口,不论调用多少次,都是不会产生错误的业务逻辑和数据的,因此无需幂等处理

  • 对于新增和修改,例如转账等操作,重复提交就会导致多次转账,这是很严重的,影响业务的接口需要做接口幂等的处理,跟前端约定好一个固定的token接口,先通过用户的id获取全局的token,写入到Redis缓存,请求时带上Token,后端做处理


image.png


4.关键接口日志打印


关键的业务代码,是需要打印日志进行监测的,在入参和返回值或者如catch代码块中的位置进行日志打印



  • 方便排查和定位线上问题,划清责任

  • 生产环境是没有办法进行debug的,必须依靠日志查问题,看看到底是出现了什么异常情况


5.核心接口要进行线程池隔离


分类查询啊,首页数据等接口,都有可能使用到线程池,某些普通接口也可能会使用到线程池,如果不做线程池隔离,万一普通接口出现bug把线程池打满了,会导致你的主业务受到影响


image.png


6.第三方接口异常重试


如果有场景出现调用第三方接口,或者分布式远程服务的话,需要考虑的问题



  • 异常处理


    比如你在调用别人提供的接口的时候,如果出现异常了,是要进行重试还是直接就是当做失败


  • 请求超时


    有时候如果对方请求迟迟无响应,难道就一直等着吗?肯定不是这样的,需要设法预估对方接口响应时间,设置一个超时断开的机制,以保护接口,提高接口的可用性,举个例子,你去调用别人对外提供的一个接口,然后你去发http请求,始终响应不回来,此时你又没设置超时机制,最后响应方进程假死,请求一直占着线程不释放,拖垮线程池。


  • 重试机制


    如果调用对外的接口失败了或者超时了,是否需要重新尝试调用呢?还是失败了就直接返回失败的数据?



7.接口是否需要采用异步处理


举个例子,比如你实现一个用户注册的接口。用户注册成功时,发个邮件或者短信去通知用户。这个邮件或者发短信,就更适合异步处理。总不能一个通知类的失败,导致注册失败吧。 那我们如何进行异步操作呢?可以使用消息队列,就是用户注册成功后,生产者产生一个注册成功的消息,消费者拉到注册成功的消息,就发送通知。


image.png


8.接口查询优化,串行优化为并行


假设我们要开发一个网站的首页,我们设计了一个首页数据查询的接口,这个接口需要查用户信息,需要查头部信息,需要查新闻信息


等等之类的,最简单的就是一个一个接口串行调用,那要是想要提高性能,那就采取并行调用的方式,同时查询,而不是阻塞


可以使用CompletableFuture(推荐)或者FutureTask(不推荐)


        Map<Long, List<SubjectLabelBO>> map = new HashMap<>();
      List<CompletableFuture<Map<Long, List<SubjectLabelBO>>>> completableFutureList =
      categoryBOList.stream().map(category ->
              CompletableFuture.supplyAsync(() -> getLabelBOList(category), labelThreadPool)
      ).collect(Collectors.toList());

      completableFutureList.forEach(future -> {
          try {
              Map<Long, List<SubjectLabelBO>> resultMap = future.get(); //这里会阻塞
              map.putAll(resultMap);
          } catch (Exception e) {
              e.printStackTrace();
          }
      });
       
public Map<Long, List<SubjectLabelBO>> getLabelBOList(SubjectCategoryBO category) {...}

9.高频接口注意限流


自定义注解 + AOP


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
   int value() default 1;
   int durationInSeconds() default 1;
}

@Aspect
@Component
public class RateLimiterAspect {

   private final ConcurrentHashMap<String, RateLimiter> rateLimiters = new ConcurrentHashMap<>();

   @Pointcut("@annotation(RateLimiter)")
   public void rateLimiterPointcut(RateLimiter rateLimiterAnnotation) {
  }

   @Around("rateLimiterPointcut(rateLimiterAnnotation)")
   public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiterAnnotation) throws Throwable {
       int permits = rateLimiterAnnotation.value();
       int durationInSeconds = rateLimiterAnnotation.durationInSeconds();

       // 使用方法签名作为 RateLimiter 的 key
       String key = joinPoint.getSignature().toLongString();
       com.google.common.util.concurrent.RateLimiter rateLimiter = rateLimiters.computeIfAbsent(key, k -> com.google.common.util.concurrent.RateLimiter.create((double) permits / durationInSeconds));

       // 尝试获取令牌,如果获取到则执行方法,否则抛出异常
       if (rateLimiter.tryAcquire()) {
           return joinPoint.proceed();
      } else {
           throw new RuntimeException("Rate limit exceeded.");
      }
  }
}

@RestController
public class ApiController {

   @GetMapping("/api/limited")
   @RateLimiter(value = 10, durationInSeconds = 60) //限制为每分钟 10 次请求
   public String limitedEndpoint() {
       return "This API has a rate limit of 10 requests per minute.";
  }

   @GetMapping("/api/unlimited")
   public String unlimitedEndpoint() {
       return "This API has no rate limit.";
  }
}

10.保障接口安全


配置黑白名单,用Bloom过滤器实现黑白名单的配置


具体代码不贴出来了,大家可以去看看布隆过滤器的具体使用


11.接口控制锁粒度


在高并发场景下,为了防止超卖等情况,我们会对共享资源进行加锁的操作来保证线程安全的问题,但是如果加锁的粒度过大,是会影响


到接口性能的。那什么是加锁粒度呢?举一个例子,你带了一封情书回家,但是不想被爸妈发现,然后你偷偷回到房间里放到一个可以锁


住的抽屉里面,而不用把房间的门锁给锁上。 无论是使用synchronized加锁还是redis分布式锁,只需要在共享临界资源加锁即可,不涉


及共享资源的,就不必要加锁。



  • 锁粒度过大:


    把方法A和方法B全部进行加锁,但是实际上我只是想要对A加锁,这就是锁粒度过大



void test(){
   synchronized (this) {
      B();
      A();
  }
}


  • 缩小锁粒度


void test(){
      B();
   synchronized (this) {
      A();
  }
}

12.避免长事务问题


长事务期间可能伴随cpu、内存升高、严重时会导致服务端整体响应缓慢,导致在线应用无法使用


产生长事务的原因除了sql本身可能存在问题外,和应用层的事务控制逻辑也有很大的关系。



  • 如何尽可能的避免长事务问题呢?


    1.RPC远程调用不要放到事务里面


    2.一些查询相关的操作如果可用,尽量放到事务外面


    3.并发场景下,尽量避免使用@Transactional注解来操作事务,使用TransactionTemplate的编排式事务来灵活控制事务的范围



在原先使用@Transactional来管理事务的时候是这样的


@Transactional
public int createUser(User user){
   //保存用户信息
   userDao.save(user);
   passCertDao.updateFlag(user.getPassId());
   // 该方法为远程RPC接口
   sendEmailRpc(user.getEmail());
   return user.getUserId();
}

使用TransactionTemplat进行编排式事务


@Resource
private TransactionTemplate transactionTemplate;

public int createUser(User user){
   transactionTemplate.execute(transactionStatus -> {
     try {
        userDao.save(user);
        passCertDao.updateFlag(user.getPassId());
    } catch (Exception e) {
        // 异常手动设置回滚
        transactionStatus.setRollbackOnly();
    }
     return true;
  });
// 该方法为远程RPC接口
   sendEmailRpc(user.getEmail());
   return user.getUserId();
}

作者:radient
来源:juejin.cn/post/7343548913034133523
收起阅读 »

如何给application.yml文件的敏感信息加密?

Hello,大家好,我是云帆。在我们传统的基于SpringBoot开发的项目中,在配置文件里,或多或少的都会有一些敏感信息,这样就会丢失一定的安全性,所以我们就需要,对敏感信息进行加密。我们可以使用jasypt工具进行加密。 好了废话不多少,直接进入正题: 1...
继续阅读 »

Hello,大家好,我是云帆。在我们传统的基于SpringBoot开发的项目中,在配置文件里,或多或少的都会有一些敏感信息,这样就会丢失一定的安全性,所以我们就需要,对敏感信息进行加密。我们可以使用jasypt工具进行加密。


好了废话不多少,直接进入正题:


1. 导入依赖


<dependency>  
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>

我的Demo里使用的是SpringBoot3.0之后的版本,所以大家如果像我一样都是基于SpringBoot3.0之后的,jasypt一定要使用3.0.5以后的版本。


2. 使用jasypt


我们在配置文件里写几行配置


jasypt:  
encryptor:
password: sdjsdbshdbfuasd
property:
prefix: ENC(
suffix: )


password是加密密码,必须配置这一项,值可以随便输入。
prefixsuffix是默认配置,也可以自定义,默认值就是ENC(),这个是自动解密使用的。



2.1. 加/解密


jasypt 提供了一个工具类接口,StringEncryptor,这个接口提供了加解密方法。下面是他的源码。


public interface StringEncryptor {  

/**
* 加密输入信息
*
* @param 要加密的信息
* @return 加密结果
*/

public String encrypt(String message);


/**
* 解密加密信息
*
* @param 加密信息(encryptedMessage) 要解密的加密信息
* @return 解密结果
*/

public String decrypt(String encryptedMessage);

}

我们在 test 测试类中,将要进行加密的文本使用encrypt方法进行加密


@SpringBootTest  
@Slf4j
class JasryptApplicationTests {

@Autowired
private StringEncryptor stringEncryptor;

@Test
void contextLoads() {
String username = stringEncryptor.encrypt("root");
String password = stringEncryptor.encrypt("root");
log.info("username encrypt is {}", username);
log.info("password encrypt is {}", password);
log.info("username decrypt is {}", stringEncryptor.decrypt(username));
log.info("password decrypt is {}", stringEncryptor.decrypt(password));
}

}

上边代码,加密的内容是,MySQL的用户名密码,同时对它们进行加密和解密,你当然可以对任意配置信息进行加解密操作。看看输出内容:


2023-07-23T18:59:50.621+08:00  INFO 9489 --- [           main] c.e.jasrypt.JasryptApplicationTests      : username encrypt is 61zSoixtNayUruXt5x84kEKO9jGnZObTGCa1+k5Yg9F7qSUiZvp5fG31AMuVqrot
2023-07-23T18:59:50.621+08:00 INFO 9489 --- [ main] c.e.jasrypt.JasryptApplicationTests : password encrypt is a6snCZCkbQFKkQqxN2bS18ags04yZxH+THwIL5RjGocEjG9sLkJvvasPFFVxEBWv
2023-07-23T18:59:50.623+08:00 INFO 9489 --- [ main] c.e.jasrypt.JasryptApplicationTests : username decrypt is root
2023-07-23T18:59:50.630+08:00 INFO 9489 --- [ main] c.e.jasrypt.JasryptApplicationTests : password decrypt is root

加密默认使用的是PBEWITHHMACSHA512ANDAES_256加密
我们将密文,替换到数据源,配置:


spring:  
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/honey?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: ENC(61zSoixtNayUruXt5x84kEKO9jGnZObTGCa1+k5Yg9F7qSUiZvp5fG31AMuVqrot)
password: ENC(a6snCZCkbQFKkQqxN2bS18ags04yZxH+THwIL5RjGocEjG9sLkJvvasPFFVxEBWv)

⚠️注意别忘了加上前缀和后缀,如上边代码。


这个时候就已经完成了,但是官方不建议我们将加密密码放到配置文件中,我们应作为系统属性、命令行参数或环境变量传递,只要其名称是 jasypt.encryptor.password,就能正常工作。


我们可以将项目打为jar包然后使用 java -jar命令


java -jar jasrypt-0.0.1-SNAPSHOT.jar --jasypt.encryptor.password=加密密码

⚠️加密密码必须与之前给属性加密时用的加密密码一致。


3. 结尾


好了,分享到这里就结束了,希望小伙伴们多多点赞,如果有建议,欢迎留言。


此致


作者:寒江雪369
来源:juejin.cn/post/7258850748149203000
收起阅读 »

我使用 GPT-4o 帮我挑西瓜

hi,这里是小榆。在 5 月 15 日,OpenAI 旗下的大模型 GPT-4o 已经发布,那时网络上已经传开, 但很多小伙伴始终没有看到 GPT-4o 的体验选项。 在周五的时候,我组建的 ChatGPT 交流群的伙伴已经发现了 GPT-4o 这个选项了,是...
继续阅读 »

hi,这里是小榆。在 5 月 15 日,OpenAI 旗下的大模型 GPT-4o 已经发布,那时网络上已经传开, 但很多小伙伴始终没有看到 GPT-4o 的体验选项。


在周五的时候,我组建的 ChatGPT 交流群的伙伴已经发现了 GPT-4o 这个选项了,是在没有充值升级 Plus 版的情况下,意味着这个模型已经更新给大众免费使用了。


图片


我看到后,立马放下手中正在编写的代码,开启 GPT 登录后果然有一个  GPT-4o 的选项,然后发现它的功能比 3.5 模型更加全面了,它不仅能够全面覆盖听觉、视觉和语音。


图片


我体验了一把语音对话,非常的丝滑没感觉到延迟,仿佛真的和“女朋友”在聊天。意味着它能够感知我们的呼吸节奏,并用更加丰富的语气实时回应,还会在适当的时候打断对话。


那么,就让我们了解 GPT-4o 这个大模型吧,首先 GPT-4 是比 3.5 版本更强的版本,即为 4.0+,后面还有一个‘o’ ,它的全称是‘Omni’,即‘全能’的意思。


图片


它能够接受文本、音频和图像的任意组合输入,并生成回答。响应速度快至 232 毫秒,平均 320 毫秒,与人类对话的速度可以说是很接近平均了。


并且,随着这次版本的发布,GPTo 与 ChatGPT Plus 会员版的所有功能,包括视觉、联网、记忆、执行代码、GPT Store 等,都会免费开放给大家。新语音模式将在几周内优先向 Plus 用户开放。


图片


在直播现场,OpenAI CTO Murati 谦虚道:“这是将 GPT-4 级别的模型开放给大家。”


同时将这一版本的模型提供 API 服务,价格随之减少一半,速度比之提高一倍,单位时间内调用次数是原来的 5 倍了。


OpenAI 的总裁 Brockman 也给大家在线演示,将两个 ChatGPT 相互对话,对话内容比较丰富了,不知不觉还唱起歌来了,整的还挺有意思。


发现还有伙伴和我一样体验到了不错的应用场景,当我使用手机版的 GPT-4o ,我可以实时拍照询问它,给我一些建议,如何挑西瓜榴莲等,询问给出差异分析,借助 AI 的力量进行挑瓜。


图片


你甚至可以拍摄一批西瓜的照片,上传给 GPT-4o。


你:“这瓜保熟吗?”


AI:“(警觉)...你故意找茬是不是。”


AI:“我一AI,还能给你挑生瓜蛋子不成?!”


图片


图片


我们可以看到上图中的西瓜是根据自己拍摄的西瓜图并且标记了序号,询问 GPT 哪个西瓜很甜,GPT 一通分析,虽然目前只能根据形状和成色来识别西瓜,推荐挑选的 6 号西瓜果然很不错,甚至皮也很薄。


聪明的你,脑洞大开已经熟练使用 AI 了,你或许会有很多问题问他。


你:“这盒牛奶含有什么成分?”


AI:“......”


你(掏出手机,打开摄像头扫描):“这盒牛奶有科技成分吗?卫生是否达标?”


AI:“......”


你(掏出手机,打开摄像头扫码):“请问这个妹妹面相如何?是否旺夫?”


AI:“......”


显然,上面有一部分是我的遐想,但我觉得已经不远了。


如果 AI 没有被一方人污染,升级完全体的情况下,它真的能够为我们参谋很多,洞悉很多潜在的信息,毕竟你能骗我,但是 AI 不会骗我。


好了,大家可以多去体验新产品吧,的确会很有趣。但是发现很多小伙伴 不仅电脑版本的 GPT 无法体验,更别说手机版本的 GPT 了。


目前来说对一些普通用户体验的确很困难,被迫使用某些企业研发的 AI 产品或套壳产品,还被迫收费。但也不是没有办法,别说我还挺想撰写一篇从 0 到 1 给大家完全科普使用。


okay,分享(暗示)到这里,大家如果有感兴趣,可以后台回复 GPT 加入群聊,将会有更多咨询和体验内容分享。


作者:程序员小榆
来源:juejin.cn/post/7370327567763816498
收起阅读 »

坚持与确定性:毒药还是良药?

前段时间跟几个大龄程序员一起吃饭,聊了大家的现状,后来写了篇博客总结了一下《从大龄程序员现状聊聊出路》,本想着给朋友们提供些观点和思路,结果被有些网友批评了。 1. 我的认知达不到赚快钱 有的网友认为我在瞎扯,有的觉得我在灌鸡汤,还有的认为我在指错路。 文中虽...
继续阅读 »

前段时间跟几个大龄程序员一起吃饭,聊了大家的现状,后来写了篇博客总结了一下《从大龄程序员现状聊聊出路》,本想着给朋友们提供些观点和思路,结果被有些网友批评了。


1. 我的认知达不到赚快钱


有的网友认为我在瞎扯,有的觉得我在灌鸡汤,还有的认为我在指错路。


文中虽然总结了一些自认为有价值的观点,本想着让看到的朋友能够少走弯路,尽早建立正确的思维方式。但是依旧没法满足部分网友想轻易赚块钱的思路。我实在是感到惭愧。


对于想赚快钱的网友,你们也可以去参考下刑法里的路子,或者去网上买点教你如何赚钱的课嘛,何必在这浪费口舌呢。


我在那篇文章里提到了坚持、积累、人脉、资源、推广等关键因素,每一个都不是速成的。每一项都需要长期努力,不是一蹴而就的。反正在我的认知里,每一个成功的背后都是:找到了正确的道路后,不停的微调,默默的坚持。


2. 少有的坚持


各位也可以试想下,想想这些年看过的赚钱课程,或者什么创业导师商学社等等,他们除了制造些焦虑,讲一些假大空不好落地的概念之外,其余的好像都是正确的废话吧?


远的不说,就以程序员做副业为例,有各种建议,比如做自媒体、接项目、搞某鱼某宝或卖课。这些导师对这些方法都提供了具体的行动方案,请问你是否开始行动了?是否坚持了?是否取得了成效?


这些路子是不是都是对的呢?无疑都是对的,但是每条路子都不好走,都需要自己去坚持去深钻。你都不愿意去深钻,怎么可能赚的了那份钱呢?


真的是应了那句话:我知道了那么多道理,依然过不好这一生。知道、做了、做到、做得好,是4个完全不同的概念。


许多人回忆自己几十年好像什么都没做,好像又做了很多事。为什么出现这种幻觉?因为没有坚持、没有产品思维,没有拿得出手的作品嘛。好的作品不用多,一个就够!


希望所有的朋友,都能具备产品思维。就是:认真、坚持的打磨好一件事情,把这件事情做到产生价值甚至巨大的价值,做到对他人有利,做到能够盈利,然后再去打磨下一个产品


坚持绝对是创业或者副业路上的一副良药。道阻且长,行则将至!与君共勉!



3. 确定性是一副毒药


后来我又思考了下,为什么有些程序员朋友对于正确的事情却无法坚持执行下去呢?这可能不是他们的问题,可能是我们程序员这个群体的问题。


为什么?因为就算坚持下去,也不一定能拿到想要的结果,这里面不确定的成分太高了,他们不愿意面对这份不确定性。


程序员这个群体是搞技术的,技术本身就是确定性的,非此即彼嘛。


大部分程序员学技术这条路子也是冲着确定性去的,比如我就算一个,当年学计算机就是为了确定性的找到一份稳定的工作,我早年特别害怕面对不确定性。


程序员学会了某个前端技术或者后端技术,对应的就能确定的拿到了一份什么等级的薪资。埋头苦干,确定的就会有一个好的结果。


但是,那些创业和副业都是不确定性的。创业和副业不是说通过学习或者努力就能达到一个确定性的位置。所以说呀,与其说有些网友在喷我,倒不如说是他们不敢面对不确定性。


但是,朋友们,时代在变化,我们每个人都要敢于面对不确定性。


早年我们处在IT红利期,只要努力,到处是机会。确定的程序,确定的路径,确定的繁荣,让我们误以为这个世界可以一直确定下去。这种确定性让我们慢慢上瘾,最终失去了面对不确定性的能力和勇气


想想当年学计算机那么多人是为了图个安稳,结果过了十几年,还是没能逃脱不确定的因素,如果早日接受不确定性,可能眼界会更开阔一些。


没想到十年前的那颗子弹最终还是中了自己的眉心。


以后,随着AI的发展,我觉得未来一定是一个超级个体的时代,我们会面临更多的不确定性。以后社会发展也会伴随着更多的不确定性,只有我们要调整好心态,接受它,才能在以后的道路上越走越宽,越走越顺。


趁现在就改变,千万别让确定性这副慢性毒药继续侵蚀我们的思想。




原文链接: mp.weixin.qq.com/s/9QZHF7ria…


作者:程序员半支烟
来源:juejin.cn/post/7379414160642654260
收起阅读 »

如何组装一台高性价比的电脑

前言最近想配置一台电脑,由于在这方面是小白,就花了一部分时间以及精力在这方面,相信你看完,也能轻轻松松装电脑。目标高性价比、小钱办大事电脑配置基础知识组装一台台式机需要考虑多个组件,以下是一份基本的装机配置清单,包括了主要硬件和一些可选配件我把他们划分为必须和...
继续阅读 »

1718195402741.png

前言

最近想配置一台电脑,由于在这方面是小白,就花了一部分时间以及精力在这方面,相信你看完,也能轻轻松松装电脑。

目标

高性价比、小钱办大事

电脑配置基础知识

组装一台台式机需要考虑多个组件,以下是一份基本的装机配置清单,包括了主要硬件和一些可选配件

我把他们划分为必须和非必须(可理解为表单的必填或者非必填)

有了必须项目的配置就可以组装出一台电脑,非必须项目属于个人需求(可理解为个性化定制)

必须项

  1. 处理器(CPU) :

    • 选择适合需求的处理器,如Intel Core系列或AMD Ryzen系列。
  2. 显卡(GPU) :

    • 集成显卡或独立显卡,如NVIDIA GeForce或AMD Radeon系列。
  3. 主板(Motherboard) :

    • 根据CPU选择相应插槽类型的主板,如ATX、Micro-ATX或Mini-ITX规格。
  4. 内存(RAM) :

    • 至少8GB DDR4内存,更高需求可以选择16GB或32GB。
  5. 固态硬盘(SSD) :

    • 用于安装操作系统和常用软件,256GB或更大容量。
  6. 散热器(Cooler) :

    • CPU散热器,盒装CPU可能附带散热器。
  7. 电源供应器(PSU) :

    • 根据系统需求选择合适的功率,如500W、600W等,并确保有80 PLUS认证。
  8. 机箱(Case) :

    • 根据主板规格和个人喜好选择机箱。

机箱严格来讲属于非必须,因为你用纸箱子也能行,但是对于小白来说还是整个便宜的比较好

  1. 外围设备

    • 显示器、键盘、鼠标。
  2. 音频设备

    • 耳机、扬声器。

非必须项

  1. 机械硬盘(HDD, 可选) :

    • 用于数据存储,1TB或更大容量。
  2. 机箱风扇(可选) :

    • 用于改善机箱内部空气流通。
  3. 光驱(可选) :

    • 如有需要,可以选择DVD或蓝光光驱。
  4. 无线网卡(可选) :

    • 如果主板不支持无线连接,可以添加无线网卡。
  5. 声卡(可选) :

    • 如果需要更好的音频性能,可以添加独立声卡。
  6. 扩展卡(可选) :

    • 如网络卡、声卡、图形卡等。
  7. 机箱装饰(可选) :

    • RGB灯条、风扇等。

处理器(CPU)天梯图

image.png

显卡(GPU)天梯图

image.png

如何选择显示器

选择电脑显示器时,有几个关键因素需要考虑:

  1. 分辨率:分辨率决定了显示器的清晰度。常见的有1080p(全高清)、1440p(2K)、2160p(4K)等。分辨率越高,画面越清晰,但同时对显卡的要求也越高。
  2. 屏幕尺寸:根据你的使用习惯和空间大小来选择。大屏幕可以提供更宽广的视野,但也需要更大的桌面空间。
  3. 刷新率:刷新率表示显示器每秒可以刷新多少次画面。常见的有60Hz、144Hz、240Hz等。高刷新率可以提供更流畅的视觉效果,特别适合游戏玩家。
  4. 响应时间:响应时间指的是像素从一种状态变化到另一种状态所需的时间,通常以毫秒(ms)为单位。响应时间越短,画面变化越快,越适合快速变化的游戏或视频。
  5. 面板类型:主要有TN、IPS、VA三种面板。TN面板响应速度快,但色彩表现一般;IPS面板色彩表现好,视角宽,但响应时间相对较慢;VA面板则介于两者之间。
  6. 连接接口:确保显示器的连接接口与你的电脑兼容,常见的有HDMI、DisplayPort、DVI、VGA等。
  7. 色彩准确性:如果你的工作涉及到图像或视频编辑,那么选择色彩准确性高的显示器非常重要。
  8. 附加功能:一些显示器可能提供额外的功能,如USB接口、扬声器、可调节支架等。
  9. 预算:根据你的预算来决定购买哪种类型的显示器。通常来说,价格越高,显示器的性能和功能也越全面。
  10. 品牌和售后服务:选择知名品牌的显示器通常可以保证较好的质量和售后服务。

根据你的具体需求和预算,综合考虑上述因素,选择最适合你的显示器。 并不是配置越高越好,重点是根据你的显卡和CPU来看,实现极致性价比,需要对应电脑配置能带动的最大区间,比方说 英特尔 i5-12400F(6核心12线程,睿频4.4GHz)+ 华硕DUAL-RTX 4060-8G游戏显卡能带起来的刷星率极限基本就是165Hz左右,那么你配置2K,180Hz的显示器就够用了,当然你有钱也可以整个4K,200+Hz的

关于外围设备(键鼠、耳机、音响等)

这个看个人喜好以及预算来决定吧,比方说有线还是无线,机械还是非机械等等....

不同价位装机性价比清单(根据目前市场上)

装机其实主要花费就在显卡(GPU)和处理器(CPU)上,显卡(GPU)金额占比超过50%比比皆是,处理器(CPU)金额占比一般是在百分之20%~30%,其他配件金额占比20%~30%左右。

如果预算足够建议优先升级显卡

CPU分为盒装和散片,预算充足盒装,预算不足散片也能用

3K推荐

  • CPU: Intel 12代酷睿i5 12400F
  • 显卡: 华硕DUAL RTX 3050 6G
  • 内存: 威刚D4 16GB 3200MHz
  • 硬盘: 英睿达 500GB PCI-E 4.0 M.2
  • 主板: 圣旗H610M
  • 电源: 爱国者额定500W
  • 跑分: 107W±
  • 合计:3.5K左右

4K极致性价比

4K-6K这个价位其实CPU最佳的就是就是在下图区间 同理可自行对比GPU天梯图

image.png

image.png

  • CPU: 英特尔 i5-12400F(6核心12线程,睿频4.4GHz)
  • 显卡: 华硕DUAL-RTX 4060-8G游戏显卡 升级选项: +299元升级至华硕TX-RTX 4060-O8G-GAMING天选白三风扇
  • 散热: 动力火车四铜管白色强劲散热器
  • 主板: 华硕PRIME B760M-FD4(支持AURA神光同步)
  • 内存: 雷克沙DDR4 16GB 3200MHz高频内存
  • 硬盘: 雷克沙500GB NVMe PCIe SSD(读速高达3300MB/S)
  • 电源: 源之力静音大师额定600W安规3C白色
  • 机箱: 动力火车琉璃海景房
  • 合计:4K左右

1718192436562.png

5K极致性价比

1718192483898.png

image.png

image.png

6K极致性价比

image.png

7K极致性价比

image.png

8K极致性价比

image.png

1W极致性价比

1W预算以上可能考虑的不单单是配置了,还有外观之类的,可以DIY定制之类的

image.png

image.png

1.2W极致性价比

1718192287164.png

1.3w极致性价比

image.png

总结下,其实根据CPU、GPU天梯图就可以找到自己的目标区间,其他配置看个人预算来就行,核心就是两大件!如果有装机高手,欢迎留言交流,有不懂的,欢迎提问。后续会根据问题持续补充


作者:凉城a
来源:juejin.cn/post/7379420157670670372
收起阅读 »

汝为傀儡,吾来操纵(🍄Puppeteer🍄)

web
puppeteer是我以前同事使用过的一个工具,用来测试页面的功能,可以模拟用户操作。这几天我也看到了一些相关的文章,也是很感兴趣的,所以准备整理输出一篇Puppeteer的文章,用来学习记忆。后面发现Puppeteer相关的内容比较多,准备分为两部分来讲,这...
继续阅读 »

IMG_20240601_170616_843.jpg

puppeteer是我以前同事使用过的一个工具,用来测试页面的功能,可以模拟用户操作。这几天我也看到了一些相关的文章,也是很感兴趣的,所以准备整理输出一篇Puppeteer的文章,用来学习记忆。

后面发现Puppeteer相关的内容比较多,准备分为两部分来讲,这一部分主要讲理论相关的,也会举些简单的实例。下一章则会主要针对实战来讲解。

介绍

Puppeteer词义解释

  • Puppet:木偶,傀儡
  • Puppeteer:操纵木偶的人

Puppeteer 是一个由 Google 开发的 Node.js 库用于控制 Chrome 或 Chromium 浏览器的高级 API。它可以模拟用户的交互行为,例如点击、填写表单、导航等,同时还可以截取页面内容、生成 PDF、执行自动化测试等功能

官方网站:github.com/GoogleChrom…

Puppeteer 中文文档

官方文档:pptr.dev/

文档地址:zhaoqize.github.io/puppeteer-a…

核心功能

Puppeteer 的核心功能包括以下几个方面:

  1. 控制浏览器:Puppeteer 可以启动一个 Chrome 或 Chromium 浏览器实例,并通过 API 控制浏览器的行为,如打开网页、点击链接、填写表单、执行 JavaScript 等操作。
  2. 页面操作:Puppeteer 可以模拟用户在页面上的操作,包括点击元素、填写表单、滚动页面、截取屏幕截图等,实现对页面的交互操作。
  3. 网页内容抓取:Puppeteer 可以获取页面的 DOM 结构、元素属性、文本内容等信息,从而实现网页内容的抓取和提取。
  4. 页面性能分析:Puppeteer 可以获取页面加载性能数据、网络请求信息、CPU 和内存使用情况等,帮助开发者进行页面性能优化和调试。
  5. 生成 PDF:Puppeteer 可以将网页内容保存为 PDF 文档,支持设置页面大小、方向、页边距等参数,方便生成打印版的网页内容。
  6. 自动化测试:Puppeteer 可以用于编写自动化测试脚本,模拟用户的操作行为,验证页面的功能和交互是否符合预期,实现自动化测试流程。
  7. 爬虫和数据采集:Puppeteer 可以用于编写网络爬虫,自动访问网页、提取数据、填写表单等,实现网页内容的自动采集和处理。

总的来说,Puppeteer 是一个功能强大的浏览器自动化工具,可以实现对浏览器的控制和页面操作,适用于各种场景下的自动化任务,如自动化测试、网页内容抓取、页面性能分析等。

下面介绍一些常用的API。

启动新的浏览器实例

puppeteer.launch()是一个用于启动一个新的浏览器实例的方法。该方法返回一个 Promise,该 Promise 在浏览器实例启动后会被解析为一个 Browser 对象,你可以通过这个对象来操作浏览器。

在 Puppeteer 中,puppeteer.launch() 方法可以接受一个可选的配置对象 options,用于指定启动浏览器实例时的一些参数和选项。下面是一些常用的配置选项:

  1. headless:布尔值,是否以 无头模式 运行浏览器。默认是 true,即以无头模式启动,不会显示浏览器界面。如果设置为 false,则会以有头模式启动,显示浏览器界面。
  2. args:一个字符串数组,传递给浏览器实例的其他参数。 这些参数可以参考 这里
  3. defaultViewport 是一个对象,用于为每个页面设置一个默认视口大小。默认是 800x600。如果为 null 的话就禁用视图口。下面是 defaultViewport 对象中可以设置的属性:

    • width:页面的宽度像素。
    • height:页面的高度像素。
    • deviceScaleFactor:设备的缩放比例,可以认为是设备像素比(device pixel ratio,DPR)。默认值为 1。

      更多

  4. ignoreHTTPSErrors: 布尔值,指定是否忽略 HTTPS 错误。默认是 false
  5. defaultViewport:一个对象,用于指定浏览器的默认视口大小,包括宽度、高度和设备比例因子等。
  6. userDataDir:一个字符串,用于指定用户数据目录的路径,用于存储浏览器的用户数据,比如缓存、Cookies 等。
  7. timeout: 数值,指定启动浏览器的超时时间,单位为毫秒。
  8. slowMo: 数值,指定 Puppeteer 操作的延迟时间,单位为毫秒。可以用来减慢操作的速度,方便调试。

下面是一个简单的示例代码,演示如何使用 puppeteer.launch() 方法来启动一个浏览器实例:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch({
headless: false, // 显示浏览器界面
executablePath: '/path/to/chrome', // 指定浏览器可执行文件路径
args: ['--no-sandbox', '--disable-setuid-sandbox'], // 额外参数
defaultViewport: { width: 1280, height: 800 }, // 默认视口大小
userDataDir: '/path/to/userDataDir'// 用户数据目录
slowMo: 100, // 延迟 100 毫秒
});

// 在这里可以进行其他操作,比如创建新页面、访问网页等

await browser.close();
})();

在上面的代码中,我们通过 puppeteer.launch() 方法启动了一个浏览器实例,并通过 options 参数配置了一些选项,比如显示浏览器界面、指定浏览器可执行文件路径、传递额外参数、设置默认视口大小和用户数据目录。你可以根据需要自定义 options 对象中的属性来满足你的需求。

需要注意的是,在使用完浏览器实例后,应该调用 browser.close() 方法来关闭浏览器,释放资源。

Browser 类

Browser 类表示一个 Chrome 或 Chromium 浏览器实例。它提供了一组方法来操作整个浏览器,如创建新页面、关闭浏览器、监听事件等。

当 Puppeteer 连接到一个 Chromium 实例的时候会通过 puppeteer.launch 或 puppeteer.connect 创建一个 Browser 对象。

以下是一些 Browser 类常用的方法:

  • newPage(): 创建一个新的页面实例。
  • close(): 关闭浏览器实例。
  • version(): 获取浏览器的版本信息。
  • pages(): 获取所有已打开的页面实例。
  • newContext(): 创建一个新的浏览器上下文。
  • target(): 获取指定目标的实例。

下面是使用 Browser 创建 Page 的例子

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.goto('https://example.com');
await browser.close();
});

Page 类

Page 类表示一个浏览器页面。它提供了一系列方法,用于操作和控制页面的行为,例如导航至指定 URL、执行 JavaScript 代码、截取页面截图等。

以下是一些 Page 类常用的方法:

  1. goto(url): 导航到指定的 URL。
await page.goto('https://www.example.com');
  1. waitForSelector(selector): 等待页面中指定的选择器出现。
await page.waitForSelector('.my-element');
  1. click(selector): 点击页面中指定的选择器。
await page.click('.my-button');
  1. type(selector, text): 在指定的输入框中输入文本。
await page.type('input[name="username"]', 'myusername');
  1. evaluate(pageFunction): 在页面上下文中执行指定的函数。
const title = await page.evaluate(() => document.title);
  1. screenshot(options): 截取当前页面的屏幕截图。
await page.screenshot({ path: 'screenshot.png' });
  1. close(): 关闭页面实例。
await page.close();

通过使用 Page 类提供的方法,我们可以模拟用户在浏览器中的操作,实现各种自动化任务,如网页截图、表单填写、点击操作等。

下面会更详细地介绍几个常用的方法。

元素获取

这些方法可以帮助我们在 Puppeteer 中获取页面元素:

  1. page.content(): 返回页面完整的 HTML 代码。
const html = await page.content();
  1. page.$(selector): 使用 document.querySelector 寻找指定元素。
const element = await page.$('.my-element');
  1. page.$$(selector): 使用 document.querySelectorAll 寻找指定元素。
const elements = await page.$$('.my-elements');
  1. page.$x(expression): 使用 XPath 寻找指定元素。
const element = await page.$x('//div[@class="my-element"]');
  1. page.$eval(selector, pageFunction, …args?): 在页面中注入方法,执行 document.querySelector 后将结果作为第一个参数传给函数体。
const text = await page.$eval('.my-element', element => element.textContent);
  1. page.$$eval(selector, pageFunction, …args?): 在页面中注入方法,执行 document.querySelectorAll 后将结果作为第一个参数传给函数体。
const texts = await page.$$eval('.my-elements', elements => elements.map(element => element.textContent));

页面操作

点击操作

  • page.click(selector, options?): 点击选择器匹配的元素,有多个元素满足匹配条件仅作用第一个。
await page.click('.my-button');
  • page.tap(selector): 点击选择器匹配的元素,有多个元素满足匹配条件仅作用第一个,主要针对手机端的触摸事件。
await page.tap('.my-button');
  • page.focus(selector): 给选择器匹配的元素获取焦点,有多个元素满足匹配条件仅作用第一个。
await page.focus('.my-input');
  • page.hover(selector): 鼠标悬浮于选择器匹配的元素,有多个元素满足匹配条件仅作用第一个。
await page.hover('.my-element');

输入操作

page.type 是 Puppeteer 中用于在指定元素上输入文本的方法。该方法接受两个参数:选择器和要输入的文本。

以下是 page.type 方法的用法示例:

await page.type('input[type="text"]', 'Hello, Puppeteer!');

在这个示例中,我们使用选择器 input[type="text"] 来定位页面上的一个文本输入框,并在该输入框中输入文本 'Hello, Puppeteer!'。

键盘模拟按键

page.keyboard 对象提供了一系列方法,可以模拟按键的按下、释放、输入等操作。

以下是一些常用的 page.keyboard 方法:

  1. keyboard.press(key[, options]): 模拟按下指定的键。
  2. keyboard.release(key): 模拟释放指定的键。
  3. keyboard.down(key): 模拟按下指定的键,保持按下状态。
  4. keyboard.up(key): 模拟释放指定的键,取消按下状态。
  5. keyboard.type(text[, options]): 模拟输入指定的文本。

下面是一个示例代码,演示如何在输入框中模拟按键操作:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.example.com');

// 获取要输入文本的输入框的选择器
const selector = 'input[type="text"]';

// 等待输入框加载完成
await page.waitForSelector(selector);

// 在输入框中模拟按键操作
await page.focus(selector); // 让输入框获得焦点
await page.keyboard.type('Hello, Puppeteer!'); // 输入文本
await page.keyboard.press('Enter'); // 模拟按下 Enter 键

await browser.close();
})();

在这个示例中,我们首先让输入框获得焦点,然后使用 page.keyboard.type 方法输入文本 'Hello, Puppeteer!',最后使用 page.keyboard.press 方法模拟按下 Enter 键。

鼠标模拟

在 Puppeteer 中,可以使用 page.mouse 对象来模拟鼠标操作。page.mouse 对象提供了一系列方法,可以模拟鼠标的移动、点击、滚动等操作。

以下是一些常用的 page.mouse 方法:

  1. mouse.move(x, y[, options]): 将鼠标移动到指定位置。
  2. mouse.click(x, y[, options]): 在指定位置模拟鼠标点击。
  3. mouse.down([options]): 模拟按下鼠标按钮。
  4. mouse.up([options]): 模拟释放鼠标按钮。
  5. mouse.wheel(deltaX, deltaY): 模拟滚动鼠标滚轮。

下面是一个示例代码,演示如何在页面中模拟鼠标操作:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.example.com');

// 获取要点击的元素的选择器
const selector = 'button';

// 等待元素加载完成
await page.waitForSelector(selector);

// 获取元素的位置
const element = await page.$(selector);
const boundingBox = await element.boundingBox();

// 在元素位置模拟鼠标点击
await page.mouse.click(boundingBox.x + boundingBox.width / 2, boundingBox.y + boundingBox.height / 2);

await browser.close();
})();

在这个示例中,我们首先等待页面中的按钮元素加载完成,然后获取按钮元素的位置信息,最后使用 page.mouse.click 方法在按钮元素的中心位置模拟鼠标点击操作。

事件监听

这些是 Puppeteer 中常用的页面事件,可以通过监听这些事件来执行相应的操作。以下是每个事件的简要说明:

  • close: 当页面被关闭时触发。
  • console: 当页面中调用 console API 时触发。
  • error: 当页面发生错误时触发。
  • load: 当页面加载完成时触发。
  • request: 当页面收到请求时触发。
  • requestfailed: 当页面的请求失败时触发。
  • requestfinished: 当页面的请求成功时触发。
  • response: 当页面收到响应时触发。
  • workercreated: 当页面创建 webWorker 时触发。
  • workerdestroyed: 当页面销毁 webWorker 时触发。

您可以通过以下示例代码来监听页面加载完成和页面请求成功的事件:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

// 监听页面加载完成事件
page.
on('load', () => {
console.log('Page loaded successfully');
});

// 监听页面请求成功事件
page.
on('requestfinished', (request) => {
console.log(`Request finished: ${request.url()}`);
});

await page.goto('https://www.example.com');

await browser.close();
})();

在这个示例中,我们使用 page.on 方法来监听页面加载完成和页面请求成功的事件,并在事件发生时打印相应的信息。

等待元素、请求、响应

在 Puppeteer 中,您可以使用以下方法来等待元素、请求和响应:

  1. page.waitForXPath(xpath, options)等待指定的 XPath 对应的元素出现。参数 options 可以包含 timeout 和 visible 选项。返回一个 ElementHandle 实例。
await page.waitForXPath('//div[@]', { visible: true });
  1. page.waitForSelector(selector, options)等待指定的选择器对应的元素出现。参数 options 可以包含 timeout 和 visible 选项。返回一个 ElementHandle 实例。
await page.waitForSelector('.example', { visible: true });
  1. page.waitForResponse(predicate)等待符合条件的响应结束。参数 predicate 是一个函数,用于判断响应是否符合条件。返回一个 Response 实例。
const response = await page.waitForResponse(response => response.url().includes('/api'));
  1. page.waitForRequest(predicate)等待符合条件的请求出现。参数 predicate 是一个函数,用于判断请求是否符合条件。返回一个 Request 实例。
const request = await page.waitForRequest(request => request.url().includes('/api'));

这些方法可以帮助您在 Puppeteer 中更精确地控制等待元素、请求和响应的时间,以便在需要时执行相应的操作。如果您有任何疑问或需要进一步的解释,请随时告诉我。我将很乐意帮助您。

网络拦截操作

page.setRequestInterception() 方法可以拦截页面中发出的网络请求,并对其进行处理。通过拦截请求,你可以修改请求的行为,例如阻止请求、修改请求的头部、修改请求的内容等。

以下是使用 page.setRequestInterception() 方法的一个示例:

const puppeteer = require('puppeteer');

(async () => {
 const browser = await puppeteer.launch();
 const page = await browser.newPage();

 // 启用请求拦截
 await page.setRequestInterception(true);

 // 监听请求事件
 page.on('request', (request) => {
   // 判断请求的 URL 是否符合条件
   if (request.url().includes('/api')) {
     request.continue(); // 继续请求
  } else {
     request.abort(); // 中止请求
  }
});

 // 导航至指定 URL
 await page.goto('https://example.com');
 
   // 等待页面加载完成
 await page.waitForNavigation();
   
  // 获取符合条件的网络响应
 const responses = await page.waitForResponse(response => response.url().includes('/api'));
 // 获取接口数据
 const responseData = await responses.json();
 console.log(responseData);
 
 await browser.close();
})();

需要注意的是,在使用请求拦截功能时,务必要确保在请求被中止或继续之前,要么调用 interceptedRequest.abort() 中止请求,要么调用 interceptedRequest.continue() 继续请求,否则可能会导致页面无法正常加载。

简单示例

截图

在 Puppeteer 中实现截图可以通过 page.screenshot() 方法来实现。

以下是一个简单的示例代码,演示如何在 Puppeteer 中对页面进行截图:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.goto('https://www.example.com');

// 在当前目录下保存截图
await page.screenshot({ path: 'example.png' });

await browser.close();
})();

在上面的示例中,我们首先启动了一个 Puppeteer 浏览器实例,然后创建了一个新页面并访问了示例网站。接着使用 page.screenshot() 方法对页面进行截图,并将截图保存在当前目录下的 example.png 文件中。最后关闭了浏览器实例。

生成pdf

在 Puppeteer 中生成 PDF 可以通过 page.pdf() 方法来实现。

以下是一个简单的示例代码,演示如何在 Puppeteer 中生成 PDF:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.goto('https://www.example.com');

// 生成 PDF 并保存在当前目录下的 example.pdf 文件中
await page.pdf({ path: 'example.pdf', format: 'A4' });

await browser.close();
})();

在上面的示例中,我们首先启动了一个 Puppeteer 浏览器实例,然后创建了一个新页面并访问了示例网站。接着使用 page.pdf() 方法生成 PDF,并将其保存在当前目录下的 example.pdf 文件中。您还可以调整生成 PDF 的格式、尺寸、页面边距等参数。

设置cookie

在 Puppeteer 中设置 cookie 可以通过 page.setCookie() 方法来实现。

以下是一个简单的示例代码,演示如何在 Puppeteer 中设置 cookie:

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();

// 设置 cookie
await page.setCookie({
name: 'username',
value: 'john_doe',
domain: 'www.example.com'
});

await page.goto('https://www.example.com');

// 在页面中获取 cookie
const cookies = await page.cookies();
console.log(cookies);

await browser.close();
})();

在上面的示例中,我们首先启动了一个 Puppeteer 浏览器实例,然后创建了一个新页面。接着使用 page.setCookie() 方法设置了一个名为 username 的 cookie,然后访问了示例网站。最后使用 page.cookies() 方法获取页面中的所有 cookie,并将其打印出来。

您可以根据需要设置更多的 cookie,以及设置 cookie 的路径、过期时间等属性。


作者:Aplee
来源:juejin.cn/post/7379512671617679412
收起阅读 »

作为前端开发,感受下 nginx 带来的魅力!🔥🔥

引言:纯干货分享,汇总了我在工作中八年遇到的各种 Nginx 使用场景,对这篇文章进行了细致的整理和层次分明的讲解,旨在提供简洁而深入的内容。希望这能为你提供帮助和启发! 对于前端开发人员来说,Node.js 是一种熟悉的技术。虽然 Nginx 和 Node...
继续阅读 »

引言:纯干货分享,汇总了我在工作中八年遇到的各种 Nginx 使用场景,对这篇文章进行了细致的整理和层次分明的讲解,旨在提供简洁而深入的内容。希望这能为你提供帮助和启发!
1802c30a7bb47ccc7cd70314829ac04796140850.jpeg


对于前端开发人员来说,Node.js 是一种熟悉的技术。虽然 Nginx 和 Node.js 在某些理念上有相似之处,比如都支持 HTTP 服务、事件驱动和异步非阻塞操作,但两者并不冲突,各有各自擅长的领域:



  • Nginx:擅长处理底层的服务器端资源,如静态资源处理、反向代理和负载均衡。

  • Node.js:更擅长处理上层的具体业务逻辑。


而两者的结合可以实现更加高效和强大的应用服务架构,下面我们就来看一下。借助文章目录阅读,效率更高。目前您可能还用不到这篇文章,不过可以先收藏起来。希望将来它能为您提供所需的帮助!


Nginx 是什么?


Nginx 是一个高性能的HTTP和反向代理服务器,由俄罗斯程序员Igor Sysoev于 2004 年使用 C 语言开发。它最初设计是为了应对俄罗斯大型门户网站的高流量挑战。


1667274211133.jpg


反向代理是什么?(🔥面试会问)


让我们先从代理说起。Nginx 常被用作反向代理,那么什么是正向代理呢?



  • 正向代理:客户端知道要访问的服务器地址,但服务器只知道请求来自某个代理,而不清楚具体的客户端。正向代理隐藏了真实客户端的信息。例如,当无法直接访问国外网站时,我们通过代理服务器访问特定网址。






  • 反向代理:多个客户端向反向代理服务器发送请求,Nginx 根据一定的规则将请求转发至不同的服务器。客户端不知道具体请求将被转发至哪台服务器,反向代理隐藏了后端服务器的信息。





Nginx 的核心特性


Nginx包含以下七个核心特性,使它成为处理高并发和大数据量请求的理想选择:


1. 事件驱动:Nginx采用高效的异步事件模型,利用 I/O 多路复用技术。这种模型使 Nginx 能在占用最小内存的同时处理大量并发连接。


2. 高度可扩展:Nginx能够支持数千乃至数万个并发连接,非常适合大型网站和高并发应用。例如:为不同的虚拟主机设置不同的 worker 进程数,以增加并发处理能力:


http {
worker_processes auto; # 根据系统CPU核心数自动设置worker进程数
}

3. 轻量级:相较于传统的基于进程的Web服务器(如Apache),Nginx的内存占用更低,得益于其事件驱动模型,每个连接只占用极小的内存空间。


4. 热部署:Nginx支持热部署功能,允许在不重启服务器的情况下更新配置和模块。例如:在修改了 Nginx 配置文件后,可以快速热部署 Nginx 配置:


sudo nginx -s reload

5. 负载均衡:Nginx内置负载均衡功能,通过upstream模块实现客户端请求在多个后端服务器间的分配,从而提高服务的整体处理能力。以下是一个简单的upstream配置,它将请求轮询分配到三个后端服务器:


upstream backend {
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}

server {
location / {
proxy_pass http://backend; # 将请求转发到upstream定义的backend组
}
}

6. 高性能:Nginx 在多项 Web 性能测试中表现卓越,能快速处理静态文件、索引文件及代理请求。比如:配置 Nginx 作为反向代理服务器,为大型静态文件下载服务:


location /files/ {
alias /path/to/files/; # 设置实际文件存储路径
expires 30d; # 设置文件过期时间为30天
}

7. 安全性:Nginx支持SSL/TLS协议,能够作为安全的Web服务器或反向代理使用。


server {
listen 443 ssl;
ssl_certificate /path/to/fullchain.pem; # 证书路径
ssl_certificate_key /path/to/privatekey.pem; # 私钥路径
ssl_protocols TLSv1.2 TLSv1.3; # 支持的SSL协议
}

搭建 Nginx 服务


1-48.jpeg


如何安装?


在 Linux 系统中,可以使用包管理器来安装 Nginx。例如,在基于 Debian 的系统上,可以使用 apt


sudo apt update
sudo apt install nginx

在基于 Red Hat 的系统上,可以使用 yum 或 dnf


sudo yum install epel-release
sudo yum install nginx

在安装完成后,通常可以通过以下命令启动 Nginx 服务:


sudo systemctl start nginx

设置开机自启动:


sudo systemctl enable nginx

启动成功后,在浏览器输入服务器 ip 地址或者域名,如果看到 Nginx 的默认欢迎页面,说明 Nginx 运行成功。


常用命令有哪些


在日常的服务器管理和运维中,使用脚本来管理 Nginx 是很常见的。这可以帮助自动化一些常规任务,如启动、停止、重载配置等。以下是一些常用的 Nginx 脚本命令,这些脚本通常用于 Bash 环境下:



  1. 启动 Nginx:nginx

  2. 停止 Nginx:nginx -s stop

  3. 重新加载 Nginx:nginx -s reload

  4. 检查 Nginx 配置文件:nginx -t(检查配置文件的正确性)

  5. 查看 Nginx 版本:nginx -v


其他常用的配合脚本命令:



  1. 查看进程命令:ps -ef | grep nginx

  2. 查看日志,在logs目录下输入指令:more access.log


。。。还有哪些常用命令,评论区一起讨论下!


配置文件构成(🔥核心重点,一定要了解)


Nginx配置文件主要由指令组成,这些指令可以分布在多个上下文中,主要上下文包括:



  1. main: 全局配置,影响其他所有上下文。

  2. events: 配置如何处理连接。

  3. http: 配置HTTP服务器的参数。

    • server: 配置虚拟主机的参数。

      • location: 基于请求的URI来配置特定的参数。






worker_processes auto;   # worker_processes定义Nginx可以启动的worker进程数,auto表示自动检测 

# 定义Nginx如何处理连接
events {
worker_connections 1024; # worker_connections定义每个worker进程可以同时打开的最大连接数
}

# 定义HTTP服务器的参数
http {
include mime.types; # 引入mime.types文件,该文件定义了不同文件类型的MIME类型
default_type application/octet-stream; # 设置默认的文件MIME类型为application/octet-stream
sendfile on; # 开启高效的文件传输模式
keepalive_timeout 65; # 设置长连接超时时间

# 定义一个虚拟主机
server {
listen 80; # 指定监听的端口
server_name localhost; # 设置服务器的主机名,这里设置为localhost

# 对URL路径进行配置
location / {
root /usr/share/nginx/html; # 指定根目录的路径
index index.html index.htm; # 设置默认索引文件的名称,如果请求的是一个目录,则按此顺序查找文件
}

# 错误页面配置,当请求的文件不存在时,返回404错误页面
error_page 404 /404.html;

# 定义/40x.html的位置
location = /40x.html {
# 此处可以配置额外的指令,如代理、重写等,但在此配置中为空
}

# 错误页面配置,当发生500、502、503、504等服务器内部错误时,返回相应的错误页面
error_page 500 502 503 504 /50x.html;

# 定义/50x.html的位置
location = /50x.html {
# 同上,此处可以配置额外的指令
}
}
}

这个配置文件设置了Nginx监听80端口,使用root指令指定网站的根目录,并为404和50x错误页面提供了位置。其中,userworker_processes指令在main上下文中,events块定义了事件处理配置,http块定义了HTTP服务器配置,包含一个server块,该块定义了一个虚拟主机,以及两个location块,分别定义了对于404和50x错误的处理。


进入正题,详细看下如何配置


a0e2c2ce0c28044531b1589f5e3fb83263cb690c.jpeg


打开 Nginx 配置世界大门


下面这段是 Nginx 配置定义了一个服务器块(server block),它指定了如何处理发往特定域名的 HTTP 请求。


server {
listen 80; # 监听80端口,HTTP请求的默认端口
client_max_body_size 100m; # 设置客户端请求体的最大大小为100MB
index index.html; # 设置默认的索引文件为index.html
root /user/project/admin; # 设置Web内容的根目录为/user/project/admin

# 路由配置,处理所有URL路径
location ~ /* {
proxy_pass http://127.0.0.1:3001; # 将请求代理到本机的3001端口
proxy_redirect off; # 关闭代理重定向

# 设置代理请求头,以便后端服务器可以获取客户端的原始信息
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# 定义代理服务器失败时的行为,如遇到错误、超时等,尝试下一个后端服务器
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_max_temp_file_size 0; # 禁止代理临时文件写入

# 设置代理连接、发送和读取的超时时间
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;

# 设置代理的缓冲区大小
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}

# 对图片文件设置缓存过期时间,客户端可以在1天内使用本地缓存
location ~ .*.(gif|jpg|jpeg|png|swf)$ {
expires 1d;
}

# 对JavaScript和CSS文件设置缓存过期时间,客户端可以在24小时内使用本地缓存
location ~ .*.(js|css)?$ {
expires 24h;
}

# 允许访问/.well-known目录下的所有文件,通常用于WebFinger、OAuth等协议
location ~ /.well-known {
allow all;
}

# 禁止访问隐藏文件,即以点开头的文件或目录
location ~ /. {
deny all;
}

# 指定访问日志的路径,日志将记录在/user/logs/admin.log文件中
access_log /user/logs/admin.log;
}

注意:Nginx 支持使用正则表达式来匹配 URI,这极大地增强了配置的灵活性。在 Nginx 配置中,正则表达式通过 ~ 来指定。


例如,location ~ /* 可以匹配所有请求。另一个例子是 location ~ .*.(gif|jpg|jpeg|png|swf)$,这个表达式用于匹配以 gif、jpg、jpeg、png 或 swf 这些图片文件扩展名结尾的请求。


Nginx 配置实战(🔥可以复制,直接拿来使用)


以下是一些常见的 Nginx 配置实战案例:


1、静态资源服务:前端web


server {
listen 80;
server_name example.com;
location / {
root /path/to/your/static/files;
index index.html index.htm;
}
location ~* \.(jpg|png|gif|jpeg)$ {
expires 30d;
add_header Cache-Control "public";
}
}

在这个案例中,Nginx 配置为服务静态文件,如 HTML、CSS、JavaScript 和图片等。通过设置 root 指令,指定了静态文件的根目录。同时,对于图片文件,通过 expires 指令设置了缓存时间为 30 天,减少了服务器的负载和用户等待时间。


2、反向代理


server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

这个案例展示了如何配置 Nginx 作为反向代理服务器。当客户端请求 api.example.com 时,Nginx 会将请求转发到后端服务器集群。通过设置 proxy_set_header,可以修改客户端请求的头部信息,确保后端服务器能够正确处理请求。


3、负载均衡


http {
upstream backend {
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

在这个负载均衡的案例中,Nginx 将请求分发给多个后端服务器。通过 upstream 指令定义了一个服务器组,然后在 location 块中使用 proxy_pass 指令将请求代理到这个服务器组。Nginx 支持多种负载均衡策略,如轮询(默认)、IP 哈希等。


4、HTTPS 配置


server {
listen 443 ssl;
server_name example.com;
ssl_certificate /path/to/your/fullchain.pem;
ssl_certificate_key /path/to/your/private.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
location / {
root /path/to/your/https/static/files;
index index.html index.htm;
}
}

这个案例展示了如何配置 Nginx 以支持 HTTPS。通过指定 SSL 证书和私钥的路径,以及设置 SSL 协议和加密套件,可以确保数据传输的安全。同时,建议使用 HTTP/2 协议以提升性能。


5、安全防护


server {
listen 80;
server_name example.com;
location / {
# 防止 SQL 注入等攻击
rewrite ^/(.*)$ /index.php?param=$1 break;
# 限制请求方法,只允许 GET 和 POST
if ($request_method !~ ^(GET|POST)$ ) {
return 444;
}
# 防止跨站请求伪造
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header X-XSS-Protection "1; mode=block";
}
}

通过 rewrite 指令,可以防止一些常见的 Web 攻击,如 SQL 注入。这种限制请求方法,可以减少服务器被恶意利用的风险。同时,添加了一些 HTTP 头部来增强浏览器安全,如防止点击劫持和跨站脚本攻击(XSS)等。


63d12faf8e9f0972ed2f0d90_1024.jpeg


Nginx 深入学习-负载均衡


在负载均衡的应用场景中,Nginx 通常作为反向代理服务器,接收客户端的请求并将其分发到一组后端服务器。这样做不仅可以分散负载、提升网站的响应速度,更能提高系统的可用性。


健康检查


Nginx 能够监测后端服务器的健康状态。如果服务器无法正常工作,Nginx 将自动将请求重新分配给其他健康的服务器。


http {
upstream myapp1 { # 定义了后端服务器组
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;

# 健康检查配置。
# 每10秒进行一次健康检查,如果连续3次健康检查失败,则认为服务器不健康;
# 如果连续2次健康检查成功,则认为服务器恢复健康。
check interval=10s fails=3 passes=2;
}

server {
listen 80;

location / {
proxy_pass http://myapp1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}


check interval=10s fails=3 passes=2; 这样的配置语法在开源版本的 NGINX 上是不支持的。这是 ngx_http_upstream_check_module 模块的特有语法,而该模块不包含在 NGINX 的开源版本中,需要自行下载、编译和安装。该模块是开源免费的,具体详情请参见 ngx_http_upstream_check_module 文档



Nginx 会定期向定义的服务器发送健康检查请求。如果服务器响应正常,则认为服务器健康;如果服务器没有响应或者响应异常,则认为服务器不健康。当服务器被标记为不健康时,Nginx 将不再将请求转发到该服务器,直到它恢复健康。


负载均衡算法


Nginx 支持多种负载均衡算法,可以适应不同的应用场景。以下是几种常见的负载均衡算法的详细说明和示例:


1、Weight轮询(默认):权重轮询算法是 Nginx 默认的负载均衡算法。它按顺序将请求逐一分配到不同的服务器上。通过设置服务器权重(weight)来调整不同服务器上请求的分配率。


upstream backend {
server backend1.example.com weight=3; # 设置backend1的权重为3
server backend2.example.com; # backend2的权重为默认值1
server backend3.example.com weight=5; # 设置backend3的权重为5
}

如果某一服务器宕机,Nginx会自动将该服务器从队列中剔除,请求代理会继续分配到其他健康的服务器上。


2、IP Hash 算法: 根据客户端IP地址的哈希值分配请求,确保客户端始终连接到同一台服务器。


upstream backend {
ip_hash; # 启用IP哈希算法
server backend1.example.com;
server backend2.example.com;
}

根据客户端请求的IP地址的哈希值进行匹配,将具有相同IP哈希值的客户端分配到指定的服务器。这样可以确保同一客户端的请求始终被分配到同一台服务器,有助于保持用户的会话状态。


3、fair算法: 根据服务器的响应时间和负载来分配请求。


upstream backend {
fair; # 启用公平调度算法
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}

它结合了轮询和IP哈希的优点,但Nginx默认不支持公平调度算法,需要安装额外的模块(upstream_fair)来实现。


4、URL Hash 算法: 根据请求的 URL 的哈希值分配请求,每个请求的URL会被分配到指定的服务器,有助于提高缓存效率。


upstream backend {
hash $request_uri; # 启用URL哈希算法
server backend1.example.com;
server backend2.example.com;
}
# 根据请求的URL哈希值来决定将请求发送到backend1还是backend2。

这种方法需要安装Nginx的hash软件包。


开源模块


Nginx拥有丰富的开源模块,有很多还有待我们探索,除了一些定制化的需求需要自己开发,大部分的功能都有开源。大家可以在 NGINX 社区、GitHub 上搜索 "nginx module" 可以找到。


image (1).png


总结


虽然前端人员可能不经常直接操作 Nginx,但了解其基本概念和简单的配置操作是必要的。这样,在需要自行配置 Nginx 的情况下,前端人员能够知晓如何进行基本的设置和调整。


作者:Sailing
来源:juejin.cn/post/7368433531926052874
收起阅读 »

一个Android App最少有多少个线程?

守护线程 Signal Catcher线程 Signal Catcher是一个守护线程,用于捕获 SIGQUIT, SIGUSR1 信号,并采取相应的行为。 Android系统中,由Zygote孵化而来的子进程,包含system_server进程和各种APP进...
继续阅读 »

守护线程


Signal Catcher线程


Signal Catcher是一个守护线程,用于捕获 SIGQUIT, SIGUSR1 信号,并采取相应的行为。


Android系统中,由Zygote孵化而来的子进程,包含system_server进程和各种APP进程都存在一个Signal Catcher线程,但是Zygote进程本身没有这个线程。


在Process类中有SIGQUIT,SIGUSR1 的定义:


    public static final int SIGNAL_QUIT = 3;
public static final int SIGNAL_KILL = 9;
public static final int SIGNAL_USR1 = 10;

当前进程的Signal Catcher线程接收到信号SIGNAL_QUIT,则挂起进程中的所有线程并DUMP所有线程的状态。


当前进程的Signal Catcher线程接收到信号SIGNAL_USR1,则触发进程强制执行GC操作。


发送信号的方式:


Process.sendSignal(android.os.Process.myPid(), Process.SIGNAL_QUIT);
Process.sendSignal(android.os.Process.myPid(), Process.SIGNAL_USR1);

当我们直接在代码中调用上诉代码会发生什么?


Process.SIGNAL_USR1:


I/Process: Sending signal. PID: 12363 SIG: 10
I/etease.popo.ap: Thread[3,tid=12373,WaitingInMainSignalCatcherLoop,Thread*=0x793d816400,peer=0x13700020,"Signal Catcher"]: reacting to signal 10
I/etease.popo.ap: SIGUSR1 forcing GC (no HPROF) and profile save
I/etease.popo.ap: Explicit concurrent copying GC freed 18773(4MB) AllocSpace objects, 7(140KB) LOS objects, 68% free, 2MB/8MB, paused 130us total 13.633ms

Process.SIGNAL_QUIT


I/Process: Sending signal. PID: 12812 SIG: 3
I/etease.popo.ap: Thread[3,tid=12823,WaitingInMainSignalCatcherLoop,Thread*=0x793d816400,peer=0x148051b0,"Signal Catcher"]: reacting to signal 3
I/etease.popo.ap: Wrote stack traces to '[tombstoned]'

RenderThread 渲染线程


Android 5.0之后新增的一个线程,用来协助UI线程进行图形绘制。所有的GL命令执行都放在这个线程上。渲染线程在RenderNode中存有渲染帧的所有信息,并监听VSync信号,因此可以独立做一些属性动画。


在清单文件APP中添加:android:hardwareAccelerated="false"
启动APP将会不存在渲染线程;硬件加速在Android中试默认开启的。


Render Thread在运行时主要是做以下两件事情:



  • Task Queue的任务,这些Task一般就是由MainThread发送过来的,例如,MainThread通过发送一个DrawFrameTask给RenderThread的TaskQueue中,请求RenderThread渲染窗口的下一帧。

  • PendingRegistrationFrameCallbacks列表的IFrameCallback回调接口。每一个IFrameCallback回调接口代表的是一个动画帧,这些动画帧被同步到Vsync信号到来由RenderThread自动执行。具体来说,就是每当Vsync信号到来时,就将一个类型为DispatchFrameCallbacks的Task添加到RenderThread的TaskQueue去等待调度。一旦该Task被调度,就可以在RenderThread中执行注册在PendingRegistrationFrameCallbacks列表中的IFrameCallback回调接口了。


FinalizerDaemon 析构守护线程


对于重写成员函数finailze的对象,他们被GC决定回收时,并没有马上被回收,而是被放入到一个队列中,等待FinalizerDaemon守护线程去调用他们的成员函数finalize,然后再被回收。


FinalizerWatchdogDaemon 析构监护守护线程。


用来监控FinalizerDaemon线程的执行。一旦检测哪些重写了成员函数finalize的对象在执行成员函数finalize时超出一定的时候,那么就会退出VM。


ReferenceQueueDaemon 引用队列守护线程


我们知道在创建引用对象的时候,可以关联一个队列。当被引用对象引用的对象被GC回收的时候呀,被引用对象就会呗加入到其创建时关联的队列中去,这个加入队列的操作就是由ReferenceQueueDaemon守护线程来完成的。这样应用程序就可以知道哪些被引用对象引用的对象已经被回收了。


HeapTrimmerDaemon 堆裁剪守护线程


用于执行裁剪堆的操作,也就是用来将哪些空闲的堆内存归还给系统。


HeapTaskDaemon 线程


Android每个进程都有一个HeapTaskDaemon线程,在该线程内进行GC操作。
HeapTaskDaemon 继承自 Daemon 对象。Daemon 对象实际是一个Runnale,并且内部会创建一个线程,用于执行当前这个 Daemon unnable,这个内部线程的线程名就叫 HeapTaskDaemon。


private static class HeapTaskDaemon extends Daemon {
private static final HeapTaskDaemon INSTANCE = new HeapTaskDaemon();

HeapTaskDaemon() {
super("HeapTaskDaemon");
}

// Overrides the Daemon.interupt method which is called from Daemons.stop.
public synchronized void interrupt(Thread thread) {
VMRuntime.getRuntime().stopHeapTaskProcessor();
}

@Override public void runInternal() {
synchronized (this) {
if (isRunning()) {
// Needs to be synchronized or else we there is a race condition where we start
// the thread, call stopHeapTaskProcessor before we start the heap task
// processor, resulting in a deadlock since startHeapTaskProcessor restarts it
// while the other thread is waiting in Daemons.stop().
VMRuntime.getRuntime().startHeapTaskProcessor();
}
}
// This runs tasks until we are stopped and there is no more pending task.
VMRuntime.getRuntime().runHeapTasks();
}
}

Binder 线程


每个APP进程在启动之后会创建一个binder线程池,用于相应IPC客户端的请求。


例如APP与AMS等服务之间可以通过IPC双向通信,当APP作为服务端的时候,就需要通过Binder线程相应来自AMS的请求。一个Server进程中有一个最大的Binder线程数限制,默认为16个binder线程。


与系统服务通信或者自行实现多进程Binder通信大致需要注意一下几点:



  • 与系统通信的方法,建议使用try cache进行防护,防止App出现崩溃。

  • 需要注意调用频率,部分API调用频率过快响应会比较慢,从而导致主线程卡顿甚至ANR。


主线程


主线程也较UI线程。


这个线程作为Android 开发都比较熟悉。


三方线程


OkHttp相关


如果你使用OkHttp作为网络请求库,那么工程中会有以下几类线程


OkHttp Dispatcher


用于实际执行HTTP请求


  @get:Synchronized
@get:JvmName("executorService") val executorService: ExecutorService
get() {
if (executorServiceOrNull == null) {
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
}
return executorServiceOrNull!!
}

OkHttp TaskRunner


4.x版本引入,用于管理和调度内部任务


  companion object {
@JvmField
val INSTANCE = TaskRunner(RealBackend(threadFactory("$okHttpName TaskRunner", daemon = true)))

val logger: Logger = Logger.getLogger(TaskRunner::class.java.name)
}

OkHttp ConnectionPool


负责清理和回收无用的连接池


  private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
override fun runOnce() = cleanup(System.nanoTime())
}

Okio Watchdog


OkHttp中使用Watchdog来处理超时逻辑


  private class Watchdog internal constructor() : Thread("Okio Watchdog") {
init {
isDaemon = true
}

override fun run() {
while (true) {
try {
var timedOut: AsyncTimeout? = null
synchronized(AsyncTimeout::class.java) {
timedOut = awaitTimeout()

// The queue is completely empty. Let this thread exit and let another watchdog thread
// get created on the next call to scheduleTimeout().
if (timedOut === head) {
head = null
return
}
}

// Close the timed out node, if one was found.
timedOut?.timedOut()
} catch (ignored: InterruptedException) {
}
}
}
}

Glide相关


如果你使用Glide加载图片,那么工程中会有以下几类线程


"glide-source-thread-x"


用于从网络、文件系统或其他数据源中加载原始图像数据


  public static GlideExecutor.Builder newSourceBuilder() {
return new GlideExecutor.Builder(/* preventNetworkOperations= */ false)
.setThreadCount(calculateBestThreadCount())
.setName(DEFAULT_SOURCE_EXECUTOR_NAME);
}

"glide-disk-cache-thread-x"


用于从缓存中读取数据


  public static GlideExecutor.Builder newDiskCacheBuilder() {
return new GlideExecutor.Builder(/* preventNetworkOperations= */ true)
.setThreadCount(DEFAULT_DISK_CACHE_EXECUTOR_THREADS)
.setName(DEFAULT_DISK_CACHE_EXECUTOR_NAME);
}

source-unlimited


  public static GlideExecutor newUnlimitedSourceExecutor() {
return new GlideExecutor(
new ThreadPoolExecutor(
0,
Integer.MAX_VALUE,
KEEP_ALIVE_TIME_MS,
TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
new DefaultThreadFactory(
new DefaultPriorityThreadFactory(),
DEFAULT_SOURCE_UNLIMITED_EXECUTOR_NAME,
UncaughtThrowableStrategy.DEFAULT,
false)));
}

animation


用于执行动画


  public static GlideExecutor.Builder newAnimationBuilder() {
int maximumPoolSize = calculateAnimationExecutorThreadCount();
return new GlideExecutor.Builder(/* preventNetworkOperations= */ true)
.setThreadCount(maximumPoolSize)
.setName(DEFAULT_ANIMATION_EXECUTOR_NAME);
}

ARouter相关


如果你使用ARouter作为路由


ARouter task pool No.x , thread No.X


创建位置


    public static DefaultPoolExecutor getInstance() {
if (null == instance) {
synchronized (DefaultPoolExecutor.class) {
if (null == instance) {
instance = new DefaultPoolExecutor(
INIT_THREAD_COUNT,
MAX_THREAD_COUNT,
SURPLUS_THREAD_LIFE,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(64),
new DefaultThreadFactory());
}
}
}
return instance;
}

另外ARouter也为开发者提供了使用自己线程池的接口:


    static synchronized void setExecutor(ThreadPoolExecutor tpe) {
executor = tpe;
}

三方库总结


关于三方库中的线程池这里仅列举了几个库。如果你的工程中含有大量的三方库,我详细也会存在大量的工作线程。


一些三方库会提供接口允许开发者将其替换成工程内部统一维护的线程池,这样可以做到工程中线程的收敛。笔者遇到最离谱的应该是腾讯X5浏览器,由另外一个小组的同事引入到项目中,集成后发现其引入了大量的线程(大概几十个)。


其他


我个人不太喜欢发这些纯概念的东西,更喜欢总结一些在工程中遇到的问题、对应的解决方案以及思考。后面考虑尽量少发这类笔记,多输出一些思考。


作者:半山居士
来源:juejin.cn/post/7372445124753883155
收起阅读 »

最全的docx,pptx,xlsx(excel),pdf文件预览方案总结

web
最近遇到了文件预览的需求,但一搜索发现,这还不是一个简单的功能。于是又去查询了很多资料,调研了一些方案,也踩了好多坑。最后总结方案如下花钱解决(使用市面上现有的文件预览服务)微软google阿里云 IMMXDOCOffice Web 365wps开放平台前端方...
继续阅读 »

最近遇到了文件预览的需求,但一搜索发现,这还不是一个简单的功能。于是又去查询了很多资料,调研了一些方案,也踩了好多坑。最后总结方案如下

  1. 花钱解决(使用市面上现有的文件预览服务)
    1. 微软
    2. google
    3. 阿里云 IMM
    4. XDOC
    5. Office Web 365
    6. wps开放平台
  2. 前端方案
    1. pptx的预览方案
    2. pdf的预览方案
    3. docx的预览方案
    4. xlsx(excel)的预览方案
    5. 前端预览方案总结
  3. 服务端方案
    1. openOffice
    2. kkFileView
    3. onlyOffice

如果有其他人也遇到了同样的问题,有了这篇文章,希望能更方便的解决。

基本涵盖了所有解决方案。因此,标题写上 最全 的文件预览方案调研总结,应该不为过吧。

一.市面上现有的文件预览服务

1.微软

docx,pptx,xlsx可以说是office三件套,那自然得看一下 微软官方 提供的文件预览服务。使用方法特别简单,只需要将文件链接,拼接到参数后面即可。

记得encodeURL

https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}

(1).PPTX预览效果:

image.png

  • 优点:还原度很高,功能很丰富,可以选择翻页,甚至支持点击播放动画。
  • 缺点:不知道是不是墙的原因,加载稍慢。

(2).Excel预览效果:

image.png

(3).Doxc预览效果

image.png

(4).PDF预览效果

这个我测试没有成功,返回了一个错误,其他人可以试试。

image.png

(5).总的来说

对于docx,pptx,xlsx都有较好的支持,pdf不行。

还有一个坑点是:这个服务是否稳定,有什么限制,是否收费,都查不到一个定论。在office官方网站上甚至找不到介绍这个东西的地方。

目前只能找到一个Q&A:answers.microsoft.com/en-us/msoff…

微软官方人员回答表示:

image.png

翻译翻译,就是:几乎永久使用,没有收费计划,不会存储预览的文件数据,限制文件10MB,建议用于 查看互联网上公开的文件

但经过某些用户测试发现:

image.png

使用了微软的文件预览服务,然后删除了文件地址,仍然可访问,但过一段时间会失效。

2.Google Drive 查看器

接入简单,同 Office Web Viewer,只需要把 src 改为https://drive.google.com/viewer?url=${encodeURIComponent(url)}即可。

限制25MB,支持以下格式:

image.png

测试效果,支持docx,pptx,xlsx,pdf预览,但pptx预览的效果不如微软,没有动画效果,样式有小部分会错乱。

由于某些众所周知的原因,不可用

3.阿里云 IMM

官方文档如下:help.aliyun.com/document_de…

image.png

付费使用

4.XDOC 文档预览

说了一些大厂的,在介绍一些其他的,需要自行分辨

官网地址:view.xdocin.com/view-xdocin…

image.png

5.Office Web 365

需要注意的是,虽然名字很像office,但我们看网页的Copyright可以发现,其实是一个西安的公司,不是微软

但毕竟也提供了文件预览的服务

官网地址:http://www.officeweb365.com/

image.png

6.WPS开放平台

官方地址:solution.wps.cn/

image.png

付费使用,价格如下:

image.png

二.前端处理方案

1.pptx的预览方案

先查一下有没有现成的轮子,目前pptx的开源预览方案能找到的只有这个:github.com/g21589/PPTX… 。但已经六七年没有更新,也没有维护,笔者使用的时候发现有很多兼容性问题。

简单来说就是,没有。对于这种情况,我们可以自行解析,主要步骤如下:

  1. 查询pptx的国际标准
  2. 解析pptx文件
  3. 渲染成html或者canvas进行展示

我们先去找一下pptx的国际标准,官方地址:officeopenxml

先解释下什么是officeopenxml:

Office OpenXML,也称为OpenXML或OOXML,是一种基于XML的办公·文档格式,包括文字处理文档、电子表格、演示文稿以及图表、图表、形状和其他图形材料。该规范由微软开发,并于2006年被ECMA国际采用为ECMA-376。第二个版本于2008年12月发布,第三个版本于2011年6月发布。该规范已被ISO和IEC采用为ISO/IEC 29500。

虽然Microsoft继续支持较旧的二进制格式(.doc、.xls和.ppt),但OOXML现在是所有Microsoft Office文档(.docx、.xlsx和.pptx)的默认格式。

由此可见,Office OpenXML由微软开发,目前已经是国际标准。接下来我们看一下pptx里面有哪些内容,具体可以看pptx的官方标准:officeopenxml-pptx

PresentationML或.pptx文件是一个zip文件,其中包含许多“部分”(通常是UTF-8或UTF-16编码)或XML文件。该包还可能包含其他媒体文件,例如图像。该结构根据 OOXML 标准 ECMA-376 第 2 部分中概述的开放打包约定进行组织。

image.png

根据国际标准,我们知道,pptx文件本质就是一个zip文件,其中包含许多部分:

部件的数量和类型将根据演示文稿中的内容而有所不同,但始终会有一个 [Content_Types].xml、一个或多个关系 (.rels) 部件和一个演示文稿部件(演示文稿.xml),它位于 ppt 文件夹中,用于Microsoft Powerpoint 文件。通常,还将至少有一个幻灯片部件,以及一张母版幻灯片和一张版式幻灯片,从中形成幻灯片。

那么js如何读取zip呢?

找到一个工具: http://www.npmjs.com/package/jsz…

于是我们可以开始尝试解析pptx了。

import JSZip from 'jszip'
// 加载pptx数据
const zip = await JSZip.loadAsync(pptxData)
  • 解析[Content_Types].xml

每个pptx必然会有一个 [Content_Types].xml。此文件包含包中部件的所有内容类型的列表。每个部件及其类型都必须列在 [Content_Types].xml 中。通过它里面的内容,可以解析其他的文件数据


const filesInfo = await getContentTypes(zip)

async function getContentTypes(zip: JSZip) {
const ContentTypesJson = await readXmlFile(zip, '[Content_Types].xml')
const subObj = ContentTypesJson['Types']['Override']
const slidesLocArray = []
const slideLayoutsLocArray = []
for (let i = 0; i < subObj.length; i++) {
switch (subObj[i]['attrs']['ContentType']) {
case 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml':
slidesLocArray.push(subObj[i]['attrs']['PartName'].substr(1))
break
case 'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml':
slideLayoutsLocArray.push(subObj[i]['attrs']['PartName'].substr(1))
break
default:
}
}
return {
slides: slidesLocArray,
slideLayouts: slideLayoutsLocArray,
}
}
  • 解析演示文稿

先获取ppt目录下的presentation.xml演示文稿的大小

由于演示文稿是xml格式,要真正的读取内容需要执行 readXmlFile

const slideSize = await getSlideSize(zip)
async function getSlideSize(zip: JSZip) {
const content = await readXmlFile(zip, 'ppt/presentation.xml')
const sldSzAttrs = content['p:presentation']['p:sldSz']['attrs']
return {
width: (parseInt(sldSzAttrs['cx']) * 96) / 914400,
height: (parseInt(sldSzAttrs['cy']) * 96) / 914400,
}
}

  • 加载主题

根据 officeopenxml的标准解释

每个包都包含一个关系部件,用于定义其他部件之间的关系以及与包外部资源的关系。这样可以将关系与内容分开,并且可以轻松地更改关系,而无需更改引用目标的源。

除了包的关系部分之外,作为一个或多个关系源的每个部件都有自己的关系部分。每个这样的关系部件都可以在部件的_rels子文件夹中找到,并通过在部件名称后附加“.rels”来命名。

其中主题的相关信息就在ppt/_rels/presentation.xml.rels


async function loadTheme(zip: JSZip) {
const preResContent = await readXmlFile(
zip,
'ppt/_rels/presentation.xml.rels',
)
const relationshipArray = preResContent['Relationships']['Relationship']
let themeURI
if (relationshipArray.constructor === Array) {
for (let i = 0; i < relationshipArray.length; i++) {
if (
relationshipArray[i]['attrs']['Type'] ===
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme'
) {
themeURI = relationshipArray[i]['attrs']['Target']
break
}
}
} else if (
relationshipArray['attrs']['Type'] ===
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme'
) {
themeURI = relationshipArray['attrs']['Target']
}

if (themeURI === undefined) {
throw Error("Can't open theme file.")
}

return readXmlFile(zip, 'ppt/' + themeURI)
}

后续ppt里面的其他内容,都可以这么去解析。根据officeopenxml标准,可能包含:

PartDescription
Comments AuthorsContains information about each author who has added a comment to the presentation.
CommentsContains comments for a single slide.
Handout MasterContains the look, position, and size of the slides, notes, header and footer text, date, or page number on the presentation's handout. There can be only one such part.
Notes MasterContains information about the content and formatting of all notes pages. There can be only one such part.
Notes SlideContains the notes for a single slide.
PresentationContains the definition of a slide presentation. There must be one and only one such part. See Presentation.
Presentation PropertiesContains all of the presentation's properties. There must be one and only one such part.
SlideContains the content of a single slide.
Slide LayoutContains the definition for a slide template. It defines the default appearance and positioning of drawing objects on the slide. There must be one or more such parts.
Slide MasterContains the master definition of formatting, text, and objects that appear on each slide in the presentation that is derived from the slide master. There must be one or more such parts.
Slide Synchronization DataContains properties specifying the current state of a slide that is being synchronized with a version of the slide stored on a central server.
User-Defined TagsContains a set of user-defined properties for an object in a presentation. There can be zero or more such parts.
View PropertiesContains display properties for the presentation.

等等内容,我们根据标准一点点解析并渲染就好了。

完整源码:ranui

使用文档:preview组件

2.pdf的预览方案

(1).iframe和embed

pdf比较特别,一般的浏览器默认支持预览pdf。因此,我们可以使用浏览器的能力:

<iframe src="viewFileUrl" />

但这样就完全依赖浏览器,对PDF的展示,交互,是否支持全看浏览器的能力,且不同的浏览器展示和交互往往不同,如果需要统一的话,最好还是尝试其他方案。

embed的解析方式也是一样,这里不举例子了

(2)pdfjs

npm: http://www.npmjs.com/package/pdf…

github地址:github.com/mozilla/pdf…

mozilla出品,就是我们常见的MDN的老大。

而且目前 火狐浏览器 使用的 PDF 预览就是采用这个,我们可以用火狐浏览器打开pdf文件,查看浏览器使用的js就能发现

image.png

需要注意的是,最新版pdf.js限制了node版本,需要大于等于18

github链接:github.com/mozilla/pdf…

image.png

如果你项目node版本小于这个情况,可能会无法使用。

如果遇到这种情况,评论区 @敲敲敲敲暴你脑袋 提出一种解决方案,以前老的版本没有限制,可以用以前版本,详情见评论区。

具体使用情况如下:

import * as pdfjs from 'pdfjs-dist'
import * as pdfjsWorker from 'pdfjs-dist/build/pdf.work.entry'

interface Viewport {
width: number
height: number
viewBox: Array<number>
}

interface RenderContext {
canvasContext: CanvasRenderingContext2D | null
transform: Array<number>
viewport: Viewport
}

interface PDFPageProxy {
pageNumber: number
getViewport: () => Viewport
render: (options: RenderContext) => void
}

interface PDFDocumentProxy {
numPages: number
getPage: (x: number) => Promise<PDFPageProxy>
}

class PdfPreview {
private pdfDoc: PDFDocumentProxy | undefined
pageNumber: number
total: number
dom: HTMLElement
pdf: string | ArrayBuffer
constructor(pdf: string | ArrayBuffer, dom: HTMLElement | undefined) {
this.pageNumber = 1
this.total = 0
this.pdfDoc = undefined
this.pdf = pdf
this.dom = dom ? dom : document.body
}
private getPdfPage = (number: number) => {
return new Promise((resolve, reject) => {
if (this.pdfDoc) {
this.pdfDoc.getPage(number).then((page: PDFPageProxy) => {
const viewport = page.getViewport()
const canvas = document.createElement('canvas')
this.dom.appendChild(canvas)
const context = canvas.getContext('2d')
const [_, __, width, height] = viewport.viewBox
canvas.width = width
canvas.height = height
viewport.width = width
viewport.height = height
canvas.style.width = Math.floor(viewport.width) + 'px'
canvas.style.height = Math.floor(viewport.height) + 'px'
const renderContext = {
canvasContext: context,
viewport: viewport,
transform: [1, 0, 0, -1, 0, viewport.height],
}
page.render(renderContext)
resolve({ success: true, data: page })
})
} else {
reject({ success: false, data: null, message: 'pdfDoc is undefined' })
}
})
}
pdfPreview = () => {
window.pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
window.pdfjsLib
.getDocument(this.pdf)
.promise.then(async (doc: PDFDocumentProxy) => {
this.pdfDoc = doc
this.total = doc.numPages
for (let i = 1; i <= this.total; i++) {
await this.getPdfPage(i)
}
})
}
prevPage = () => {
if (this.pageNumber > 1) {
this.pageNumber -= 1
} else {
this.pageNumber = 1
}
this.getPdfPage(this.pageNumber)
}
nextPage = () => {
if (this.pageNumber < this.total) {
this.pageNumber += 1
} else {
this.pageNumber = this.total
}
this.getPdfPage(this.pageNumber)
}
}

const createReader = (file: File): Promise<string | ArrayBuffer | null> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
resolve(reader.result)
}
reader.onerror = (error) => {
reject(error)
}
reader.onabort = (abort) => {
reject(abort)
}
})
}

export const renderPdf = async (
file: File,
dom?: HTMLElement,
): Promise<void> => {
try {
if (typeof window !== 'undefined') {
const pdf = await createReader(file)
if (pdf) {
const PDF = new PdfPreview(pdf, dom)
PDF.pdfPreview()
}
}
} catch (error) {
console.log('renderPdf', error)
}
}

3.docx的预览方案

我们可以去查看docx的国际标准,去解析文件格式,渲染成htmlcanvas,不过比较好的是,已经有人这么做了,还开源了

npm地址:http://www.npmjs.com/package/doc…

使用方法如下:

import { renderAsync } from 'docx-preview'

interface DocxOptions {
bodyContainer?: HTMLElement | null
styleContainer?: HTMLElement
buffer: Blob
docxOptions?: Partial<Record<string, string | boolean>>
}

export const renderDocx = (options: DocxOptions): Promise<void> | undefined => {
if (typeof window !== 'undefined') {
const { bodyContainer, styleContainer, buffer, docxOptions = {} } = options
const defaultOptions = {
className: 'docx',
ignoreLastRenderedPageBreak: false,
}
const configuration = Object.assign({}, defaultOptions, docxOptions)
if (bodyContainer) {
return renderAsync(buffer, bodyContainer, styleContainer, configuration)
} else {
const contain = document.createElement('div')
document.body.appendChild(contain)
return renderAsync(buffer, contain, styleContainer, configuration)
}
}
}

4.xlsx的预览方案

我们可以使用这个:

npm地址:http://www.npmjs.com/package/@vu…

支持vue2vue3,也有js的版本

对于xlsx的预览方案,这个是找到最好用的了。

5.前端预览方案总结

我们对以上找到的优秀的解决方案,进行改进和总结,并封装成一个web components组件:preview组件

为什么是web components组件?

因为它跟框架无关,可以在任何框架中使用,且使用起来跟原生的div标签一样方便。

并编写使用文档: preview组件文档, 文档支持交互体验。

源码公开,MIT协议。

目前docx,pdf,xlsx预览基本可以了,都是最好的方案。pptx预览效果不太好,因为需要自行解析。不过源码完全公开,需要的可以提issuepr或者干脆自取或修改,源码地址:github.com/chaxus/ran/…

三.服务端预览方案

1.openOffice

由于浏览器不能直接打开docx,pptx,xlsx等格式文件,但可以直接打开pdf和图片.因此,我们可以换一个思路,用服务端去转换下文件的格式,转换成浏览器能识别的格式,然后再让浏览器打开,这不就OK了吗,甚至不需要前端处理了。

我们可以借助openOffice的能力,先介绍一下openOffice:

Apache OpenOffice是领先的开源办公软件套件,用于文字处理,电子表格,演示文稿,图形,数据库等。它有多种语言版本,适用于所有常用计算机。它以国际开放标准格式存储您的所有数据,还可以从其他常见的办公软件包中读取和写入文件。它可以出于任何目的完全免费下载和使用。

官网如下:http://www.openoffice.org/

需要先下载opneOffice,找到bin目录,进行设置

configuration.setOfficeHome("这里的路径一般为C:\\Program Files (x86)\\OpenOffice 4");

测试下转换的文件路径

    public static void main(String[] args) {
convertToPDF("/Users/Desktop/asdf.docx", "/Users/Desktop/adsf.pdf");
}

完整如下:


package org.example;

import org.artofsolving.jodconverter.OfficeDocumentConverter;
import org.artofsolving.jodconverter.office.DefaultOfficeManagerConfiguration;
import org.artofsolving.jodconverter.office.OfficeManager;

import java.io.File;

public class OfficeUtil {

private static OfficeManager officeManager;
private static int port[] = {8100};

/**
* start openOffice service.
*/

public static void startService() {
DefaultOfficeManagerConfiguration configuration = new DefaultOfficeManagerConfiguration();
try {
System.out.println("准备启动office转换服务....");
configuration.setOfficeHome("这里的路径一般为C:\\Program Files (x86)\\OpenOffice 4");
configuration.setPortNumbers(port); // 设置转换端口,默认为8100
configuration.setTaskExecutionTimeout(1000 * 60 * 30L);// 设置任务执行超时为30分钟
configuration.setTaskQueueTimeout(1000 * 60 * 60 * 24L);// 设置任务队列超时为24小时
officeManager = configuration.buildOfficeManager();
officeManager.start(); // 启动服务
System.out.println("office转换服务启动成功!");
} catch (Exception e) {
System.out.println("office转换服务启动失败!详细信息:" + e);
}
}

/**
* stop openOffice service.
*/

public static void stopService() {
System.out.println("准备关闭office转换服务....");
if (officeManager != null) {
officeManager.stop();
}
System.out.println("office转换服务关闭成功!");
}

public static void convertToPDF(String inputFile, String outputFile) {
startService();
System.out.println("进行文档转换转换:" + inputFile + " --> " + outputFile);
OfficeDocumentConverter converter = new OfficeDocumentConverter(officeManager);
converter.convert(new File(inputFile), new File(outputFile));
stopService();
}

public static void main(String[] args) {
convertToPDF("/Users/koolearn/Desktop/asdf.docx", "/Users/koolearn/Desktop/adsf.pdf");
}
}

2.kkFileView

github地址:github.com/kekingcn/kk…

支持的文件预览格式非常丰富 image.png

接下来是 从零到一 的启动步骤,按着步骤来,任何人都能搞定

  1. 安装java:
brew install java
  1. 安装maven,java的包管理工具:
brew install mvn
  1. 检查是否安装成功

执行java --versionmvn -v。我这里遇到mvn找不到java home的报错。解决方式如下:

我用的是zsh,所以需要去.zshrc添加路径:

export JAVA_HOME=$(/usr/libexec/java_home)

添加完后,执行

source .zshrc
  1. 安装下libreoffice:

kkFileView明确要求的额外依赖,否则无法启动

brew install libreoffice 
  1. mvn安装依赖

进入项目,在根目录执行依赖安装,同时清理缓存,跳过单测(遇到了单测报错的问题)

mvn clean install -DskipTests
  1. 启动项目

找到主文件,主函数mian,点击vscode上面的Run即可执行,路径如下图

image.png

  1. 访问页面

启动完成后,点击终端输出的地址

image.png

  1. 最终结果

最终展示如下,可以添加链接进行预览,也可以选择本地文件进行预览

image.png

预览效果非常好

3.onlyOffice

官网地址:http://www.onlyoffice.com/zh

github地址:github.com/ONLYOFFICE

开发者版本和社区版免费,企业版付费:http://www.onlyoffice.com/zh/docs-ent…

预览的文件种类没有kkFileView多,但对office三件套有很好的支持,甚至支持多人编辑。

四.总结

  1. 外部服务,推荐微软的view.officeapps.live.com/op/view.aspx,但只建议预览一些互联网公开的文件,不建议使用在要求保密性和稳定性的文件。
  2. 对保密性和稳定性有要求,且不差钱的,可以试试大厂服务,阿里云解决方案。
  3. 服务端技术比较给力的,使用服务端预览方案。目前最好最全的效果是服务端预览方案。
  4. 不想花钱,没有服务器的,使用前端预览方案,客户端渲染零成本。

五.参考文档:



    作者:然燃
    来源:juejin.cn/post/7268530145208451124
    收起阅读 »

    程序员男盆友给自己做了一款增进感情的小程序

    web
    前言 又是无聊的一天,逛GitHub的时候发现一个给女朋友做了一个互动微信小程序,据说女朋友更爱自己了,所以当晚。。。。给自己做了丰盛的晚餐,我当即点开立马开发粘贴复制起来,想到做的小程序可以和未来的女朋友增进感觉,越加猩粪。。。 回到正题,这个库有1.1k的...
    继续阅读 »

    前言


    又是无聊的一天,逛GitHub的时候发现一个给女朋友做了一个互动微信小程序,据说女朋友更爱自己了,所以当晚。。。。给自己做了丰盛的晚餐,我当即点开立马开发粘贴复制起来,想到做的小程序可以和未来的女朋友增进感觉,越加猩粪。。。


    回到正题,这个库有1.1k的star,推荐新人入坑原生小程序的可以学习


    项目地址:github.com/UxxHans/Rai…


    image.png


    云开发情侣互动小程序(做任务,攒积分,换商品)


    这是使用云开发能力构建的情侣互动小程序,可以跟女朋友互动哦,其中使用了云开发基础能力的使用:



    • 数据库:对文档型数据库进行读写和管理

    • 云函数:在云端运行的代码,开发者只需编写业务逻辑代码


    使用逻辑


    打个比方:



    • 女朋友发布任务->女朋友来做任务->做完后由你来确认完成->女朋友收到积分

    • 你发布商品(洗碗券)->女朋友使用积分购买->商品进入到女朋友的库存->女朋友拿着洗碗券叫你洗碗->你洗碗->女朋友将物品(洗碗券)标记为已使用(不可逆)

    • 这样做的原因是 不想给任何一方能自说自话 增加自己或者对方积分的能力[点击完成任务的人不能是获得积分的人也不能是自己]


    版本新增



    • 将所有非云函数的云逻辑封装为云函数

    • 新增了仓库系统,购买了的商品会存入仓库,然后再被使用

    • 新增了搜索框,可以搜索物品和任务

    • 新增了滑动窗,可以自动播放显示多张图片

    • 新增了商品和任务预设,添加商品或任务可以使用预设,非常迅速

    • 将新增按钮变为可拖拽的页面悬浮按钮

    • 购买,上架,新建任务的时间都会被记录并显示

    • 取消了点击左边圆圈来完成或者购买,统一改为左滑菜单

    • 左滑菜单统一用图标显示,更加精简

    • 使用特效升级了详细信息页面与添加页面的美观度

    • 添加任务或物品界面积分文本框改为滑块

    • 在商城添加了顶栏显示积分,更直观

    • 使用表情符号简单的增加了美感


    效果图与动画


    Animation.gif
    image.png


    部署方式



    image.png



    • 登录之后先在主页完成小程序信息类目

    • 然后可以在管理中的版本管理成员管理中发布小程序体验版并邀请对象使用


    image.png



    • 随后可以在开发中的开发工具里下载微信开发者工具

    • 打开微信开发工具->登录->导入我的文件夹-进入工具

    • 在左上角五个选项中选择云开发->按照提示开通云开发(这里可以选择免费的,不过限量,我开发用的多,6块够用了)


    image.png



    • 进入后点击数据库->在集合名称添加四个集合:MarketListMissionListStorageListUserList

    • 之前使用过上一个版本的,需要清空所有数据,因为字段结构不一样


    image.png



    • UserList中添加两个默认记录, 在两个记录中分别添加两个字段:


    字段 = _openid | 类型 = string | 值 = 先不填
    字段 = credit | 类型 = number | 值 = 0


    • 打开云开发的控制台的概览选项->复制环境ID

    • 打开 miniprogram/envList.js 将内容全部替换成如下,注意替换环境ID


    module.exports = {
    envList: [{
    envId:'上述步骤中你获得的环境ID (保留单引号)'
    }]
    }


    • 右键点击 cloudfunctions 中的每个文件夹并选择云函数云端安装依赖上传 (有点麻烦但是这是一定要做的)


    image.png



    • 如果云开发里面的云函数页面是这样的就是成功了


    image.png



    • 没有安装npm或者NodeJs, 需要先在这里安装: nodejs.org/dist/v16.15…

    • 安装好的,就直接运行cloudfunctions/Install-WX-Server-SDK.bat

    • 不成功的话可以在命令行输入 npm install --save wx-server-sdk@latest

    • 然后创建体验版小程序->通过开发者账号分享到女朋友手机上(要先登录小程序开发者账号)

    • 在两个手机上运行小程序->分别在两个手机上的小程序里新建任务

    • 然后回到云开发控制台的missionlist数据库集合->找自己和女朋友的_openid变量并记录

    • 把这两个记录下来的_openid拷贝到云开发控制台UserList数据集合里刚刚没填的_openid变量中

    • 把这两个记录下来的_openid拷贝到miniprogram/app.js里的_openidA_openidB的值里(A是卡比,B是瓦豆)

    • miniprogram/app.js里把userAuserB改成自己和女朋友的名字

    • 然后再试试看是不是成功了! (别忘了任务和物品左滑可以完成和购买)

    • 消息提醒功能:

    • 参考blog.csdn.net/hell_orld/a…allsobaiduend~default-2-110675777-null-null.142^v87^insert_down28v1,239^v2^insert_chatgpt&utm_term=%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E9%80%9A%E7%9F%A5%E4%BA%91%E5%BC%80%E5%8F%91&spm=1018.2226.3001.4187配置自己想要的模板\

    • miniprogram/pages/MainPage/index.jsminiprogram/pages/MissionAdd/index.js里把模板号换成自己想要的模板号

    • cloudfunctions/information/index.js里把UserA和UserB的openid值进行修改就能使用消息提醒功能了


    image.png



    • 别忘了最后点击右上角上传->然后在开发者账号上设置小程序为体验版->不用去发布去审核


    image.png



    旧版效果图


    image.png


    作者:嚣张农民
    来源:juejin.cn/post/7298966889358196788
    收起阅读 »

    两台Android 设备同一个局域网下如何自由通信?

    一、背景 笔者前段时间开发了一款非常有意思的项目,已知在同一个局域网下有两款App分别运行在不同的设备上,业务上两款App分别具有不同的功能,同时两款App需要支持互相通信。后面我们称两款设备上的两款App一个为Server,一个为Client。 基于此我们需...
    继续阅读 »

    一、背景


    笔者前段时间开发了一款非常有意思的项目,已知在同一个局域网下有两款App分别运行在不同的设备上,业务上两款App分别具有不同的功能,同时两款App需要支持互相通信。后面我们称两款设备上的两款App一个为Server,一个为Client。


    基于此我们需要考虑以下几点



    • 如何发现对方

    • 两款App如何通信

    • 通信协议如何选择


    二、发现对方


    在局域网中查找识别我们可以基于DNS-SD协议。


    1、DNS-SD协议介绍


    DNS-SD(Domain Name System - Service Discovery)是一种用于在局域网(Local Area Network,LAN)中发现服务的协议。它使用DNS协议扩展了域名系统(Domain Name System,DNS)的功能,使得客户端能够在局域网中查找特定类型的服务,并获取有关该服务的信息。


    在Android 中也有对应的Api可以使用:NsdManager。
    NsdManager主要实现三个功能:



    • 注册

    • 发现

    • 解析


    其中一台设备作为服务端(即上面的Server App)通过NsdManager注册服务,提供自己的IP地址与端口号,同时可以提供用于标识自己的信息,例如设备ID。当一个局域网中有多台服务时,客户端在发现服务后,可以基于标识信息确认是否为自己需要找到的服务端。


    另一台设备作为客户端(即上面的Client App),通过NsdManager的发现接口去发现服务。发现服务之后,再调用解析接口,获取到对应的信息,如服务端的IP地址、端口号、设备标识信息等。



    详细的官方介绍地址:developer.android.com/reference/a…



    2、注册


    作为服务端的Server App,通过NsdManager注册。


    NsdManager nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
    NsdServiceInfo serviceInfo = new NsdServiceInfo();
    serviceInfo.setServiceName(ServiceName);
    serviceInfo.setServiceType(ServiceType);
    int localPort = getLocalPort();
    serviceInfo.setPort(localPort);
    serviceInfo.setAttribute(Attribute_UUID, UuidManager.INSTANCE.getUUID());
    String ipAddress = LocalIpUtils.getIPAddress();
    serviceInfo.setAttribute(Attribute_IP, ipAddress);
    nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, new NsdManager.RegistrationListener() {
    @Override
    public void onRegistrationFailed(NsdServiceInfo nsdServiceInfo, int i) {
    //异常上报
    }

    @Override
    public void onUnregistrationFailed(NsdServiceInfo nsdServiceInfo, int i) {}

    @Override
    public void onServiceRegistered(NsdServiceInfo nsdServiceInfo) {
    //注册成功
    }

    @Override
    public void onServiceUnregistered(NsdServiceInfo nsdServiceInfo) {}
    });


    3、发现


    Client App,调用NsdManager#discoverServices()接口去发现服务。


    NsdManager nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
    discoveryListener = new NsdManager.DiscoveryListener() {
    @Override
    public void onStartDiscoveryFailed(String s, int i) {}

    @Override
    public void onStopDiscoveryFailed(String s, int i) {}

    @Override
    public void onDiscoveryStarted(String s) {}

    @Override
    public void onDiscoveryStopped(String s) {}

    @Override
    public void onServiceFound(NsdServiceInfo nsdServiceInfo) {}

    @Override
    public void onServiceLost(NsdServiceInfo nsdServiceInfo) {}
    };
    nsdManager.discoverServices(NsdServer.ServiceType, NsdManager.PROTOCOL_DNS_SD, discoveryListener);


    4、解析


    Client App 收到onServcieFound回调之后,就可以调用解析接口解析数据了


    nsdManager.resolveService(nsdServiceInfo, new NsdManager.ResolveListener() {
    @Override
    public void onServiceResolved(NsdServiceInfo nsdServiceInfo) {
    Map<String, byte[]> attributes = nsdServiceInfo.getAttributes();
    }

    @Override
    public void onResolveFailed(NsdServiceInfo nsdServiceInfo, int i) {}
    });


    二、TCP通信


    发现设备之后,我们要考虑两台设备要如何通信。此时我们可以有两套方案,分别是HTTP以及TCP。


    HTTP


    使用HTTP的话,就是在Server App上启动一个HTTP服务,我们可以预定义一些接口用于和Client来通信。
    优点是比较简单,缺点是Server App没有办法主动通知Client App。


    TCP


    直接使用TCP协议的话,我们可以考虑选择一个支持TCP通信的框架,自定义一个简易的通信协议。优点是客户端与服务端可以互相通信,满足我们的需求。缺点是相对复杂一些。


    在我们的项目中,我们有两台设备交互通信的需求,所以选择了TCP协议。


    1、TCP通信库的选择


    在我们的项目中使用了Netty作为TCP通信框架。关于Netty其大名鼎鼎,网络上的分析文章一大把,这里就不详细介绍了。Netty在国内有一位步道的大神名为李林峰(在华为工作),有兴趣的同学可以去看看大神的书。


    2、简易的TCP协议设计


    由于业务比较简单,所以这里我们将TCP通信协议进行简化。整体协议大致如下:[Int][String]。


    其中Int用于指定后面String字符串长度,我们可以基于这个Int来处理拆包、粘包的问题,这个逻辑后面详细介绍。String可以是JSON结构,可以依据业务来定义JSON中字段。


    3、TCP服务端


    使用Netty实现TCP服务端大致代码


    bossGr0up = new NioEventLoopGr0up();
    workerGr0up = new NioEventLoopGr0up();
    //构建引导程序
    mBootstrap = new ServerBootstrap();
    //设置EventGr0up
    mBootstrap.group(bossGr0up, workerGr0up);
    //设置Channel
    mBootstrap.channel(NioServerSocketChannel.class);
    mBootstrap.option(ChannelOption.SO_BACKLOG, 128);
    //设置的好处是禁用Nagle算法。表示不延迟立即发送
    //这个算法试图减少TCP包的数量和结构性开销,将多个较小的包组合较大的包进行发送。
    //这个算法收TCP延迟确认影响,会导致相继两次向链接发送请求包。
    mBootstrap.option(ChannelOption.TCP_NODELAY, false);
    mBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
    mBootstrap.childHandler(new CustomChannelInitializer());
    channelFuture = mBootstrap.bind(inetPort).sync();

    以上TCP服务就启动了。


    4、TCP客户端


    使用Netty实现TCP客户端大致代码:


    初始化


    //构建线程池
    mGr0up = new NioEventLoopGr0up();
    //构建引导程序
    mBootstrap = new Bootstrap();
    //设置EventGr0up
    mBootstrap.group(mGr0up);
    //设置Channel
    mBootstrap.channel(NioSocketChannel.class);
    //设置的好处是禁用Nagle算法。表示不延迟立即发送
    //这个算法试图减少TCP包的数量和结构性开销,将多个较小的包组合较大的包进行发送。
    //这个算法收TCP延迟确认影响,会导致相继两次向链接发送请求包。
    mBootstrap.option(ChannelOption.TCP_NODELAY, false);
    mBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
    mBootstrap.remoteAddress(ip.getIp(), ip.getPort());
    mBootstrap.handler(new CustomChannelInitializer());

    建立连接


    ChannelFuture channelFuture = mBootstrap.connect();
    channelFuture.addListener(new FutureListener() {
    @Override
    public void operationComplete(Future future) {
    final boolean isSuccess = future.isSuccess();
    writeLog("operation complete future.isSuccess: " + isSuccess);
    }
    });


    断开连接


    public void disConnect(boolean onPurpose) {
    try {
    if (mGr0up != null) {
    mGr0up.shutdownGracefully();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    try {
    if (mChannel != null) {
    mChannel.close();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    try {
    if (mChannelHandlerContext != null) {
    mChannelHandlerContext.close();
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    我们可以在operationComplete()方法中,确认建立连接成功。


    ChannelInitializer


    为了让更多协议和其他各种方式处理数据,Netty有了Handler组件。Handler就是为了处理Netty里面的置顶事件或一组事件。 ChannelInitializer 的作用就是将Handler 添加到ChannelPipeline中。当你发送或收到消息的时候,这些Handler就决定怎么处理你的数据。


    public class CustomChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) {
    ChannelPipeline pipeline = socketChannel.pipeline();
    mCustomDecoder = new CustomDecoder();
    mCustomEncoder = new CustomEncoder();
    mCustomHandler = new CustomHandler();

    pipeline.addLast(mCustomDecoder);
    pipeline.addLast(mCustomEncoder);
    pipeline.addLast(mCustomHandler);
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    super.handlerAdded(ctx);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    super.exceptionCaught(ctx, cause);
    }
    }


    ChannelPipeline 是一个管道,Handler就是里面一层一层要对数据进行处理的事件。所有的Handler都一个顶层接口ChannelHandler。ChannelHandler有两个子接口:



    • ChannelInboundHandler

    • ChannelOutboundHandler


    Netty中数据流有2个方向:



    • 数据进(应用收到消息)的时候与ChannelInboundHandler有关。

    • 数据出(应用发出数据)的时候与ChannelOutboundHandler有关。


    为了将数据从一端发送到另一端,一般都会有一个或多个ChannelHandler用各种方式对数据进行操作。 决定这些Handler以一种特定的顺序处理数据的是ChannelPipeline。


    ChannelInboundHandler与ChannelOutboundHandler可以混在同一个ChannelPipeline里面。当应用收到数据时,首先从ChannelPipeline的头部进入到一个ChannelInboundHandler 。第一个ChannelInboundHandler处理后传给下一个ChannelInboundHandler。 然后ChannelPipeline中没有其他的ChannelInboundHandler 了数据就会到达ChannelPipeline的尾部,也就是应用对数据的处理已经完成了。


    数据流出的过程是返过来的,首先从ChannelPipeline的尾部开始进入到最后一个ChannelOutboundHandler,最后一个ChannelOutboundHandler处理后,传给前面一个ChannelOutboundHandler。 和进不同的,进是从前到后,而出是从后到前。没有多余的ChannelOutboundHandler的时候,数据进入实际网络中传输,触发一些IO操作。


    一旦一个ChannelHandler被添加到ChannelPipeline中,它就会获得一个ChannelHandlerContext。一般情况下,获得这个对象并持有它是安全的, 不过在数据包协议的时候不一样安全,例如UDP协议。 在Netty中有两种发送数据的方式。你可以写到Channel中或者使用ChannelhandlerContext对象。他们的主要区别是,直接写到Channel,则数据会从ChannelPipeline头部传到尾部。 每一个ChannelHandler都会处理数据,而使用ChannelHandlerContext则是将数据传送到下一个ChannelHandler。


    一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。 入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。


    我们在 CustomChannelInitializer#initChannel()方法中添加编码、解码器。其中 CustomHandler 继承自SimpleChannelInboundHandler用来接收消息。


    三、编解码


    当你用Netty接收或发送消息,必须将其从一种格式转成另一种格式。比如收消息,你需要从字节转为Java对象。发消息就是将Java对象转成字节发出去。


    Netty中有各种各样的编码器和解码器基类。



    • ByteToMessageDecoder

    • MessageToByteEncoder

    • ProtobufEncoder

    • ProtobufDecoder

    • StringDecoder

    • StringEncode


    这里,编码器都是继承自ChannelInboundHandlerAdapter或者实现了ChannelInboundHandler。当读到数据时,会调用ChannelRead方法。重写此方法,然后就会调用decode 方法进行解码。并且会执行ChannelHandlerContext,fireChannelRead方法,将解码后的消息传给下一个Channelhandler。 当发送消息的时候,也执行类似的过程,编码器将消息转为字节,然后传给下一个ChannelOutboundHandler。


    在我们的项目中,由于自定义了协议所以使用了ByteToMessageDecoder、MessageToByteEncoder。


    1、编码


    编码器比较简单,我们可以自定义类继承 MessageToByteEncoder 来实现解码。
    需要注意的是,按照我们定义的协议顺序向 ByteBuf 中写入数据就好了。示例代码:


    public class CustomEncoder extends MessageToByteEncoder<CustomMessage> {

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, CustomMessage message, ByteBuf byteBuf) {
    //byteBuf.writeInt();
    //byteBuf.writeByte();
    }

    }


    2、解码


    解码我们要解决TCP拆包、粘包的问题。


    一般TCP中应对拆包、粘包基本有以下几种方案:



    • 消息定长,这但比较好理解,由于定长了,我们可以直接判断ByteBuffer中数组长度。不过在实践过程中,一般我们不会使用这个方案,因为扩展性太差了。

    • 使用特殊字符作为结尾。

    • 自定义协议。


    前面我们有提到我们的通信协议为[Int][String]实际上就是一个非常简易的自定义协议(由于业务简单,所以这里没有设计的非常复杂)。我们使用Int来标记后面的String长度,这样两端收到消息后,按照这个格式解析实际上就知道消息的长度了。


    在Netty中,我们可以自定义类继承自 ByteToMessageDecoder,来处理解码。Netty中 ByteBuf 来处理数据。具体的步骤是:



    • 先标记已读位置:byteBuf.markReaderIndex();

    • 判断 ByteBuf 可读是否达到4个字节长度,如果不足直接返回,重置已读位置。

    • 读取前4个字节,转为Int。

    • 如果字符长度 > 0,那么接下来继续读 length 长度的字符,转为String。

    • 这样整个协议就解码完成了。


    这里需要注意的是:客户端要与服务端约定好是大端字节序还是小端字节序。


    以上在局域网中,两款App进行TCP通信就已经搭建好了,我们有了协议上层业务就可以基于此来封装业务需要的逻辑了。实际上很多IM 通信SDK,基本上都是自定义通信协议,只是协议会比本篇中举例的协议要复杂的多。


    四、优化点


    项目上线一段时间后,用户反馈偶现存在两台设备无法通信的问题。后面经过调研发现是服务端进程一直存在(即Server App 并没有挂),只是TCP服务挂了。


    我们在客户端有处理断线重连逻辑,但是在服务端没有做任何监控重启逻辑。


    如何优化


    经过调研,我们在服务端Server App中,另外启动一个客户端来与TCP服务端建立连接(相当于是我监控我自己了),如果建立失败,就重启TCP服务。



    • 建立一个TCP客户端,启动轮询逻辑

    • 如果该客户端没有与服务端建立连接,那么尝试建立连接。

    • 如果连续三次都无法建立成功连接,那么认为此时TCP服务存在异常,重启TCP服务。

    • 如果可以正常与TCP服务建立连接,那么开始向TCP服务发送心跳包

    • 如果连续三次TCP服务没有回复心跳回包,那么也认为TCP服务存在异常,重启TCP服务。
      以上,就是我们针对TCP服务端的优化,上面逻辑上线后,无法建立连接的反馈就没有了。


    五、总结


    本篇主要是介绍如何在局域网中,两个APP使用Dns-SD协议来发现对方。自定义协议进行TCP通信,使用Netty作为TCP通信框架。


    作者:半山居士
    来源:juejin.cn/post/7375275474006802443
    收起阅读 »

    别再用后缀判断文件类型了,来认识一下魔数头

    引言 最近公司整改,招了个安全测试,安全测试的同事搞了个危险的文件,悄咪咪加了个.png的后缀,就丢到我们的生产服务器上面去了,这么勇,你敢信? 不管你信不信,事实他就是发生了,就问你怕不怕。 好了,这里也就暴露了一个很大的问题,我们的开发同事判断文件类型,...
    继续阅读 »

    引言



    最近公司整改,招了个安全测试,安全测试的同事搞了个危险的文件,悄咪咪加了个.png的后缀,就丢到我们的生产服务器上面去了,这么勇,你敢信?


    不管你信不信,事实他就是发生了,就问你怕不怕。


    好了,这里也就暴露了一个很大的问题,我们的开发同事判断文件类型,是使用文件后缀来判断的,所以被直接跳过。咱来看看更加科学的识别方式。



    一、认识魔数


    魔数:也被称为签名或文件签名,是一种用于识别文件类型和格式的短序列字节。它们通常位于文件的开头,并被设计为易于识别,以便软件可以快速确定文件是否是它所支持的格式。


    魔数是一种简单的识别机制,它由一系列字节组成,这些字节在特定的文件格式中是唯一的。当一个程序打开一个文件时,它会检查文件的开始处是否包含这些特定的字节。如果找到,程序就认为文件是该格式的,并按照相应的规则进行解析和处理。


    二、文件类型检测的原理



    文件类型检测通常基于文件的“魔数”(magic number),也称为签名或文件签名。魔数是文件开头的字节序列,用于标识文件格式。以下是文件类型检测的原理和步骤:



    2.1 文件头的读取方法


    image.png


    打开文件:首先,需要以二进制模式打开文件,以便能够读取文件的原始字节。


    读取字节:接着,读取文件开头的一定数量的字节(通常是前几个字节)。


    关闭文件:读取完成后,关闭文件以释放资源。


    2.2 如何通过文件头识别文件类型


    image.png


    比较魔数:将读取的字节与已知的文件类型魔数进行比较。


    匹配类型:如果字节序列与某个文件类型的魔数匹配,则可以确定文件类型。


    处理异常:如果字节序列与任何已知魔数都不匹配,可能需要进一步的分析或返回未知文件类型。


    三、Java实现文件类型检测


    当需要通过文件头(魔数头)判断文件类型时,可以按照以下文字描述的流程进行实现:


    graph LR
    F[打开文件] --> B[读取文件头]
    B --> C[判断文件类型]
    C --> D[比较文件头]
    D --> E[输出文件类型]

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px


    1. 打开文件:使用文件输入流(FileInputStream)打开待判断类型的文件。

    2. 读取文件头:从文件中读取一定长度的字节数据作为文件头。通常,文件头的长度为固定的几个字节,一般是 2-8 个字节。

    3. 判断文件类型:根据不同文件类型的魔数头进行判断。魔数头是文件中特定位置的字节序列,用于标识文件类型。每种文件类型都有不同的魔数头。

    4. 比较文件头:将读取到的文件头与已知文件类型的魔数头进行比较。如果匹配成功,则确定文件类型。

    5. 输出文件类型:根据匹配的文件类型,输出相应的文件类型描述信息。


    3.1 具体实现方法


    3.1.1 定义枚举类


    /**
    * 文件类型魔数枚举
    * 使用场景:用于判断文件类型
    * 使用方法:FileUtils.isFileType(new FileInputStream(file), FileTypeEnum.XLSX)
    *
    * @author bamboo panda
    * @version 1.0
    * @date 2024/5/23 19:37
    */

    public enum FileTypeEnum {
    /**
    * JPEG
    */

    JPEG("JPEG", "FFD8FF"),

    /**
    * PNG
    */

    PNG("PNG", "89504E47"),

    /**
    * GIF
    */

    GIF("GIF", "47494638"),

    /**
    * TIFF
    */

    TIFF("TIFF", "49492A00"),

    /**
    * Windows bitmap
    */

    BMP("BMP", "424D"),

    /**
    * CAD
    */

    DWG("DWG", "41433130"),

    /**
    * Adobe photoshop
    */

    PSD("PSD", "38425053"),

    /**
    * Rich Text Format
    */

    RTF("RTF", "7B5C727466"),

    /**
    * XML
    */

    XML("XML", "3C3F786D6C"),

    /**
    * HTML
    */

    HTML("HTML", "68746D6C3E"),

    /**
    * Outlook Express
    */

    DBX("DBX", "CFAD12FEC5FD746F "),

    /**
    * Outlook
    */

    PST("PST", "2142444E"),

    /**
    * doc;xls;dot;ppt;xla;ppa;pps;pot;msi;sdw;db
    */

    OLE2("OLE2", "0xD0CF11E0A1B11AE1"),

    /**
    * Microsoft Word/Excel
    */

    XLS_DOC("XLS_DOC", "D0CF11E0"),

    /**
    * Microsoft Access
    */

    MDB("MDB", "5374616E64617264204A"),

    /**
    * Word Perfect
    */

    WPB("WPB", "FF575043"),

    /**
    * Postscript
    */

    EPS_PS("EPS_PS", "252150532D41646F6265"),

    /**
    * Adobe Acrobat
    */

    PDF("PDF", "255044462D312E"),

    /**
    * Windows Password
    */

    PWL("PWL", "E3828596"),

    /**
    * ZIP Archive
    */

    ZIP("ZIP", "504B0304"),

    /**
    * ARAR Archive
    */

    RAR("RAR", "52617221"),

    /**
    * WAVE
    */

    WAV("WAV", "57415645"),

    /**
    * AVI
    */

    AVI("AVI", "41564920"),

    /**
    * Real Audio
    */

    RAM("RAM", "2E7261FD"),

    /**
    * Real Media
    */

    RM("RM", "2E524D46"),

    /**
    * Quicktime
    */

    MOV("MOV", "6D6F6F76"),

    /**
    * Windows Media
    */

    ASF("ASF", "3026B2758E66CF11"),

    /**
    * MIDI
    */

    MID("MID", "4D546864"),
    /**
    * xlsx
    */

    XLSX("XLSX", "504B0304"),
    /**
    * xls
    */

    XLS("XLS", "D0CF11E0A1B11AE1");

    private String key;
    private String value;

    FileTypeEnum(String key, String value) {
    this.key = key;
    this.value = value;
    }

    public String getValue() {
    return value;
    }

    public String getKey() {
    return key;
    }
    }

    3.1.2 文件类型判断工具类


    import java.io.IOException;
    import java.io.InputStream;

    /**
    * 文件类型判断工具类
    *
    * @author bamboo panda
    * @version 1.0
    * @date 2024/5/23 19:38
    */

    public class FileTypeUtils {

    /**
    * 获取文件头
    *
    * @param inputStream 输入流
    * @return 16 进制的文件投信息
    * @throws IOException io异常
    */

    private static String getFileHeader(InputStream inputStream) throws IOException {
    byte[] b = new byte[28];
    inputStream.read(b, 0, 28);
    inputStream.close();
    return bytes2hex(b);
    }

    /**
    * 将字节数组转换成16进制字符串
    *
    * @param src 文件字节数组
    * @return 16进制字符串
    */

    private static String bytes2hex(byte[] src) {
    StringBuilder stringBuilder = new StringBuilder("");
    if (src == null || src.length <= 0) {
    return null;
    }
    for (byte b : src) {
    int v = b & 0xFF;
    String hv = Integer.toHexString(v);
    if (hv.length() < 2) {
    stringBuilder.append(0);
    }
    stringBuilder.append(hv);
    }
    return stringBuilder.toString();
    }

    /**
    * 判断指定输入流是否是指定文件格式
    *
    * @param inputStream 输入流
    * @param fileTypeEnum 文件格式枚举
    * @return true 是; false 否
    * @throws IOException io异常
    */

    public static boolean isFileType(InputStream inputStream, FileTypeEnum fileTypeEnum) throws IOException {
    if (null == inputStream) {
    return false;
    }
    String fileHeader = getFileHeader(inputStream);
    return fileHeader.toUpperCase().startsWith(fileTypeEnum.getValue());
    }

    }

    3.1.3 测试方法


    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;

    /**
    * 测试:判断文件是否是excel
    *
    * @author bamboo panda
    * @version 1.0
    * @date 2024/5/23 19:33
    */

    public class Test {

    public static void main(String[] args) {
    File file = new File("C:\Users\Admin\Desktop\temp\Import file.xlsx");
    try (FileInputStream fileInputStream = new FileInputStream(file)) {
    if (!FileTypeUtils.isFileType(fileInputStream, FileTypeEnum.XLSX) || !FileTypeUtils.isFileType(fileInputStream, FileTypeEnum.XLS)) {
    System.out.println(true);
    } else {
    System.out.println(false);
    }
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    }

    四、其它注意事项


    在处理文件类型检测和数据保护时,安全性和隐私是两个非常重要的考虑因素。以下是一些相关的安全性问题和最佳实践:


    4.1 魔数检测的安全性问题


    graph LR
    F(文件类型判断)
    B(魔数检测的安全性问题)
    C(误报和漏报)
    D(恶意文件伪装)
    E(更新和维护)

    F ---> B
    B ---> C
    B ---> D
    B ---> E

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px


    1. 误报和漏报:魔数检测可能因为文件损坏或不完整而产生误报或漏报。一些恶意软件可能会模仿合法文件的魔数来逃避检测。

    2. 恶意文件伪装:攻击者可能故意在文件中嵌入合法的魔数,使得恶意文件看起来像是合法的文件类型。

    3. 更新和维护:随着新文件类型的出现和旧文件类型的淘汰,魔数列表需要定期更新,否则检测系统可能会变得不准确或过时。


    4.2 数据保护和隐私的最佳实践


    graph LR
    I(文件类型判断)

    A(数据保护和隐私的最佳实践)

    G(最小权限原则)
    B(数据加密)
    C(安全的数据传输)
    D(访问控制)
    E(定期审计和测试)
    F(数据最小化)

    I ---> A
    A ---> B
    A ---> C
    A ---> D
    A ---> E
    A ---> F
    A ---> G

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
    style G fill:#ADD8E6,stroke:#ADD8E6,stroke-width:2px
    style A fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
    style I fill:#EEDD82,stroke:#EEDD82,stroke-width:2px


    1. 最小权限原则:确保应用程序只请求执行其功能所必需的权限,不要求额外的权限。

    2. 数据加密:对敏感数据进行加密,无论是在传输中还是存储时,都应使用强加密标准。

    3. 安全的数据传输:使用安全的协议(如HTTPS)来保护数据在网络中的传输。

    4. 访问控制:实施严格的访问控制措施,确保只有授权用户才能访问敏感数据。

    5. 定期审计和测试:定期进行安全审计和渗透测试,以发现和修复潜在的安全漏洞。

    6. 数据最小化:只收集完成服务所必需的最少数据量,避免收集不必要的个人信息。


    4.3 魔数的局限性和风险


    魔数判断文件类型是一种常用的方法,但也存在一些局限性和风险,包括以下几点:


    graph LR
    I(文件类型判断)

    A(魔数的局限性和风险)

    B(可伪造性)
    C(文件类型扩展性)
    D(文件损坏或篡改)
    E(多重文件类型)
    F(文件类型模糊性)

    I ---> A
    A ---> B
    A ---> C
    A ---> D
    A ---> E
    A ---> F

    style B fill:#FFC0CB,stroke:#FFC0CB,stroke-width:2px
    style C fill:#FFA07A,stroke:#FFA07A,stroke-width:2px
    style D fill:#FFFFE0,stroke:#FFFFE0,stroke-width:2px
    style E fill:#98FB98,stroke:#98FB98,stroke-width:2px
    style F fill:#B2FFFF,stroke:#B2FFFF,stroke-width:2px
    style A fill:#E6E6FA,stroke:#E6E6FA,stroke-width:2px
    style I fill:#EEDD82,stroke:#EEDD82,stroke-width:2px


    1. 可伪造性:魔数头是文件中的特定字节序列,攻击者可以通过修改文件的魔数头来伪装文件类型。这可能导致误判文件类型或绕过文件类型检测。

    2. 文件类型扩展性:随着新的文件类型的出现,魔数头的定义可能需要不断更新,以适应新的文件类型。如果应用程序不及时更新对新文件类型的判断逻辑,可能无法正确识别新的文件类型。

    3. 文件损坏或篡改:如果文件的魔数头部分被损坏或篡改,可能导致无法正确判断文件类型,或者将文件错误地归类为不正确的类型。

    4. 多重文件类型:某些文件可能具有多重文件类型,即使使用魔数头判断了其中一种类型,也可能存在其他类型。这可能导致文件类型的混淆和判断的不准确性。

    5. 文件类型模糊性:某些文件类型可能具有相似或相同的魔数头,这可能导致在这些类型之间进行区分时出现困难。这可能增加了误判文件类型的风险。


    五、总结


    好了,到这里魔数怎么用的就说明白了。


    魔数的广泛的应用在在文件类型检测中。魔数是文件开头的特定字节序列,帮助软件快速识别文件格式。


    然而,魔数检测存在安全性问题,如误报、恶意伪装等,需定期更新魔数库。此外,应用魔数检测时要考虑文件损坏、多重类型等局限性,结合实际情况采取综合措施,如数据加密、访问控制等,确保安全性和准确性。



    希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。


    同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。


    感谢您的支持和理解!



    作者:竹子爱揍功夫熊猫
    来源:juejin.cn/post/7372100124636381194
    收起阅读 »

    我有点想用JDK17了

    大家好呀,我是summo,JDK版本升级的非常快,现在已经到JDK20了。JDK版本虽多,但应用最广泛的还得是JDK8,正所谓“他发任他发,我用Java8”。 其实我也不太想升级JDK版本,感觉投入高,收益小,不过有一次我看到了一些使用JDK17新语法写的代码...
    继续阅读 »

    大家好呀,我是summo,JDK版本升级的非常快,现在已经到JDK20了。JDK版本虽多,但应用最广泛的还得是JDK8,正所谓“他发任他发,我用Java8”。


    其实我也不太想升级JDK版本,感觉投入高,收益小,不过有一次我看到了一些使用JDK17新语法写的代码,让我改变了对升级JDK的看法,因为这些新语法我确实想用!


    废话不多说,上代码!


    一、JDK17语法新特性


    1. 文本块



    这个更新非常实用。在没有这个特性之前,编写长文本非常痛苦。虽然IDEA等集成开发工具可以自动处理,但最终效果仍然丑陋,充满拼接符号。现在,通过字符串块,我们可以轻松编写JSON、HTML、SQL等内容,效果更清爽。这个新特性值得五颗星评价,因为它让我们只需关注字符串本身,而无需关心拼接操作。



    原来的写法


    /**
    * 使用JDK8返回HTML文本
    *
    * @return 返回HTML文本
    */

    public static final String getHtmlJDK8() {
    return "<html>\n" +
    " <body>\n" +
    " <p>Hello, world</p>\n" +
    " </body>\n" +
    "</html>";
    }

    新的写法


    /**
    * 使用JDK17返回HTML文本
    *
    * @return 返回HTML文本
    */

    public static final String getHtmlJDK17() {
    return """
    <html>
    <body>
    <p>Hello, world</p>
    </body>
    </html>
    """
    ;
    }


    推荐指数:⭐️⭐️⭐️⭐️⭐️



    2. NullPointerException增强



    这一功能非常强大且实用,相信每位Java开发者都期待已久。空指针异常(NPE)一直是Java程序员的痛点,因为报错信息无法直观地指出哪个对象为空,只抛出一个NullPointerException和一堆堆栈信息,定位问题耗时且麻烦。尤其在遇到喜欢级联调用的代码时,逐行排查更是令人头疼。如果在测试环境中,可能还需通过远程调试查明空对象,费时费力。为此,阿里的编码规范甚至不允许级联调用,但这并不能彻底解决问题。Java17终于在这方面取得了突破,提供了更详细的空指针异常信息,帮助开发者迅速定位问题源头。



    public static void main(String[] args) {
    try {
    //简单的空指针
    String str = null;
    str.length();
    } catch (Exception e) {
    e.printStackTrace();
    }
    try {
    //复杂一点的空指针
    var arr = List.of(null);
    String str = (String)arr.get(0);
    str.length();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    运行结果



    推荐指数:⭐️⭐️⭐️⭐️⭐️



    3. Records



    在Java中,POJO对象(如DO、PO、VO、DTO等)通常包含成员变量及相应的Getter和Setter方法。尽管可以通过工具或IDE生成这些代码,但修改和维护仍然麻烦。Lombok插件为此出现,能够在编译期间自动生成Getter、Setter、hashcode、equals和构造函数等代码,使用起来方便,但对团队有依赖要求。
    为此,Java引入了标准解决方案:Records。它通过简洁的语法定义数据类,大大简化了POJO类的编写,如下所示。虽然hashcode和equals方法仍需手动编写,但IDE能够自动生成。这一特性有效解决了模板代码问题,提升了代码整洁度和可维护性。



    package com.summo.jdk17;

    /**
    * 3星
    *
    * @param stuId 学生ID
    * @param stuName 学生名称
    * @param stuAge 学生年龄
    * @param stuGender 学生性别
    * @param stuEmail 学生邮箱
    */

    public record StudentRecord(Long stuId,
    String stuName,
    int stuAge,
    String stuGender,
    String stuEmail)
    {
    public StudentRecord {
    System.out.println("构造函数");
    }

    public static void main(String[] args) {
    StudentRecord record = new StudentRecord(1L, "张三", 16, "男", "xxx@qq.com");
    System.out.println(record);
    }
    }


    推荐指数:⭐️⭐️⭐️⭐️



    4. 全新的switch表达式



    有人可能问了,Java语言不早已支持switch了嘛,有什么好提的?讲真,这次的提升还真有必要好好地来聊一聊了。在Java12的时候就引入了switch表达式,注意这里是表达式,而不是语句,原来的switch是语句。如果不清楚两者的区别的话,最好先去了解一下。主要的差别就是就是表达式有返回值,而语句则没有。再配合模式匹配,以及yield和“->”符号的加入,全新的switch用起来爽到飞起来。



    package com.summo.jdk17;

    public class SwitchDemo {
    /**
    * 在JDK8中获取switch返回值方式
    *
    * @param week
    * @return
    */

    public int getByJDK8(Week week) {
    int i = 0;
    switch (week) {
    case MONDAY, TUESDAY:
    i = 1;
    break;
    case WEDNESDAY:
    i = 3;
    break;
    case THURSDAY:
    i = 4;
    break;
    case FRIDAY:
    i = 5;
    break;
    case SATURDAY:
    i = 6;
    break;
    case SUNDAY:
    i = 7;
    break;
    default:
    i = 0;
    break;
    }

    return i;
    }

    /**
    * 在JDK17中获取switch返回值
    *
    * @param week
    * @return
    */

    public int getByJDK17(Week week) {
    // 1, 现在的switch变成了表达式,可以返回值了,而且支持yield和->符号来返回值
    // 2, 再也不用担心漏写了break,而导致出问题了
    // 3, case后面支持写多个条件
    return switch (week) {
    case null -> -1;
    case MONDAY -> 1;
    case TUESDAY -> 2;
    case WEDNESDAY -> 3;
    case THURSDAY -> {yield 4;}
    case FRIDAY -> 5;
    case SATURDAY, SUNDAY -> 6;
    default -> 0;
    };
    }

    private enum Week {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
    }
    }


    推荐指数:⭐️⭐️⭐️⭐️



    5. 私有接口方法



    从Java8开始,允许在interface里面添加默认方法,其实当时就有些小困惑,如果一个default方法体很大怎么办,拆到另外的类去写吗?实在有些不太合理,所以在Java17里面,如果一个default方法体很大,那么可以通过新增接口私有方法来进行一个合理的拆分了,为这个小改进点个赞。



    public interface PrivateInterfaceMethod {
        /**
         * 接口默认方法
         */

        default void defaultMethod() {
            privateMethod();
        }

        // 接口私有方法,在Java8里面是不被允许的,不信你试试
        private void privateMethod() {
        }
    }


    推荐指数:⭐️⭐️⭐️



    6. 模式匹配



    在JDK 17中,模式匹配主要用于instanceof表达式。模式匹配增强了instanceof的语法和功能,使类型检查和类型转换更加简洁和高效。在传统的Java版本中,我们通常使用instanceof结合类型转换来判断对象类型并进行处理,这往往会导致冗长的代码。



    原来的写法


    /**
    * 旧式写法
    *
    * @param value
    */

    public void matchByJDK8(Object value) {
    if (value instanceof String) {
    String v = (String)value;
    System.out.println("遇到一个String类型" + v.toUpperCase());
    } else if (value instanceof Integer) {
    Integer v = (Integer)value;
    System.out.println("遇到一个整型类型" + v.longValue());
    }
    }

    新的写法


    /**
    * 转换并申请了一个新的变量,极大地方便了代码的编写
    *
    * @param value
    */

    public void matchByJDK17(Object value) {
    if (value instanceof String v) {
    System.out.println("遇到一个String类型" + v.toUpperCase());
    } else if (value instanceof Integer v) {
    System.out.println("遇到一个整型类型" + v.longValue());
    }
    }


    推荐指数:⭐️⭐️⭐️⭐️



    7. 集合类的工厂方法



    在Java8的年代,即便创建一个很小的集合,或者固定元素的集合都是比较麻烦的,为了简洁一些,有时我甚至会引入一些依赖。



    原来的写法


    Set<String> set = new HashSet<>();
    set.add("a");
    set.add("b");
    set.add("c"

    新的写法


    Set<String> set = Set.of("a", "b", "c");


    推荐指数:⭐️⭐️⭐️⭐️⭐️



    二、其他的新特性


    1. 新的String方法



    • repeat:重复生成字符串

    • isBlank:不用在引入第三方库就可以实现字符串判空了

    • strip:去除字符串两边的空格,支持全角和半角,之前的trim只支持半角

    • lines:能根据一段字符串中的终止符提取出行为单位的流

    • indent:给字符串做缩进,接受一个int型的输入

    • transform:接受一个转换函数,实现字符串的转换


    2. Stream API的增强



    增加takeWhile, dropWhile, ofNullable, iterate以及toList的API,越来越像一些函数式语言了。用法举例如下。



    // takeWhile 顺序返回符合条件的值,直到条件不符合时即终止继续判断,
    // 此外toList方法的加入,也大大减少了节省了代码量,免去了调用collect(Collectors::toList)方法了
    List<Integer> list = Stream.of(2,2,3,4,5,6,7,8,9,10)
            .takeWhile(i->(i%2==0)).toList(); // 返回2, 2

    // dropWhile 顺序去掉符合条件的值,直到条件不符合时即终止继续判断
    List<Integer> list1 = Stream.of(2,2,3,4,5,6,7,8,9,10)
            .dropWhile(i->(i%2==0)).toList(); //返回3, 4, 5, 6, 7, 8, 9, 10

    // ofNullable,支持传入空流,若没有这个且传入一个空流,那么将会抛NPE
    var nullStreamCount = Stream.ofNullable(null).count(); //返回0

    // 以下两行都将输出0到9
    Stream.iterate(0, n -> n < 10, n -> n + 1).forEach(x -> System.out.println(x));
    Stream.iterate(0, n -> n + 1).limit(10).forEach(x -> System.out.println(x));

    3. 全新的HttpClient



    这个API首次出现在9之中,不过当时并非是一个稳定版本,在Java11中正式得到发布,所以在Java17里面可以放心地进行使用。原来的JDK自带的Http客户端真的非常难用,这也就给了很多像okhttp、restTemplate、Apache的HttpClient和feign这样的第三方库极大的发挥空间,几乎就没有人愿意去用原生的Http客户端的。但现在不一样了,感觉像是新时代的API了。FluentAPI风格,处处充满了现代风格,用起来也非常地方便,再也不用去依赖第三方的包了,就两个字,清爽。



    // 同步请求
    HttpClient client = HttpClient.newBuilder()
            .version(Version.HTTP_1_1)
            .followRedirects(Redirect.NORMAL)
            .connectTimeout(Duration.ofSeconds(20))
            .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80)))
            .authenticator(Authenticator.getDefault())
            .build();
       HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
       System.out.println(response.statusCode());
       System.out.println(response.body());
    // 异步请求
    HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://foo.com/"))
            .timeout(Duration.ofMinutes(2))
            .header("Content-Type", "application/json")
            .POST(BodyPublishers.ofFile(Paths.get("file.json")))
            .build();
       client.sendAsync(request, BodyHandlers.ofString())
            .thenApply(HttpResponse::body)
            .thenAccept(System.out::println);
     

    4. jshell



    在新的JDK版本中,支持直接在命令行下执行java程序,类似于python的交互式REPL。简而言之,使用 JShell,你可以输入代码片段并马上看到运行结果,然后就可以根据需要作出调整,这样在验证一些简单的代码的时候,就可以通过jshell得到快速地验证,非常方便。



    5. java命令直接执行java文件



    在现在可以直接通过执行“java xxx.java”,即可运行该java文件,无须先执行javac,然后再执行java,是不是又简单了一步。



    6. ZGC



    在ParallelOldGC、CMS和G1之后,JDK 11引入了全新的ZGC(Z Garbage Collector)。这个名字本身就显得很牛。官方宣称ZGC的垃圾回收停顿时间不超过10ms,能支持高达16TB的堆空间,并且停顿时间不会随着堆的增大而增加。那么,ZGC到底解决了什么问题?Oracle官方介绍它是一个可伸缩的低延迟垃圾回收器,旨在降低停顿时间,尽管这可能会导致吞吐量的降低。不过,通过横向扩展服务器可以解决吞吐量问题。官方已建议ZGC可用于生产环境,这无疑将成为未来的主流垃圾回收器。要了解更多,请参阅官方文档



    三、小结一下


    作为程序员,持续学习和充电非常重要。随着Java8即将停止免费官方支持,越来越多的项目将转向Java17,包括大名鼎鼎的Spring Boot 3.0,它在2022年1月20日发布的第一个里程碑版本(M1)正是基于Java17构建的。该项目依赖的所有组件也将快速升级,未来如果想利用某些新特性,在Java8下将无法通过编译.,到这时候再换就真的晚了... ...


    作者:summo
    来源:juejin.cn/post/7376444924424241162
    收起阅读 »

    Dialog 可不可以传Application

    自定义Dialog继承Dialogclass SourceDialog(context: Context, themeResId: Int) : Dialog(context, themeResId) { constructor(contex...
    继续阅读 »

    自定义Dialog

    1. 继承Dialog
    class SourceDialog(context: Context, themeResId: Int) : Dialog(context, themeResId) {  

    constructor(context: Context) : this(context, R.style.CustomDialogTheme)

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.source_layout)
    }
    }
    1. 创建自己的主题样式
    <style name="CustomDialogTheme" parent="@android:style/Theme.Dialog">  
    <item name="android:windowBackground">@android:color/transparentitem> //透明背景
    <item name="android:windowNoTitle">trueitem> //没有标题
    <item name="android:windowFullscreen">trueitem> //是否全屏
    <item name="android:backgroundDimEnabled">trueitem> //背景黑暗
    <item name="android:backgroundDimAmount">0.5item> //背景黑暗透明度
    style>

    可以传入自己创建的主题,也可以不传,Android 会有默认的主题

    Dialog(@UiContext @NonNull Context context, @StyleRes int themeResId,  
    boolean createContextThemeWrapper) {
    if (createContextThemeWrapper) {
    if (themeResId == Resources.ID_NULL) {
    final TypedValue outValue = new TypedValue();
    //这里会指定默认的主题,如果不传主题
    context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
    themeResId = outValue.resourceId;
    }
    mContext = new ContextThemeWrapper(context, themeResId);
    } else {
    mContext = context;
    }

    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setOnWindowSwipeDismissedCallback(() -> {
    if (mCancelable) {
    cancel();
    }
    });
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);
    mListenersHandler = new ListenersHandler(this);
    }
    1. 使用
    val dialog=SourceDialog(context) 
    dialog.show()

    遇到的问题

    1. Dialog 可不可以传Application ?

    背景:这几天接到一个需求,收到动作需要在任何界面上弹出信号源选择器页面(铺满整个屏幕),我一开始是选择了Service+WindowManager 添加View显示的。之前也看了一下公司的CommonUI (展示一下亮度条,音量条之类的全局UI) 用到的是Dialog 弹出界面的。我也跟着写一个,才发现一个一个坑接着来。

    答案是可以的,是要window 传一个 type

    这是我的Dialog

    class SourceDialog(context: Context, themeResId: Int) : Dialog(context, themeResId) {  

    constructor(context: Context) : this(context, R.style.CustomDialogTheme)

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.source_layout)
    }
    }

    //传入Application
    val dialog=SourceDialog(MyApplication.CONTEXT)
    dialog.show()

    我就很疑惑,提示Activity需要运行

    屏幕截图 2024-05-24 151913.png

    我后来换成了,正常运行

    //传入activity
    val dialog=SourceDialog(this@MainActivity)
    dialog.show()

    很疑惑,对比同事负责的项目发现我需要给window设置了一些东西

    //Dialog 

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.source_layout)
    window?.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
    }

    //这样就可以正常展示
    val dialog=SourceDialog(MyApplication.CONTEXT)
    dialog.show()

    1. 设置Dialog 全屏宽高不成功

    我的布局文件 最外层是线性布局

    <LinearLayout android:id="@+id/group_source"  
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:focusable="false"
    android:gravity="center"
    android:orientation="horizontal"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">


    LinearLayout>

    但是我发现 设置主题样式true 不起作用。 我在这里参考了 三句代码创建全屏Dialog或者DialogFragment

    • 粗暴一点直接设置window 大小 ==需要在setContentView 之后设置window的大小才会生效,如果在setContentView 之前设置,此时window的dectorView为空不会更新布局
    class SourceDialog(context: Context, themeResId: Int) : Dialog(context, themeResId) {  

    constructor(context: Context) : this(context, R.style.CustomDialogTheme)

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.source_layout)
    window?.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)
    window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT) //也可以换成具体数值宽高
    }
    }
    • 主题样式增加一样 false 因为很多默认的Dialog 主题这个属性一般为true。
    <style name="CustomDialogTheme" parent="@android:style/Theme.Dialog">  
    <item name="android:windowBackground">@android:color/transparentitem>
    <item name="android:windowNoTitle">trueitem>
    <item name="android:windowFullscreen">trueitem>
    <item name="android:windowIsFloating">falseitem>
    <item name="android:backgroundDimEnabled">trueitem>
    <item name="android:backgroundDimAmount">0.5item>
    style>

    Dialog setContentView 会走到PhoneWindow 的这个方法 走到installDecor -> generateLayout

    屏幕截图 2024-05-24 160441.png

    屏幕截图 2024-05-24 160620.png

    屏幕截图 2024-05-24 160736.png 会发现这个属性为true的话,会根据内容展示。我们给这个属性设置为false这样就不用给Window设置大小了。


    作者:很好881
    来源:juejin.cn/post/7372396174249738278
    收起阅读 »

    对象存储URL被刷怕了,看我这样处理

    个人项目:社交支付项目(小老板) 作者:三哥,j3code.cn 文档系统:admire.j3code.cn/note 社交支付类的项目,怎么能没有图片上传功能呢! 涉及到文件存储我第一时间就想到了 OSS 对象存储服务(腾讯叫 COS),但是接着我又想到了...
    继续阅读 »

    个人项目:社交支付项目(小老板)


    作者:三哥,j3code.cn


    文档系统:admire.j3code.cn/note



    社交支付类的项目,怎么能没有图片上传功能呢!


    涉及到文件存储我第一时间就想到了 OSS 对象存储服务(腾讯叫 COS),但是接着我又想到了”OSS 被刷 150 T 的流量,1.5 W 瞬间就没了?“。


    本来想着是自己搭建一套 MinIO ,但后来一想服务器的开销又要大了,还是作罢了。就在此时,我脑袋突然灵光了一下,既然对象存储的流量是由于资源 url 泄漏导致的外界不停的访问 url 使公网流量剧增从而引起巨额消费,那我能不能不泄露这个 url 呢!


    理论上是可以不直接给用户云存储的 url ,那用户如何访问资源?



    转换,当用户上传图片时,将云存储的 url 保存入库,而返回用户一个本系统的资源访问接口。当用户访问该接口时,系统从库中获取真实 url 进行资源访问,并返回资源给用户,完成一次转换。


    虽然可以解决 url 泄漏问题,但是也是有性能消耗(从直接访问,变为间接访问,而且系统挂了,资源就不可用)。



    方案,虽然曲折了点,但为了 money ,牺牲一点是值得的(后来思考了一下,觉得还是有些问题,文章最后会说)。而且即使有人通过刷系统的接口访问资源,也没事,系统有很强的限流和黑名单处理,不会产生过多的公网流量费用的。


    那下面我们就先开通相关功能,然后再编码实现。


    1、腾讯云对象存储创建


    地址:console.cloud.tencent.com/cos


    开通对象存储的步骤还是非常简单的,具体步骤如下:


    1)开通功能


    Snipaste_2023-07-14_15-27-24.png


    2)配置存储桶


    Snipaste_2023-07-14_15-28-24.png


    下一步


    Snipaste_2023-07-14_15-30-35.png


    下一步


    Snipaste_2023-07-14_15-33-10.png


    3)创建访问的密钥


    腾讯的所有 API 接口都需要这个访问密钥,如果以前创建过就可以直接拿来使用


    Snipaste_2023-07-14_15-34-05.png


    下一步


    Snipaste_2023-07-14_15-35-26.png


    基本的功能我们已经开通了,而且以后我们只需向这个存储桶中上传图片即可。


    2、SpringBoot 对接对象存储


    既然准备工作都已经完成了,那就开始编写上传文件的代码吧!当然,这里我们还是要借助官方文档,便于我们开发,地址如下:



    cloud.tencent.com/document/pr…



    2.1 配置准备


    先来思考一下,对于腾讯 COS 文件上传需要那些配置:



    1. 云 API 的 SecretId 和 SecretKey

    2. 桶名称

    3. 文件上传大小限制

    4. 再加一个 cos 上传后的访问域名


    ok,大致就这些,那咱们就先来写个配置文件:application-cos.yml


    tx.cos:
    # 云 API 的 SecretId
    secret-id: ENC(X7Uu6Y0QD6aCeUmNhyqv1jcr8fSN+fqM/FSP/rqhM+6pkbte2LW5gR3wntsm24n3NAg6sIwBC3pqm1lSNWwElc3iuGe3lE4L/k3zih+EstM=)
    # 云 API 的 SecretKey
    secret-key: ENC(ui3jqYJpyTRtPAizYdtll2Zc1EVzUjK28vjTyD+t3AIydQO6I+JQOVacc5+NJVybsbFptELswKhY55OQLW+BKfujNTOYEM/zb4CMi+AK80w=)
    # 域名访问
    domain: ENC(oRsaRjwRCVLEYfcNB0CjPGyqSMxGM5uzWnSpSifauLF7c5YMt5hZFi7xAthJI4CjmOLVA810Jbgy8lnkKrXUH0g1ee14cr67xSdtPRy1ZaJOXQOMlBgCKNO2wDBg2YW2)
    # 文件上传的桶名称
    bucket-name: ENC(TUsQfDEFx6KSAOpRwG7UYOJbGwnFT0Z9tjS4h+/HeenAE3XbhKsCwn3TTo80n5tUUP9Dzrnu+Ck84FNSYQk5fw==)

    spring:
    servlet:
    multipart:
    # 限制文件上传大小
    max-request-size: 5MB
    max-file-size: 5MB

    注:这里,我的配置值是加密的,所以你们需要配置自己的值


    再根据这个配置文件,写一个对应的配置类:


    地址:cn.j3code.common.config


    @Slf4j
    @Data
    @Configuration
    @ConfigurationProperties(prefix = "tx.cos")
    public class TxCosConfig {
    /**
    * 访问域名
    */

    private String domain;

    /**
    * 桶名称
    */

    private String bucketName;

    /**
    * api密钥中的secretId
    */

    private String secretId;

    /**
    * api密钥中的应用密钥
    */

    private String secretKey;
    }

    2.2 上传文件代码


    这里,我们先实现单个文件的上传,那来思考一下,上传文件应该需要那些步骤:



    1. 校验文件名称

    2. 重新生成一个新文件名称

    3. 腾讯 COS 文件存储路径生成

    4. 文件上传

    5. 拼接文件访问 url


    对应此步骤的流程图,如下:


    Snipaste_2023-07-16_17-31-32.jpg


    1)controller 编写


    位置:cn.j3code.other.api.v1.controller


    @Slf4j
    @AllArgsConstructor
    @ResponseResult
    @RestController
    @RequestMapping(UrlPrefixConstants.WEB_V1 + "/image/upload")
    public class ImageUploadController {

    private final FileService fileService;


    /**
    * 图片上传
    * @param file 文件
    * @return 返回文件 url
    */

    @PostMapping("")
    public String upload(@RequestParam("file") MultipartFile file){
    return fileService.imageUpload(file);
    }
    }

    2)service 编写


    位置:cn.j3code.other.service


    public interface FileService {
    String imageUpload(MultipartFile file);
    }

    @Slf4j
    @AllArgsConstructor
    @Service
    public class FileServiceImpl implements FileService {

    /**
    * 允许上传的图片类型
    */

    public static final Set<String> IMG_TYPE = Set.of("jpg", "jpeg", "png", "gif");

    /**
    * 腾讯 cos 配置
    */

    private final TxCosConfig txCosConfig;
    private final UrlKeyService urlKeyService;

    /**
    * 图片上传
    *
    * @param file
    * @return
    */

    @Override
    public String imageUpload(MultipartFile file) {
    // 文件名称
    String newFileName = getNewFileName(file);

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    String format = formatter.format(LocalDate.now());
    // key = /用户id/年月日/文件
    String key = SecurityUtil.getUserId() + "/" + format + "/" + newFileName;

    String prefix = newFileName.substring(0, newFileName.lastIndexOf(".") - 1);
    String suffix = newFileName.substring(newFileName.lastIndexOf(".") + 1);
    File tempFile = null;
    File rename = null;
    try {
    // 生成临时文件
    tempFile = File.createTempFile(prefix, "." + suffix);
    file.transferTo(tempFile);
    // 重命名文件
    rename = FileUtil.rename(tempFile, newFileName, true, true);
    // 上传
    upload(new FileInputStream(rename), key);
    } catch (Exception e) {
    log.error("imageUpload-error:", e);
    } finally {
    if (Objects.nonNull(tempFile)) {
    FileUtil.del(tempFile);
    }
    if (Objects.nonNull(rename)) {
    FileUtil.del(rename);
    }
    }
    // 返回访问链接
    return initUrl(key);
    }

    /**
    * 初始化图片文件访问 url(本地url和第三方url)
    *
    * @param key 路径
    * @return
    */

    private String initUrl(String key) {
    // 组装第三方 url
    String imageUrl = txCosConfig.getDomain() + "/" + key;

    // 保存 url 到 数据库
    UrlKey urlKey = new UrlKey()
    .setUrl(imageUrl)
    .setKey(RandomUtil.randomString(16) + RandomUtil.randomString(16) + RandomUtil.randomString(16))
    .setUserId(SecurityUtil.getUserId());

    // 保存成功,返回本地中转的 url 出去
    boolean save = Boolean.FALSE;
    try {
    save = urlKeyService.save(urlKey);
    } catch (Exception e) {
    }

    if (save) {
    return CallbackUrlConstants.IMAGE_OPEN_URL + urlKey.getKey();
    }
    // 保存失败,直接把第三方 url 返回给用户
    return imageUrl;
    }

    /**
    * 文件上传到第三方
    *
    * @param fileStream 文件流
    * @param path 路径
    */

    private void upload(InputStream fileStream, String path) {
    PutObjectResult putObjectResult = COSClientUtil.getCosClient(txCosConfig)
    .putObject(new PutObjectRequest(txCosConfig.getBucketName(), path, fileStream, null));
    log.info("upload-result:{}", JSON.toJSONString(putObjectResult));
    }

    /**
    * 生成一个新文件名称
    * 会校验文件名称和类型
    *
    * @param file 文件
    * @return
    */

    private String getNewFileName(MultipartFile file) {
    String originalFilename = file.getOriginalFilename();
    if (StringUtil.isEmpty(originalFilename)) {
    throw new SysException("文件名称获取失败!");
    }
    String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

    if (!IMG_TYPE.contains(suffix.substring(1))) {
    throw new SysException(String.format("仅允许上传这些类型图片:%s", JSON.toJSONString(IMG_TYPE)));
    }

    return RandomUtil.randomString(8) + SnowFlakeUtil.getId() + suffix;
    }
    }

    代码写的很详细了,应该能看懂,但,有两点我没有提,就是:COSClientUtil 和 UrlKeyService,下面就来结介绍。


    2.2.1 cos 客户端配置提取


    系统中肯定有很多的文件上传,难道是每上传一次,就配置一次 cos 客户端吗?显然不是,这个 cos 客户端肯定是要抽出来的,全局系统中我们只配置一次。也即只有第一次过来是创建 cos 客户端,后续过来的文件上传请求直接返回创建好的 cos 客户端就行。


    COSClientUtil 类就是我抽的公共 cos 客户获取类,具体实现如下:


    位置:cn.j3code.other.util


    public class COSClientUtil {

    /**
    * 统一 cos 上传客户端
    */

    private static COSClient cosClient;

    public static COSClient getCosClient(TxCosConfig txCosConfig) {
    if (Objects.isNull(cosClient)) {
    synchronized (COSClient.class) {
    if (Objects.isNull(cosClient)) {
    // 1 初始化身份
    COSCredentials cred = new BasicCOSCredentials(txCosConfig.getSecretId(), txCosConfig.getSecretKey());
    // 2 创建配置,及设置地域
    ClientConfig clientConfig = new ClientConfig(new Region("ap-guangzhou"));
    // 3 生成 cos 客户端。
    cosClient = new COSClient(cred, clientConfig);
    }
    }
    }
    return cosClient;
    }
    }

    私有构造器,且之对外提供 getCosClient 方法获取 COSClient 对象,保证全局只有一个 cos 客户端配置。


    2.2.2 隐藏云存储 URL 处理


    还记得 FileServiceImpl 类中有个 UrlKeyService 属性嘛,这个类就是做 云存储 URL 隐藏及中转功能的。


    具体做法如图:


    Snipaste_2023-07-16_18-14-59.jpg


    文件上传部分我们已经写好了,不过有点超前的意思了,不过没关系,看整体就行。


    从上面我们要开始抓住一个细节了,就是映射关系,即 key 和 url 的映射。这里我用的是 MySQL 保存,也即用表来存,并没有用 Redis。这里我的考虑是,后续可以把表中的数据定时刷到 Redis 中,接着访问的顺序是从 Redis 中找映射,没有再去 MySQL 中找。


    不过,我们首先还是把数据先存表再说,先来看看映射表结构字段:



    id


    user_id


    key


    url


    create_time


    update_time



    ok,就这些字段,把用户 id 加上是为了好回溯看看是谁上传了图片。


    SQL 如下:


    CREATE TABLE `sb_url_key` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `key` varchar(64) COLLATE utf8_unicode_ci NOT NULL COMMENT 'key',
    `url` varchar(200) COLLATE utf8_unicode_ci NOT NULL COMMENT '资源url',
    `user_id` bigint(20) DEFAULT NULL COMMENT '上传用户',
    `create_time` datetime DEFAULT NULL COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL COMMENT '修改时间',
    PRIMARY KEY (`id`),
    KEY `key` (`key`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

    紧接着就是通过 MyBatisX 插件生成对应的实体、service、mapper 代码了,不过多赘述。那,现在就来开发用户访问图片资源,咱们如何去请求第三方,然后返回用户图片 byte[] 资源数组吧!


    1)controller 编写


    位置:cn.j3code.other.api.v1.controller


    @Slf4j
    @AllArgsConstructor
    @RestController
    @RequestMapping(UrlPrefixConstants.OPEN + "/resource/image")
    public class ImageResourceController {

    private final UrlKeyService urlKeyService;

    /**
    * 获取图片 base64
    *
    * @param key
    * @return
    * @throws Exception
    */

    @GetMapping("/base64/{key}")
    public String imageBase64(@PathVariable("key") String key) throws Exception {
    UrlKey urlKey = urlKeyService.oneByKey(key);
    return "data:image/jpg;base64," + Base64Encoder.encode(IoUtil.readBytes(new URL(urlKey.getUrl()).openStream()));
    }


    /**
    * 获取图片 byte 数组
    *
    * @param key
    * @return
    * @throws Exception
    */

    @GetMapping(value = "/io/{key}", produces = MediaType.IMAGE_JPEG_VALUE)
    public byte[] imageIo(@PathVariable("key") String key) throws Exception {
    UrlKey urlKey = urlKeyService.oneByKey(key);

    return IoUtil.readBytes(new URL(urlKey.getUrl()).openStream());
    }
    }

    注意:这里写了两个方法,目的是返回两种不同形式的图片资源:base64 和 byte[]。且,这种资源访问的接口,我们系统的相关拦截器请放行,如:认证,ip 记录等拦截器。


    2)service 编写


    位置:cn.j3code.other.service


    public interface UrlKeyService extends IService<UrlKey> {
    UrlKey oneByKey(String key);
    }
    @Service
    public class UrlKeyServiceImpl extends ServiceImpl<UrlKeyMapper, UrlKey>
    implements UrlKeyService {

    @Override
    public UrlKey oneByKey(String key) {
    UrlKey urlKey = lambdaQuery().eq(UrlKey::getKey, key).one();
    if (Objects.isNull(urlKey)) {
    throw new SysException(SysExceptionEnum.NOT_DATA_ERROR);
    }
    return urlKey;
    }
    }

    ok,这样咱们就处理好了,但是仔细想想这种中转的方法有什么问题。


    2.3 思考


    2.2 节我们已经实现了文件上传和防止 cos 访问 url 泄露的操作,但是我留了个问题,就是思考这种方式有什么问题。


    下面是我的思考:



    1. 用户上传的图片,访问时每次都会经过本系统,造成了本系统的压力

    2. 如果一个页面需要回显的图片过多,那页面响应会不会很慢

    3. 如果系统崩溃了或者服务崩溃了,会导致图片不可访问,但其实第三方 url 是没有问题的


    好吧,其实上面总结就两个问题,即:性能可用性


    这里的解决方法是,如果资金充裕而且 COS 做了黑白名单等之类的防御措施可以直接把 COS 的原始 url 返回出去,没必要把图片资源压力给我我们本系统。如果你不是这种情况,那么就给图片访问接口增加部署资源,即升级服务器增加内存和带款,提高资源访问效率及系统性能。


    以上就是本节内容,如果文章的中转方法有啥不足或者您有什么意见,欢迎一起讨论研究。


    作者:J3code
    来源:juejin.cn/post/7256306281538928701
    收起阅读 »

    我们如何让Android客户端暴瘦了100M

    一、 引言随着Android应用功能的日益丰富,客户端的体积也逐渐膨胀,过大的安装包体积不仅给用户带来了下载和存储的压力,还会影响应用的启动速度和整体性能;传统的图片压缩、冗余资源移除、代码混淆等优化手段可以在一定程度上降低安装包大小,但是在面对大型复杂应用的...
    继续阅读 »

    一、 引言

    随着Android应用功能的日益丰富,客户端的体积也逐渐膨胀,过大的安装包体积不仅给用户带来了下载和存储的压力,还会影响应用的启动速度和整体性能;传统的图片压缩、冗余资源移除、代码混淆等优化手段可以在一定程度上降低安装包大小,但是在面对大型复杂应用的时候,效果往往很有限,本文将详细介绍我们在包大小优化方面的实践经验,并通过一系列技术手段实现了显著的包体积缩减。

    二、  安装包大小分析

    Android的apk通常有以下几部分组成:

    1. 代码:包含应用中Java/Kotlin代码,在包中以dex的形式存在
    2. 资源:包含图片、布局文件等
    3. lib库: 包含了应用的Native代码库,以.so文件的形式存在
    4. assets:包含了应用运行时所需的非代码资源,如音频、视频、字体、配置文件等
    5. 其他:签名文件、资源索引文件等

    通过分析安装包大小的组成,我们发现项目中lib库和assets占比达到70%,代码占比20%,资源和其他占比10%。

    三、基础优化方案

    1. 代码优化:开启代码混淆,混淆可以帮助缩减代码尺寸、移除无用代码,通过分析反编译后的代码,我们发现很多本该混淆的类没有混淆,最终定位到工程中引入的一些三方库的混淆规则keep范围过大,导致混淆效果不理想;通过混淆规则的优化,包大小缩减了6M左右;
    2. 资源优化:解压apk文件,把res目录下的图片按照大小进行排序,我们发现项目中有一些尺寸较大的图片,把图片格式转为webp格式后,尺寸大大降低;同时通过对比资源的md5值,发现一些资源名字虽然不一样,但是内容是一样的,这些重复资源可以移除;通过资源的优化,包大小缩减了15M左右;
    3. assets资源优化:通过分析apk assets目录下的文件,我们发现里面有很多不用的文件,比如arm64位包中存在x86、armeabi-v7a等其他架构的so库,这些assets目录下的so库是三方库引入的,运行时动态加载,由于设置abiFilters无法过滤掉这些so库,导致打包进apk中;我们通过自定义构建流程,在mergeAssetsTask执行结束后移除assets中不用的so库;通过assets资源的优化,包大小缩减了4M左右.
    project.afterEvaluate {
    android.applicationVariants.all { variant ->
    def mergeAssetsTask = project.getTasksByName("merge${variant.name.capitalize()}Assets", false)
    mergeAssetsTask.doLast {
    def assetsDir = mergeAssetsTask.outputDir.get().toString()
    // 移除无用的assets资源
    removeUnusedAssets(assetsDir)
    }
    }
    }

    通过基础的代码和资源等的优化,包大小缩减了25M左右,但是对于一个180M的apk来说,效果非常有限,需要探索其他方案进一步降低包大小。

    三、  进阶优化方案

    上面我们分析过apk中的lib库和assets文件占比达到70%,因此我们重点针对lib库和assets文件尺寸大的问题进行优化,我们可以把这些文件放到云端,在应用启动的时候下载到本地,但是这样做有以下问题:

    1. 一些lib库和assets文件在应用启动的时候就会用到,如果放到云端,会导致应用启动时间变长甚至崩溃;
    2. 应用中加载assets资源是通过系统API AssetManager.open来加载的,但是把assets文件从apk中移除后,需要修改使用AssetManager.open的地方,改为从本地私有目录加载,这样会导致改动的地方很多,而且容易漏改和错改;一些三方库由于没有开源,修改起来会更加困难;
    3. 应用中加载lib库是通过系统API System.loadLibrary来加载的,如果把so库从apk中移除后,需要修改为使用System.load加载私有目录下的so库,同样存在改动地方多,不开源的三方库修改困难的问题;
    4. 把移除的so库和assets文件打包成一个文件下发会存在由于文件尺寸大导致下载时间长,容易下载失败问题,同时会导致当用户使用到相关功能的时候需要长时间的等待,体验差。

    针对以上问题,我们采用了以下优化策略:

    1. 选择性移除:只把一些尺寸大,用户使用频次较低的功能中使用的assets和so库从apk包中移除,在不影响用户体验的同时,降低安装包大小。
    2. 分包下载:需要移除的so库和assets文件按功能模块进行分包,首次使用时再去下载对应的资源包,这样能确保功能模块依赖的云端资源尽可能的小,大幅降低下载时间,提升下载成功率,减少用户等待时间。
    3. 自动化构建:通过编写gradle脚本,自定义构建过程,在构建阶段自动把assets和so库从apk包中移除并打包。
    project.afterEvaluate {
    android.applicationVariants.all { variant ->
    def mergeAssetsTask = project.getTasksByName("merge${variant.name.capitalize()}Assets", false)
    mergeAssetsTask.doLast {
    def assetsDir = mergeAssetsTask.outputDir.get().toString()
    // 把assetsDir中需要移除的assets文件移除,放到模块指定的目录下
    }

    def mergeNativeLibsTask = project.getTasksByName("merge${variant.name.capitalize()}NativeLibs", false)
    mergeNativeLibsTask.doLast {
    def libDir = mergeNativeLibsTask.outputDir.get().toString()
    // 把 libDir中需要移除的so库移除,放到模块指定的目录下
    // 打包压缩模块目录
    }
    }
    }
    1. 字节码插桩:开发gradle插件,使用字节码插桩技术,在编译阶段自动把调用AssetManager.openSystem.loadLibrary的地方替换为我们的自定义加载器,工程中的代码和三方闭源库无需做任何改动。
    public class MyMethodVisitor extends MethodVisitor {
    @Override
    public void visitMethodInsn(int opcode, String owner, String name, string desc, boolean isInterface) {
    // 替换System.loadLibrary为DynamicLoader.loadLibrary
    if ("java/lang/System".equals(owner) && "loadLibrary".equals(name)) {
    return super.visitMethodInsn(opcode, "com/xxx/loader/DynamicLoader", name, desc, isInterface);
    }

    // 替换AssetManager.open为DynamicLoader.openAsset
    if ("android/content/res/AssetManager".equals(owner) && "open".equals(name) && "(Ljava/lang/String;)Ljava/io/InputStream".equals(desc)) {
    return super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/xxx/loader/DynamicLoader", "openAsset", "(Landroid/content/res/AssetManager;Ljava/lang/String;)Ljava/io/InputStream");
    }
    return super.visitMethodInsn(opcode, owner, name, desc, isInterface);
    }
    }
    1. 双重加载机制:在自定义加载器中先尝试加载apk内置的so库和assets文件,如果出现异常,则从动态下发的文件中查找并加载,这样可以保证无论so库是否移除都可以正常加载。
    // 自定义加载器

    public class DynamicLoader {
    public static void loadLibrary(string libname) throw Throwable {
    try {
    // 先加载apk包中的so库
    System.loadLibrary(libname);
    return
    } catch(Throwable e) {
    }

    String soPath = findLibrary(libName);
    // apk包中的so库加载失败时加载动态下发的so库
    return System.load(soPath);
    }

    public static InputStream openAsset(AssetManager am, String fileName) throw IOException {
    try {
    // 先加载apk包中的asset文件
    return am.open(fileName);
    } catch(IOException e) {
    }

    // apk包中的asset文件加载失败时加载动态下发的asset文件
    String assetPath = findAsset(fileName);
    return new FileInputStream(assetPath);
    }
    }

    四、  实施效果

    采用上述包优化方案后,我们的Android客户端安装包大小从180M缩减到78M,实现了显著的包体积缩减。同时,通过监控优化后版本的崩溃率和用户反馈,未出现明显的崩溃率升高和用户体验下降的情况。

    五、  未来展望

    应用的安装包大小优化是一个长期的过程,需要建立一套包大小的监控、预警、原因分析、自动优化等机制,确保安装包大小在合理范围,我们将从以下几个方面进行探索:

    1. 设定安装包大小基准,持续监控安装包大小的变化,当安装包大小偏移基准值过大的时候,触发预警,并自动分析包大小增加原因,找出导致包大小增大的文件;
    2. 优化构建流程,构建阶段自动压缩大图片为webp格式,自动合并重复资源;
    3. 持续优化应用的性能表现和用户体验,并根据实际情况进行进一步的优化调整。


    作者:jack5288
    来源:juejin.cn/post/7379168502455222311
    收起阅读 »

    MyBatis居然也有并发问题

    为了节省dalaos时间先说结论:确实是个问题,issue链接:github.com/mybatis/myb… 下面就是源码分析环节,及处理过程,感兴趣的可以看看。 bug,任何时候都要解决!不解决不行,你们想想,你早上刚到公司,打开电脑,写着需求听着歌,突...
    继续阅读 »

    为了节省dalaos时间先说结论:确实是个问题,issue链接:github.com/mybatis/myb…


    下面就是源码分析环节,及处理过程,感兴趣的可以看看。



    bug,任何时候都要解决!不解决不行,你们想想,你早上刚到公司,打开电脑,写着需求听着歌,突然就被ding了……所以没有bug的日子才是好日子!



    日志


    上了服务器一看,Mybatis报错,接口还是个相当频繁的接口,一想,完了,绩效大概率不保。


    2023-08-08 09:52:05,386|aaaaaaaaa|XXXXXXXXXXXXXX|unknown exception occurred
    org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 'projects != null and projects.size() > 0 '. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [aaa,bbb,ccc] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Arrays$ArrayList with modifiers "public"]
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:75) ~[mybatis-spring-1.2.2.jar:1.2.2]
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:371) ~[mybatis-spring-1.2.2.jar:1.2.2]
    at com.sun.proxy.$Proxy57.selectList(Unknown Source) ~[na:na]
    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:198) ~[mybatis-spring-1.2.2.jar:1.2.2]
    at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:119) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:63) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:52) ~[mybatis-3.2.8.jar:3.2.8]
    at com.sun.proxy.$Proxy102.queryExperienceCardOrder(Unknown Source) ~[na:na]
    // 业务相关堆栈,保险起见不贴了
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:652) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at com.xxxxxxxxxxxxxxxxxxxxxx$$EnhancerBySpringCGLIB$$b85a94bd.queryHasExperienceCardNew(<generated>) ~[zuhao-user-service-1.0.0.jar:na]
    at sun.reflect.GeneratedMethodAccessor564.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) [spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) [spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at xxxxxxxxxxxxx.common.interceptor.ApiInterceptor.invoke(ApiInterceptor.java:79) ~[common-0.0.9-20211228.052440-12.jar:na]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) [spring-aop-4.3.6.RELEASE.jar:4.3.6.RELEASE]
    at com.sun.proxy.$Proxy185.queryHasExperienceCardNew(Unknown Source) [na:na]
    at com.alibaba.dubbo.common.bytecode.Wrapper36.invokeMethod(Wrapper36.java) [na:2.5.3]
    at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:46) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:72) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:64) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke$original$LFhJaVNd(MonitorFilter.java:65) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke$original$LFhJaVNd$accessor$urPnHrIw(MonitorFilter.java) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.monitor.support.MonitorFilter$auxiliary$RJHyKBeq.call(Unknown Source) [dubbo-2.5.3.jar:2.5.3]
    at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86) [skywalking-agent.jar:8.16.0]
    at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.filter.TimeoutFilter.invoke(TimeoutFilter.java:42) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter.invoke(TraceFilter.java:78) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.filter.ContextFilter.invoke(ContextFilter.java:60) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:112) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.filter.EchoFilter.invoke(EchoFilter.java:38) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol$1.reply(DubboProtocol.java:108) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.handleRequest(HeaderExchangeHandler.java:84) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:170) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:52) [dubbo-2.5.3.jar:2.5.3]
    at com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:82) [dubbo-2.5.3.jar:2.5.3]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_131]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_131]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
    Caused by: org.apache.ibatis.builder.BuilderException: Error evaluating expression 'projects != null and projects.size() > 0 '. Cause: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [u号租, 租号牛, 租号酷] [java.lang.IllegalAccessException: Class org.apache.ibatis.ognl.OgnlRuntime can not access a member of class java.util.Arrays$ArrayList with modifiers "public"]
    at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:47) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:32) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:33) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.scripting.xmltags.TrimSqlNode.apply(TrimSqlNode.java:54) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.scripting.xmltags.TrimSqlNode.apply(TrimSqlNode.java:54) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:32) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:40) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:278) ~[mybatis-3.2.8.jar:3.2.8]
    at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:83) ~[pagehelper-5.1.4.jar:na]
    at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:60) ~[mybatis-3.2.8.jar:3.2.8]
    at com.sun.proxy.$Proxy234.query(Unknown Source) ~[na:na]
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:108) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:102) ~[mybatis-3.2.8.jar:3.2.8]
    at sun.reflect.GeneratedMethodAccessor347.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:358) ~[mybatis-spring-1.2.2.jar:1.2.2]
    ... 54 common frames omitted
    Caused by: org.apache.ibatis.ognl.MethodFailedException: Method "size" failed for object [aaa,bbb,ccc]
    at org.apache.ibatis.ognl.OgnlRuntime.callAppropriateMethod(OgnlRuntime.java:837) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.ObjectMethodAccessor.callMethod(ObjectMethodAccessor.java:61) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.OgnlRuntime.callMethod(OgnlRuntime.java:860) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.ASTMethod.getValueBody(ASTMethod.java:73) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.ASTChain.getValueBody(ASTChain.java:109) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.ASTGreater.getValueBody(ASTGreater.java:49) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.ASTAnd.getValueBody(ASTAnd.java:56) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:170) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.SimpleNode.getValue(SimpleNode.java:210) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:333) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.ognl.Ognl.getValue(Ognl.java:310) ~[mybatis-3.2.8.jar:3.2.8]
    at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:45) ~[mybatis-3.2.8.jar:3.2.8]
    ... 72 common frames omitted


    赶紧查了下这个接口的调用情况,大部分没问题,偶尔冒了这么个错(还好还好)


    根据堆栈反查错误位置,有点想不通,这里会有问题?那就只能翻源码了



    源码分析


    经过排查,ognl表达式中用到的方法,会通过反射,获取method,并缓存至静态变量中,所以,存在多线程状态中,产生并发问题,往下看



    这里是缓存方法的逻辑,org.apache.ibatis.ognl.OgnlRuntime#getMethods(java.lang.Class, boolean)感兴趣的可以自己看


    这里就是bug点,如果调用一旦多,存在A线程修改成true,还没调用方法,B线程就修改成false,此时调用失败,这不是个坑吗


    image.png
    mybatis中一搜果然有这个issue:github.com/mybatis/myb…



    作者给的方案呢是升级mybatis。




    你以为就这样结束了?


    升级是不可能升级的,这辈子都不可能升级的,代码这么稳定,行行都像诗句[狗头]


    开个玩笑,看看如何避免



    原因呢就是Arrays.asList返回的是内部类,是private。所以导致了(!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers()))这个条件为true,进入了设置
    accessible的逻辑,后面又给设置回原样


    总结



    • 问题:如果需要ognl的对象的方法和类不是public,那么会存在并发问题

    • 解决1:针对并发问题,升级Mybatis

    • 解决2:Lists.newArrayList或者其他写法代替,反正看下,内部类是不是private


    有问题希望留言指出哈


    作者:山间小僧
    来源:juejin.cn/post/7264921613551730722
    收起阅读 »

    在Spring Boot中使用Sa-Token实现路径拦截和特定接口放行

    在Spring Boot中使用Sa-Token实现路径拦截和特定接口放行 很喜欢的一段话:别想太多,好好生活,也许日子过着过着就会有答案,努力走着走着就会有温柔的着落。 春在路上,花在枝上,所有的美好都在路上,努力过好自己的生活,偶尔慌乱,偶尔平稳,都各有...
    继续阅读 »

    在Spring Boot中使用Sa-Token实现路径拦截和特定接口放行


    在这里插入图片描述



    很喜欢的一段话:别想太多,好好生活,也许日子过着过着就会有答案,努力走着走着就会有温柔的着落。
    春在路上,花在枝上,所有的美好都在路上,努力过好自己的生活,偶尔慌乱,偶尔平稳,都各有滋味,怀着诚恳,好好努力好好生活,闲事勿虑,别让鸡零狗碎的破事,耗尽你对美好生活的所有向往。



    1. 引入依赖


    首先,在pom.xml文件中引入Sa-Token相关的依赖。Sa-Token是一个轻量级的Java权限认证框架,可以帮助我们轻松实现用户登录状态的管理和权限认证。


    <dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.27.0</version>
    </dependency>

    2. 创建配置类 SecurityProperties


    定义一个配置类SecurityProperties,用于读取和存储从配置文件中加载的排除路径信息。这里使用了Spring Boot的@ConfigurationProperties注解来绑定配置文件中的属性。


    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    import lombok.Data;

    @Data
    @Component
    @ConfigurationProperties(prefix = "security")
    public class SecurityProperties {
    /**
    * 排除路径
    */

    private String[] excludes;
    }


    • @Data:这是Lombok的注解,自动生成getter和setter方法。

    • @Component:将该类注册为Spring的组件。

    • @ConfigurationProperties:指定前缀security,从配置文件中读取以该前缀开头的属性,并将这些属性映射到该类的字段上。


    3. 编写配置文件


    在配置文件application.yml或者application.properties中,配置需要排除的路径。例如:


    application.yml:


    security:
    excludes:
    - "/public/**"
    - "/login"
    - "/register"

    application.properties:


    security.excludes=/public/**,/login,/register


    • /public/**:排除所有以/public/开头的路径。

    • /login:排除/login路径。

    • /register:排除/register路径。


    4. 配置拦截器


    创建一个配置类WebConfig,实现WebMvcConfigurer接口,在其中配置Sa-Token的拦截器,并将排除的路径应用到拦截器中。


    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    import cn.dev33.satoken.interceptor.SaInterceptor;
    import cn.dev33.satoken.router.SaRouter;
    import cn.dev33.satoken.stp.StpUtil;

    @Configuration
    public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SaInterceptor(handler -> {
    // 获取所有的URL并进行检查
    SaRouter.match("/**").check(() -> {
    // 检查是否登录
    StpUtil.checkLogin();
    });
    }))
    .addPathPatterns("/**") // 拦截所有路径
    .excludePathPatterns(securityProperties.getExcludes()); // 排除指定路径
    }
    }


    • @Configuration:标识这是一个配置类。

    • addInterceptors:重写该方法,向Spring的拦截器注册中心添加自定义的拦截器。

    • SaInterceptor:Sa-Token提供的拦截器,主要用于权限验证。

    • SaRouter.match("/**"):匹配所有路径。

    • StpUtil.checkLogin():Sa-Token提供的登录状态检查方法,用于验证用户是否已登录。

    • excludePathPatterns:从拦截中排除指定的路径,这些路径从SecurityProperties中获取。


    5. 验证拦截效果


    启动Spring Boot应用程序,验证配置是否生效。以下是一些测试步骤:



    1. 访问排除路径



      • 尝试访问配置文件中排除的路径,如/public/**/login/register

      • 这些路径应不会触发登录检查,可以直接访问。



    2. 访问其他路径



      • 尝试访问其他未排除的路径,如/admin/user/profile等。

      • 这些路径应触发Sa-Token的登录验证逻辑,如果用户未登录,将会被拦截,并返回相应的未登录提示。




    代码解析



    • SecurityProperties:通过@ConfigurationProperties注解,Spring Boot会自动将前缀为security的配置属性绑定到该类的excludes字段上,从而实现排除路径的配置。

    • 配置文件:在配置文件中定义需要排除的路径,以便动态加载到SecurityProperties中。

    • WebConfig:实现WebMvcConfigurer接口,通过addInterceptors方法添加Sa-Token的拦截器,并使用excludePathPatterns方法将配置文件中定义的排除路径应用到拦截器中。


    详细解释


    依赖配置


    Sa-Token是一个轻量级的权限认证框架,可以帮助我们轻松实现用户登录状态的管理和权限认证。通过引入sa-token-spring-boot-starter依赖,我们可以很方便地将其集成到Spring Boot项目中。


    配置类 SecurityProperties


    SecurityProperties类的作用是将配置文件中定义的排除路径读取并存储到excludes数组中。通过使用@ConfigurationProperties注解,我们可以将前缀为security的属性绑定到该类的excludes字段上。这样做的好处是,排除路径可以通过配置文件进行动态配置,方便管理和维护。


    配置文件


    在配置文件中,我们定义了需要排除的路径。这些路径将不会被拦截器拦截,可以直接访问。配置文件支持YAML格式和Properties格式,根据项目需要选择合适的格式进行配置。


    拦截器配置


    WebConfig类中,我们实现了WebMvcConfigurer接口,并重写了addInterceptors方法。在该方法中,我们创建了一个Sa-Token的拦截器,并通过SaRouter.match("/**")匹配所有路径。对于匹配到的路径,我们使用StpUtil.checkLogin()方法进行登录状态检查。如果用户未登录,将会被拦截,并返回相应的未登录提示。


    通过excludePathPatterns方法,我们将从SecurityProperties中获取的排除路径应用到拦截器中。这样一来,配置文件中定义的排除路径将不会被拦截器拦截,可以直接访问。


    总结


    通过本文的介绍,我们了解了如何在Spring Boot中使用Sa-Token实现路径拦截和特定接口放行。我们首先引入了Sa-Token的依赖,然后定义了一个配置类SecurityProperties,用于读取和存储排除路径信息。接着,在配置文件中定义了需要排除的路径,并在WebConfig类中配置了Sa-Token的拦截器,将排除路径应用到拦截器中。最后,通过测试和验证,确保配置生效,实现了对特定路径的放行和其他路径的权限验证。


    这种方式可以帮助开发者更灵活地管理Web应用中的访问控制,提升系统的安全性和可维护性。如果你有更多的自定义需求,可以根据Sa-Token的文档进行进一步配置和扩展。


    作者:IT小辉同学
    来源:juejin.cn/post/7379117970797183030
    收起阅读 »

    一文了解 WWDC2024 重要更新

    Ultrawide Mac 显示器在 visionOS 2 中的展现 这就是 visionOS 2 Apple Vision Pro即将进驻这些国家! 有关 iOS 18 新的控制中心 深色模式中的深色图标 iOS 18 中的主屏幕定制 你...
    继续阅读 »

    image.png


    Ultrawide Mac 显示器在 visionOS 2 中的展现


    image.png


    这就是 visionOS 2


    image.png


    Apple Vision Pro即将进驻这些国家!


    image.png


    有关 iOS 18


    image.png


    新的控制中心


    image.png



    • 深色模式中的深色图标
      image.png

    • iOS 18 中的主屏幕定制


    image.png



    • 你现在可以在网格任意位置放置你的应用程序


    image.png



    • 您现在可以在 iOS 18 中更改锁屏上默认的手电筒和相机图标


    image.png



    • 使用面部ID锁定应用程序


    image.png



    • 您现在可以在 iOS 18 #WWDC24中的 iMessage 中使用任意表情符号进行点击回复


    image.png



    • 你现在可以在 iOS 18 中设定信息发送时间


    image.png



    • 在你没有信号时,你现在可以通过 iOS 18 中的卫星发送消息


    image.png



    • iOS 18 简直太疯狂了!


    image.png



    • 你现在可以在 iOS 18 点击付款为苹果现金


    image.png



    • 在 iOS 18 中的照片应用程序重新设计


    image.png


    **iOS 18 中的全新改动!


    ** image.png


    AirPods 和 Apple TV 的新功能


    image.png


    watchOS 11 引入训练负荷功能


    image.png


    watchOS 11 中的全新改动


    image.png


    iPadOS 18中的全新改动


    image.png


    Apple 在 iPadOS 18 中为应用程序引入新的标签栏设计


    image.png


    计算器应用程序终于来到 iPad 中!


    image.png


    iPad 上的计算器应用程序引入了数学笔记!


    image.png


    macOS Sequoia


    image.png


    macOS Sequoia 的全新改动


    image.png



    • 你现在可以把iPhone界面镜像到你的Mac上


    image.png



    • 你现在可以在你的Mac上接收你的iPhone通知


    image.png


    作者:Captaincc
    来源:juejin.cn/post/7378328335036612647
    收起阅读 »

    一个轻量的后台管理模板

    web
    特色: 当前模板将自定义样式配置通过css变量的方式提取了出来,直接通过可视化去配置成你喜欢的样式效果,在下面预览地址中可以体验。 预览地址 可视化配置面版不够好看的话,可以把地址上的vue-admin改为vue-admin-el 项目地址 描述 无UI框...
    继续阅读 »

    特色: 当前模板将自定义样式配置通过css变量的方式提取了出来,直接通过可视化去配置成你喜欢的样式效果,在下面预览地址中可以体验。



    描述


    无UI框架依赖的后台管理模板


    当前项目是基于vue.js去实现的一套后台管理模板,早在2019年就已经在持续迭代,目前已经是较新的vue3.x版本;


    因为在中后台项目中,大多数核心功能只有页面框架样式侧边菜单栏功能,所以除了底层 js 框架vue+vue-router以外,所有样式、功能都采用自行实现方式;之所以不使用第三方UI库的理由是:



    • 不受UI框架的约束,可以使用任何一款自己喜欢的第三方库;

    • 轻量化,因为用到的依赖极少,所以体积非常轻量,同时保证了常用到的大部分功能保留;所有的工程化配置根据自身需求去加入即可,当前模板只做代码减法;

    • 兼容性、拓展性高,模板中每个部分都是可以独立抽离和替换的,并无上手成本;当在引用某一款UI库使用时,直接引入依赖并使用即可,无需修改模板已有功能组件;

    • 别人写的模板代码太多了,都不好改!


    当前模板项目的 package.json 做到了极致的精简


    {
    "dependencies": {
    "nprogress": "0.2.0",
    "vue": "3.4.21",
    "vue-router": "4.3.0"
    },
    "devDependencies": {
    "@types/node": "20.11.28",
    "@types/nprogress": "0.2.0",
    "@vitejs/plugin-vue": "5.0.4",
    "@vitejs/plugin-vue-jsx": "3.1.0",
    "sass": "~1.71.0",
    "typescript": "~5.4.0",
    "vite": "5.2.8",
    "vue-tsc": "~1.8.0"
    }
    }

    功能目录清单



    • vue-router 权限路由功能、路由记录初始进入路径功能

    • layout 部分:可视化配置样式功能、顶部伸缩布局 + 多级侧边菜单栏、路由面包屑、路由历史记录标签栏、整体自适应窗口大小布局、滚动条(类似

    • utils 只保留使用频率极高的:日期格式化、复制、类型判断、网络请求、和一些核心功能函数

    • UI控件 + 通用组件:消息提示条、对话框、高度自适应折叠组件、dialog 组件


    layout 核心布局整体


    大多数情况开发者在选用开源模板时,只是为了侧边菜单栏和顶部的布局不同而选择对应的模板,所以当前项目直接将两种布局写成可以动态切换,并且加入可视化的样式配置操作,这样连css代码都不需要去看了:


    微信截图_20240327154406.png


    侧边菜单栏为什么没有整一个折叠缩略的功能?理由是我觉得这个操作逻辑不是那么的理想,缩小后,我需要鼠标一层一层的放上去找到需要的子菜单,这一点都不方便;而且缩小菜单的目的是为了获得更大内容可视区域,所以缩小后的菜单依然还占用了一部分空间,同时使用功能变得繁琐,那干脆在收起菜单时,将她整个推出屏幕区域,这样就能使可视区域最大化。


    微信截图_20240327152829.png


    路由权限设置


    完全继承了vue-router的数据结构,只在meta对象中加入auth作为路由数组过滤操作去实现权限控制;另外根部对象的name字段则作为路由缓存的唯一值。


    import { RouteRecordRaw } from "vue-router";

    export interface RouteMeta {
    /** 侧边栏菜单名、document.title */
    title: string
    /** 外链地址,优先级会比`path`高 */
    link?: string
    /** `svg`名 */
    icon?: string
    /** 是否在侧边菜单栏不显示该路由 */
    hidden?: boolean
    /**
    * 路由是否需要缓存
    * - 当设置该值为`true`时,路由必须要设置`name`,页面组件中的`name`也是,不然路由缓存不生效
    */

    keepAlive?: boolean
    /**
    * 可以访问该权限的用户类型数组,与`userInfo.type`对应;
    * 传空数组或者不写该字段代表可以全部用户访问
    *
    * | number | 用户类型 |
    * | --- | --- |
    * | 0 | 超级管理员 |
    * | 1 | 普通用户 |
    */

    auth?: Array<number>
    }

    /** 自定义的路由类型-继承`RouteRecordRaw` */
    export type RouteItem = {
    /**
    * 路由名,类似唯一`key`
    * - 路由第一层必须要设置,因为动态路由删除时需要用到,且唯一
    * - 当设置`meta.keepAlive`为`true`时,该值必填,且唯一,另外组件中的`name`也需要对应的同步设置,不然路由缓存不生效
    */

    name?: string
    /** 子级路由 */
    children?: Array<RouteItem>
    /** 标头 */
    meta: RouteMeta
    } & RouteRecordRaw


    代码演示


    状态管理


    Vue3之后不需要Vuex了(虽然我在Vue2中也没用),而是采用另外一种更简单的方式:参考 你不需要vuex


    ts的项目中,因为可以用Readonly去声明状态对象,所以这套程序设计会发挥得最好,具体示例可以在src/store/README.md中查看


    网络请求


    这里我使用的是根据个人习惯用原生写的ajax代码地址


    理由是:



    • 代码少,功能足以覆盖常用的大部分场景

    • ts中可以更友好的声明接口返回类型


    文件:api.ts request中的泛型不是必须的,不传下面 .vue 文件中res.data中的类型则是any


    export interface TableItem {
    id: number
    type: "load" | "update"
    time: string
    }

    /**
    * @param params
    */

    export function getData(params: PageInfo) {
    return request<Api.List<TableItem>>("GET", "/getList", params)
    }

    文件:demo.vue 建议直接在vscode中用鼠标去看提示,那样会更加的直观


    <script lang="ts" steup>
    import { ref } from "vue";
    import { type TableItem, getData } from "@/api.ts";

    const tableData = ref<Array<TableItem>>([]);

    async function getTableData() {
    const res = await getData({
    pageSize: 10,
    currentPage: 1
    })
    if (res.code === 1) {
    tableData.value = res.data.list; // 这里的 .list 就是接口 传入的类型 TableItem
    // do some...
    }
    }
    script>

    强力建议请求函数的封装时,都始终执行 Promise.resolve 去作为正确和错误的响应。接口获取后始终以res.code === 1为判断成功,无需在内部用 try + catch 去包一层


    更多使用示例请在src/api/README.md中查看



    另外可根据自己喜好可以扩展 axios 这类型第三方库。



    SVG 图标组件


    使用方式:到阿里云图标库中下载想要的图标,然后下载svg文件,最后放到src/icons/svg目录下即可


    也是自己写的一个加载器,代码十分简单:


    import { readFileSync, readdirSync } from "fs";

    // svg-sprite-loader 这个貌似在 vite 中用不了
    // 该文件只能作为`vite.config.ts`导入使用
    // 其他地方导入会报错,因为浏览器环境不支持`fs`模块

    /** `id`前缀 */
    let idPerfix = "";

    const svgTitle = /+].*?)>/;

    const clearHeightWidth = /(width|height)="([^>+].*?)"/g;

    const hasViewBox = /(viewBox="[^>+].*?")/g;

    const clearReturn = /(\r)|(\n)/g;

    /**
    * 查找`svg`文件
    * @param dir 文件目录
    */

    function findSvgFile(dir: string): Array<string> {
    const svgRes: Array<string> = []
    const dirents = readdirSync(dir, {
    withFileTypes: true
    });
    dirents.forEach(function(dirent) {
    if (dirent.isDirectory()) {
    svgRes.push(...findSvgFile(dir + dirent.name + "/"));
    } else {
    const svg = readFileSync(dir + dirent.name).toString().replace(clearReturn, "").replace(svgTitle, function(_, group) {
    // console.log(++i)
    // console.log(dirent.name)
    let width = 0;
    let height = 0;
    let content = group.replace(clearHeightWidth, function(val1: string, val2: string, val3: number) {
    if (val2 === "width") {
    width = val3;
    } else if (val2 === "height") {
    height = val3;
    }
    return "";
    });
    if (!hasViewBox.test(group)) {
    content += `viewBox="0 0 ${width} ${height}"`;
    }
    return `${idPerfix}-${dirent.name.replace(".svg", "")}" ${content}>`;
    }).replace("", "");
    svgRes.push(svg);
    }
    });
    return svgRes;
    }

    /**
    * `svg`打包器
    * @param path 资源路径
    * @param perfix 后缀名(标签`id`前缀)
    */

    export function svgBuilder(path: string, perfix = "icon") {
    if (path.trim() === "") return;
    idPerfix = perfix;
    const res = findSvgFile(path);
    // console.log(res.length)
    return {
    name: "svg-transform",
    transformIndexHtml(html: string) {
    return html.replace("",
    `

    ${res.join("")}
    `
    )
    }
    }
    }


    作者:黄景圣
    来源:juejin.cn/post/7350874162011750400
    收起阅读 »

    大部分公司都是草台班子,甚至更水

    我第一份实习是在一家咨询公司,我以为我们能够给我们的客户提供极具商业价值的战略指导,但其实开始干活了之后,发现我们就是PPT和调研报告的搬运工。后来我去了一家互联网大厂,我以为我的身边全都是逻辑超强的技术和产品大佬,直到我们的产品带着一堆的bug上线了.......
    继续阅读 »

    我第一份实习是在一家咨询公司,我以为我们能够给我们的客户提供极具商业价值的战略指导,但其实开始干活了之后,发现我们就是PPT和调研报告的搬运工。后来我去了一家互联网大厂,我以为我的身边全都是逻辑超强的技术和产品大佬,直到我们的产品带着一堆的bug上线了......


    大四秋招的时候,我跟一个应届生一起面试,他简历上写了精通数据分析,还有很多获奖。我当时很羡慕他的能力,直到一起入职之后发现他只会用Excel......


    刚从学生变成打工人的时候,我觉得每一家公司都是一个严丝合缝,非常精密的巨大的仪器,我要达到某一个水平或者有某种资质,我才能够去做一些工作,或者达到一些成就。但后来随着工作久了,我就想明白了一件事情,让我觉得之前的班真是白上了。


    其实一个公司它的运营机制,并不是有很多个有远见的领导把规划都想明白,然后再有很多个能力强的下属把这些规划全部落地,这种太理想化了。电影里都没有这么演的。公司的运营机制就是面多了就加水,水多就加面,所有的公司都是大的草台班子。这里边绝大多数的工作,它的粗糙程度都远超过我们的想象。我们根本不用陷入所谓的入场券陷阱,觉得别人都很厉害,别人都是科班出身的,我得像别人一样厉害,一样有资质了,我才能够去做,这只不过是我给我自己设的一个假想敌。


    想明白这一点之后,我的焦虑和内耗就好多了。既然大家都很水,那在职场这个大的草台班子上,我如果不去争取机会,那就被还不如我的人抢走了。勇敢的人先享受生活,同样勇敢的打工人也会先当上生活中的主演。


    在争取机会的过程中,难免你就会用到一些职场作弊小技巧,就是自我包装。身边就有几个这样的人,敢于勇敢地表现自己,让别人觉得他能够创造很多价值。包装造势在掠夺职场资源的竞争力是非常有效的。


    包装的方式分为职业和爱好。在职场上一定不要沉迷那些琐碎的工作中无法自拔,不要显得自己每天都很忙,加班都很晚,效率低下偷懒的人才要加班,不要不满现状,导致不想思考,不要未经选择直接就开始低效率的行动,所以要适当的停下来,寻找自我包装的发力点。


    就像我们公司今年越来越重视数据分析,所以我就利用下班的时间多学习了数据分析。包装它肯定不只是一句空话,不然用不了多久就露馅了,所以要找到快速高效的学习方法。


    如果你的职业发展方向也是产品运营,市场数据分析类似的岗位,那就要尽早的培养起你的数据分析能力,用好SQL,Python,统计学还有Excel,这些都会帮助你去提取处理和分析数据,再结合上你所在行业的专业知识,技能buff叠加在职场中会非常的加分。


    职场里其实并没有那么多很厉害的人,大家都是在现学现卖,反正都是在草台班上演戏,不妨大胆一点去探索新的东西,去尝试你想尝试的,去找到能够把自己包装好的那个点,然后去大大方方的展示和表现自己。


    作者:程序员Winn
    来源:juejin.cn/post/7304867278566899764
    收起阅读 »

    鸿蒙实现动态增删 View,看这一篇就够了!!

    在 Android 开发的过程中,我们经常使用 addView ,removeView等实现在 java 代码中动态添加和删除 View 的能力 但是在鸿蒙中,组件是相对于静态的结构,而且鸿蒙也没有提供类似于 addView ,removeView的方法,那我...
    继续阅读 »

    在 Android 开发的过程中,我们经常使用 addViewremoveView等实现在 java 代码中动态添加和删除 View 的能力


    但是在鸿蒙中,组件是相对于静态的结构,而且鸿蒙也没有提供类似于 addViewremoveView的方法,那我们怎么来实现动态化增删组件的能力呢


    1. 使用 ForEach 实现动态化增删组件


    1.1. ForEach 的入参


    翻阅了鸿蒙的官方文档,终于看到了一种方法来解决这个问题,那就是使用 ForEach这个组件


    这个组件的官方定义如下


    ForEach(
    arr: Array,
    itemGenerator: (item: any, index: number) => void,
    keyGenerator?: (item: any, index: number) => string
    )


    • arr


      arr 有多少个元素,ForEach 就会渲染多少个组件


    • itemGenerator


      将 arr 中的元素转换为对应的组件样式


      决定了组件长什么样子


    • keyGenerator


      生成唯一的 key



    1.2. ForEach 的渲染


    在ForEach循环渲染过程中,系统会为每个数组元素生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。


    ForEach 的渲染分为两种:



    • 首次渲染


    在ForEach首次渲染时,会根据前述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。



    • 非首次渲染:


    在ForEach组件进行非首次渲染时,它会检查新生成的键值是否在上次渲染中已经存在。如果键值不存在,则会创建一个新的组件;如果键值存在,则不会创建新的组件,而是直接渲染该键值所对应的组件。


    2. ForEach 的简单 demo


    @Entry
    @Component
    export struct MyPage {
    @State items: Item[] = [
    { text: "1" },
    { text: "2" },
    { text: "3" },
    ]

    build() {
    Column() {
    ForEach(
    this.items,
    (item: Item, index: number) => {
    Text(item.text)
    .width(40)
    .height(40)
    }
    )

    Button("add item ")
    .onClick(
    ()=>{
    this.items.push(
    { text: "11" },
    )
    }
    )
    }
    .height('100%')
    .width('100%')
    }
    }


    interface Item {
    text: string,
    }

    运行后的状态为这样:每次点击按钮,都会新增一个



    3. ForEach 的注意事项


    3.1. 数组中元素子属性发生变化时的处理


    数组中元素子属性发生变化时,鸿蒙默认是不会触发渲染的


    解决办法是:使用@Observed@ObjectLink


    3.2. 最好自定义 key,key 中不含index


    鸿蒙的默认 key


    鸿蒙 Foreach 的默认key 为


    (item: T, index: number) => {
    //👇 这里带着 index
    return index + '__' + JSON.stringify(item);
    }

    默认的 key 里面带着 index,如果我们在对数组进行操作的过程中,将元素的 index改变了,就会导致 index 改变的元素对应的组件被重绘


    鸿蒙 ForEach 的渲染


    鸿蒙通过 key 来判断组件是新组件还是现有组件


    index 改变,key 就改变了,鸿蒙就会执行两个操作



    1. 删除原来的 key 对应的组件

    2. 重新创建组件


    就会导致原来的组件中的所有状态全部没有了比如说我们正在执行动画,但是 key 被改变了,动画就会中断,重新创建的组件没有动画执行的状态,如果我们想继续执行动画,那么必须保存动画的状态,这样成本太大了,


    而我们只需要自定义 key,让 index 不在 key 里面,就解决这个问题了


    为什么 index 会改变?


    比如说中间的元素被删除了,后面的元素 index 就会改变


    而且我们不可能在执行时,一定保证移除的是最后一个元素,添加的一定在最后面


    而如果我们自定义key 里面没有 index,那么我们可以随意的增删数组中的元素


    自定义 key 的优点



    • 可以让我们任意对数组进行操作,

    • 优化性能


    比如说我们只是删除了数组中间的一个元素,后面的元素没有任何的改变


    如果我们使用鸿蒙默认的 key,后面的元素 index 改变,key 改变,全部会重绘


    如果我们自定义 key,后面的元素就不会重绘了,节省性能


    自定义 key 的注意事项



    1. 一定不要包含 index

    2. key 的组成部分尽量全部为常量,不要变量


    如果是变量,保证组件生命周期内不会变化


    或者如果生命周期发生变化,是自己预期内的



    作者:wddin
    来源:juejin.cn/post/7374984900372709412
    收起阅读 »

    为什么阿里巴巴为什么不推荐使用keySet()进行遍历HashMap?

    引言 HashMap相信所有学Java的都一定不会感到陌生,作为一个非常重用且非常实用的Java提供的容器,它在我们的代码里面随处可见。因此遍历操作也是我们经常会使用到的。HashMap的遍历方式现如今有非常多种: 使用迭代器(Iterator)。 使用 k...
    继续阅读 »

    引言


    HashMap相信所有学Java的都一定不会感到陌生,作为一个非常重用且非常实用的Java提供的容器,它在我们的代码里面随处可见。因此遍历操作也是我们经常会使用到的。HashMap的遍历方式现如今有非常多种:



    1. 使用迭代器(Iterator)。

    2. 使用 keySet() 获取键的集合,然后通过增强的 for 循环遍历键。

    3. 使用 entrySet() 获取键值对的集合,然后通过增强的 for 循环遍历键值对。

    4. 使用 Java 8+ 的 Lambda 表达式和流。


    以上遍历方式的孰优孰劣,在《阿里巴巴开发手册》中写道:


    image.png


    这里推荐使用的是entrySet进行遍历,在Java8中推荐使用Map.forEach()。给出的理由是遍历次数上的不同。



    1. keySet遍历,需要经过两次遍历。

    2. entrySet遍历,只需要一次遍历。



    其中keySet遍历了两次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。



    其中后面一段话很好理解,但是前面这句话却有点绕,为什么转换成了Iterator遍历了一次?


    我查阅了各个平台对HashMap的遍历,其中都没有或者原封不动的照搬上句话。(当然也可能是我没有查阅到靠谱的文章,欢迎指正)


    keySet如何遍历了两次


    我们首先写一段代码,使用keySet遍历Map。


    public class Test {


    public static void main(String[] args) {
    Map<String, String> map = new HashMap<>();
    map.put("k1", "v1");
    map.put("k2", "v2");
    map.put("k3", "v3");
    for (String key : map.keySet()) {
    String value = map.get(key);
    System.out.println(key + ":" + value);
    }
    }

    }

    运行结果显而易见的是


    k1:v1
    k2:v2
    k3:v3

    两次遍历,第一次遍历所描述的是转为Iterator对象我们好像没有从代码中看见,我们看到的后面所描述的遍历,也就是遍历map,keySet()所返回的Set集合中的key,然后去HashMap中拿取value的。


    Iterator对象呢?如何遍历转换为Iterator对象的呢?


    image.png


    首先我们这种遍历方式大家都应该知道是叫:增强for循环,for-each


    这是一种Java的语法糖~。可以看上篇文章了解~


    我们可以通过反编译,或者直接通过Idea在class文件中查看对应的Class文件


    image.png
    public class Test {
    public Test() {
    }

    public static void main(String[] args) {
    Map<String, String> map = new HashMap();
    map.put("k1", "v1");
    map.put("k2", "v2");
    map.put("k3", "v3");
    Iterator var2 = map.keySet().iterator();

    while(var2.hasNext()) {
    String key = (String)var2.next();
    String value = (String)map.get(key);
    System.out.println(key + ":" + value);
    }

    }
    }

    和我们编写的是存在差异的,其中我们可以看到其中通过map.keySet().iterator()获取到了我们所需要看见的Iterator对象。


    那么它又是怎么转换成的呢?为什么需要遍历呢?我们查看iterator()方法


    iterator()


    image.png

    发现是Set定义的一个接口。返回此集合中元素的迭代器


    HashMap.KeySet#iterator()


    我们查看HashMap中keySet类对该方法的实现。


    image.png
    image.png
        final class KeySet extends AbstractSet<K> {
    public final int size() { return size; }
    public final void clear() { HashMap.this.clear(); }
    public final Iterator<K> iterator() { return new KeyIterator(); }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
    return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator<K> spliterator() {
    return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super K> action) {
    Node<K,V>[] tab;
    if (action == null)
    throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
    int mc = modCount;
    for (int i = 0; i < tab.length; ++i) {
    for (Node<K,V> e = tab[i]; e != null; e = e.next)
    action.accept(e.key);
    }
    if (modCount != mc)
    throw new ConcurrentModificationException();
    }
    }
    }

    其中的iterator()方法返回的是一个KeyIterator对象,那么究竟是在哪里进行了遍历呢?我们接着往下看去。


    HashMap.KeyIterator


    image.png
        final class KeyIterator extends HashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().key; }
    }

    这个类也很简单:



    1. 继承了HashIterator类。

    2. 实现了Iterator接口。

    3. 一个next()方法。


    还是没有看见哪里进行了遍历,那么我们继续查看HashIterator


    HashMap.HashIterator


    image.png
        abstract class HashIterator {
    Node<K,V> next; // next entry to return
    Node<K,V> current; // current entry
    int expectedModCount; // for fast-fail
    int index; // current slot

    HashIterator() {
    expectedModCount = modCount;
    Node<K,V>[] t = table;
    current = next = null;
    index = 0;
    if (t != null && size > 0) { // advance to first entry
    do {} while (index < t.length && (next = t[index++]) == null);
    }
    }

    public final boolean hasNext() {
    return next != null;
    }

    final Node<K,V> nextNode() {
    Node<K,V>[] t;
    Node<K,V> e = next;
    if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
    if (e == null)
    throw new NoSuchElementException();
    if ((next = (current = e).next) == null && (t = table) != null) {
    do {} while (index < t.length && (next = t[index++]) == null);
    }
    return e;
    }

    public final void remove() {
    Node<K,V> p = current;
    if (p == null)
    throw new IllegalStateException();
    if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
    current = null;
    K key = p.key;
    removeNode(hash(key), key, null, false, false);
    expectedModCount = modCount;
    }
    }

    我们可以发现这个构造器中存在了一个do-while循环操作,目的是找到一个第一个不为空的entry


            HashIterator() {
    expectedModCount = modCount;
    Node<K,V>[] t = table;
    current = next = null;
    index = 0;
    if (t != null && size > 0) { // advance to first entry
    do {} while (index < t.length && (next = t[index++]) == null);
    }
    }

    KeyIterator是extendHashIterator对象的。这里涉及到了继承的相关概念,大家忘记的可以找相关的文章看看,或者我也可以写一篇~~dog。


    例如两个类


    public class Father {

    public Father(){
    System.out.println("father");
    }
    }

    public class Son extends Father{

    public static void main(String[] args) {
    Son son = new Son();
    }
    }

    创建Son对象的同时,会执行Father构造器。也就会打印出father这句话。


    那么这个循环操作就是我们要找的循环操作了。


    总结



    1. 使用keySet遍历,其实内部是使用了对应的iterator()方法。

    2. iterator()方法是创建了一个KeyIterator对象。

    3. KeyIterator对象extendHashIterator对象。

    4. HashIterator对象的构造方法中,会遍历找到第一个不为空的entry



    keySet->iterator()->KeyIterator->HashIterator



    大家想更清楚了解这个entry是什么?可以看我的HashMap文章~。文章如果存在错误,欢迎大家评论区指正~~


    image.png


    作者:以范特西之名
    来源:juejin.cn/post/7295353579002396726
    收起阅读 »

    协程Job的取消,你真的用对了吗?

    前言我们知道,调用协程的lifecycleScope的launch方法后会生成一个Job对象,Job可以调用cancel()方法来取消,也可以由lifecycle宿主在生命周期结束时自行取消。但job取消后,并不代表其后面的代码都不执行了,在老油条同事的代码里...
    继续阅读 »

    前言

    我们知道,调用协程的lifecycleScope的launch方法后会生成一个Job对象,Job可以调用cancel()方法来取消,也可以由lifecycle宿主在生命周期结束时自行取消。但job取消后,并不代表其后面的代码都不执行了,在老油条同事的代码里也发现了同样的问题,cancel后并没有真正停掉后台的任务

    结论

    先说结论,协程Job的cancel()方法并不会立即中断后续代码的执行,只是将任务状态isActive改为false。只有当执行下一个可取消的suspend方法时,才会抛出一个CancellationException,停掉后面的代码。 这意味着,如果一个Job在任务过程中不存在一个可取消suspend方法的调用,那么直到任务结束都不会停止,即使是调用了cancel()方法。

    fun jobTest() {
    runBlocking {
    val job1 = launch(Dispatchers.IO) {
    Log.d(TAG, "job1 start")
    Thread.sleep(2_000)
    Log.d(TAG, "job1 finish")
    }
    val job2 = launch {
    Log.d(TAG, "job2 start")
    delay(2_000)
    Log.d(TAG, "job2 finish")
    }
    delay(1000)
    job1.cancel()
    job2.cancel()
    }
    }
    2024-06-10 23:05:37.407 21238-21272 JobTest    D  job1 start
    2024-06-10 23:05:37.407 21238-21327 JobTest D job2 start
    2024-06-10 23:05:39.407 21238-21272 JobTest D job1 finish

    如上述示例中,job1跟job2都调用了cancel()方法取消,但由于job1任务内没有suspend方法,job1在cancel后依然执行完了代码;而job2在第二个delay方法前取消了,后面的代码也不再执行。

    虽然说协程任务的错误取消,通常情况下也不会导致逻辑出错或者业务异常,但还是会造成一些后台资源的浪费或者内存泄漏问题。而且也由于没有太大影响,很多时候也难以被发现,像是代码刺客一样的东西在危害着项目。

    如何取消协程

    1. 既然job取消后会改变任务状态,可以在代码语句中根据isActive状态决定是否继续执行
    lifecycleScope.launch(Dispatchers.IO) {
    val job = launch {
    Log.d(TAG, "job start")
    while (isActive) {
    //..
    }
    Log.d(TAG, "job finish")
    }
    delay(1000)
    job.cancel()
    Log.d(TAG, "job cancel")
    }
    2024-06-10 23:54:46.430  4094-4353  JobTest        D  job start
    2024-06-10 23:54:47.434 4094-4330 JobTest D job cancel
    2024-06-10 23:54:47.434 4094-4353 JobTest D job finish

    1. 在代码执行语句中有suspend修饰的挂起方法,在协程取消后执行到suspend方法会抛出异常,从而停止协程job
    lifecycleScope.launch(Dispatchers.IO) {
    val job = launch {
    Log.d(TAG, "job start")
    while (true) {
    delay(1)
    }
    Log.d(TAG, "job finish")
    }
    job.invokeOnCompletion {
    Log.d(TAG, "invokeOnCompletion:$it")
    }
    delay(1000)
    job.cancel()
    Log.d(TAG, "job cancel")
    }
    2024-06-10 23:59:22.531 10172-10371 JobTest        D  job start
    2024-06-10 23:59:23.536 10172-10270 JobTest D job cancel
    2024-06-10 23:59:23.539 10172-10380 JobTest D invokeOnCompletion:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@3df8870

    可以看到任务抛出了JobCancellationException,并且不会执行到job finish语句。

    两种任务停止方式的区别在于,第二种方式因为delay()这个suspend方法抛出了异常而终止执行,第一种由于没有遇到suspend方法并不会抛出异常,可以执行到结束。

    那么只要是suspend方法就一定能停止协程吗?

    lifecycleScope.launch(Dispatchers.IO) {
    val job = launch {
    Log.d(TAG, "job start")
    while (true) {
    emptySuspend()
    }
    Log.d(TAG, "job finish")
    }
    job.invokeOnCompletion {
    Log.d(TAG, "invokeOnCompletion:$it")
    }
    delay(1000)
    job.cancel()
    Log.d(TAG, "job cancel")
    }

    private suspend fun emptySuspend() {
    return suspendCoroutine {
    it.resume(Unit)
    }
    }

    2024-06-11 00:04:45.144 14010-14234 JobTest        D  job start
    2024-06-11 00:04:46.151 14010-14241 JobTest D job cancel

    运行后等待数秒,发现并不会抛出异常。明明一直在调用suspend方法,任务取消后却不会响应。

    事实上,普通suspend方法并不会处理cancel标志,只有suspendCancelable类型方法会在执行前判断cancel状态并抛出异常。而常见的delay、emit方法都是suspendCancelable类型。

    将emptySuspend()方法做一个修改如下

    private suspend fun emptySuspend() {
    return suspendCancellableCoroutine {
    it.resume(Unit)
    }
    }

    运行后发现任务可以被cancel()掉而停止

    2024-06-11 00:09:11.169 17728-17872 JobTest        D  job start
    2024-06-11 00:09:12.174 17728-17865 JobTest D job cancel
    2024-06-11 00:09:12.177 17728-17872 JobTest D invokeOnCompletion:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@7cc1e91

    协程取消原理

    再简单从协程的实现原理解释一下为什么协程Job要在执行suspend方法时才能中断。

    挂起方法

    用suspend修饰的方法称为挂起方法,需要在协程作用域才能调用。

    suspend fun delaySuspend() {
    Log.d(TAG, "start delay: ")
    delay(100)
    Log.d(TAG, "delay end")
    }

    挂起方法会编译成Switch状态机模式,每个挂起方法都是其中一个case,每个case执行都依赖前面的case,这就是协程切换与挂起停止的原理。协程本质上是产生了一个 switch 语句,每个挂起点之间的逻辑都是一个 case 分支的逻辑。 参考 协程是如何实现的 中的例子:

    Function1 lambda = (Function1)(new Function1((Continuation)null) {
        int label;

        @Nullable
        public final Object invokeSuspend(@NotNull Object $result) {
            byte text;
            @BlockTag1: {
                Object result;
                @BlockTag2: {
                    result = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                    switch(this.label) {
                        case 0:
                            ResultKt.throwOnFailure($result);
                            this.label = 1;
                            if (SuspendTestKt.dummy(this) == result) {
                                return result;
                            }
                            break;
                        case 1:
                            ResultKt.throwOnFailure($result);
                            break;
                        case 2:
                            ResultKt.throwOnFailure($result);
                            break @BlockTag2;
                        case 3:
                            ResultKt.throwOnFailure($result);
                            break @BlockTag1;
                        default:
                            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                }

                text = 1;
                System.out.println(text);
                this.label = 2;
                if (SuspendTestKt.dummy(this) == result) {
                    return result;
                }
            }

            text = 2;
            System.out.println(text);
            this.label = 3;
            if (SuspendTestKt.dummy(this) == result) {
                return result;
            }
        }
        text = 3;
        System.out.println(text);
        return Unit.INSTANCE;
    }

    @NotNull
    public final Continuation create(@NotNull Continuation completion) {
        Intrinsics.checkNotNullParameter(completion, "completion");
        Function1 funcation = new constructor>(completion);
        return funcation;
    }

    public final Object invoke(Object object) {
        return (()this.create((Continuation)object)).invokeSuspend(Unit.INSTANCE);
            }
    });

    任务取消

    任务取消后,对于suspendCancelable方法的分支,会因为取消的状态而抛出JobCancellationException,停止后续代码的执行。如果在job中对于异常进行捕获,将可能导致任务取消失败。

    lifecycleScope.launch(Dispatchers.IO) {
    val job = launch {
    Log.d(TAG, "job start")
    kotlin.runCatching {
    while (true) {
    emptySuspend()
    }
    }.onFailure {
    Log.e(TAG, "catch: $it")
    }
    Log.d(TAG, "job finish")
    }
    delay(1000)
    job.cancel()
    Log.d(TAG, "job cancel")
    }
    2024-06-11 00:22:22.686 25890-26199 JobTest        D  job start
    2024-06-11 00:22:23.690 25890-26217 JobTest D job cancel
    2024-06-11 00:22:23.696 25890-26199 JobTest E catch: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@e557022
    2024-06-11 00:22:23.696 25890-26199 JobTest D job finish

    由于捕获了JobCancellationException,导致job finish语句正常执行了。在一些情况下,可能会由于JobCancellationException被捕获导致任务没有及时取消。因此在job内捕获异常时,选择性的过滤掉JobCancellationException,将异常再度抛出

    kotlin.runCatching {
    // ...
    }.onFailure {
    if (it is CancellationException) {
    throw it
    }
    }

    协程异常处理

    协程遇到无法处理的异常后,会按照停止自身子任务-停止自身任务-停止父任务的顺序依次停掉任务,并将异常抛给父作用域。当所有作用域都无法处理异常,会抛给unCautchExceptionHandler。如果异常一直没被处理,则可能引起崩溃。

    值得一提的是,由Job.cancel()方法引起的CancellationException并不会传给父Job,在cancelParent之前会被过滤掉,也就是cancel()方法只能取消自身和子协程,不会影响父协程,也不会引起程序崩溃。


    作者:护城河编程大师
    来源:juejin.cn/post/7378363694939635722
    收起阅读 »

    我们Model3也要有自己的预览网站!

    web
    通过Three.js创建一个互动的在线展示平台,可视化特斯拉Model 3的的部分技术。网站利用Three.js提供的API,实现了Model 3的三维模型展示、动画效果以及与用户交互的功能。预览地址:model3.newhao2021.top/github地...
    继续阅读 »

    preview.gif

    通过Three.js创建一个互动的在线展示平台,可视化特斯拉Model 3的的部分技术。网站利用Three.js提供的API,实现了Model 3的三维模型展示、动画效果以及与用户交互的功能。

    预览地址:model3.newhao2021.top/

    github地址:github.com/varrff/Mode…

    使用

    安装依赖

    pnpm i

    本地调试

    pnpm run dev

    构建

    pnpm run build

    预览

    pnpm run preview

    关键概念

    Catmull-Rom样条曲线

    Catmull-Rom样条曲线是一种平滑的插值曲线,可以用于创建自然的路径和轨迹。在Three.js中,THREE.CatmullRomCurve3类用于生成三维空间中的Catmull-Rom样条曲线。该曲线通过一组控制点进行插值,生成光滑的曲线,常用于动画路径、相机路径等。

    这里使用了样条曲线创建了Autopilot部分的距离预警线

    管道几何体(Tube Geometry)

    管道几何体(Tube Geometry)是Three.js中用于创建沿着一条路径生成的管状三维几何体的类。这种几何体在表示道路、轨迹、隧道等需要具有实际厚度的三维结构时非常有用。下面我们将详细介绍管道几何体的概念、创建方法及其应用。

    这里使用了样条曲线创建了FSD部分的行驶预测路线

    代码部分

    World文件结构

    - src
    - World
    - CameraShake.ts: 摄像机抖动效果文件。
    - Car.ts: 汽车部分。
    - City.ts: Autopilot部分文件。
    - Road.ts: FSD部分文件。
    - Speedup.ts: 加速效果文件。
    - StartRoom.ts: 起始房间对象文件。
    - TestObject.ts: 测试对象文件。
    - World.ts: 世界管理文件,负责加载和管理整个场景中的所有对象和效果。

    首页部分

    首页的加速流光效果以及相机抖动部分推荐alphardex大佬的文章:juejin.cn/post/735276…

    也非常感谢大佬热心帮助我解决了部分问题。

    Autopilot部分(road.ts)

    addExisting 方法

    这个方法用于将现有的模型添加到场景中,并启动动画。

    • 加载模型: 从base.am.items中获取已经加载的GLTF模型。
    • 设置模型位置和缩放: 调整模型的位置和缩放比例,使其适应场景。
    • 添加模型到容器: 将模型添加到当前组件的容器中。

    run 方法

    负责启动模型的动画循环。

    • 启动汽车运行: 调用carRun方法开始汽车动画。
    • 递归动画: 使用requestAnimationFrame进行递归动画,每帧更新模型的位置。

    carRun 方法

    用于处理汽车模型的动画效果。

    • 克隆模型: 使用SkeletonUtils.clone确保每次都是新的克隆对象。
    • 设置材质: 创建并应用新的材质,使汽车模型支持光照和反射。
    • 创建护盾: 调用createShield方法生成护盾效果。
    • 添加汽车到容器: 将新的汽车模型添加到容器中。
    • 定义动画参数和函数: 定义汽车动画的参数和递归动画函数animateCar

    createShield 方法

    用于创建护盾效果。

    • 定义控制点: 使用THREE.Vector3定义护盾的路径控制点。
    • 创建曲线和几何体: 用THREE.CatmullRomCurve3创建样条曲线,并生成对应的管道几何体。
    • 创建材质和纹理: 用Canvas创建线性渐变纹理,并应用到管道材质上。
    • 设置动画: 使用gsap实现护盾渐变动画和控制点的动态更新。

    updateControlPoint 方法

    更新控制点的位置,使护盾效果更加动态。

    • 递增或递减操作: 根据目标值和步长更新控制点的x和z坐标。
    • 更新曲线和几何体: 更新样条曲线的控制点,并重新生成管道几何体。

    removeAllModelsAndAnimations 方法

    用于移除所有模型并停止所有动画。

    • 移除模型和对象: 从容器中移除道路模型、管道和汽车模型,并释放相关资源。
    • 停止动画循环: 取消所有动画帧请求,停止动画。

    playAuto 方法

    用于播放背景音乐。

    • 加载和播放音乐: 使用Howl.js库加载并播放背景音乐。

    FSD部分(city.ts)

    setCar 方法

    用于设置汽车模型,目前只是加载了汽车模型数据。

    createRoad 方法

    用于创建道路。

    • 定义控制点: 使用THREE.Vector3定义道路的路径控制点。
    • 更新道路几何体: 调用updateRoadGeometry方法,根据控制点创建道路几何体。
    • 设置材质和动画: 创建材质并使用GSAP动画库实现过渡动画。

    updateRoadGeometry 方法

    更新道路几何体。

    • 检查控制点: 确认控制点存在。
    • 创建曲线和几何体: 用THREE.CatmullRomCurve3创建样条曲线,并生成管道几何体。
    • 调整顶点位置: 调整几何体顶点的y坐标。
    • 创建材质和纹理: 用Canvas创建线性渐变纹理,并应用到管道材质上。
    • 更新或创建道路对象: 更新现有道路对象的几何体或创建新的道路对象并添加到场景中。

    updateControlPoint 方法

    更新控制点的位置,使道路效果更加动态。

    • 递增或递减操作: 根据目标值和步长更新控制点的x和z坐标。
    • 更新道路几何体: 调用updateRoadGeometry方法更新几何体。

    runRoad 方法

    负责启动道路的动画。

    • 定义多个动画步骤: 使用GSAP库定义一系列动画,平滑地移动和旋转模型。
    • 启动控制点动画: 定义和启动控制点更新动画,使道路效果动态变化。

    不足

    1. 特效效果还是没办法跟大佬的比,有待优化。
    2. 手机上横屏时控制器依然是竖屏的逻辑,没有翻转。
    3. 在手机上显示时,由于刷新率的不同,FSD部分的路线动画会有延迟。

    感慨

    也不知道什么时候能买上一辆Model3嘞,第一次看见总觉得这个流线感真好看,也不像豪车那样的价格遥不可及,尽管后来特斯拉的车都成了街车了,也有太多国产电车后来居上,本地化做的也比特斯拉好。但每次看见,都依然觉得真t*好看。


    作者:超级无敌攻城狮
    来源:juejin.cn/post/7378459137418838016
    收起阅读 »

    每一个前端,都要拥有属于自己的埋点库~

    web
    前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 简介 sunshine-track 应用于前端监控, 基于 行为上报,实现了 用户行为、错误监控、页面跳转、页面白屏检测、页面性能检测等上报功能。适用于 Vu...
    继续阅读 »

    前言


    大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~



    简介


    sunshine-track 应用于前端监控, 基于 行为上报,实现了 用户行为、错误监控、页面跳转、页面白屏检测、页面性能检测等上报功能。适用于 Vue、React、Angular 等框架



    本项目源码:github.com/sanxin-lin/…
    各位兄弟姐妹如果觉得喜欢的话,可以点个star 哦~



    功能


    sunshine-track具备以下功能:



    • ✅ 用户行为上报:包括 点击、跳转页面、跳转页面记录数组、请求

    • ✅ 用户手动上报:提供 Vue 自定义指令 以及add、report函数,实现用户手动上报

    • ✅ 自定义上报:提供 格式化上报数据、自定义上报函数、自定义决定上不上报 等配置项,更灵活地上报数据

    • ✅ 请求数据上报:提供 检测请求返回、过滤请求 等配置项,让用户决定上报哪些请求数据

    • ✅ 上报方式:提供 上报方式 配置项,用户可选择 img、http、beacon 三种方式,http方式又支持 xhr、fetch 两种,且支持 自定义headers

    • ✅ 上报数据缓存:可配置 本地缓存、浏览器本地缓存、IndexedDB 三种方式

    • ✅ 上报数据阈值:可配置上报数据 阈值 ,达到 阈值 后进行上报操作

    • ✅ 全局点击上报:可通过配置 选择器、元素文本,对全局DOM节点进行点击上报

    • ✅ 页面的性能检测,包括 白屏、FP、FCP、LCP、CLS、TTFB、FID


    上报数据格式


    选项描述类型
    uuid   上报数据的idstring
    type   上报数据的类型string
    data   上报数据any
    time    上报时间number
    status    上报状态string
    domain    当前域名string
    href    当前网页路径string
    userAgent    当前user-agentstring
    deviceInfo   设备的相关信息object

    安装



    使用



    全局点击监听


    可以通过配置globalClickListeners来对于某些DOM节点进行点击监听上报



    配置上报阈值


    上报分为几种:



    • 用户行为上报:点击、跳转页面、请求,这些上报数据会缓存着,当达到阈值时再进行上报

    • 错误上报:请求报错、代码报错、异步错误,这些是立即上报

    • 页面性能上报:白屏、FP、FCP、LCP、CLS、TTFB、FID,这些是立即上报


    用户行为上报的阈值默认是 10,支持自定义 maxEvents



    配置缓存方式


    如果你想要避免用户重新打开网页之后,造成上报数据的丢失,那么你可以配置缓存方式,通过配置cacheType



    • normal:默认,本地缓存

    • storage:浏览器 localStorage 本地缓存

    • db:浏览器 IndexedDB 本地缓存


    app.use(Track, {
    ...options,
    cacheType: 'storage' // 配置缓存方式
    })

    打印上报数据


    可以通过配置 log ,开启打印上报数据



    灵活上报请求数据


    请求也是一种行为,也是需要上报的,或许我们有这个需求



    • 过滤:某些请求我们并不想上报

    • 自定义校验请求响应数据:每个项目的响应规则可能都不同,我们想自己判断哪些响应是成功,哪些是失败



    格式化上报数据、自定义决定上不上报、自定义上报


    如果你想在数据上报之前,格式化上报数据的话,可以配置report中的format



    如果你想要自己决定某次上报的时候,进行取消,可以配置report中的isReport



    如果你不想用这个库自带的上报功能,想要自己上报,可以配置report中的customReport



    手动上报


    手动上报分为三种:



    • 手动添加上报数据:添加到缓存中,等到达到阈值再上报

    • 手动执行数据上报:立即上报

    • 自定义指令上报:如果你是 Vue 项目,支持指令上报



    如果你是 Vue 项目,可以使用指令v-track进行上报



    配置参数


    选项描述类型
    projectKey   项目keystring
    userId   用户idstring
    report.url   上报urlstring
    report.reportType  上报方式img、http、beacon
    report.headers  上报自定义请求头,http 上报模式生效object
    report.format  上报数据格式化function
    report.customReport  自定义上报function
    report.isReport  自定义决定上不上报function
    cacheType   数据缓存方式normal、storage、db
    globalClickListeners   上报状态array
    log   当前域名boolean
    maxEvents   上报阈值number
    historyUrlsNum   需要记录的url跳转数组number
    checkHttpStatus   判断响应数据function
    filterHttpUrl   过滤上报请求数据function
    switchs.xhr   是否开启xhr请求上报boolean
    switchs.fetch   是否开启fetch请求上报boolean
    switchs.error   是否开启错误上报boolean
    switchs.whitescreen   是否开启白屏检测上报boolean
    switchs.hashchange   是否开启hash变化请求上报boolean
    switchs.history   是否开启history变化上报boolean
    switchs.performance   是否开启页面性能上报boolean


    本项目源码:github.com/sanxin-lin/…
    各位兄弟姐妹如果觉得喜欢的话,可以点个star 哦~





    作者:Sunshine_Lin
    来源:juejin.cn/post/7377901375001198655
    收起阅读 »

    很想做副业?但是副业不等于赚钱

    前言 Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。 最近半年发的文章中,我开头总是说,我是一个正在探索个人IP&副业的后端程序员。 今天开始,我准备把副业两个字去掉了,因为我在最近的半年的时间里,都没有理解副业和赚钱的关系。 副业可以分...
    继续阅读 »

    前言


    Hi 你好,我是东东拿铁,一个正在探索个人IP的后端程序员。


    最近半年发的文章中,我开头总是说,我是一个正在探索个人IP&副业的后端程序员。


    今天开始,我准备把副业两个字去掉了,因为我在最近的半年的时间里,都没有理解副业和赚钱的关系。


    副业可以分为两种:



    1. 与主业相关,借助副业扩大自己的知识、影响力,最终回馈于主业。去达到自己的职业生涯的第二增长曲线。

    2. 与主业无关,一般是到了30岁左右,会存在一定的职场危机,建立副业也就是在职业风险侧来控制,当主业出现危机,现金流不会断裂。选择这种副业的重点就是围绕着自己的爱好或者业余擅长的事情来做副业发展。


    一般来说第二类朋友们居多,我自己也是属于第二种,副业的目的,就是解决主业的风险问题,保证自己的现金流,目的就是赚钱。


    但是,副业!=赚钱


    想要赚钱,不如我们先试着了解一下,赚钱,到底是怎么样的一件事情,话不多说,我们开始吧。


    赚钱的本质是什么


    赚钱的本质,是交易,是买卖。


    我们谈到所谓赚钱,其实核心思想是,交易,只有交易才能给你带来金钱,只有交易才能产生价值。


    那么什么是交易,不是只有卖货是交易,你在职场获取⼀份offer,也是⼀种交易,公司买到了你的时间。


    首先,建立⼀个认知基础,赚钱的本质,是交易。


    那买卖的产品是什么


    万物皆产品,只要能满足需求。


    一套话术,一段文案,一个眼神,一个电话,一篇文章,一个课程,一个商品,一个咨询,一个关心,一套方法论,一本书,一次交谈,一个回答,一个社群,一本日历,一套方案等等。


    以上皆是产品,只要能解决对方的问题。


    比如,假设我把自己定位职场问题专家,那么这个问题可以拆分成几类



    1. 求职面试

    2. offer选择咨询

    3. 技术难题解答

    4. 晋升&管理经验分享


    所以说,目光所及之处,皆为产品可以售卖。


    你能卖什么产品


    打造一个产品前,哪些地方需要我们去考虑



    1. 自身优势

    2. 天花板高

    3. 利他有价值

    4. 符合时代趋势

    5. 能长期积累

    6. 门槛不高,能够入场

    7. 合理合法


    我第一点列的是自我优势,所以重点聊一下个人优势这块。


    每个人的特质不同,每个人都是独一无二的存在。按照盖洛普对于人4个领域的分类,有以下四种



    1. 执行力,懂得如何完成任务,对于明确的事情,都能够做的很好。

    2. 影响力,比如知道如何掌握局势,给人一种很有力量的感觉,又或者很有气场

    3. 关系建立,擅长建立牢固的关系,凝聚团队,产生更大的价值。

    4. 战略思维,喜欢获取信息、加工信息,并作出决策,比如享受思考


    那么,每个人擅长的领域不同,就诞生了不同的细分岗位,比如有人适合做内容型产品,写文章、写专栏。有人适合做工具型产品,开发小程序、APP等。有人适合做运营,比如搞线上线下活动。岗位太多,就不一一列举了。


    任何一个小的领域,都有人能赚到大钱,但是我们去做不一定能成功。


    所以,你能卖什么产品,要结合自己的兴趣、优势、能力、资源来考虑,有前景固然重要,但是自己能掌控住,更重要。


    比如微商曾经也是风口,我在过去一段时间,也一直在懊悔怎么没抓住微商的这个机会,但对于我这种偏内向,朋友圈一年都发不了几条的人来说,微商自然是不适合。


    所以,风口和机会,都是建立在个人优势、爱好、行业积累上的。


    别人为什么要从你这买?


    信息不对称,不知道哪里可以买。
    最早学习JVM的时候,我翻看了网上的很多资料,每看一篇文章,我都觉着,这个作者好厉害。
    后来我知道,周志明老师写了一本,《深入了解JVM虚拟机》,然后很多网上的资料,不过是这本书的学习笔记。


    这就是信息不对称,利用信息差可以赚到钱。


    交付不便利,不方便从其他人那里买。
    举个简单的例子,比如楼下小商店卖的东西,大部分超市也都可以买到,网购还更实惠,但是很多东西你必须要去买,比如你渴了。


    信用不传递,信任这个事情,挺玄学的。
    比如我很喜欢的博主半佛仙人,他文章里推荐过很多东西,便宜一点的如洗地机,贵一点的比如宝珀,如果我需要,我一定优先选择他推荐过的。


    赚钱和圈层有什么关系


    什么是圈层?


    我们的生活质量提升后,社会就开始分层了。以车友圈为例,如果你是奥迪车友,奥迪的车友圈可能除了分成不同车系的车友圈,还分成了RS群、瓦罐群。


    那么对于程序员呢?分成了前后端、客户端、算法等圈子。


    那么还有不同地域、不同收入、兴趣的人,也构成了一个又一个小圈子,不同身份比如学生、宝妈、备婚、备孕同理。


    职场中呢也同理,有躺平的,奋斗努力的和核心的,高职级的就是比普通员工有着更多的信息差。


    所以你看,社会越发达,从消费层面可选择的越多,领域的细分分类也越多。


    所以,上面提到的信息差,就差在圈层这里,圈层越多,差值越多,信息差的获利就越容易。


    怎么利用圈层


    浸泡的圈层越多,你越能发现不同圈层的需求,进行搬运,因为一个圈层能做成的事情,在另一个圈层里也能做成。


    教人赚钱、做副业这个事情,是最近才有的吗,已经存在很久了。在你7、8年前朋友圈里面的微商,就在发朋友圈试图这样做了。


    但是在程序员这个圈子里,副业成为了一个热门词汇,很多程序员在探索做副业。


    主要原因还是裁员潮、35岁危机等原因,让大家焦虑了,所以诞生了副业的诉求,那么谁更早的发现这部分诉求,谁就能赚到钱。


    怎么赚到更多钱


    卖的多


    从量级阶段,想要卖的更多,有几个要素



    • 流量

    • 转化率

    • 毛利率

    • 复购率

    • 转介绍率


    那么在单价不变的情况下,提升上面任意一个环境,那么都能够带来收入的提升。


    卖的贵


    资源稀缺,才能有定价主动权。


    比如说小米su7,在刚刚上市的时候,很多人就是想抢到第一波提车,最早的甚至能让雷总给你开车门。但新品上市,产能有限,那么抢到更早提车的客户,自然就能加价售出自己的提车名额。


    卖的多元


    那如何带来多元的价值呢。


    比如微信公众号,本身程序化广告带来了你的一部分收入。


    那么等粉丝量、影响力上涨,我们可以接品牌的广告,为广告单独发文。


    头部账号,可以帮助一些初期账号、腰部账号转发文章,帮助他们扩大影响力。


    赚钱,是一个过程


    开眼、摸索


    普通人在任何一个行业久了,很容易形成思维定式,偶尔觉着工作压力过大,抬起头看看,发现自己能想到的,还是和工作、行业有关的事情。


    大道理谁都懂,可是我们缺的是眼界和案例。


    那么开了眼界,就要开始梳理自己,找到定位的大概方向,然后细分定位,找到自己可以卖的东西。


    找到了一件可以卖出去的东西后,下面要做的就是不断发现问题、分析问题、解决问题、改进问题。


    比如流量不足,你的微信列表只有几百人,你需要去扩充流量。比如学习营销,持续的曝光自己,让更多的人看到你卖的东西等等。


    在第一阶段,网上有充足的资料供你选择,只要你虚心学习,坚持下去,不要去跳过一些环节,也不要去创新,跟着一些有经验的人,一步步走下去就好。


    但是在摸索这个阶段一定是最难熬的,所以很多大佬在劝一些想要赚钱的人时,都会说,先赚到工资外的第一块钱。


    当然,赚到第一块会很简单,但能不能做到月入过万,那或许就很难了,如果你能够做到稳定月入过万,那么我们接着看。



    个人总结
    放下偏见,打开思路,找到感兴趣的,跟着教程,先开始做起来再说。



    放大、差异


    因为在第一阶段学习到的知识、方法,只能让你赚到小钱,毕竟是别人教给你的,当一个赚钱的套路被足够多的人知道,那么他就不再赚钱了。


    因此你要抓紧走向下一阶段,进行放大与差异化。


    两个方向
    做的比别人好十倍
    比如电商,别人运营一个店铺就已经心力交瘁,你通过工具、雇人等方式,运营10个店铺,那么你做的就比别人好十倍。


    这个很常见,现在的短视频、公众号都在搞矩阵,电商平台都在搞店群。
    有门槛的差异化
    比如你做面试辅导,你是字节的3-1,那么你比其他没有任何背书的人做面试辅导,就更有优势。


    转型


    利用上面两步积累的经验和认知,从一个方向换到另一个方向。


    总结一下


    你看,赚钱是一个升级打怪的过程,是不是也很像我们的职业生涯呢?


    开眼和摸索阶段,就像是刚毕业我们,对这个行业一无所知,对编程技能、开源框架还不够熟悉,但是不怕,互联网上的教程、知识,都可以支撑着我们在这个行业不断学习,逐渐变得优秀。


    放大阶段,你在互联网上学习到的知识,已经不适用了,你遇到的是一个个技术难题、业务难题,或许互联网上没有解决方案。你需要能够解决这部分问题,比如提高10x效率,比如独特的行业解决方案。


    转型阶段,在程序员这条路上,你积累的行业经验、技术经验、人脉、资源,都是通用的,你可以利用这些去做不同的工作。


    说在最后


    说了很多,感谢你能看到最后。


    上学、工作,我们不断学习社会要求我们学习的知识和技能,但几乎没有人能够教我们与赚钱、财务相关的知识,所以在学习过程中,把自己认为重要的地方总结整理出来,希望对你有帮助。


    作者:东东拿铁
    来源:juejin.cn/post/7376324160484884480
    收起阅读 »

    给圆点添加呼吸动画,老板说我很有想法

    web
    需求简介 这几天老板安排了一个活:要实现一些异常信息点的展示,展示的方式就是画一个红色的点。 需求很简单,我也快速实现了。但是想着我刚入职不久,所以得想办法表现一下自己。于是,我自作主张,决定给这个小圆点实现一个呼吸的效果动画。 实现方案 要实现这样一个小...
    继续阅读 »

    需求简介


    这几天老板安排了一个活:要实现一些异常信息点的展示,展示的方式就是画一个红色的点



    需求很简单,我也快速实现了。但是想着我刚入职不久,所以得想办法表现一下自己。于是,我自作主张,决定给这个小圆点实现一个呼吸的效果动画



    实现方案


    要实现这样一个小圆点的动画非常简单,借助css的animation实现即可


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Breathing Circle Animation</title>
    <link rel="stylesheet" href="styles.css">
    </head>
    <body>
    // 白边红色小圆点
    <div class="dot">
    // 小圆点的背景元素
    <div class="breathing-background"></div>
    </div>
    </body>
    </html>

    .dot {
    display: inline-block;
    width: 10px;
    height: 10px;
    border: 2px solid #fff;
    border-radius: 50%;
    background-color: red;
    position: relative;
    z-index: 1;
    }
    .breathing-background {
    position: absolute;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    opacity: 0.2;
    animation: breathing 2s cubic-bezier(0, 0, 0.25, 1) infinite;
    }
    // 动画 变大再变小
    @keyframes breathing {
    0% {
    transform: scale(1);
    }
    50% {
    transform: scale(5);
    opacity: 0.2;
    }
    100% {
    transform: scale(5);
    opacity: 0;
    }
    }

    上面的动画实现主要依赖于CSS关键帧动画和定位属性。



    • 定位:通过设置.dot为相对定位(position: relative)和.breathing-background为绝对定位(position: absolute),确保两个元素在同一个位置上重叠。

    • 层叠顺序:使用z-index属性确保.dot在.breathing-background的前面,从而保证红色小圆点在呼吸动画背景上显示。

    • 动画效果:@keyframes breathing定义了从正常尺寸到放大再到透明的动画过程,通过transform: scale和opacity属性的变化来实现呼吸效果。

    • 动画循环:通过animation属性设置动画的持续时间、缓动函数和无限循环,使呼吸动画效果持续进行。


    上面的代码很简单,实现的效果也简单粗暴



    老板反应


    做完之后,我很高兴的就提交代码了,我很满意自己小改动。

    过了很久,老板看后,把我叫到办公室,深色凝重的说了一句:你很有想法


    随后老板又问我,你加这个闪烁的背景想表达啥?


    我一时语塞,解释:这样不是看起来更好看,更能清晰的表达这个异常的状态吗?


    老板又怼我,谁让你乱加动画了?时间多的没处用是吧?删了。


    我不太理解老板为啥生气,回去后也是默默地删除了代码。。。。。



    后来我反思了一下,程序员还是别乱加自己的想法在需求里,毕竟我们还是不懂产品,做的越多,错的越多。做好本分工作就行了。



    作者:快乐就是哈哈哈
    来源:juejin.cn/post/7376172288977879091
    收起阅读 »

    盘点Lombok的几个骚操作

    前言 本文不讨论对错,只讲骚操作。 有的方法看看就好,知道可以这么用,但是否应用到实际开发中,那就仁者见仁,智者见智了。 一万个读者就会有一万个哈姆雷特,希望这篇文章能够给您带来一些思考。 耐心看完,你一定会有所收获。 正文 @onX 例如 onConstr...
    继续阅读 »

    前言


    本文不讨论对错,只讲骚操作。


    有的方法看看就好,知道可以这么用,但是否应用到实际开发中,那就仁者见仁,智者见智了。


    一万个读者就会有一万个哈姆雷特,希望这篇文章能够给您带来一些思考。


    耐心看完,你一定会有所收获。


    giphy (2).gif


    正文


    @onX


    例如 onConstructor, oMethod, 和 onParam 允许你在生成的代码中注入自定义的注解。一个常见的用例是结合 Spring 的 @Autowired


    在 Spring 的组件(如 @Service@Controller@Component@Repository 等)中使用 @RequiredArgsConstructor(onConstructor = @__(@Autowired)),可以让 Lombok 在生成构造函数时也加上 @Autowired 注解,这样,Spring 就可以自动注入所需的依赖。


    例如下面这段代码


    @Service
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class MyService {
    private final AnotherService anotherService;
    }

    上述代码片段使用 Lombok 和 Spring 注解,Lombok 会为其生成以下代码


    @Service
    public class MyService {
    private final AnotherService anotherService;

    @Autowired
    public MyService(AnotherService anotherService) {
    this.anotherService = anotherService;
    }
    }


    从生成的代码中可以看出:



    • MyService 生成了一个构造函数,该构造函数接受一个 AnotherService 类型的参数。

    • 由于构造函数上有 @Autowired 注解,Spring 会自动查找合适的 AnotherService bean 实例并注入到 MyService 中。


    这种方式结合了 Lombok 的自动代码生成功能和 Spring 的依赖注入功能,使得代码更为简洁。


    但是,使用此技巧时要确保团队成员都理解其背后的含义,以避免混淆。


    @Delegate


    @Delegate可以让你的类使用其他类的方法,而不需要自己写代码。


    比如,你有一个类叫做A,它有一个方法叫做sayHello(),你想让另一个类B也能用这个方法,那就可以在B类中加上一个A类型的字段,并在这个字段上加上@Delegate注解,这样,B类就可以直接调用sayHello()方法,就像它是自己的方法一样。看个例子:


    // 一个类,有一个方法
    public class A {
    public void sayHello() {
    System.out.println("Hello");
    }
    }

    // 一个类,委托了A类的方法
    public class B {
    @Delegate // 委托A类的方法
    private A a = new A();

    public static void main(String[] args) {
    B b = new B();
    b.sayHello(); // 调用A类的方法
    }
    }

    这样写最大的好处就是可以避免类的层次过深或者耦合过紧,提高代码的可读性和可维护性,各种继承来继承去是真的看得头疼。


    @Cleanup


    @Cleanup可以自动管理输入输出流等各种需要释放的资源,确保安全地调用close方法。


    它的使用方法是在声明的资源前加上@Cleanup,例如:


    @Cleanup InputStream in = new FileInputStream("some/file");

    这样,当你的代码执行完毕后,Lombok会自动在一个try-finally块中调用in.close()方法,释放资源。


    如果要释放资源的方法名不是close,也可以指定要调用的方法名,例如:


    @Cleanup("release") MyResource resource = new MyResource();

    Lombok会自动在try-finally块中调用resource.release()方法,释放资源。


    可以看到,这比手动写try-finally要简洁得太多了,只要使用@Cleanup就能管理任何有无参方法的资源,指定正确的方法名即可。


    @Singular 和 @Builder 组合


    @Builder让你的类支持链式构造,而@Singular让集合类型字段可以更方便的维护。


    @Singular注解可以用在集合类型的字段上,它会生成两个方法,一个是添加单个元素的方法,一个是添加整个集合的方法。这两个方法可以和 @Builder 生成的其他方法一起链式调用,给你的类的所有字段赋值。


    这么讲可能有点懵,直接看示例:


    @Data
    @Builder
    public class User {
    private String name;
    private int age;
    @Singular
    private List<String> hobbies;
    }

    // 使用 @Builder 和 @Singular 生成的方法
    User user = User.builder()
    .name("练习时长两年半")
    .age(28)
    .hobby("篮球") // 添加单个元素
    .hobby("唱歌") // 添加单个元素
    .hobbies(Arrays.asList("跳舞", "其他")) // 添加整个集合
    .build(); // 构造 User 对象

    可以看出,使用 @Singular 注解的好处是,你可以灵活地添加集合类型的字段,而不需要自己创建和初始化集合对象。


    另外,使用 @Singular 注解生成的集合字段,在调用 build() 方法后,会被转换为不可变的集合,这样可以保证对象的不变性和线程安全性。你也可以使用 clear() 方法来清空集合字段,例如:


    User user = User.builder()
    .name("签")
    .age(28)
    .hobby("说唱")
    .hobby("跳舞")
    .clearHobbies() // 清空集合字段
    .hobby("踩缝纫机") // 重新添加元素
    .build();

    但需要注意的是,如果你的类继承了一个父类,那么 @Builder 只会生成当前类的字段和参数,不包括父类的。


    结尾


    请注意,尽管 Lombok 提供了许多方便的功能,但过度使用不当使用可能会导致代码难以理解和维护。


    因此,在使用这些功能时,务必始终保持审慎,并且要充分考虑其影响。


    作者:一只叫煤球的猫
    来源:juejin.cn/post/7322724142779252762
    收起阅读 »

    网上被吹爆的Spring Event事件订阅有缺陷,一个月内我被坑了两次!

    Spring Event事件订阅框架,被网上一些人快吹上天了,然而我们在新项目中引入后发现,这个框架缺陷很多,玩玩可以,千万不要再公司项目中使用。还不如自己手写一个监听者设计模式,那样更稳定、可靠。 之前我已经被Spring Event(事件发布订阅组件)坑过...
    继续阅读 »

    Spring Event事件订阅框架,被网上一些人快吹上天了,然而我们在新项目中引入后发现,这个框架缺陷很多,玩玩可以,千万不要再公司项目中使用。还不如自己手写一个监听者设计模式,那样更稳定、可靠。


    之前我已经被Spring Event(事件发布订阅组件)坑过一次。那次是在服务关闭期间,有请求未处理完成,当调用Spring Event时,出现异常。


    根源是:Spring关闭期间,不得调用GetBean,也就是无法使用Spring Event 。详情点击这里查看


    然而新项目大量使用了Spring Event,在另一个Task服务还未来得及移除Spring Event的情况下,出现了类似的问题。


    当领导听说新引入的Spring Event再次出现问题时,非常愤怒,因为一个月内出现了两次故障。在复盘会议上,差点爆粗口。


    在上线过程中,丢消息了?


    “五哥,你看一眼钉钉给你发的监控截图,线上好像有丢消息?” 旁边同事急匆匆的跟我说。


    “线上有问题?强哥在上线,我让他先暂停下~”,于是我赶紧通知强哥,先暂停发布。优先排查线上问题~


    怎么会有问题呢?我有点意外,正好我和强哥各有代码上线,我只改动很小一段代码。我对这次代码变更很自信,坚信不会有问题,所以我并没有慌乱和紧张。搁之前,我的小心脏早就怦怦跳了!


    诡异的情况


    出现问题的业务逻辑是 消费A 消息,经过业务处理后,再发送B消息。


    image.png
    从线上监控和日志分析,Task服务收到了 A 消息,然后处理失败了。诡异之处是没有任何异常日志和异常打点,仿佛凭空消失了。


    分析代码分支后,我和同事十分确信,任何异常退出的代码分支都有打印异常日志和上报异常监控打点,出现异常不可能不留一丝痕迹。


    正当陷入困境之时,我们发现蹊跷之处。“丢消息”的时间只有 3秒钟,之后便恢复正常。问题出在启动阶段,消息A进入Task服务,服务还未完全发布完成时,导致不可预测的情况发生。


    当分析Spring 源代码以后,我们发现原因出在 Spring Event……


    在详细说明问题根源前,我简单介绍一下 SpringEvent使用,熟悉它的读者,可以自行跳过。


    Spring Event的简单使用


    声明事件


    自定义事件需要继承Spring ApplicationEvent。我选择使用泛型,支持子类可以灵活关联事件的内容。


    public class BaseEvent<T> extends ApplicationEvent {
    private final T data;

    public BaseEvent(T source) {
    super(source);
    this.data = source;
    }

    public T getData() {
    return data;
    }
    }

    发布事件


    使用Spring上下文 ApplicationContext发布事件


    applicationContext.publishEvent(new BaseEvent<>(param));

    Idea为Spring提供了跳转工具,点击绿色按钮位置,就可以 跳转到事件的监听器列表。


    image.png


    监听事件


    监听器只需要 在方法上声明为 EventListener注解,Spring就会自动找到对应的监听器。Spring会根据方法入参的事件类型和 发布的事件类型 自动匹配。


    @EventListener
    public void handleEvent(BaseEvent<PerformParam> event) {
    //消费事件
    }

    服务启动阶段,Spring Event 注册严重滞后


    在Kafka 消费逻辑中,通过Spring Event发布事件,业务逻辑都封装在 Event Listenr 中。经过分析和验证后,我们终于发现问题所在。


    当Kafka 消费者已经开始消费消息,但Spring Event 监听者还没有注册到Spring ApplicationContext中, 所以Spring Event 事件发布后,没有Event Listener消费该事件。3秒钟以后,Event Listener被注册到Spring后,异常就消失了。


    问题根源在:Event Listener 注册的时间点滞后于 init-method 的时间点!


    image.png


    init-method ——— Kafka 开始监听的时间点


    Kafka 消费者的启动点 在 Spring init-method中,例如下面的 XML中,init-method 声明 HelloConsumer 的初始化方法为 init方法。在该方法中注册到Kafka中,抢占分片,开始消费消息。


    <bean id="kafkaConsumer" class="com.helloworld.KafkaConsumer" init-method="init" destroy-method="destroy">


    如果在init-method 方法中,成功注册到Kafka,抢占到分片,然而 Spring Event Listener还未注册到Spring ,就会 “Spring事件丢失” 的现象。


    EventListener注册到Spring 的时间点


    在Spring的启动过程中,EventListener 的启动点滞后于 init-method 。如下图Spring的启动顺序所示。


    其中init-methodInitializingBean中被触发,而 EventListenerSmartInitializingSingleton 中初始化。由于启动顺序的先后关系,当init-method的执行时间较长时(例如连接超时),就会出现Kafka已开始消费,但EventListener还未注册的问题。


    Spring 启动顺序
    image.png


    InitializingBean 的初始化代码


    通过分析 Spring源代码。InitializingBean 阶段, invokeInitMethod 会执行init-method方法,Kafka消费者就是在init-method 执行完成后开始消费kafka消息。
    image.png


    SmartInitializingSingleton


    继续分析Spring源代码。 EventListenerMethodProcessorSmartInitializingSingleton 子类,该类负责解析Spring 中所有的Bean,如果有方法添加EventListener注解,则将 EventListener方法 注册到 Spring 中


    以下是代码截图
    image.png


    Spring Event很好,我劝你别用


    通过代码分析可以发现,在Spring中,init-method方法会先执行,然后才会解析和注册Event Listener。因此,在消费Kafka和注册EventListener之间存在一个时间间隔,如果在这期间发布了Spring Event,该事件将无法被消费。


    通常情况下,这个时间间隔非常短暂,但是当init-method执行较慢时,比如Kafka消费者 A 初始化很快,但是Kafka消费者 B 建立连接超时导致init-method执行时间较长,就会出现问题。在这段时间内,Kafka消费者 A 发布的Spring事件无法被消费。


    尽管这不是一个稳定必现的问题,但是当线上流量较大时,它发生的概率会增加,后果也会更严重。我们在上线3个月后,线上环境才首次遇到这个问题。


    《服务关闭期,Spring Event消费失败》这篇文章中,有读者评论提到了这个问题。


    image.png



    有朋友说: 这和spring event有什么关系,自己实现一套,不也有同样的问题吗?关键是得优雅停机啊!



    他所说的是正确的,如果服务能够完美地进行优雅发布,即使是在大流量场景下,Spring Event也不会出现问题。


    一般情况下,公司的项目通常会在 init-method 方法中,统一初始化消息队列 MQ 消费者。如果想要安全地使用Spring Event,必须等到Spring完全发布完成之后才能初始化 Kafka 消费者。


    对于公司的项目来说,稳定性非常重要。引入 SpringEvent 前,一定要确保服务的入口流量在正确的时点开启。


    作者:五阳
    来源:juejin.cn/post/7302740437529296907
    收起阅读 »

    🚀独立开发,做的页面不好看?我总结了一些工具与方法🚀

    web
    前言 我有时候会自己开发一些项目,但是不比在公司里面,自己开发项目的时候没有设计稿,所以做出来的页面比较难看。 开发了几个项目之后,我也总结了以下的一些画页面的资源或者方法,希望对大家有帮助~ 颜色&字体 这一部分主要参考的是antd的方案,主要包括颜...
    继续阅读 »

    前言


    我有时候会自己开发一些项目,但是不比在公司里面,自己开发项目的时候没有设计稿,所以做出来的页面比较难看。


    开发了几个项目之后,我也总结了以下的一些画页面的资源或者方法,希望对大家有帮助~


    颜色&字体


    这一部分主要参考的是antd的方案,主要包括颜色与字体(包括字体的颜色、大小)的使用与搭配。


    颜色


    对于颜色来说,整个站点最好有一个主题色,然后有一种色彩生成算法,基于这个主题色去生成一套色板。在 antd 的官网中共计 120 个颜色,包含 12 个主色以及衍生色。


    image.png


    12 种颜色方案都是比较好看的,如果你想定义自己的主题色,这里也有一个色板生成工具


    image.png


    同样你也可以将这套色板生成算法引入到你的程序中,这是他的npm包


    确认好主题色之后,再来看看中性色。


    image.png


    这里它也提供了我们相对常用的一些中性色,有了主题色与中性色之后,我们就可以定义一个 less/sass 文件,把我们常用的这些颜色写成变量导入使用,确保我们的站点色彩是保持统一的。


    @primary-color: #1890ff;
    @primary-text-color: #000000e0;
    @first-text-color: #000000e0;
    @sceond-text-color: #000000a6;
    @border-color: #d9d9d9ff;
    @disabled-color: #00000040;
    @divider-color: #0505050f;
    @background-color: #f5f5f5ff;

    这几种色彩看起来如下:


    image.png


    字号


    image.png


    antd 中,它同样对字体大小也有着十分深厚的研究,我们这里就简单一点,大多数浏览器的默认字体大小是 16px,我们就以这个值为基准,来设计 5 个字号如下:


    @smallest-size: 12px;
    @small-size: 14px;
    @size: 16px;
    @large-size: 20px;
    @largest-size: 24px;

    这五种字号看起来如下:


    image.png


    渐变


    UI 设计中,渐变是一种将两种或多种颜色逐渐过渡或混合在一起的效果。渐变可以增加界面的视觉吸引力、深度和层次感,并帮助引导用户的视线,提高用户体验。


    渐变在以下几个方面有着重要的意义:



    1. 引导视线:通过渐变的色彩变化,可以引导用户的视线,突出重要内容或者引导用户进行特定的操作。

    2. 增加层次感:渐变可以使界面元素看起来更具立体感和深度,提高UI设计的质感和视觉吸引力。

    3. 提升品牌形象:使用特定颜色的渐变可以帮助强化品牌形象,让界面更具有品牌特色和辨识度。

    4. 增强用户体验:合理使用渐变可以使界面更加舒适和美观,从而提升用户体验和用户满意度。


    这里我一般用的是这个渐变生成工具,可以比较方便的调出来需要的渐变色,支持生成多种渐变色+代码,并支持实时预览。


    image.png


    阴影


    同时,阴影在UI设计中也是不可或缺的部分,它有如下几个重要的意义:



    1. 层次感和深度感:阴影可以帮助界面元素之间建立层次感和深度感。通过添加阴影,设计师可以模拟光源的位置和界面元素之间的距离,使得用户能够更清晰地理解界面的结构。

    2. 突出重点:阴影可以用来突出重点,比如突出显示某个按钮或者卡片。适当的阴影可以使重要的元素脱颖而出,引导用户的注意力。

    3. 视觉吸引力:精心设计的阴影可以增加界面的美感和吸引力。合适的阴影可以使界面看起来更加立体和生动,从而提升用户的体验。

    4. 可视化元素状态:阴影还可以用来表达界面元素的状态,比如悬停或者按下状态。通过微调阴影的属性,可以使用户更清晰地感知到界面元素的交互状态。


    我一般用这个阴影生成工具,它同样也支持在线修改多个阴影及预览,同时支持复制代码。


    image.png


    字体图标


    想让我们的网页更生动,那怎么能少的了一个个可爱的 icon 呢,下面就是几个开源 icon 的网站。



    image.png



    image.png



    image.png



    image.png



    image.png


    图片素材


    除了 icon 之外,图片素材也是必不可少的,这里介绍我主要用的两个网站。


    第一个是花瓣网,这个网站可能找过素材的同学都不会陌生,上面确实有大量的素材供你选择。


    image.png


    另外一个是可画,它是一个图像编辑器,但是提供了大量的模版,我们也很轻松可以从中提取素材。


    image.png


    组件库


    最后要介绍的是组件库,组件库一来可以提供大量的基础组件,降低开发成本,而来也可以让我们站点的交互更加统一。以下是我常用的组件库:



    最后


    以上就是我独立开发项目时会思考以及参照的工具,如果你有一些其他想法,欢迎评论区交流。觉得有意思的话,点点关注点点赞吧~


    作者:可乐鸡翅kele
    来源:juejin.cn/post/7359854125912227894
    收起阅读 »

    也谈一下 30+ 程序员的出路

    前言 前两天和一个前端同学聊天,他说不准备再做前端了,准备去考公。不过难度也很大。 从 2015 2016 年那会儿开始互联网行业爆发,到现在有 7、8 年了,当年 20 多岁的小伙子们,现在也都 30+ 了 大量的人面临这个问题:大龄程序员就业竞争力差,未...
    继续阅读 »

    前言


    前两天和一个前端同学聊天,他说不准备再做前端了,准备去考公。不过难度也很大。


    3.png


    从 2015 2016 年那会儿开始互联网行业爆发,到现在有 7、8 年了,当年 20 多岁的小伙子们,现在也都 30+ 了


    大量的人面临这个问题:大龄程序员就业竞争力差,未来该如何安身立命?


    先说我个人的看法:



    • 除非你有其他更好的资源,否则没有更好的出路

    • 认真搞技术,保持技术能力,你大概率不会失业(至少外包还在招人,外包也不少挣...)


    考公之我见


    如果真的上岸了,极大概率不会失业,这是最大的优势。


    有优势肯定也有劣势,要考虑全面。凡事都符合能量守恒定律。


    你得到什么,你就得付出什么。或者你爸爸、爷爷提前付出为你过了,或者你儿子、孙子到最后为你买单。


    任何一个企业、单位,无论什么形式,无论效率高低,总是需要人干活的,甚至有很多脏活累活。


    你有依靠当然好。但你如果孤零零的进去,这些活你猜会是谁干?


    什么,努力就一定能有收获?—— 对,肯定有收货。但收件人不一定是谁。(也符合能量守恒定律)


    转岗,转什么?


    去干产品经理,那不跟程序员一样吗?只是不写代码了而已。文档,不一定就比代码好写。


    努力晋升转管理岗,那得看公司有没有坑。当下环境中,公司业务不增长的话,也不可能多出管理岗位。


    其他没啥可转的岗位了,总不能转岗做 HR 吧~ 木讷的程序员也干不了 HR 。


    副业,红利期早已过去


    做自媒体,做讲师,红利期早就过去了。我去年开始在某音上做小视频,到现在也就积累不到 2000 粉丝,播放量非常少。


    接外包,这得看你本事了。这不单单是一个技术活,你这是一个人干了一个公司所有角色的活:推广、需求、解决方案、开发、测试、部署、维护、升级…


    不过,虽然现在副业情况不好,但我还是建议大家,在业余时候多输出技术内容(博客、视频、开源等),看能否积累一些流量和粉丝。以后说不定副业情况会好起来,到时候你临时抱佛脚可来不及。


    回归二线城市


    相比于一线城市的互联网公司,二线城市对于年龄的容忍度更高一些。我认识很多 35-40 岁的人,在二线城市做开发工作也非常稳定。


    在二线城市最好能找一个传统行业的软件公司,如做医疗,财务,税务,制造业等软件产品的。这种软件的特点是,不要求有多么高精尖的技术,也不要求什么大数据、极致性能,它对业务流程和功能的依赖更多一些。你只要能尽快把业务功能熟悉起来(挺多专业知识,不是那么容易的),你在公司就基本稳定了,不用去卷技术。


    二线城市是非常适合安家定居的。房价便宜,生活节奏慢 —— 当然,工资也会相对低一些。


    另外,回归二线城市也不是说走就走的,你得提前准备、规划,把路铺好。


    总结


    当前互联网、软件行业,已经没有了前些年的增量,但依然有大量的存量,依然需要大量技术人员去维护当前的系统和功能。


    所以别总想着去转行(除非有其他好的资源),其他行业也不会留着好位子等着你。有那个精力多给自己充充电,有竞争力是不会失业的。只要互联网和软件行业还存在,就一直需要前端工作。


    作者:前端双越老师
    来源:juejin.cn/post/7287020579831267362
    收起阅读 »

    WSPA台灣分部在2024年第二季度以6億美元TvPv表現亮眼

    根據歐盟總部最新財務報表數據顯示,Wisdom Square Prosperous Ark Fintech (WSPA)台灣分部在2024年第二季度(Q2)創下驚人的6億美元交易量收益率(TvPv)。這一卓越的表現獲得了歐盟高層的高度認可,並在最近召開的股東會...
    继续阅读 »

    根據歐盟總部最新財務報表數據顯示,Wisdom Square Prosperous Ark Fintech (WSPA)台灣分部在2024年第二季度(Q2)創下驚人的6億美元交易量收益率(TvPv)。這一卓越的表現獲得了歐盟高層的高度認可,並在最近召開的股東會上宣布,將釋出25個策略案名額,供台灣分部社群用戶使用。為了表彰台灣分部在今年的傑出表現,這25個策略案被統一命名為「QCA藍圖策略案」。這不僅是對台灣分部成績的讚揚,也是對其在歐盟WSPA集團中突出貢獻的一種榮譽表彰。這一特別命名顯示了歐盟對台灣分部的高度重視以及其在金融領域中的卓越表現。

    這25個名額將通過線上或線下預約方式提供,這是一次極為珍貴的機會。參與者將有機會獲得獨特的策略案和專業指導,從中學習最前沿的財務戰略和技術支持。WSPA集團希望通過這次機會,讓台灣分部的社群用戶受益於最新的財務戰略和技術支持,進一步提升他們的競爭力和市場影響力。此次釋出的「QCA藍圖策略案」不僅是對台灣分部過去成績的肯定,更是WSPA集團對其未來發展的期許。這些策略案將為台灣分部社群用戶提供獨特的財務戰略洞見和專業支持,幫助他們在全球金融市場中持續保持競爭優勢。

    收起阅读 »

    【技巧】JS代码这么写,前端小姐姐都会爱上你

    web
    前言 🍊缘由 JS代码小技巧,教你如何守株待妹 🍍你想听的故事: 顶着『前端小王子』的称号,却无法施展自己的才能。 想当年本狗赤手空拳打入前端阵地,就是想通过技术的制高点来带动前端妹子。奈何时不待我,前端妹子成了稀有资源,只剩下抠脚大汉前端大叔。 秉承没有妹...
    继续阅读 »

    前言


    🍊缘由


    JS代码小技巧,教你如何守株待妹



    🍍你想听的故事:


    顶着『前端小王子』的称号,却无法施展自己的才能


    想当年本狗赤手空拳打入前端阵地,就是想通过技术的制高点来带动前端妹子。奈何时不待我,前端妹子成了稀有资源,只剩下抠脚大汉前端大叔。


    秉承没有妹子也得继续学习的态度,本狗将实际代码编写中JS使用技巧总结。分享给小伙伴们,希望这些姿势知识 能够成为吸引妹子的引路石。


    正文


    一.JS解构赋值妙用


    1.采用短路语法防止报错



    解构时加入短路语法兜底,防止解构对象如果为 undefined 、null 时,会报错



    const user = null;
    // 短路语法,如果user为undefined 、null则以{}作为解构对象
    const {name, age, sex} = user || {};

    举例🌰


    通过接口获取用户user对象,解构对象信息


    ❌错误示例


    未使用短路语法兜底,不严谨写法


    // 模拟后端接口返回user为null时
    const user = null;
    const {name, age, sex} = user;
    console.log("用户信息name=", name, "age=", age, "sex=", sex);

    // 控制台直接报错
    // Cannot destructure property 'name' of 'user' as it is null.


    ✅正确示例


    使用短路语法兜底,严谨写法


    // 模拟后端接口返回user为null时
    const user = null;
    // 加入短路语法,意思为如果user为空则以{}作为解构对象
    const {name, age, sex} = user || {};
    console.log("用户信息name=", name, "age=", age, "sex=", sex);

    // 控制台打印
    // 用户信息name= undefined age= undefined sex= undefined


    2.深度解构



    解构赋值可以深度解构:嵌套的对象也可以通过解构进行赋值



    举例🌰


    通过模拟接口获取用户user对象,解构user对象中联系人concat信息


    // 深度解构
    const user = {
    name:'波',
    age:'18',
    // 联系人
    concat: {
    concatName:'霸',
    concatAge:'20',
    },
    };
    const {concat: {concatName, concatAge}} = user || {};
    console.log("用户联系人concatName=", concatName, "concatAge=", concatAge);

    // 控制台打印
    // 用户联系人concatName= 霸 concatAge= 20


    3.解构时赋值默认值



    解构赋值时可以采取默认值填充



    举例🌰


    通过模拟接口获取用户user对象,解构user对象时,没有dept科室字段时,可以加入默认值


    // 解构时设置默认值
    const user = {
    name:'波',
    age:'18',
    };
    const {name, age, dept = '信息科'} = user || {};
    console.log("用户信息name=", name, "age=", age, "dept=", dept);

    // 控制台打印
    // 用户信息name= 波 age= 18 dept= 信息科




    二.数组小技巧


    1.按条件向数组添加数据



    根据条件向数组中添加数据



    举例🌰


    设置一个路径白名单数组列表,当是开发环境添加部分白名单路径,若生产环境则不需要添加



    // 不是生产环境
    const isEnvProduction = false;

    // 基础白名单路径
    const baseUrl = [
    '/login',
    '/register'
    ]

    // 开发环境白名单路径
    const devUrl = [
    '/test',
    '/demo'
    ]
    // 如果是生产环境则不添加开发白名单
    const whiteList = [...baseUrl, ...(isEnvProduction? [] : devUrl)];

    console.table(whiteList)


    // 控制台打印
    // Array(4) ["/login", "/register", "/test", "/demo"]


    // 是生产环境
    const isEnvProduction = true;

    // 基础白名单路径
    const baseUrl = [
    '/login',
    '/register'
    ]

    // 开发环境白名单路径
    const devUrl = [
    '/test',
    '/demo'
    ]
    // 如果是生产环境则不添加开发白名单
    const whiteList = [...baseUrl, ...(isEnvProduction? [] : devUrl)];

    console.table(whiteList)
    // 控制台打印
    // Array(2) ["/login", "/register"]


    2.获取数组最后一个元素



    给到一个数组,然后访问最后一个元素



    举例🌰


    获取一个数组中最后一个值


    const arr = [1, 2, 3, 4];
    // 通过slice(-1) 获取只包含最后一个元素的数组,通过解构获取值
    const [last] = arr.slice(-1) || {};
    console.log('last=',last)

    // 控制台打印
    // last= 4


    3.使用 includes 优化 if



    灵活使用数组中方法includes可以对if-else进行优化



    举例🌰


    如果条件a值是 1,2,3时,打印有个男孩叫小帅


    一般写法


    const a = 1;

    // 基本写法
    if(a==1 || a==2 || a==3){
    console.log('基本写法:有个男孩叫小帅');
    }

    // 优化写法
    if([1, 2, 3].includes(a)){
    console.log('优化写法:有个男孩叫小帅');
    }

    // 控制台打印
    // 基本写法:有个男孩叫小帅
    // 优化写法:有个男孩叫小帅





    三.JS常用功能片段


    1.通过URL解析搜索参数



    通过页面URL获取解析挂参参数,适用于当前页面需要使用到URL参数时解析使用




    // 通过URL解析搜索参数

    const getQueryParamByName = (key) => {
    const query = new URLSearchParams(location.search)
    return decodeURIComponent(query.get(key))
    }

    const url = "http://javadog.net?user=javadog&age=31"

    // 模拟浏览器参数(此处是模拟浏览器参数!!!)
    const location = {
    search: '?user=javadog&age=31'
    }

    console.log('狗哥名称:', getQueryParamByName('user'));
    console.log('狗哥年龄:', getQueryParamByName('age'));

    // 控制台打印
    // 狗哥名称: javadog
    // 狗哥年龄: 31


    2.页面滚动回到顶部



    页面浏览到某处,点击返回顶部



    // 页面滚动回到顶部
    const scrollTop = () => {
    // 该函数用于获取当前网页滚动条垂直方向的滚动距离
    const range = document.documentElement.scrollTop || document.body.scrollTop
    // 如果大于0
    if (range > 0) {
    // 该函数用于实现页面的平滑滚动效果
    window.requestAnimationFrame(scrollTop)
    window.scrollTo(0, range - range / 8)
    }
    }



    3.获取页面滚动距离



    获取页面滚动距离,根据滚动需求处理业务



    // 该函数用于获取当前页面滚动的位置,可选参数target默认为window对象
    const getPageScrollPosition = (target = window) => ({
    // 函数返回一个包含x和y属性的对象,分别表示页面在水平和垂直方向上的滚动位置。函数内部通过判断target对象是否具有pageXOffset和pageYOffset属性来确定滚动位置的获取方式,如果存在则使用该属性值,否则使用scrollLeft和scrollTop属性。
    x: target.pageXOffset !== undefined ? target.pageXOffset : target.scrollLeft,
    y: target.pageYOffset !== undefined ? target.pageYOffset : target.scrollTop,
    })

    getPageScrollPosition()



    总结


    这篇文章主要介绍了JavaScript编程中的几个实用技巧,包括解构赋值的妙用、数组操作以及一些常用的JS功能片段,总结如下:


    解构赋值妙用



    • 短路语法防止报错:在解构可能为undefined或null的对象时,使用短路语法(|| {})来避免错误。

    • 深度解构:可以解构嵌套的对象,方便地获取深层属性。

    • 解构时赋值默认值:在解构时可以为未定义的属性提供默认值。


    数组小技巧



    • 按条件向数组添加数据:根据条件动态地决定是否向数组添加特定元素。

    • 获取数组最后一个元素:使用slice(-1)获取数组的最后一个元素。

    • 使用includes优化if语句:用includes检查元素是否在数组中,简化条件判断。


    JS常用功能片段



    • 通过URL解析搜索参数:创建函数解析URL的查询参数,便于获取URL中的参数值。

    • 页面滚动回到顶部:实现页面平滑滚动回顶部的函数。

    • 获取页面滚动距离:获取页面滚动位置的函数,可用于处理滚动相关的业务逻辑。


    🍈猜你想问


    如何与狗哥联系进行探讨


    关注公众号【JavaDog程序狗】

    公众号回复【入群】或者【加入】,便可成为【程序员学习交流摸鱼群】的一员,问题随便问,牛逼随便吹,目前群内已有超过200+个小伙伴啦!!!


    2.踩踩狗哥博客

    javadog.net



    大家可以在里面留言,随意发挥,有问必答






    🍯猜你喜欢


    文章推荐


    【工具】珍藏免费宝藏工具,不好用你来捶我


    【插件】IDEA这款插件,爱到无法自拔


    【规范】看看人家Git提交描述,那叫一个规矩


    【工具】用nvm管理nodejs版本切换,真香!


    【项目实战】SpringBoot+uniapp+uview2打造H5+小程序+APP入门学习的聊天小项目


    【项目实战】SpringBoot+uniapp+uview2打造一个企业黑红名单吐槽小程序


    【模块分层】还不会SpringBoot项目模块分层?来这手把手教你!


    【ChatGPT】SpringBoot+uniapp+uview2对接OpenAI,带你开发玩转ChatGPT



    作者:JavaDog程序狗
    来源:juejin.cn/post/7376532114105663539
    收起阅读 »

    我是DB搬运工,我哪会排查问题。。。

    今天说说如何排查线上问题,首先声明如果想看什么cpu优化。jvm优化的,我这不适合,我这属于广大底层人士的,纯纯的CRUD,没那么多的性能优化; 开干 报错信息的问题 首先说一个报错信息的问题:对于线上显而易见的界面提示错误,我们要完全避免不要将后台的报错打到...
    继续阅读 »

    今天说说如何排查线上问题,首先声明如果想看什么cpu优化。jvm优化的,我这不适合,我这属于广大底层人士的,纯纯的CRUD,没那么多的性能优化;


    开干


    报错信息的问题


    首先说一个报错信息的问题:对于线上显而易见的界面提示错误,我们要完全避免不要将后台的报错打到前台界面上来,不要将后台的报错打到前台界面上来,不要将后台的报错打到前台界面上来,重要的说三遍,我看到很多线上生产系统报出java报错信息和php报错信息了;外人来看可能看不懂,觉得炫酷,内行人看简直了,垮diao;类似于我找的这个网图


    image.png


    如何排查问题


    再说下我们开发人员前后端都写的情况下如何排查问题,对于前后端都开发的人员其实避免了很多扯皮的事情,也少了很多沟通的问题,如果我们环境点击报错,我们可以



    1. 打开浏览器的f12查看该请求的地址

    2. 按该地址找到后台对应的接口地址,启动本地,打上断点

    3. 如果没有走进后台断点处那么存在三个问题,一个是contentType或者请求方式两者没有保持一致,这个一般开发自测的时候就可以测出来,另一个就是你的地址可能中间环节有路由,路由有问题一般对于大部分功能都有影响,不会是小范围的,还有一种就是我们的后台有拦截器但是我们不熟悉这块,一般大家接手项目的时候估计只会扫一眼这块,恰好这块对于某些业务权限卡的很的项目来说会经常发生这种事,而你恰好不熟悉所以你排查半天也不会有头绪;

    4. 进入断点以后,我们按流程往下执行就能找到报错的地方了

    5. 如果你日志打的详细而且也可以轻松获取生产的日志,那就在日志中就可以找到我们报错的信息;

    6. 如果你是传回前台后报错,那么我们需要在浏览器上打断点,然后去定位是不是咱们传的参数和前台解析的参数属性不一致还是一些其他的问题,以上就形成了闭环;


    如果我们是只写后端,分离项目的那种,那咱们就是加强沟通,和气生财,一切问题出在我后端,前端都是完美的,来问题了你先排查起来,确定没问题了,再去告诉项目大哥,让前端兄弟排查一下,有些新手可能会问为什么不让前端先排查,这个其实不该问,只要是前后端分离的,业务层其实都是摆在后端的,而问题大部分是出在业务上的,所以后端干就完了;


    image.png
    如果我们使用了一些中间件,要没事带关注这些玩意,有时候大家共用的Redis,你不知道别人怎么操作,然后Redis崩了,你能怎么办,如果你是业务前置部门,虽然与你无瓜,但客户的感知就是你报错了,别人躲在后面到不了那一步,所以你得去各方联系重启机器;


    ABUIABACGAAg9b-EhwYo0omnkwUwkAM4kAM.jpg


    项目执行过程真的报oom了呢,那你必须去生产环境捞日志,找到位置,看看机器配置,看看项目执行占用资源情况,纯小白方式直接top命令查看,资源的确给的少了,那么我们启动的时候调整下jvm参数,把它调大,如果是代码执行循环导致的,那么我们就得优化代码,如果是执行任务之类的,比如给个无界队列,那么队列也会把数据撑爆,这时候我们也需要调整业务逻辑,(**记住,队列撑爆内存千万别直接把队列弄成有界的,一定要去沟通怎么优化,得到认可才能干,我们开发对于业务场景是没有产品经理清晰的**)这种挤爆jvm的不是那么多见,但的确很长见识的;


    部署打包


    排查完、修改完我们就要打包了,其实我特别不建议本地打包那种方式(应该禁止),万一哪个卧龙本地打包后认为活结束了然后忘了提交,然后他离职了然后电脑重置然后over;不管有意无意,环节得控制好我在第一篇就说了,避免后期维护压力,要控制好每一个环节,其实很简单,代码上传git或者svn,用jenkins来打,Jenkins还会记录每一次的打包时间,然后下载发给生产,我觉得比本地打包优秀多了;


    jenkins.jpg


    还有就是我们上生产的配置文件尽量读取服务器上的配置,不要和打包一起,你的项目可能部署在很多地方用,单独的配置避免了频繁的找文件,如果需要直接生产copy一份然后修改再上传,


    ok!完成


    四、总结



    我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!



    作者:小红帽的大灰狼
    来源:juejin.cn/post/7374380071531216934
    收起阅读 »

    一次偶然提问引发的惊喜体验

    大家有没有一种感受,虽然我们在一个满是信息的时代,却往往在寻求精确答案时感到迷茫,因为五花八门的说法太多了。然而,最近在一个网站上的一个随意提问,却让我见证了知识共享的魅力,体验到了前所未有的惊喜——解答的非常快速,也确实解决了我的问题。那天,出于好奇也是有点...
    继续阅读 »

    大家有没有一种感受,虽然我们在一个满是信息的时代,却往往在寻求精确答案时感到迷茫因为五花八门的说法太多了。然而,最近在一个网站上的一个随意提问,却让我见证了知识共享的魅力,体验到了前所未有的惊喜——解答的非常快速,也确实解决了我的问题。

    那天,出于好奇也是有点着急需要解答一个写代码中的问题,我在一个网站上键入了心中的疑惑原因是我在知乎上看到说这个是一个专业的IT一站式学习服务平台,本以为就写一写就算了,没想到,短短几分钟内,就有人来解答了而且回答还挺精准,让我一下恍然大悟。而且里面的AI回复也挺智能,挺有意思的,之后,我就认真逛了一下这个网站,他里面是专门一个帮助专栏的,就和我们发朋友圈一样的感觉,但是是单独拎出来的一个板块,我看大家在里面的提问都有人或者官方去回复的,也可能是因为这是一个新站点,也是IT的一个垂直领域,东西没那么杂,里面的人也都是和IT相关的,所以才能比较快得到答案。对了,这个网站叫云端源想,百度直接搜索就可以找到的,编程过程中需要寻求帮助的小伙伴可以去看看哈。

    收起阅读 »

    uniApp新模式: 使用Vue3 + Vite4 + Pinia + Axios技术栈构建

    背景 使用Vue3 + Vite4 + Pinia + Axios + Vscode模式开发之后,感叹真香!不用再单独去下载HBuilderX。废话不多说,直接上干货! 版本号 node: v16.18.0 vue: ^3.3.4, vite: 4.1.4 ...
    继续阅读 »

    背景


    使用Vue3 + Vite4 + Pinia + Axios + Vscode模式开发之后,感叹真香!不用再单独去下载HBuilderX。废话不多说,直接上干货!


    版本号



    • node: v16.18.0

    • vue: ^3.3.4,

    • vite: 4.1.4

    • sass: ^1.62.1

    • pinia: 2.0.36

    • pinia-plugin-unistorage: ^0.0.17

    • axios: ^1.4.0

    • axios-miniprogram-adapter: ^0.3.5

    • unplugin-auto-import: ^0.16.4


    如遇到问题,请检查版本号是否一致!!!


    项目目录结构


    └── src # 主目录
    ├── api # 存放所有api接口文件
    │ ├── user.js # 用户接口
    ├── config # 配置文件
    │ ├── net.config.js # axios请求配置
    ├── pinia-store # 配置文件
    │ ├── user.js # axios请求配置
    ├── utils # 工具类文件
    │ ├── request.js # axios请求封装


    开发流程


    建议去uni-preset-vue仓库下载vite分支zip包,熟练ts的童鞋下载vite-ts


    安装



    • 下载之后进入项目


    cd uni-preset-vue


    • 安装依赖


    # pnpm
    pnpm install
    # yarn
    yarn
    # npm
    npm i

    运行


    pnpm dev:mp-weixin

    打开微信开发者工具,找到dist/dev/mp-weixin运行,可以看到默认的页面


    安装pinia


    pnpm add pinia 

    使用pinia


    src目录下构建 pinia-store/user.js文件


    /**
    * @description 用户信息数据持久化
    */

    import { defineStore } from 'pinia'

    export const useUserStore = defineStore('user', {
    state() {
    return {
    userInfo: {}
    }
    },
    actions: {
    setUserInfo(data) {
    this.userInfo = data
    }
    }
    })


    • 修改main.js文件


    import {
    createSSRApp
    } from "vue";
    import * as Pinia from 'pinia';
    import App from "./App.vue";
    export function createApp() {
    const app = createSSRApp(App);
    const store = Pinia.createPinia();
    app.use(store);

    return {
    app,
    Pinia
    };
    }

    pinia数据持久化


    安装pinia-plugin-unistorage


    pnpm add pinia-plugin-unistorage

    修改main.js文件,增加如下代码:


    // pinia数据持久化
    import { createUnistorage } from 'pinia-plugin-unistorage'
    store.use(createUnistorage());
    app.use(store);

    完整代码如下:


    import { createSSRApp } from "vue";

    import * as Pinia from 'pinia';
    // pinia数据持久化
    import { createUnistorage } from 'pinia-plugin-unistorage'
    import App from "./App.vue";
    export function createApp() {
    const app = createSSRApp(App);

    const store = Pinia.createPinia();
    store.use(createUnistorage());
    app.use(store);

    return {
    app,
    Pinia
    };
    }


    在页面中使用:


    <script setup>
    import { useUserStore } from '@/pinia/user.js'
    const user = useUserStore()

    // 设置用户信息
    const data = { userName: 'snail' }
    user.setUser(data)
    // 打印用户信息
    console.log(user.userInfo)
    </script>

    安装axios


    pnpm add axios

    适配小程序,需要另外安装axios-miniprogram-adapter插件


    pnpm add axios-miniprogram-adapter

    使用axios


    utils创建utils/request.js文件


    import axios from 'axios';
    import mpAdapter from "axios-miniprogram-adapter";
    axios.defaults.adapter = mpAdapter;
    import { netConfig } from '@/config/net.config';
    const { baseURL, contentType, requestTimeout, successCode } = netConfig;

    let tokenLose = true;

    const instance = axios.create({
    baseURL,
    timeout: requestTimeout,
    headers: {
    'Content-Type': contentType,
    },
    });

    // request interceptor
    instance.interceptors.request.use(
    (config) => {
    // do something before request is sent
    return config;
    },
    (error) => {
    // do something with request error
    return Promise.reject(error);
    }
    );

    // response interceptor
    instance.interceptors.response.use(
    /**
    * If you want to get http information such as headers or status
    * Please return response => response
    */

    (response) => {
    const res = response.data;

    // 请求出错处理
    // -1 超时、token过期或者没有获得授权
    if (res.status === -1 && tokenLose) {
    tokenLose = false;
    uni.showToast({
    title: '服务器异常',
    duration: 2000
    });

    return Promise.reject(res);
    }
    if (successCode.indexOf(res.status) !== -1) {
    return Promise.reject(res);
    }
    return res;
    },
    (error) => {
    return Promise.reject(error);
    }
    );

    export default instance;


    其中net.config.js文件需要在src/config目录下创建,完整代码如下:


    /**
    * @description 配置axios请求基础信息
    * @author hu-snail 1217437592@qq.com
    */

    export const netConfig = {
    // axios 基础url地址
    baseURL: 'https://xxx.cn/api',
    // 为开发服务器配置 CORS。默认启用并允许任何源,传递一个 选项对象 来调整行为或设为 false 表示禁用
    cors: true,
    // 根据后端定义配置
    contentType: 'application/json;charset=UTF-8',
    //消息框消失时间
    messageDuration: 3000,
    //最长请求时间
    requestTimeout: 30000,
    //操作正常code,支持String、Array、int多种类型
    successCode: [200, 0],
    //登录失效code
    invalidCode: -1,
    //无权限code
    noPermissionCode: -1,
    };

    src目录下创建src/api/user.jsapi文件


    import request from '@/utils/request'

    /**
    * @description 授权登录
    * @param {*} data
    */

    export function wxLogin(data) {
    return request({
    url: '/wx/code2Session',
    method: 'post',
    params: {},
    data
    })
    }

    /**
    * @description 获取手机号
    * @param {*} data
    */

    export function getPhoneNumber(data) {
    return request({
    url: '/wx/getPhoneNumber',
    method: 'post',
    params: {},
    data
    })
    }


    在页面中使用


    <script setup>
    import { wxLogin, getPhoneNumber } from '@/api/user.js'
    /**
    * @description 微信登录
    */

    const onWxLogin = async () => {
    uni.login({
    provider: 'weixin',
    success: loginRes => {
    state.wxInfo = loginRes
    const jsCode = loginRes.code
    wxLogin({jsCode}).then((res) => {
    const { openId } = res.data
    user.setUserInfo({ openId })
    })
    }
    })
    }

    </script>

    配置vue自动导入


    安装unplugin-auto-import插件


    pnpm add unplugin-auto-import -D

    修改vite.config.js文件:


    import AutoImport from 'unplugin-auto-import/vite'
    plugins: [
    AutoImport({
    imports: ["vue"]
    })
    ],

    页面中使用,需要注意的事每次导入新的vue指令,需要重新运行!!


    <script setup>
    onBeforeMount(() => {
    console.log('----onBeforeMount---')
    })
    </script>

    安装uni-ui


    pnpm add @dcloudio/uni-ui

    使用uni-ui


    修改pages.json文件,增加如下代码:


    "easycom": {
    "autoscan": true,
    "custom": {
    "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
    }
    },

    在页面中使用


    <template>
    <uni-icons type="bars" size="16"></uni-icons>
    </template>

    到此已基本可以完成程序的开发,其他功能按照自己的需求做增删改查即可!


    作者:蜗牛前端
    来源:juejin.cn/post/7244192313844154424
    收起阅读 »

    解决vite项目首次打开页面卡顿的问题

    web
    问题描述 在vite项目中我们可能会遇到这样一种情况。 在我们本地开发,第一次进入页面的时候,页面会卡顿很长时间。越是复杂的卡顿时间越久。 要是我们一天只专注于一两个页面 那这个就不是问题。 但有的时候我们要开发一个流程性的东西,要进入各种各样的页面查看。这样...
    继续阅读 »

    问题描述


    在vite项目中我们可能会遇到这样一种情况。


    在我们本地开发,第一次进入页面的时候,页面会卡顿很长时间。越是复杂的卡顿时间越久。


    要是我们一天只专注于一两个页面 那这个就不是问题。


    但有的时候我们要开发一个流程性的东西,要进入各种各样的页面查看。这样就很痛苦了。


    问题原因


    为什么会出现这种情况呢?因为路由的懒加载与vite的编译机制。


    路由的懒加载:没有进入过的页面不加载


    vite的编译机制:没有加载的不编译。


    这样就会出现 我们在进入一个新页面的时候他才会编译。我们感觉卡顿的过程就是他编译的过程。


    解决思路


    问题找到了,那么解决起来就简单了。我们本地开发的时候,取消路由的懒加载就可以了。


    const routes = [
    {
    path: `/home`,
    name: `Home`,
    component: () => import(`@/views/home/HomePage.vue`),
    meta: { title: `首页` },
    },
    {
    path: `/test1`,
    name: `test1`,
    component: () => import(`@/views/demo/Test1.vue`),
    meta: { title: `测试1` },
    },
    {
    path: `/test2`,
    name: `test2`,
    component: () => import(`@/views/demo/Test2.vue`),
    meta: { title: `测试2` },
    }
    ]

    if (import.meta.env.MODE === `development`) {
    routes.forEach(item => item.component())
    }

    示例代码如上。上述的问题是解决了,但是又产生了新的问题。项目太大的时候启动会非常慢。


    于是我想了一个折中的方案。初始打开项目的时候路由还是懒加载的,然后我在浏览器网络空闲的时候去加载资源。这样你首次进系统打开的第一个页面可能还是需要等待,但是之后的所有页面就不需要等待了。


    那么问题又来了?怎么监听浏览器的网络空闲呢?这就要用的浏览器的一个api PerformanceObserver。这个api可能很多小伙伴都不知道,它主要是帮助你监控和观察与性能相关的各种指标。想要详细了解的可以点击这里查看


    我们今天用的就是resource类型监听所有的网络请求,代码示例如下


     const observer: PerformanceObserver = new PerformanceObserver((list: PerformanceObserverEntryList) => {
    const entries: PerformanceEntryList = list.getEntries()
    for (const entry of entries) {
    if (entry.entryType === `resource`) {
    //网络请求结束
    }
    }
    })
    observer.observe({ entryTypes: [`resource`] })

    监听到网络请求后,我们怎么判断是否空闲呢?也很简单,只要一秒钟以内没有新的网络请求出现我们就认为当前网络是空闲的。这不就防抖函数嘛。


    const routes = [
    {
    path: `/home`,
    name: `Home`,
    component: () => import(`@/views/home/HomePage.vue`),
    meta: { title: `首页` },
    },
    {
    path: `/test1`,
    name: `test1`,
    component: () => import(`@/views/demo/Test1.vue`),
    meta: { title: `测试1` },
    },
    {
    path: `/test2`,
    name: `test2`,
    component: () => import(`@/views/demo/Test2.vue`),
    meta: { title: `测试2` },
    }
    ]

    if (import.meta.env.MODE === `development`) {
    const componentsToLoad = routes.map(item => item.component)
    const loadComponentsWhenNetworkIdle = debounce(
    () => {
    if (componentsToLoad.length > 0) {
    const componentLoader = componentsToLoad.pop()
    componentLoader && componentLoader()
    // eslint-disable-next-line
    console.log(`剩余${componentsToLoad.length}个路由未加载`, componentsToLoad)
    }
    },
    1000,
    false
    )

    const observer: PerformanceObserver = new PerformanceObserver((list: PerformanceObserverEntryList) => {
    const entries: PerformanceEntryList = list.getEntries()
    for (const entry of entries) {
    if (entry.entryType === `resource`) {
    loadComponentsWhenNetworkIdle()
    }
    }
    })
    observer.observe({ entryTypes: [`resource`] })
    }

    完整的代码如上。当我们判断出网络空闲后,就从componentsToLoad数组中删除一个组件,并加载删除的这个组件,然后就会重新触发网络请求。一直重复这个流程,直到componentsToLoad数组为空。


    这只是个示例的代码,获取componentsToLoad变量防抖函数的配置(初始化不执行,无操作后1秒钟后执行)还要根据你的实际项目进行修改!


    可优化项


    以上方法确实是按照我们的预期实现了,但是还有一些小小的问题。例如:



    1. 我们在加载组件的时候如果恰好是当前打开的页面,是不会重新触发网络请求的。因此可能会断掉componentsToLoad数组的删除,加载组件,触发网络请求这个流程。不过问题不大,你在当前页面如果有操作重新触发网络请求了,这个流程还会继续走下去,直到componentsToLoad数组为空。

    2. 每次刷新页面componentsToLoad数组都是会重新获取到值的,也就是我们走过的流程会重新走。不过问题不大,第二次走都是走缓存了,执行速度很快,而且也是本地开发那点性能损坏可以忽略不计。


    这些问题影响都不是很大,所以我就没继续做优化。有兴趣的小伙伴可以继续研究下去。


    作者:热心市民王某
    来源:juejin.cn/post/7280745727160811579
    收起阅读 »

    有了这玩意,分分钟开发公众号功能!

    大家好,我是程序员鱼皮。 不论在企业、毕设还是个人练手项目中,很多同学或多或少都会涉及微信相关生态的开发,例如微信支付、开放平台、公众号等等。 一般情况下,我们需要到官网查阅这些模块对应的 API 接口,自己编写各种对接微信服务器的代码,结果很多时间都花在了看...
    继续阅读 »

    大家好,我是程序员鱼皮。


    不论在企业、毕设还是个人练手项目中,很多同学或多或少都会涉及微信相关生态的开发,例如微信支付、开放平台、公众号等等。


    一般情况下,我们需要到官网查阅这些模块对应的 API 接口,自己编写各种对接微信服务器的代码,结果很多时间都花在了看文档和理解流程上。


    好在,某位大佬开源了一个 WxJava 库,它可以让我们更高效快速地开发微信相关的功能。


    什么是 WxJava?


    WxJava 是一个开箱即用的 SDK,封装了微信生态后端开发绝大部分的 API 接口为现成的方法,包括微信支付、开放平台、小程序、企业微信、公众号等。我们开发时直接调用这个 SDK 提供的方法即可,同时作者针对这个 SDK 还提供了很多接入的 Demo,大部分场景跟着 demo 就能很快上手,非常高效!不需要深入阅读微信开发者官方文档,也能学会微信开发。


    WxJava 开发 Demo


    这个项目在 GitHub 上 已经有 29.1k 的 star ,社区活跃,且在持续维护更新中。



    下面我会通过一个实战案例《公众号的菜单管理功能》,带大家入门 WxJava。


    公众号的菜单管理开发实战


    1、功能介绍


    正常情况下,公众号的管理员可以在公众号网页后台来编辑菜单,例如下面这个页面:



    上图中,我在菜单栏分别添加了三个按钮:主菜单一、点击事件、主菜单三。


    用户点击 主菜单一 后,就会打开我们设置的跳转网页地址。



    上图的 url 仅为演示,实际仅能填写跟公众号相关的网址。



    用户点击 点击事件 后,就会自动回复一条消息:您点击了菜单。



    你可能会好奇了:公众号网页后台都自带了菜单管理能力,我们还开发什么?


    举个例子,如果我们希望用户点了菜单后,调用我们的后端完成新用户注册,就必须要自定义菜单了,因为需要对接我们自己的后端服务器。


    而一旦你在后台配置了自己的服务器,就无法使用公众号自带的网页后台来管理菜单和自动回复了,如图:



    这种情况下,就只能完全自己在后端写代码来实现这些功能。


    2、开发实战


    接下来我们用 WxJava 提供的 SDK,通过代码来实现上述同样的功能。


    首先,我们需要在 maven 中引入 sdk:


    <dependency>
      <groupId>com.github.binarywang</groupId>
      <artifactId>wx-java-mp-spring-boot-starter</artifactId>
      <version>4.4.0</version>
    </dependency>

    然后在配置文件中添加公众号的 appId 和 appSecret 配置:



    按照 WxJava 的规则,编写一个配置类,构建 WxMpService 的 Bean 实例,注入到 Spring 容器中。



    上图中的 WxMpService 就是 WxJava 提供的操作微信公众号相关服务的工具类。


    接下来,就可以直接创建菜单啦!示例代码如下图:




    再次备注:对应 url 内容填写仅为演示,实际 url 对应的网址必须是当前公众号的内容



    执行上述代码,其实就可以配置菜单了,你甚至感受不到跟微信服务器 “打交道” 的流程。


    这里再简单介绍下菜单二的点击事件,如上面演示,点击 点击事件 公众号会自动回复:“您点击了菜单”。


    这个动作被定义为一个叫 CLICK_MENU_KEY 的 key,当用户点击这个按钮后,公众号就会向我们部署的后端服务发送这个事件 key,根据 key 的内容可以执行不同的动作,例如上面说的回复一段文字。


    我们仅需把这个 key 绑定到路由上,当触发这个事件就调用对应的 handler 即可,典型的事件驱动设计~



    EventHandler 的动作就是返回 “您点击了菜单” 这段文字:



    3、其他功能演示


    再举例个小功能,如果我们要删除菜单怎么办呢?


    非常简单,可以先调用获取菜单的方法:


    WxMenu wxMenu = wxMpService.getMenuService().menuGet();

    然后根据菜单 ID 就可以调用删除方法来删除菜单:


    wxMpService.getMenuService().menuDelete(menuId);

    如果要修改菜单,可以再次调用 menuCreate 直接覆盖即可。


    最后


    利用 WxJava 我们已经实现了菜单的管理,可以看到接口定义非常清晰,使用起来也很方便。当然,以上只是个 Demo,实际企业中如果要操作公众号菜单,不可能每次都是手动执行代码,而是会有一个对应的公众号管理前端,或者再省点事,直接用接口文档来调用操作菜单的接口。感兴趣的同学可以自己实现~


    总之希望大家通过这篇教程能够明白,微信相关的开发,并没有那么难,多去做一些调研、多主动搜索一些方案,你会发现很多路前人已经帮你打通了!


    可访问我的 Github:github.com/liyupi ,了解更多技术和项目内容。


    作者:程序员鱼皮
    来源:juejin.cn/post/7368319486779375642
    收起阅读 »

    为什么list.sort()比Stream().sorted()更快?

    昨天写了一篇文章《小细节,大问题。分享一次代码优化的过程》,里面提到了list.sort()和list.strem().sorted()排序的差异。 说到list sort()排序比stream().sorted()排序性能更好。 但没说到为什么。 有朋友也...
    继续阅读 »

    昨天写了一篇文章《小细节,大问题。分享一次代码优化的过程》,里面提到了list.sort()和list.strem().sorted()排序的差异。

    说到list sort()排序比stream().sorted()排序性能更好。

    但没说到为什么。


    企业微信截图_16909362105085.png


    有朋友也提到了这一点。


    本文重新开始,先问是不是,再问为什么。




    真的更好吗?




    先简单写个demo


    List userList = new ArrayList<>();
    Random rand = new Random();
    for (int i = 0; i < 10000 ; i++) {
    userList.add(rand.nextInt(1000));
    }
    List userList2 = new ArrayList<>();
    userList2.addAll(userList);

    Long startTime1 = System.currentTimeMillis();
    userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
    System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");

    Long startTime = System.currentTimeMillis();
    userList.sort(Comparator.comparing(Integer::intValue));
    System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");

    输出


    stream.sort耗时:62ms
    List.sort()耗时:7ms

    由此可见list原生排序性能更好。

    能证明吗?

    证据错了。




    再把demo变换一下,先输出stream.sort


    List userList = new ArrayList<>();
    Random rand = new Random();
    for (int i = 0; i < 10000 ; i++) {
    userList.add(rand.nextInt(1000));
    }
    List userList2 = new ArrayList<>();
    userList2.addAll(userList);

    Long startTime = System.currentTimeMillis();
    userList.sort(Comparator.comparing(Integer::intValue));
    System.out.println("List.sort()耗时:"+(System.currentTimeMillis()-startTime)+"ms");

    Long startTime1 = System.currentTimeMillis();
    userList2.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
    System.out.println("stream.sort耗时:"+(System.currentTimeMillis() - startTime1)+"ms");

    此时输出变成了


    List.sort()耗时:68ms
    stream.sort耗时:13ms

    这能证明上面的结论错误了吗?

    都不能。

    两种方式都不能证明什么。


    使用这种方式在很多场景下是不够的,某些场景下,JVM会对代码进行JIT编译和内联优化。


    Long startTime = System.currentTimeMillis();
    ...
    System.currentTimeMillis() - startTime

    此时,代码优化前后执行的结果就会非常大。


    基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

    基准测试使得被测试代码获得足够预热,让被测试代码得到充分的JIT编译和优化。




    下面是通过JMH做一下基准测试,分别测试集合大小在100,10000,100000时两种排序方式的性能差异。


    import org.openjdk.jmh.annotations.*;
    import org.openjdk.jmh.infra.Blackhole;
    import org.openjdk.jmh.results.format.ResultFormatType;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.RunnerException;
    import org.openjdk.jmh.runner.options.Options;
    import org.openjdk.jmh.runner.options.OptionsBuilder;

    import java.util.*;
    import java.util.concurrent.ThreadLocalRandom;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;

    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    @Warmup(iterations = 2, time = 1)
    @Measurement(iterations = 5, time = 5)
    @Fork(1)
    @State(Scope.Thread)
    public class SortBenchmark {

    @Param(value = {"100", "10000", "100000"})
    private int operationSize;


    private static List arrayList;

    public static void main(String[] args) throws RunnerException {
    // 启动基准测试
    Options opt = new OptionsBuilder()
    .include(SortBenchmark.class.getSimpleName())
    .result("SortBenchmark.json")
    .mode(Mode.All)
    .resultFormat(ResultFormatType.JSON)
    .build();
    new Runner(opt).run();
    }

    @Setup
    public void init() {
    arrayList = new ArrayList<>();
    Random random = new Random();
    for (int i = 0; i < operationSize; i++) {
    arrayList.add(random.nextInt(10000));
    }
    }


    @Benchmark
    public void sort(Blackhole blackhole) {
    arrayList.sort(Comparator.comparing(e -> e));
    blackhole.consume(arrayList);
    }

    @Benchmark
    public void streamSorted(Blackhole blackhole) {
    arrayList = arrayList.stream().sorted(Comparator.comparing(e -> e)).collect(Collectors.toList());
    blackhole.consume(arrayList);
    }

    }


    性能测试结果:



    可以看到,list sort()效率确实比stream().sorted()要好。




    为什么更好?




    流本身的损耗




    java的stream让我们可以在应用层就可以高效地实现类似数据库SQL的聚合操作了,它可以让代码更加简洁优雅。


    但是,假设我们要对一个list排序,得先把list转成stream流,排序完成后需要将数据收集起来重新形成list,这部份额外的开销有多大呢?


    我们可以通过以下代码来进行基准测试


    import org.openjdk.jmh.annotations.*;
    import org.openjdk.jmh.infra.Blackhole;
    import org.openjdk.jmh.results.format.ResultFormatType;
    import org.openjdk.jmh.runner.Runner;
    import org.openjdk.jmh.runner.RunnerException;
    import org.openjdk.jmh.runner.options.Options;
    import org.openjdk.jmh.runner.options.OptionsBuilder;

    import java.util.ArrayList;
    import java.util.Comparator;
    import java.util.List;
    import java.util.Random;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;

    @BenchmarkMode(Mode.AverageTime)
    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    @Warmup(iterations = 2, time = 1)
    @Measurement(iterations = 5, time = 5)
    @Fork(1)
    @State(Scope.Thread)
    public class SortBenchmark3 {

    @Param(value = {"100", "10000"})
    private int operationSize; // 操作次数


    private static List arrayList;

    public static void main(String[] args) throws RunnerException {
    // 启动基准测试
    Options opt = new OptionsBuilder()
    .include(SortBenchmark3.class.getSimpleName()) // 要导入的测试类
    .result("SortBenchmark3.json")
    .mode(Mode.All)
    .resultFormat(ResultFormatType.JSON)
    .build();
    new Runner(opt).run(); // 执行测试
    }

    @Setup
    public void init() {
    // 启动执行事件
    arrayList = new ArrayList<>();
    Random random = new Random();
    for (int i = 0; i < operationSize; i++) {
    arrayList.add(random.nextInt(10000));
    }
    }

    @Benchmark
    public void stream(Blackhole blackhole) {
    arrayList.stream().collect(Collectors.toList());
    blackhole.consume(arrayList);
    }

    @Benchmark
    public void sort(Blackhole blackhole) {
    arrayList.stream().sorted(Comparator.comparing(Integer::intValue)).collect(Collectors.toList());
    blackhole.consume(arrayList);
    }

    }

    方法stream测试将一个集合转为流再收集回来的耗时。


    方法sort测试将一个集合转为流再排序再收集回来的全过程耗时。




    测试结果如下:



    可以发现,集合转为流再收集回来的过程,肯定会耗时,但是它占全过程的比率并不算高。


    因此,这部只能说是小部份的原因。




    排序过程




    我们可以通过以下源码很直观的看到。




    • 1 begin方法初始化一个数组。

    • 2 accept 接收上游数据。

    • 3 end 方法开始进行排序。

      这里第3步直接调用了原生的排序方法,完成排序后,第4步,遍历向下游发送数据。


    所以通过源码,我们也能很明显地看到,stream()排序所需时间肯定是 > 原生排序时间。


    只不过,这里要量化地搞明白,到底多出了多少,这里得去编译jdk源码,在第3步前后将时间打印出来。


    这一步我就不做了。

    感兴趣的朋友可以去测一下。


    不过我觉得这两点也能很好地回答,为什么list.sort()比Stream().sorted()更快。


    补充说明:



    1. 本文说的stream()流指的是串行流,而不是并行流。

    2. 绝大多数场景下,几百几千几万的数据,开心就好,怎么方便怎么用,没有必要去计较这点性能差异。


    作者:是奉壹呀
    来源:juejin.cn/post/7262274383287500860
    收起阅读 »

    utf8和utf8mb4有什么区别?

    utf8或者utf-8是大家常见的一个词汇,它是一种信息的编码格式,特别是不同开发平台的系统进行对接的时候,编码一定要对齐,否则就容易出现乱码。 什么是编码? 先说说什么是编码。编码就像我们日常生活中的语言,不同的地方说不同的话,编码就是计算机用来表示这些“话...
    继续阅读 »

    utf8或者utf-8是大家常见的一个词汇,它是一种信息的编码格式,特别是不同开发平台的系统进行对接的时候,编码一定要对齐,否则就容易出现乱码。


    什么是编码?


    先说说什么是编码。编码就像我们日常生活中的语言,不同的地方说不同的话,编码就是计算机用来表示这些“话”的一种方式。比如我们使用汉字来说话,计算机用二进制数来表示这些汉字的方式,就是编码。


    utf8就是这样一种编码格式,正式点要使用:UTF-8,utf8是一个简写形式。


    为什么需要utf8?


    在计算机早期,主要使用ASCII编码,只能表示128个字符,汉字完全表示不了。后来,才出现了各种各样的编码方式,比如GB2312、GBK、BIG5,但这些编码只能在特定的环境下使用,不能全球通用。


    UTF-8就像一个万能翻译官,它的全称是“Unicode Transformation Format - 8 bit”,注意这里不是说UTF-8只能使用8bit来表示一个字符,实际上UTF-8能表示世界上几乎所有的字符。


    它的特点是:



    • 变长编码:一个字符可以用1到4个字节表示,英文字符用1个字节(8bit),汉字用3个字节(24bit)。

    • 向后兼容ASCII:ASCII的字符在UTF-8中还是一个字节,这样就兼容了老系统。

    • 节省空间:对于英文字符,UTF-8比其他多字节编码更省空间。


    UTF-8适用于网页、文件系统、数据库等需要全球化支持的场景。


    经常接触代码的同学应该还经常能看到 Unicode 这个词,它和编码也有很大的关系,其实Unicode是一个字符集标准,utf8只是它的一种实现方式。Unicode 作为一种字符集标准,为全球各种语言和符号定义了唯一的数字码位(code points)。其它的Unicode实现方式还有UTF-16和UTF-32:



    • UTF-16 使用固定的16位(2字节)或者变长的32位(4字节,不在常用字符之列)来编码 Unicode 字符。

    • UTF-32 每一个字符都直接使用固定长度的32位(4字节)编码,不论字符的实际数值大小。这会消耗更多的存储空间,但是所有字符都可以直接索引访问。



    图片来源:src: javarevisited.blogspot.com/2015/02/dif…


    utf8mb4又是什么?


    utf8mb4并不常见,它是UTF-8的一个扩展版本,专门用于MySQL数据库。MySQL在 5.5.3 之后增加了一个utf8mb4的编码,mb4就是最多4个字节的意思(most bytes 4),它主要解决了UTF-8不能表示一些特殊字符的问题,比如Emoji表情,这在论坛或者留言板中也经常用到。大家使用小红书时应该见过各种各样的表情符号,小红书后台也可能使用utf8mb4保存它们。


    编码规则和特点:



    • 最多4个字节:utf8mb4中的每个字符最多用4个字节表示。

    • 支持更多字符:能表示更多的Unicode字符,包括Emoji和其他特殊符号。


    utf8和utf8mb4的比较


    存储空间



    • 数据库:utf8mb4每个字符最多用4个字节,比UTF-8多一个字节,存储空间会增加。

    • 文件:类似的,文件用utf8mb4编码也会占用更多的空间。


    性能影响



    • 数据库:utf8mb4的查询和索引可能稍微慢一些,因为占用更多的空间和内存。

    • 网络传输:utf8mb4编码的字符会占用更多的带宽,传输速度可能会稍慢。


    不过因为实际场景中使用的utf8mb4的字符也不多,其实对存储空间和性能的影响很小,大家基本没有必要因为多占用了一些空间和流量,而不是用utf8mb4。


    只是我们在定义字段长度、规划数据存储空间、网络带宽的时候,要充分考虑4字节带来的影响,预留好足够的空间。


    实战选择


    在实际开发中,选择编码要根据具体需求来定。如果你的网站或者应用需要支持大量的特殊字符和Emoji,使用utf8mb4是个不错的选择。如果主要是英文和普通中文文本,utf8足够应付。


    注意为了避免乱码问题,前端、后端、数据库都应该使用同一种编码,比如utf8,具体到编码时就是要确保数据库连接、网页头部、文件读写都设置为相同的编码。


    另外还需要注意Windows和Linux系统中使用UTF-8编码的文件可能是有差别的,Windows中的UTF-8文件可能会携带一个BOM头,方便系统进行识别,但是Linux中不需要这个头,所以如果要跨系统使用这个文件,特别是程序脚本,可能需要在Linux中去掉这个头。




    以上就是本文的主要内容,如有问题欢迎留言讨论。


    关注萤火架构,加速技术提升!


    作者:萤火架构
    来源:juejin.cn/post/7375504338758025254
    收起阅读 »

    鸿蒙,ArkTs 一段诡异的代码

    分享一段我之前在学习 ArkTs 的时候,看到的一段诡异的代码。我们来看看下面的代码,按照你多年的经验,分析一下下面的代码是否可以正常执行,如果可以执行的话,能说出运行结果吗。for (let i = 0; i < 3; i++) { let i =...
    继续阅读 »

    分享一段我之前在学习 ArkTs 的时候,看到的一段诡异的代码。我们来看看下面的代码,按照你多年的经验,分析一下下面的代码是否可以正常执行,如果可以执行的话,能说出运行结果吗。

    for (let i = 0; i < 3; i++) {
    let i = 'abc';
    console.log(i);
    }

    以上代码是可以正常运行的,这段代码的执行结果,将会输出了 3 个 'abc' :

    abc
    abc
    abc

    这段代码中的 for 循环尝试执行三次循环,每次循环中都声明了一个新的局部变量 i,并将其赋值为字符串 'abc'。然后,它打印出这个新的局部变量 i

    在每次迭代中,尽管外部循环的控制变量也叫 i,但内部的 let i = 'abc'; 实际上创建了一个新的、同名的局部变量 i,这个变量的作用域仅限于 for 循环的块内部。因此,每次迭代打印的都是这个块作用域内的字符串 'abc',而不是外部循环控制变量的数值。

    从执行结果也能间接说明,for 循环内部声明的变量 i 和循环变量 i 不在同一个作用域,它们有各自单独的作用域,设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域,这是 for 循环的特别之处。

    如果在同一个作用域,是不可使用 let 或者 const 重复声明相同名字的变量。比如下面的代码会报错。

    if(true){
    let a = 1;
    let a = 2; // 报错

    const b = 3;
    const b = 4; // 报错
    }

    这就引发出了另外一问题 块作用域

    块作用域是指变量在定义它的代码块或者说是大括号 {} 内有效的作用域。使用 let 或者 const 关键字声明的变量具有块级作用域(block scope),这意味着变量在包含它的块(在这个例子中是 for 循环的大括号内)以及任何嵌套的子块中都是可见的。

    块作用域示例:

    let blockScopedVariable = 'I am dhl';
    if (true) {
    let blockScopedVariable = 'I am block scoped';
    console.log(blockScopedVariable); // 输出: I am block scoped
    }
    console.log(blockScopedVariable); // 输出: I am dhl

    从执行结果可以看出,在 if 语句中定义的变量 blockScopedVariable,仅在代码块内有效,外层变量不会被内层同名变量的声明和赋值影响。

    但是需要注意,在 ArkTS 中不能使用 for .. in,否则会有一个编译警告。

    之所以不能使用 for .. in 是因为在 ArkTS 中,对象的布局在编译时是确定的,且不能在程序执行期间更改对象的布局,换句话说,ArkTS 禁止以下行为:

    • 向对象中添加新的属性或方法
    • 从对象中删除已有的属性或方法
    • 将任意类型的值赋值给对象属性

    如果修改对象布局会影响代码的可读性以及运行时性能。

    从开发的角度来说,在某处定义类,然后又在其它地方修改了实际对象布局,这很容易引入错误,另外如果修改对象布局,需要在运行时支持,这样会增加执行开销降低性能。


    作者:程序员DHL
    来源:juejin.cn/post/7376158566598705167
    收起阅读 »

    人生第一次线上 OOM 事故,竟和 where 1 = 1 有关

    这篇文章,聊聊一个大家经常使用的编程模式 :Mybatis +「where 1 = 1 」。 笔者人生第一次重大的线上事故 ,就是和使用了类似的编程模式 相关,所以印象极其深刻。 这几天在调试一段业务代码时,又遇到类似的问题,所以笔者觉得非常要必要和大家絮叨絮...
    继续阅读 »

    这篇文章,聊聊一个大家经常使用的编程模式 :Mybatis +「where 1 = 1 」。


    笔者人生第一次重大的线上事故 ,就是和使用了类似的编程模式 相关,所以印象极其深刻。


    这几天在调试一段业务代码时,又遇到类似的问题,所以笔者觉得非常要必要和大家絮叨絮叨。


    1 OOM 事故


    笔者曾服务一家电商公司的用户中心,用户中心提供用户注册,查询,修改等基础功能 。用户中心有一个接口 getUserByConditions ,该接口支持通过 「用户名」、「昵称」、「手机号」、「用户编号」查询用户基本信息。



    我们使用的是 ibatis (mybatis 的前身), SQLMap 见上图 。当构建动态 SQL 查询时,条件通常会追加到 WHERE 子句后,而以 WHERE 1 = 1 开头,可以轻松地使用 AND 追加其他条件。


    但用户中心在上线后,竟然每隔三四个小时就发生了内存溢出问题 ,经过通过和 DBA 沟通,发现高频次出现全表查询用户表,执行 SQL 变成 :



    查看日志后,发现前端传递的参数出现了空字符串,笔者在代码中并没有做参数校验,所以才出现全表查询 ,当时用户表的数据是 1000万 ,调用几次,用户中心服务就 OOM 了。


    笔者在用户中心服务添加接口参数校验 ,即:「用户名」、「昵称」、「手机号」、「用户编号」,修改之后就再也没有产生这种问题了。


    2 思维进化


    1、前后端同时做接口参数校验


    为了提升开发效率,我们人为的将系统分为前端、后端,分别由两拨不同的人员开发 ,经常出现系统问题时,两拨人都非常不服气,相互指责。



    有的时候,笔者会觉得很搞笑,因为这个本质是个规约问题。


    要想系统健壮,前后端应该同时做接口参数校验 ,当大家都遵循这个规约时,出现系统问题的风险大大减少。


    2、复用和专用要做平衡


    笔者写的这个接口 getUserByConditions ,支持四种不同参数的查询,但是因为代码不够严谨,导致系统出现 OOM 。


    其实,在业务非常明确的场景,我们可以将复用接口,拆分成四个更细粒度的接口 :



    • 按照用户 ID 查询用户信息

    • 按照用户昵称查询用户信息

    • 按照手机号查询用户信息

    • 按照用户名查询用户信息


    比如按照用户 ID 查询用户信息 , SQLMAP 就简化为:



    通过这样的拆分,我们的接口设计更加细粒度,也更容易维护 , 同时也可以规避 where 1 =1 产生的问题。


    有的同学会有疑问:假如拆分得太细,会不会增加我编写 接口和 SQLMap 的工作量 ?


    笔者的思路是:通过代码生成器动态生成,是绝对可以做到的 ,只不过需要做一丢丢的定制。


    3、编写代码时,需要考虑资源占用量,做好预防性编程


    笔者刚入行的时候,只是机械性的完成任务,并没有思考代码后面的资源占用,以及有没有可能产生恶劣的影响。


    随着见识更多的系统,学习开源项目,笔者慢慢培养了一种习惯:



    • 这段代码会占用多少系统资源

    • 如何规避风险 ,做好预防性编程。


    其实,这和玩游戏差不多 ,在玩游戏的时,我们经常说一个词,那就是意识。



    上图,后裔跟墨子在压对面马可蔡文姬,看到小地图中路铠跟小乔的视野,方向是往下路来的,这时候我们就得到了一个信息。


    知道对面的人要来抓,或者是协防,这种情况我们只有两个人,其他的队友都不在,只能选择避战,强打只会损失两名“大将”。


    通过小地图的信息,并且想出应对方法,就是叫做“猜测意识”。


    编程也是一样的,我们思考代码可能产生的系统资源占用,以及可能存在的风险,并做好防御性编程,就是编程的意识


    4 写到最后


    当我们在使用 :Mybatis +「where 1 = 1 」编程模式时,需要如下三点:



    1. 前后端同时做好接口参数校验 ;

    2. 复用和专用要做平衡,条件允许情况下将复用 SQLMap 拆分成更细粒度的 SQLMap ;

    3. 编写代码时,需要考虑资源占用量,做好预防性编程 ;




    文章片段推荐:



    生命就是这样一个过程,一个不断超越自身局限的过程,这就是命运,任何人都是一样,在这过程中我们遭遇痛苦、超越局限、从而感受幸福。


    所以一切人都是平等的,我们毫不特殊。


    --- 史铁生





    作者:勇哥Java实战
    来源:juejin.cn/post/7375345204046266368
    收起阅读 »

    因为git不熟练,我被diss了

    浅聊一下 在百度实习快一个月了,公司的需求从提需到上线也是走过一遍了,个人认为最基础的也是最重要的就是git了...为什么这么说?因为本人git不熟练挨diss了😭😭😭,虽然之前也会使用git将代码提交到github,但是会使用到也就几条指令,今天就来总结一下...
    继续阅读 »

    浅聊一下


    在百度实习快一个月了,公司的需求从提需到上线也是走过一遍了,个人认为最基础的也是最重要的就是git了...为什么这么说?因为本人git不熟练挨diss了😭😭😭,虽然之前也会使用git将代码提交到github,但是会使用到也就几条指令,今天就来总结一下在公司使用git的常规操作,刚进厂的掘友可以参考一下...


    git


    什么是git?


    Git 是一款免费、开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。是的,我对git的介绍就一条,想看简介的可以去百度一下😘😘😘


    为什么要用git?


    OK,想象一下,我是一名作家,现在我要开始写一本小说了,我想要将我的小说每天都发布到“github小说网”上,一日两更。我想要一个工具,它要具备的功能如下:



    1. 将我每天写的小说章节发布

    2. 我发现昨天写的章节有问题,它可以帮我撤回

    3. 一周后,我又想找到上周我撤回的章节,它能帮我找到

    4. 我想写一个“漫威宇宙的系列”,我需要雇人和我一起写,它可以帮我们同步进度

    5. 我想要查看每个人写了什么,什么时候写的

    6. ...


    想要的有点多了,我知道很难满足,但是git就能满足我的一切需求...


    写小说


    我进厂写小说了,厂长说:你先下一个git。那我必须得下一个git


    下载git


    直接跑到这个git官网http://www.git-scm.com/downloads ,可以搜个教程跟着安装,这里就不细说了


    基本配置


    把git下载下来了,那我不得登录一下,免得到时候小说写的有问题都不知道是谁写的,为了不背锅!


    $ git config --global user.name 
    $ git config --global user.email

    参与写小说


    来到“github小说网”,要将之前的章节全部拷贝到你的电脑上,才能开始续写


    image.png


    使用git clone 命令来完成


    $ git clone https://github.com/vuejs/vue.git

    这样就将代码克隆到你的本地了


    小说版本


    我们的小说每天都在迭代更新,master分支就是我们的主分支,也就是目前发布的最新的小说内容


    image.png


    每当我们向master提交代码,master都会向前移动一步。


    想象一个场景,有十个人都在写同一本小说,那么十个人都同时向master提供代码,会发生什么事情?



    • 并行开发受限:没有分支意味着无法支持并行开发,因为每个人都只能基于master进行工作,这可能会导致团队成员之间的代码冲突。

    • 代码管理困难:由于所有更改都直接应用于master,代码管理会变得混乱,很难跟踪谁提交了哪些更改,以及何时进行了更改。

    • 风险高:由于没有分支,每次更改都直接影响master,这可能增加了引入错误或破坏现有功能的风险。

    • 难以撤销更改:没有分支意味着难以进行实验性更改或回滚到先前的版本,因为没有办法轻松地隔离或恢复更改。


    所以我们每个人都需要创建自己的分支,最后再将自己的分支与master合并


    当我们创建了新的分支,比如叫 myBranch ,git 就会新建一个指针叫 myBranch,指向 master 相同的提交,在把 HEAD 指向 myBranch,就表示当前分支在 myBranch 上。


    image.png


    从现在开始,对工作区的修改和提交都是针对 myBranch 分支了,如果我们修改后再提交一次,myBranch指针就会向前移动一步,而master指针不变,当我们将myBranch开发完毕以后,再将它与master合并



    • 查看当前分支


    $ git branch


    • 创建分支


    $ git checkout -b 分支名

    git checkout 命令加上-b参数,表示创建分支并切换,它相当于下面的两个命令:


    $ git branch dev        //创建分支
    $ git checkout dev //切换到创建的分支

    提交


    在上面,我们已经创建好了一个分支myBranch,我们一天要写两章小说,当我每写完一章以后,我要将它先存入暂存区,当一天的工作完毕以后,统一将暂存区的代码提交到本地仓库,最后再上传到远程仓库,并且合并



    • 上传暂存区


    $ git add .    //将修改的文件全部上传
    $ git add xxx //将xxx文件上传


    • 提交到本地仓库


    git commit -m '提交代码的描述'


    • 提交到远程仓库的对应分支


    $ git push origin xxx    //xxx是对应分支名


    • 合并分支


    $ git checkout master    //首先切换分支到master
    $ git merge mybranch


    • 删除分支


    当你合并完分支以后,mybranch分支就可以删除了


    $ git branch -d mybranch

    解决冲突


    Git 合并分支产生冲突的原因通常是因为两个或多个分支上的相同部分有了不同的修改。这可能是因为以下几个原因:



    1. 并行开发:团队中的不同成员在不同的分支上同时开发功能或修复 bug。如果他们修改了相同的文件或代码行,就会导致合并冲突。

    2. 分支基于旧版本:当从一个旧的提交创建分支,然后在原始分支上进行了更改时,可能会导致冲突。这是因为在创建分支后,原始分支可能已经有了新的提交。

    3. 重命名或移动文件:如果一个分支重命名或移动了一个文件,而另一个分支对同一文件进行了修改,就会导致冲突。

    4. 合并冲突的解决方法不同:在合并分支时,有时会使用不同的合并策略或解决方法,这可能会导致冲突。

    5. 历史分叉:如果两个分支的历史分叉很远,可能会存在较大的差异,从而导致合并时出现冲突。


    于是我们需要将冲突解决再重新合并分支,解决冲突也就是查看文件新增了哪些代码,你需要保留哪些代码,把不需要的删去就可以了...


    我们还需养成一个好习惯,就是在开发之前先git pull 一下,更新一下自己本地的代码确保版本是最新的。


    添砖加瓦


    如果我已经使用git commit -m 'xxx'将代码提交到了本地仓库,但是我后续还想向这个提交中添加文件,那我该怎么办呢?



    1. 首先将你想添加到文件使用git add xxx加入暂存区

    2. 然后运行以下命令:


    $ git commit --amend

    这将会打开一个编辑器,让你编辑上一次提交的提交信息。如果你只是想要添加文件而不改变提交信息,你可以直接保存并关闭编辑器。



    1. Git 将会创建一个新的提交,其中包含之前的提交内容以及你刚刚添加的文件。


    您撤回了一次push


    代码推送到远程仓库的master上以后,我发现有bug,挨批是不可避免了,批完还得接着解决...



    1. 撤销最新的提交并保留更改


    $ git reset HEAD^

    这会将最新的提交从 master 分支中撤销,但会保留更改在工作目录中。你可以修改这些更改,然后重新提交。



    1. 撤销最新的提交并丢弃更改


    $ git reset --hard HEAD^

    这会完全撤销最新的提交,并丢弃相关的更改。慎用,因为这将永久丢失你的更改



    1. 创建新的修复提交


    如果你不想删除最新的提交,而是创建一个新的提交来修复问题,可以进行如下操作:



    • 在 master 分支上创建一个新的分支来进行修复:


    $ git checkout -b fix-branch master


    • 在新分支上进行修改,修复代码中的问题。

    • 提交并推送修复:


    $ git add .
    $ git commit -m "Fixing the issue"
    $ git push origin fix-branch

    结尾


    当你学会以上操作的时候, 你就可以初步参加公司的代码开发了,从挨批中进步!!!


    作者:滚去睡觉
    来源:juejin.cn/post/7375928754147246107
    收起阅读 »

    Git 代码提交规范,feat、fix、chore 都是什么意思?

    写在前面 经常看到别人提交的代码记录里面包含一些feat、fix、chore等等,而我在提交时也不会区分什么,直接写下提交信息,今天就来看一下怎么个事,就拿 element-plus/ant-design 来看一下。 其实这么写是一种代码提交规范,当然不是...
    继续阅读 »

    写在前面


    经常看到别人提交的代码记录里面包含一些feat、fix、chore等等,而我在提交时也不会区分什么,直接写下提交信息,今天就来看一下怎么个事,就拿 element-plus/ant-design 来看一下。



    image.png
    其实这么写是一种代码提交规范,当然不是为了炫技,主要目的是为了提高提交记录的可读性和自动化处理能力。


    当然如果团队没有要求,不这么写也可以。


    git 提交规范


    commit message = subject + :+ 空格 + message 主体


    例如: feat:增加用户注册功能


    常见的 subject 种类以及含义如下:



    1. feat: 新功能(feature)



      • 用于提交新功能。

      • 例如:feat: 增加用户注册功能



    2. fix: 修复 bug



      • 用于提交 bug 修复。

      • 例如:fix: 修复登录页面崩溃的问题



    3. docs: 文档变更



      • 用于提交仅文档相关的修改。

      • 例如:docs: 更新README文件



    4. style: 代码风格变动(不影响代码逻辑)



      • 用于提交仅格式化、标点符号、空白等不影响代码运行的变更。

      • 例如:style: 删除多余的空行



    5. refactor: 代码重构(既不是新增功能也不是修复bug的代码更改)



      • 用于提交代码重构。

      • 例如:refactor: 重构用户验证逻辑



    6. perf: 性能优化



      • 用于提交提升性能的代码修改。

      • 例如:perf: 优化图片加载速度



    7. test: 添加或修改测试



      • 用于提交测试相关的内容。

      • 例如:test: 增加用户模块的单元测试



    8. chore: 杂项(构建过程或辅助工具的变动)



      • 用于提交构建过程、辅助工具等相关的内容修改。

      • 例如:chore: 更新依赖库



    9. build: 构建系统或外部依赖项的变更



      • 用于提交影响构建系统的更改。

      • 例如:build: 升级webpack到版本5



    10. ci: 持续集成配置的变更



      • 用于提交CI配置文件和脚本的修改。

      • 例如:ci: 修改GitHub Actions配置文件



    11. revert: 回滚



      • 用于提交回滚之前的提交。

      • 例如:revert: 回滚feat: 增加用户注册功能




    总结


    使用规范的提交消息可以让项目更加模块化、易于维护和理解,同时也便于自动化工具(如发布工具或 Changelog 生成器)解析和处理提交记录。


    通过编写符合规范的提交消息,可以让团队和协作者更好地理解项目的变更历史和版本控制,从而提高代码维护效率和质量。


    作者:JacksonChen
    来源:juejin.cn/post/7374295163625521161
    收起阅读 »

    请一定要使用常量和枚举

    1.魔法值和硬编码 在代码编写的场景中,会遇到提示避免去使用 魔法值(magic numbers)和硬编码(hardcoding)。 魔法值就是在代码中直接使用的,没有提供任何注释或解释说明其用途和含义的常数值。 硬编码指的是在程序中直接使用特定的值或信息,...
    继续阅读 »

    1.魔法值和硬编码


    在代码编写的场景中,会遇到提示避免去使用 魔法值(magic numbers)和硬编码(hardcoding)。



    • 魔法值就是在代码中直接使用的,没有提供任何注释或解释说明其用途和含义的常数值。

    • 硬编码指的是在程序中直接使用特定的值或信息,而不是通过变量、常量或其他可配置的方式来表示。这些值通常是字面量字符串、数字或其他原始数据类型,在代码中写死了,无法修改。


    缺点:


    不便于维护:如果需要修改值,必须手动在代码中查找并替换,会增加代码修改的复杂度和风险。


    可读性差:硬编码的值缺乏描述和注释,不易于理解和解释。在工作中,协作开发,其他开发人员在阅读代码时可能无法理解这些值的含义和作用。


    维护困难:当需要修改值的时候,需要在代码中找到所有使用该值的地方进行手动修改。这样容易出错,而且增加了代码维护的复杂性。


    2.定义常量


    场景:设π取小数点后五位数(即3.14159)计算圆的面积


    Java常量定义是指在Java程序中定义一个不可修改的值,Java常量的定义使用关键字final,一般与static关键字一起使用。


    此时可以通过定义一个常量作为π


    public class MyClass {  
    //圆周率π
    public static final double PI = 3.14159;
    }

    上面这个定义在类中的常量称为 类常量,可以通过类名访问。


    通过定义常量,就避免在代码中直接使用没有明确含义的硬编码数字。取而代之,将这些数字赋值给具有描述性名称的常量。


    3.if - else if - else if - else if.....else


    在项目中看过这面这段代码,通过判断天气给出建议


    public void handleWeather(String weather) {  
    if (weather.equals("晴天")) {
    System.out.println("做好防晒");
    } else if (weather.equals("阴天")) {
    System.out.println("户外活动");
    } else if (weather.equals("小雨")) {
    System.out.println("带雨伞");
    } else if (weather.equals("雷雨")) {
    System.out.println("避免户外活动");
    } else {
    System.out.println("未知天气");
    }
    }

    这段代码的判断条件 "晴天"、"阴天"、"小雨"等,这些条件在项目不止使用到了一次,比如在另外一个方法中也有一个判断,但是判断执行的方法体不同,如下


    public void handleWeather(String weather) {  
    if (weather.equals("晴天")) {
    System.out.println("出太阳");
    } else if (weather.equals("阴天")) {
    System.out.println("有乌云");
    }
    ....
    }

    现在如果需要 把 晴天 这个天气情况修改为 高温天,那么就需要修改两处地方,在实际项目中可能更多。


    所以这里必须要定义枚举提高代码的可维护性


    4.定义枚举


    定义枚举类如下


    public enum WeatherType {  
    SUNNY("晴天"),
    CLOUDY("阴天"),
    LIGHT_RAIN("小雨"),
    THUNDERSTORM("雷雨"),
    UNKNOWN("未知天气");

    private final String message;

    WeatherType(String message) {
    this.message = message;
    }

    public String getMessage() {
    return message;
    }
    }

    将代码用枚举结合switch case来替换


    public void handleWeather(String weather) {  
    WeatherType weatherType = WeatherType.valueOf(weather);
    switch (weatherType) {
    case SUNNY:
    System.out.println("做好防晒");
    break;
    case CLOUDY:
    System.out.println("户外活动");
    break;
    case LIGHT_RAIN:
    System.out.println("带雨伞");
    break;
    case THUNDERSTORM:
    System.out.println("避免户外活动");
    break;
    case UNKNOWN:
    System.out.println("未知天气");
    break;
    }
    }

    5.结语


    在日常工作中,会有很多状态类型的字段,比如淘宝订单,状态可以为:待付款、待发货、已发货、已签收、交易成功等,真实场景状态可能更多。


    而状态也会被很多代码给使用到,所以必须通过集中统一的方式来定义。


    通过常量、枚举,可以很好的解决问题,一旦状态有新增、修改、删除都只需要修改一处地方,其它代码直接引用就行。


    作者:CoderMagic
    来源:juejin.cn/post/7273875079657160743
    收起阅读 »