注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

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

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
收起阅读 »

什么?这个 App 抓不到包

有个朋友说,某个车联网的 app 竟然抓不到包,让我帮忙看下,行吧,那就看下。 样本:byd海洋(v1.4.0) 小试牛刀 先用 Charles软件试试,使用这个软件抓 App 的包,有几个前提: 手机端要配置好Charles的证书 电脑端安装好 Cha...
继续阅读 »

有个朋友说,某个车联网的 app 竟然抓不到包,让我帮忙看下,行吧,那就看下。



  • 样本:byd海洋(v1.4.0)


小试牛刀


先用 Charles软件试试,使用这个软件抓 App 的包,有几个前提:



  • 手机端要配置好Charles的证书

  • 电脑端安装好 Charles 的根证书,并信任

  • 手机端证书要放到系统证书目录 (Android 系统 7.0 一下,这一步可以省略)


没配置好的话是抓不到 https 请求的,环境配置好后先抓个 https 的包测试一下
image.png
可以正常获取到数据,说明抓包环境配置成功。
现在对目标 app 抓包,看下是否成功,结果如下图
image.png
可以发现,并没有成功,为什么会这样呢🤔


为什么抓不到App包


目前App用到的网络请求库都是 OKHttp3,这个库有一个 api:CertificatePinner这个 API 的作用是



用于自定义证书验证策略,以增强网络安全性。在进行TLS/SSL连接时,服务器会提供一个SSL证书,客户端需要验证该证书的有效性以确保连接的安全性。CertificatePinner允许你指定哪些SSL证书是可接受的,从而有效地限制了哪些服务器可以与你的应用程序通信。


具体来说,CertificatePinner允许你指定一组预期的证书公钥或证书固定哈希值(SHA-256),以便与服务器提供的证书进行比较。如果服务器提供的证书与你指定的公钥或哈希值不匹配,则连接会被拒绝,从而防止中间人攻击和其他安全风险。



我们大胆的猜测一下,目标 App 就是用到了这个 API,添加了自定义的证书验证策略,我们既要大胆猜测,又要小心验证,怎么验证呢?这就要用到 Frida 了,用Frida Hook 这个 API,使其失效。Frida 代码如下


try {

var CertificatePinner = Java.use('okhttp3.CertificatePinner');

quiet_send('OkHTTP 3.x Found');

CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function () {

quiet_send('OkHTTP 3.x check() called. Not throwing an exception.');
}

} catch (err) {

// If we dont have a ClassNotFoundException exception, raise the
// problem encountered.
if (err.message.indexOf('ClassNotFoundException') === 0) {

throw new Error(err);
}
}

验证了一下,发现还是抓不到包。
除了这个 API 可以用来防止中间人攻击,OKHttp3还有其他防止中间人攻击的方法,如X509TrustManager



X509TrustManager 是Java安全架构中的一个接口,位于 javax.net.ssl 包中,主要用于处理 SSL(Secure Sockets Layer)和后续的 TLS(Transport Layer Security)协议中的证书信任管理功能。在基于SSL/TLS的网络通信中,特别是HTTPS连接,X509TrustManager扮演着至关重要的角色,它的主要职责是验证远程主机提供的X.509证书链的有效性。



同样,再用 Frida 验证一下,是不是X509TrustManager导致的抓不到包,代码如下


  var TrustManager;
try {
TrustManager = Java.registerClass({
name: 'org.wooyun.TrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function (chain, authType) {
},
checkServerTrusted: function (chain, authType) {
},
getAcceptedIssuers: function () {
return [];
}
}
});
} catch (e) {
quiet_send("registerClass from X509TrustManager >>>>>>>> " + e.message);
}


// Prepare the TrustManagers array to pass to SSLContext.init()
var TrustManagers = [TrustManager.$new()];

try {
// Prepare a Empty SSLFactory
var TLS_SSLContext = SSLContext.getInstance("TLS");
TLS_SSLContext.init(null, TrustManagers, null);
} catch (e) {
quiet_send(e.message);
}

send('Custom, Empty TrustManager ready');

// Get a handle on the init() on the SSLContext class
var SSLContext_init = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom');

// Override the init method, specifying our new TrustManager
SSLContext_init.implementation = function (keyManager, trustManager, secureRandom) {

quiet_send('Overriding SSLContext.init() with the custom TrustManager');

SSLContext_init.call(this, null, TrustManagers, null);
};

Frida 执行上面的代码,再看下是否抓到包
image.png
这次就可以顺利的抓到数据了。


抓包工具推荐


虽然利用上面 Frida 的脚本可以成功抓包,但是上面的操作还是略显复杂,况且有的 App 还会有 Frida 检测,操作起来就难度骤升。
这里推荐一个抓包工具 httptoolkit,官网界面如下
image.png
使用起来也很简单



  • 下载安装这个软件

  • 连接手机

  • 选择 Android Device Via ADB选项卡

  • 打开目标应用,开始抓包


看下这个软件的抓包效果
image.png
可以看到,数据都出来了并且不用额外的设置。


Xposed 方案


如果你手机刷了 Xposed,那就很简单了,只需要安装 JustTrustMe++模块就可以了。安装之后,也可以通过 Charles软件直接抓包了。



本文的目的只有一个就是学习更多的逆向技巧和思路,如果有人利用本文技术去进行非法商业获取利益带来的法律责任都是操作者自己承担,和本文以及作者没关系.


本文涉及到的代码项目可以去 爱码者说 知识星球自取,欢迎加入知识星球一起学习探讨技术。
关注公众号 爱码者说 及时获取最新推送文章。



作者:平行绳
来源:juejin.cn/post/7374665776537567286
收起阅读 »

前端大师课:“鬼剑士,听我指令,砍碎屏幕”是怎么实现的?

web
前言:属于我们那个年代的"地下城与勇士"的手游上线了,为了做好推广和裂变,有个特别游戏意思的效果你可能在各个微信群里都看到了:你只需要在微信群里发送"鬼剑士,听我指令,砍碎屏幕"、“鬼剑士”、“地下城与勇士”这些关键词,就会触发特别炫酷的动画效果。那这种效果如...
继续阅读 »

前言:属于我们那个年代的"地下城与勇士"的手游上线了,为了做好推广和裂变,有个特别游戏意思的效果你可能在各个微信群里都看到了:你只需要在微信群里发送"鬼剑士,听我指令,砍碎屏幕"、“鬼剑士”、“地下城与勇士”这些关键词,就会触发特别炫酷的动画效果。
那这种效果如果让我们技术来做:
1.要怎么实现呢?
2.有几种实现方法呢?
3.关键代码能给我看看吗?

方案简述

为了提供更详细的解析,我们可以进一步探讨“地下城与勇士手游”(DNF手游)在微信聊天中实现“鬼剑士,听我指令,砍碎屏幕”这一互动特效的可能技术细节。虽然没有直接的源码分析,我们可以基于现有的技术框架和前端开发实践来构建一个理论上的实现模型。

前端监听设计

  • 关键词识别: 微信聊天界面的输入检测可能是通过前端JavaScript监听input事件,配合正则表达式匹配用户输入的关键词(如“鬼剑士,听我指令,砍碎屏幕”)。一旦匹配成功,就向后端发送请求或直接触发前端动画逻辑。

后端交互

  • 请求处理: 用户输入关键词后,前端可能通过Ajax请求或WebSocket向服务器发送一个事件。服务器确认后,返回一个响应,指示前端继续执行动画展示或直接携带福袋奖励信息。

前端动画实现

  • 动画序列: 利用HTML5 元素或WebGL技术,开发者可以创建复杂的2D或3D动画。对于“砍碎屏幕”的效果,可能事先设计好一系列帧动画或使用骨骼动画技术来展现鬼剑士的动作和屏幕碎裂的过程。
  • 碎片生成与物理模拟: 通过JavaScript库(如Three.js的粒子系统或matter.js)模拟屏幕碎裂后的碎片效果,包括碎片的随机分布、速度、旋转和重力影响等,增加真实感。
  • 音频同步: 使用Web Audio API同步播放砍击和碎裂的音效,增强用户的沉浸感。

福袋奖励机制

  • • 动画结束后展示福袋: 动画播放完毕后,前端动态插入一个福袋图标或弹窗,作为用户交互元素。这可能是通过DOM操作实现的,如创建一个新的
    元素并应用CSS样式使其表现为福袋。
  • • 点击事件处理: 给福袋元素绑定点击事件,触发领奖逻辑。这可能涉及再次向服务器发送请求验证用户资格,并根据响应展示奖励内容。

优化与兼容性

  • 性能优化: 动画应考虑在不同设备上的流畅度,可能采用分层渲染、帧率限制、资源按需加载等策略。
  • 跨平台兼容: 确保在微信内置浏览器上的表现良好,需要对微信环境下的特定API和限制有深入了解,比如微信小程序的Canvas组件和其特定的适配要求。

安全与隐私

  • 数据保护: 在处理用户交互和服务器通信时,确保遵循数据保护法规,比如加密传输敏感信息,避免泄露用户隐私。

综上所述,这个互动特效的实现是一个从用户输入监测、前后端交互、动画设计与渲染、到用户反馈与奖励领取的全链路流程,需要综合运用多种前端技术和良好的产品设计思路。

微信聊天界面元素震动效果设计及API应用

虽然微信没有直接公开针对UI元素震动的特定API,但在微信小程序或基于微信环境的H5游戏中设计类似聊天界面元素的震动效果,利用一些基础的动画技术和微信小程序提供的动画库来模拟这种效果。比如通过CSS动画与微信小程序的动画接口来实现这一功能。以下是两种主流实现方式:

1. CSS动画实现震动效果(H5环境)

核心概念

  • @keyframes: CSS的关键帧动画,用于定义一个动画序列中不同时间点的样式变化。
  • transform: CSS属性,用于改变元素的形状、大小和位置。其中,translateX()用于水平移动元素。

实现步骤

  1.  定义动画样式:在CSS中,创建一个名为.shake的类,利用@keyframes定义震动序列。动画包括了元素在原位置与左右轻微偏移之间的快速切换,营造出震动感。

    .shake {
      animation: shake 0.5s/* 动画名称与持续时间 */
      transform-origin: center center; /* 设置变换中心点 */
    }

    @keyframes shake {
      0%100% { transformtranslateX(0); } /* 开始与结束位置 */
      10%30%50%70%90% { transformtranslateX(-5px); } /* 向左偏移 */
      20%40%60%80% { transformtranslateX(5px); } /* 向右偏移 */
    }

2. 应用动画:在JavaScript中,通过动态添加或移除.shake类到目标元素上,触发这个震动动画。

2. 微信小程序wx.createAnimation实现震动

核心概念

  • wx.createAnimation: 微信小程序提供的动画实例创建方法,允许更精细地控制动画过程。
  • step() : 动画实例的方法,用于生成当前动画状态的数据,用于在setData中更新视图。

实现步骤

  1. 初始化动画数据:在Page的data中定义一个空的animationData对象,用于存储动画实例导出的状态数据。

    data: {
      animationData: {},
    },

2. 创建震动动画逻辑:定义一个函数,如shakeElement,使用wx.createAnimation创建动画实例,并定义震动序列。通过连续的translateX操作模拟震动,然后通过step()函数记录每个阶段的状态,并通过setData更新到视图上。

```
shakeElement: function () {
  let animation = wx.createAnimation({
    duration: 300// 动画持续时间
    timingFunction: 'ease'// 动画速度曲线
  });

  // 震动序列定义
  animation.translateX(-5).step(); // 向左偏移
  this.setData({ animationData: animation.export() });
  setTimeout(() => {
    animation.translateX(5).step(); // 向右偏移
    this.setData({ animationData: animation.export() });
  }, 100);
  setTimeout(() => {
    animation.translateX(0).step(); // 回到原位
    this.setData({ animationData: animation.export() });
  }, 200);
},
```

3. 应用动画数据:在WXML模板中,为目标元素绑定动画数据。

```
style="{{animationData}}" class="your-element-class">震动的文字或图标
```

注意事项与最佳实践

  • 性能监控:频繁或长时间的动画可能影响应用性能,尤其是低配置设备。适时停止或限制动画触发频率。
  • 用户体验:震动效果应适度且符合用户预期,过度使用可能造成用户反感。
  • 跨平台兼容性:虽然上述方法主要针对微信环境,但在实现时也应考虑浏览器的兼容性问题,特别是对于H5应用。
  • 动画细节调整:根据实际需求调整震动幅度、频率和持续时间,以达到最佳视觉效果。

动手能力强的你,可以去试试,下一节,将讲一个具体的demo给大家演示一下哈。


    作者:蜡笔小新爱学习
    来源:juejin.cn/post/7371423076661542952
    收起阅读 »

    运营:别再让你的页面一直loading 了

    运营:别再让你的页面一直loading 了 第一轮 battle Q: 我想下载一个大文件,界面一直转圈,很耽误时间,我想在下载的时候还做点其他事情 A:一直转圈就一直等呗,反正还能摸会(奈何小姐姐太想做牛马了) 第二轮 battle Q: 不行,为什么别人...
    继续阅读 »

    运营:别再让你的页面一直loading 了


    May-17-2024 15-36-38.gif


    第一轮 battle


    Q: 我想下载一个大文件,界面一直转圈,很耽误时间,我想在下载的时候还做点其他事情


    A:一直转圈就一直等呗,反正还能摸会(奈何小姐姐太想做牛马了)


    第二轮 battle


    Q: 不行,为什么别人的浏览器,下载软件/文件 就能操作界面,你这就一直转圈,什么都做不了


    A: 我们js 是单线程,一个时间只能做一件事,你不能在下载文件的时候,还操作界面吧...逐渐语无伦次,行,我给你试着优化优化..


    image.png


    最终效果


    save.gif


    无敌.gif


    可以看到,下载文件 页面不再转圈,并且可以在界面操作,但是在点击操作1,2,到3的时候,会卡顿一下,下面会说为什么会卡这一下


    开始分析



    1. 执行文件下载操作,把转圈逻辑去掉不就行了,


    but: 是不转圈了,下载的时候,依然操作不了界面



    1. js 是一个单线程,一个时间只能做一件事,密集的cpu 计算,导致网站反应迟钝,就像卡了一样


    resolve: 把下载文件这个耗时操作,放在其他线程操作,等到操作完毕,再通知主线程,执行完了。就像发布订阅模式一样,主线程不用执行密集的计算,也不用特意等密集计算的结果,执行完,告诉我就行了


    技术使用 Web Workers


    摘自 MDN developer.mozilla.org/zh-CN/docs/…


    Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,它们可以使用 XMLHttpRequest(尽管 responseXML 和 channel 属性总是为空)或 fetch(没有这些限制)执行 I/O。一旦创建,一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理器(反之亦然)。


    为什么要用它:worker 的一个优势在于能够执行处理器密集型的运算



    不会阻塞 UI 线程


    不会阻塞 UI 线程


    不会阻塞 UI 线程


    不会阻塞 UI 线程


    重要的事情说三遍 🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣


    基本使用


    主线程生成一个专用 worker


    const myWorker = new Worker("worker.js"); // worker.js 是一个脚本的 URI 来执行 worker 线程

    专用 worker 中消息的接收和发送


    就俩主要方法 postMessage onmessage


    引入脚本与库


    Worker 线程能够访问一个全局函数 importScripts() 来引入脚本,该函数接受 0 个或者多个 URI 作为参数来引入资源;以下例子都是合法的:


    importScripts(); /* 什么都不引入 */
    importScripts("foo.js"); /* 只引入 "foo.js" */
    importScripts("foo.js", "bar.js"); /* 引入两个脚本 */
    importScripts("//example.com/hello.js"); /* 你可以从其他来源导入脚本 */

     ESModule 模式


    const worker = new Worker('worker.js', 
    { type: 'module' // 指定 worker.js 的类型 }
    );

    文件下载代码



    • baseCode


    import { writeFile, utils } from 'xlsx'
    /**模拟生成大文件数据 */
    const generateLargeFileData = () => {
    const data = []
    for (let i = 0; i < 10000; i++) {
    data.push({
    id: i + 1,
    name: `User ${i + 1}`,
    email: `user${i + 1}@example.com`,
    age: Math.floor(Math.random() * 100) + 1
    })
    }
    return data
    }


    • 一只转圈的代码


    /**下载大文件 */
    const downloadExcel = async () => {
    // 模拟生成大文件数据
    const data = generateLargeFileData()
    loading.value = true
    // 模拟一段短暂的等待时间,确保状态更新
    await delay(1000)
    // 卡死的罪魁祸者
    // 将数据转换为 Excel 格式
    const ws = utils.json_to_sheet(data)
    const wb = utils.book_new()
    utils.book_append_sheet(wb, ws, 'Sheet1')
    writeFile(wb, 'test.xlsx')
    loading.value = false
    }


    • 使用webworker,将耗时计算放到 webworker 线程,解决阻塞ui的问题


    主线程



    const myWorker = new Worker('downloadWorker.js')
    myWorker.onmessage = (event) => {
    let wb = event.data
    // 这里也会占用主线程的ui渲染,所以会卡一下
    writeFile(wb, 'test.xlsx')
    ElMessage.success('下载任务已在后台运行,可以继续操作界面其他任务')
    }

    /**下载大文件 */
    const downloadExcel = async () => {
    const data = generateLargeFileData()
    myWorker.postMessage(data)
    }


    worker 线程


    image.png


    // 非模块化文件, public 打包本身就是线上文件了
    importScripts("./xlsx.js"); // 线上地址,或者本地地址

    self.onmessage = (e) => {
    // 将数据转换为 Excel 格式
    const ws = XLSX.utils.json_to_sheet(e.data)
    const wb = XLSX.utils.book_new()
    XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')
    // writeFile(wb, 'test.xlsx') // 这里会操作dom, 所以将操作dom放到 主线程做
    self.postMessage(wb)
    self.close()
    }

    细节补充



    1. 本文主要介绍了专用worker,其实还有 共享 worker【主要做多页面标签通信】, ServiceWorkers 【主要做网络拦截,可以看一下之前写的pwa文章【https://juejin.cn/post/7062681470116036616】,离线缓存就是使用ServiceWorkers】

    2. 在主线程中使用时,onmessage 和 postMessage() 必须挂在 worker 对象上,而在 worker 中使用时不用这样做。原因是,在 worker 内部,worker 是有效的全局作用域(就像window.xxx ,window 一般可以不写)

    3. worker的关闭


    // main.js(主线程)
    const myWorker = new Worker('/worker.js'); // 创建worker
    myWorker.terminate(); // 关闭worker

    // worker.js(worker线程) 
    self.close(); // 直接执行close方法就ok了


    1. worker 错误监听 messageerror

    2. 关于主线程里的 new Worker('downloadWorker.js')


    这个脚本,必须是本地/或者网络地址,这里写的是项目运行地址 匹配相应的worker。这里大家也会发现一个问题,就是这个worker是全局性的,放在public 是一个不错的选择,再者打包后,public 下本身也是会放在服务器上



    1. 用完worker, 要及时关闭,他是不会自己结束的。选择 在worker 关闭,或者主线程关闭,会有区别

    2. 其实小文件下载,用worker 有点画蛇添足,本身使用worker 也是一种消耗



    详细的参考资料以及代码地址


    MDN



    MDN code仓库



    可以下载下来直接调试,最好是起一个本地服务: http-server


    image.png





    代码地址


    gitee.com/Big_Cat-AK-…





    作者:赵小川
    来源:juejin.cn/post/7369633749418934335
    收起阅读 »

    MybatisPlus 使用技巧与隐患

    前言 MP 从出现就一直有争议 感觉一直 都存在两种声音 like: 很方便啊 通过函数自动拼接 Sql 不需要去 XML 再去使用标签 之前一分钟写好的 Sql 现在一秒钟就能写好 简直不要太方便 dislike: 侵入 Service 层 不好维护 可读性...
    继续阅读 »

    前言


    MP 从出现就一直有争议 感觉一直 都存在两种声音


    like:


    很方便啊 通过函数自动拼接 Sql 不需要去 XML 再去使用标签 之前一分钟写好的 Sql 现在一秒钟就能写好 简直不要太方便


    dislike:


    侵入 Service 层 不好维护 可读性差 代码耦合 效率不行 sql 优化比较难


    之前也有前辈说少用 MP 理由就是不好维护 但是这个东西真的是方便 只要不是强制不让用 就还是会去使用 存在集合里 最近也确实有一些体会 就从两个角度去看一下 MP


    优点


    操作简洁


    就从我们编码中最常用的增删改查去说


    按照我们之前去使用 Mybatis 的喜欢我们就要去建立一个 XML 文件 去编写 Sql 语句 算是半自动 我们可以直接去操控 Sql 语句 但是会比较麻烦 很多简单的数据查询我们都要去写一个标签 感觉这种没有意义的操作还是比较烦的 那么 MP 里面怎么实现


    第一种: 最简单我们就是直接去使用提供的方法 我们非常简单就能做到这些操作 但是这个就有一个问题


    nodeMapper.selectById(1);
    nodeMapper.deleteById(2);
    nodeMapper.updateById(new Node());
    nodeMapper.insert(new Node());

    维护性差 以查询为例 这个默认提供的方法都是查询所有字段我们都知道在编写 Sql 的时候第一条优化准则就是不要使用 Select * 因为这种写法是很 Low


    这个就是上面selectById执行的结果


    SELECT Id,name,pid FROM node WHERE Id=?

    这种 Sql 肯定是不好的所以我们在使用 MP 的时候尽量不要去使用自带的快捷查询 我们可以去使用它里面的构造器


    nodeMapper.selectOne(new QueryWrapper().eq("id",1).select("id"));

    这汇总写法 我们可以通过后面的 select() 去指定我们需要查询的字段 算是解决上面那个问题吗 但是这个就完事了吗? 这还有一个问题


    我们在开发中经常会说一个叫魔法值的东西


    //这个就是魔法值 
    if ("变成派大星".equals(node.getName())){
       System.out.println("魔法值");
    }

    之所以不要多用魔法值就是为了后期维护 我们建议使用枚举 或者建一个常量类 通过 Static final 修饰


    上面那段代码是不是也有同样问题 "id"算不算魔法值呢 这种构造器产生的问题就是 不好维护


    假设 我们的这Node类是高度使用的 我们到处都在写


    nodeMapper.selectOne(new QueryWrapper().eq("id",1).select("id"));

    刚开始没事 我们乐呵呵的 但是一旦我去修改 Id 的字段名怎么办



    我修改成 test(数据库同步修改) 现在这个实体类中没有这个字段 我们再去看我们的代码



    没有什么反应 没有给我提示报错 我这个时候去运行怎么办 我要一个个去找这个错误吗 这明显很费时间


    这个确实是一个问题 但是也是可以解决的


    Node node = nodeMapper.selectOne(new LambdaQueryWrapper().eq(Node::getId, 1).select(Node::getId));

    上面这种代码就可以去解决这个问题 我们在使用的时候可以多用这个东西



    一旦修改字段就会立马报错


    但是 这就万事大吉了吗 NO No NO 我们要是处理稍微复杂的语句怎么办? 比如如我们字段求和 这个 LambdaQueryWrapper 还是存在限制的


    如果我们想实现这种 怎么去做呢


    select SUM(price_count) from  bla_order_data LIMIT 100

    首先这种写法肯定是不太行的 编译不通过



    除非去使用QueryWrapper



    还有就是分页查询


    // 条件查询
    LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(UserInfo::getAge, 20);
    // 分页对象
    Page queryPage = new Page<>(page, limit);
    // 分页查询
    IPage iPage = userInfoMapper.selectPage(queryPage , queryWrapper);
    // 数据总数
    Long total = iPage.getTotal();
    // 集合数据
    List list = iPage.getRecords();

    这个还是非常简单的


    简单总结


    MP 在做一些简单的单表查询可以去使用但是对于一些复杂的 SQl 操作还是不要用


    1、SQL 侵入 Service 的问题我们可以仿照 Mybatis 建一个专门存放 MP 查询的包


    2、关于维护性 我们可以尽量去使用 LambdaQueryWrapper 去构造


    3、MP 是有内置的主键生成策略


    4、内置分页插件:基于 Mybatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询。


    缺点


    我就说一个最大的缺点就是对于复杂 Sql 的操作性很不舒服 比如我们去多表查询 你怎么去写呢


    看一个例子




    就是通过


    @Select 注解

    Mp的查询条件嵌入进去
    ${ew.customSqlSegment}


    咱就是一整个大问号 联表老老实实去写 XML 吧 这种真的不要去用 太丑了


    总结


    没有过多的东西 基本都是最近看到的东西


    1、复杂语句不推荐使用 MP 能用最好也别用 可读性差 难维护 使用刚开始没感觉 后期业务扩充 真的恶心的


    2、可以使用 MP 中的分页 比较舒服 逐渐生成策略也舒服


    3、尽量不要去使用 MP 中自带的selectById 等全表查询的方法


    4、尽量使用LambdaQueryWrapper的书写形式 至少比较好维护


    5、简单重复 Sql 可以用 MP。复杂 SQL 不要用




    作者:臻大虾
    来源:juejin.cn/post/7265624177774854204
    收起阅读 »

    面试官喜欢什么样的离职原因?

    hi,你好,我是猿java 面试中,我们似乎总是会被问到一个敏感的话题:你从上家公司离职的原因是什么?如何机智地回答才能获得面试官的芳心呢?今天我们一起来聊一聊。 1.为什么会问离职原因? 马云曾经说过,人离职的原因主要有两种:一是钱给少了,一是心委屈了!既然...
    继续阅读 »

    hi,你好,我是猿java


    面试中,我们似乎总是会被问到一个敏感的话题:你从上家公司离职的原因是什么?如何机智地回答才能获得面试官的芳心呢?今天我们一起来聊一聊。


    1.为什么会问离职原因?


    马云曾经说过,人离职的原因主要有两种:一是钱给少了,一是心委屈了!既然都已经是一个公开的问题,为什么面试官(特别是 HR)还是喜欢提起它?


    其实,面试官问这个问题,绝大情况下是常规操作,担心你入职后会不会也因同样的原因离职;另一个原因是担心小部分人有违法乱纪行为被开除,所以有必要提前筛选一下。


    作为打工人,对于离职原因,我们不鼓励欺骗,但绝对可以高情商回答,让那些看起来对我们不利的离职原因,可以表达得更能够被面试官接受,因此,本文总结了以下几个离职原因的表达方式:


    2.常见的离职原因


    这里,我们列举了几个最常见的离职理由,然后给出了低情商和高情商两种回答,假如你是面试官,也能快速作出判断,最终选择谁!


    2.1 加班太严重


    卷似乎已经成了国内上班的代名词,为了找到一份好工作,即便加班虐你千百次,你也要在面试官面前“违心”地表现出我待加班如初恋的态度,除非你不care这份工作或者有资本对加班NO。那么,面对加班太严重而离职,我们该如何违心地回答呢?


    🚫 低情商回答:前公司加班太严重,太卷了,是头牛都受不了,我实在是卷不动,想躺平。


    ✅ 高情商回答:面试官,您好! 在上家公司,我能高效高质量地完成工作,但公司总会把加班时长作为一个重要的考核指标,导致很多员工为了加班而加班,效率低下,我不反对加班,但是不太认同这种低效的卷,我希望为更人性化的公司创造更大的价值。


    2.2 薪资太低


    雇佣关系绝大多数是建立在合理的薪酬之上,如果工作内容或者强度和薪资严重失衡却得不到调整,那么,离职的概率就会大大增加。因此,面对薪资太低离职,我们该如何回答?


    🚫 低情商回答:上家公司给的工资太低了,而且一直没有调薪,做得没有动力,所以离职了。


    ✅ 高情商回答:面试官,您好! 我过去 3年,在公司和领导的帮助以及自身的努力下,技术能力有了质的提升,为公司做了很多降本增效的项目,领导一直很认可我,可是公司的薪资结构有一些硬指标,无法满足我的涨薪需求,所以想看看新机会,寻找一个可以长期稳步发展的平台。


    2.3 领导很low


    职场上,并不是每个领导都擅长管理,每个领导爬上去的原因也不尽相同,所以,如果职场上遇到优秀的领导请好好珍惜,如果遇到所谓的low领导,也不用太撕破脸,反而需要更加修炼自己的职场软技能,学会和不同的人打交道,因此,遇到low领导而离职该如何表达?


    🚫 低情商回答:前领导太 low,对管理一窍不通,只会吹嘘拍马,跟着他看不到前途,所以离职。


    ✅ 高情商回答:面试官,您好! 因为前公司的业务过于稳定,大部分人每天工作都是在重复,我希望进入一个持续学习和提升的平台,接受更多的挑战。


    2.4 被裁员


    口罩事件之后,裁员弥漫在每一个公司的角落,恐惧也充斥着每个打工人的心里,曾经香饽饽的互联网,如今裁员潮不断,而这些裁员不管你能力有多突出,加班有多厉害,只是纯粹的不赚钱,所以,面对裁员,我们要如何表达自己的无辜?


    🚫 低情商回答:我在之前公司的 KPI不行,被公司裁员了。


    ✅ 高情商回答:面试官,您好! 前公司业务有很大的调整,想让我调岗到其他业务线上,而我个人很想在xxx领域深耕,贵公司的这个岗位和我现在的很匹配,很符合我的职业规划。


    2.5 无法晋升


    晋升在一定程度上是对员工的认可,同时也是改善待遇的窗口,如果一直都在努力付出却得不到晋升机会,难免会打击工作的积极性,如果,当工作了几年之后,能力提升了很多,却一直得不到晋升,这种离职理由该如何来表达?


    🚫 低情商回答:我在前公司做牛做马这么多年,结果晋升的都是老板的亲信,晋升无门,我只能选择离职。


    ✅ 高情商回答:面试官,您好! 我在前公司一直很受领导重用,负责过多个核心业务,领导多次提名我晋升,但都被突发原因夭折了,而我不想安于现状,想找一个更能发挥自己才能的平台。


    2.6 和同事相处不和谐


    职场上,最重要的事情就是与人相处,和同事一起成事,与人打交道绝对是一门终身的必修课,如果是因为和同事相处不和谐离职,如何才能更好地表达出不是因为自己不能融入集体?


    🚫 低情商回答:前公司的同事们都很奇葩,处不到一个好朋友,太孤单了所以离职。


    ✅ 高情商回答:面试官,您好! 我在前公司的沟通能力一直被领导认可,但因为公司内耗较大,很多工作都花在无效的环节上,所以希望找一个氛围好,内耗低的团队长期发展。


    通过上述低情商和高情商的回答对比,我们就可以很清晰的感受到语言的魅力,同样一个理由,在不说谎的前提下,只要我们可以表达的更好,面试官很多也是打工人,他们也能共情到,所以,如果你能高情商的回答,只要不是技能能力不过关,一般也很难被面试官淘汰。


    3.一些现象和建议


    经历过口罩事件后经济下行的这几年,领悟了很多,也看到了很多奇怪的现象:


    职场上,没有谁是不可替代的


    求职时,很多面试官都是曾经和自己一起奋斗的同事


    入职后,发现直属领导或者更上级的领导,曾经是下属


    给予援助之手的,绝大多数是曾经和你关系好或者认可你的人


    给打工人的一些建议:


    选择了一份工作就要全力以赴,不遗余力的提升自己,不要太计较外在因素,这些拼搏绝对是你日后无形的财富。


    日常工作中,一定要和同事保持友善互助的关系,尽量交往几个能相互正向提升的同事,日后他们可能就是你的贵人。


    维护好与领导的关系,不论他优秀与否,相互磨合,相互成就,说不定他日后就可以帮到你。


    如果你是领导,请善待自己的下属,这个时代变化太大,三十年河东,三十年河西,与人方面就是与己方便。打工人需要相互帮扶!


    最后,每个人的阅历不一样,上面 4个建议不一定每条都适合,但是绝对有一个可以受益。




    作者:猿java
    来源:juejin.cn/post/7375086929404837914
    收起阅读 »

    入职第一天,看了公司代码,牛马沉默了

    入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。打开代码发现问题不断读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置 一边获取WEB-INF下的配置文件,一...
    继续阅读 »

    入职第一天就干活的,就问还有谁,搬来一台N手电脑,第一分钟开机,第二分钟派活,第三分钟干活,巴适。。。。。。

    4f7ca8c685324356868f65dd8862f101~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

    打开代码发现问题不断

    1. 读取配置文件居然读取两个配置文件,一个读一点,不清楚为什么不能一个配置文件进行配置

    image.png

    image.png

    image.png 一边获取WEB-INF下的配置文件,一边用外部配置文件进行覆盖,有人可能会问既然覆盖,那可以全在外部配置啊,问的好,如果全用外部配置,咱们代码获取属性有的加上了项目前缀(上面的两个put),有的没加,这样配置文件就显得很乱不可取,所以形成了分开配置的局面,如果接受混乱,就写在外部配置;不能全写在内部配置,因为

    prop_c.setProperty(key, value);

    value获取外部配置为空的时候会抛出异常;properties底层集合用的是hashTable

    public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
    throw new NullPointerException();
    }
    }
    1. 很多参数写死在代码里,如果有改动,工作量会变得异常庞大,举例权限方面伪代码
    role.haveRole("ADMIN_USE")
    1. 日志打印居然sout和log混合双打

    image.png

    image.png

    先不说双打的事,对于上图这个,应该输出包括堆栈信息,不然定位问题很麻烦,有人可能会说e.getMessage()最好,可是生产问题看多了发现还是打堆栈好;还有如果不是定向返回信息,仅仅是记录日志,完全没必要catch多个异常,一个Exception足够了,不知道原作者这么写的意思是啥;还是就是打印日志要用logger,用sout打印在控制台,那我日志文件干啥;

    4.提交的代码没有技术经理把关,下发生产包是个人就可以发导致生产环境代码和本地代码或者数据库数据出现不一致的现象,数据库数据的同步是生产最容易忘记执行的一个事情;比如我的这家公司上传文件模板变化了,但是没同步,导致出问题时开发环境复现问题真是麻烦;

    5.随意更改生产数据库,出不出问题全靠开发的职业素养;

    6.Maven依赖的问题,Maven引pom,而pom里面却是另一个pom文件,没有生成的jar供引入,是的,我们可以在dependency里加上

    <type>pom

    来解决这个问题,但是公司内的,而且实际也是引入这个pom里面的jar的,我实在不知道这么做的用意是什么,有谁知道;求教 a972880380654b389246a3179add2cca~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.jpg

    以上这些都是我最近一家公司出现的问题,除了默默接受还能怎么办;

    那有什么优点呢:

    1. 不用太怎么写文档
    2. 束缚很小
    3. 学到了js的全局调用怎么写的(下一篇我来写,顺便巩固一下)

    解决之道

    怎么解决这些问题呢,首先对于现有的新项目或升级的项目来说,spring的application.xml/yml 完全可以写我们的配置,开发环境没必要整外部文件,如果是生产环境我们可以在脚本或启动命令添加 nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=server/src/main/config/application.properties -jar xxx.jar & 来告诉jar包引哪里的配置文件;也可以加上动态配置,都很棒的,

    其次就是规范代码,养成良好的规范,跟着节奏,不要另辟蹊径;老老实实的,如果原项目上迭代,不要动源代码,追加即可,没有时间去重构的;

    我也曾是个快乐的童鞋,也有过崇高的理想,直到我面前堆了一座座山,脚下多了一道道坑,我。。。。。。!


    作者:小红帽的大灰狼
    来源:juejin.cn/post/7371986999164928010
    收起阅读 »

    从江西到北京2000公里 在少年得到找到了第一份归宿 追求自己的北漂梦

    2000公里来到北京,追求自己的北漂梦 哈喽哈喽,大家好,我是你们的金樽清酒。在经历了很多的面试之后,我找到了第一份实习,从江西到北京两千公里,也就是一张车票和一份努力。这篇文章可能不带太多的技术性,都是我个人到北京到公司的一些感悟。 火急火燎的入职 收到公司...
    继续阅读 »

    2000公里来到北京,追求自己的北漂梦


    哈喽哈喽,大家好,我是你们的金樽清酒。在经历了很多的面试之后,我找到了第一份实习,从江西到北京两千公里,也就是一张车票和一份努力。这篇文章可能不带太多的技术性,都是我个人到北京到公司的一些感悟。


    火急火燎的入职


    收到公司的的offer之后,确定入职的时间,我最快的入职时间就是学校的考试之后,四点多考完之后,收拾收拾就开始赶高铁。从抚州到南昌,再到南昌坐地铁到火车站,说实话,这是我第一次坐地铁,蛮新奇的,后面也基本每天跟地铁打交道了,可能这是从小城市来到大城市的新奇感吧。


    第一次坐上火车。怎么说呢,一言难尽,只能用彻夜难眠来解释。火车上人声的嘈杂,颠簸以极其难闻的味道,但是怎么说,内心也有点激动,毕竟火车的终点是大都市北京。到了北京,前往在网上看好的房子,一个小单间,怎么说,初到北京只能住小单间了,但是老实说我还住过更差的房子,在暑假做家教的时候,一个没有窗户的暗无天日的小房子,关灯就是黑夜,暗无天日,所以说在北京的房子我感觉还好啦。由于赶着入职,把行李给房东就赶着去公司了,一身的疲惫。


    坐地铁去公司。老实说,早高峰的北京地铁,就是脸贴脸的那种。以后,每次坐地铁都是站在门口。由于不认识路,一路误打误撞来到公司办入职。各种的入职手续,领了电脑之后就被hr带到工位。老实说,在北京也就公司能给我一点安慰。我们公司没有打卡,完成自己手头上的事情就可以下班,一切都是约定俗成但是井然有序。公司里面的人也特别的随和,哈哈哈哈哈哈,第一天来做了好多个自我介绍。


    然后,我就认识了我的老师,应该叫mentor吧。


    mentor对我的教诲


    第一天mentor交给我的任务就是配置环境。说实话,这是我第一次配置环境。mentor说自己配环境的机会不多,你想想自己配置环境的机会有多少。我就开始按照网上的教程一步步的摸索如何配置node,怎么用n来管理node的版本。
    第一天,mentor给我单独开了个会,列出了我这一周要学习的东西。我都拿小本本记下来了。


    WechatIMG1.jpg


    WechatIMG2.jpg


    从配置环境到运行项目到开发流程。然后我就开始了开发的前置准备。


    第一天,我只完成了环境的配置。然后就要把项目拉去下来运行项目了。当天开了四个会,一个早会,一个周总,一个部门的技术分享,还有一个是mentor叫我留下来,总结今天的学习。然后就给我解决了项目运行不了的问题,原来是我的node版本过低,为什么要用n或者nvm进行node的版本管理呢?因为不同的项目依赖的node版本不一样。然后又交代给我一个任务,找到一个页面,看懂里面的每一行代码,讲给他听。然后mentor就会把里面我不会的讲解给我听。然后mentor拉我去开评审和复述的会,开始写项目排期了。排期是一项技术活,不能太长或太短,不要给自己留空窗期,以及不能到测试时间完不成任务,他给我盯着排期呢。以后自己排期可是一项技术活。


    现在是开发的第二天了,每天都要review今天的代码,mentor给我指出了很多改进的地方。代码书写的规范,以及写注释,性能优化等。比如一些v-if可以用v-show。为什么,因为可以减少回流重绘。还好,九天的任务,我二天就完成一大半,到联调的阶段了,但不是我有多强,而是我在暑假的时候实习过一个公司写过后台管理,以及mentor叫我看懂每一行代码和element-ui组件库,没开玩笑我现在强的可怕。加油,成长的每一天。


    在北京的难点


    资金窘迫。去的火车票,押一付一的房租,以及提前15天交租,以及各种七七八八的,还得来回学校处理一些事情,来来回回,七七八八,说实话还没赚钱真的喘不过气。有钱男子汉,没钱汉子难啊。每天要挤的地铁,在外面的孤独感。以及不合口味的饮食。北京偏甜,我是江西萍乡的,属于很能吃辣的,到北京感觉都是甜的。mentor知道我吃辣,给我一罐辣酱,说很辣,他蘸一点点都辣的不行。我吃了,额,甜的,一点辣味都没有。唉,生活总是如此的艰难,还好公司不打卡,能学到很多东西,可以摸鱼。加油吧,开心点向前看,在这个世界上谁不难呢,身边基本都是北漂,可以说北京很包容啦。


    总结


    初到北京很激动,我很高兴到少年得到这家公司,我会在这家公司好好实习,提升自己的能力,完成自己的北漂路,人生艰难,但是依旧值得。给你们看看我的公司吧,我还看到了泉灵老师哦,跟我们一起上下班。


    WechatIMG3.jpg


    WechatIMG4.jpg


    WechatIMG5.jpg


    WechatIMG6.jpg


    作者:jinzunqinjiu
    来源:juejin.cn/post/7371633297154621494
    收起阅读 »

    小毛驴 40km 通勤上班:不一样的工作日!

    从到公司上班之后因为距离变远了,也不能像之前一样小毛驴上下班了。 所以通勤方案就变成了: 上班: 小毛驴 15min ----- 地铁 40min ----- 公交OR共享单车 12min + 步行 5min 下班: 公交 12min ----- 地铁 ...
    继续阅读 »

    从到公司上班之后因为距离变远了,也不能像之前一样小毛驴上下班了。


    所以通勤方案就变成了:


    上班:

    小毛驴 15min ----- 地铁 40min ----- 公交OR共享单车 12min + 步行 5min

    下班:

    公交 12min ----- 地铁 40min ----- 小毛驴 15min

    通勤费用: 小毛驴一块钱充电可以开两天。地铁 + 公交 来回 12块。


    这半年下来地铁已经坐够够了。🤦‍♂️ 有的时候实在是不想坐了。就动了开小毛驴的心思。


    但是百度地图看从家到公司的距离是 34km。之前公司到家的百度距离是 18km,其实等于翻翻了。


    而且之前的路况很好么有什么红绿灯而且路上的人也很少。所以基本没有什么时间浪费18km大概半个小时左右就到了。


    本来是想直接买一个新电瓶车来通勤用的,但是碰到那个什么新国标要去考摩托车驾-照就耽搁了。


    然后正好这两天天气还行不冷不热。我就想要买今天就开小毛驴去公司得了。正好熟悉下路况。


    早上还是按照正常出门的时间 7.25 出门。然后按照百度导航直接走。因为第一次开,路况不熟悉。按照百度走的路线全是走的人多的地方。早上正好又是上班高峰期。非机动车道上全部都是人。而且路上的红绿灯贼多。基本遇到一个红绿灯就要停下来。


    前半程车的电量充足速度可以很快,但是路况太差了。路上人太多,而且有占着超车道一直慢悠悠的。开的血压飙升。所以就导致速度起不来。然后到了后半程的时候全是大路。而且没有什么红绿灯也没啥人,但是电量下去了,速度又上不来。脑壳痛!


    最后到公司楼下的时候是 8.42。百度地图显示 34km 需要 2 小时零五分。实际电瓶车里程显示 40km ,耗时一小时 20 分。


    其实 1 小时开车的时间是感知不到的。前半程因为都是人所以精神高度集中。


    另外路上的风景也是不错的。可以走之前没有走到的地方。可以愉快的画图。


    下面早上的时候拍的,因为第一次。怕时间不够。就随便瞎拍了两张记录了一下。


    IMG_20240428_105957.jpg


    IMG_20240428_105852.jpg


    IMG_20240428_110048.jpg


    IMG_20240428_104954.jpg


    IMG_20240428_110017.jpg


    等会晚上回去的时候看看能不能走另外一条路会不会快点。


    IMG_20240427_221436.jpg


    IMG_20240427_221603.jpg


    MVIMG_20240426_192534.jpg


    IMG_20240427_205345.jpg


    IMG_20240427_222136.jpg


    IMG_20240427_221712.jpg


    IMG_20240427_222732.jpg


    IMG_20240427_221628.jpg


    IMG_20240427_221326.jpg


    IMG_20240427_221537.jpg


    作者:执行上下文
    来源:juejin.cn/post/7362729128476524563
    收起阅读 »

    一边敲代码一边晋升宝爸

    2024年农历三月初一,我成功晋升为宝爸。 这一天的10:16分,医生把装着熊宝的婴儿车从产房中推出,医生说:“母子平安,男孩,6斤7两。” 看着这个陌生的、小小的男人,我忐忑的心变得安宁。 在这个世界,我们彼此相遇,有点不知所措,如在梦中。 他嘹亮的啼哭,让...
    继续阅读 »

    成品.jpg


    2024年农历三月初一,我成功晋升为宝爸。


    这一天的10:16分,医生把装着熊宝的婴儿车从产房中推出,医生说:“母子平安,男孩,6斤7两。”


    看着这个陌生的、小小的男人,我忐忑的心变得安宁。


    在这个世界,我们彼此相遇,有点不知所措,如在梦中。


    他嘹亮的啼哭,让我的心弦随之颤动。


    我一动不动的看着他,不敢摸他。


    浓浓的血脉,注定了这一世的羁绊。


    ……


    现在是熊宝出生的第21天了,在护理师的帮助下,我学会了抱娃、拍嗝、换尿布,算是初步顺上手了。


    至此,我可以有些时间写点生娃的回忆和经验,分享给大家。


    我的爱人是在2023年7月份怀孕的。


    清晰记得那天早晨,我爱人跟我说她已经40多天没有来月经了,她可能怀孕了。


    我赶紧在美团买了个验孕棒,一测,出现了两条红线,心中大喜。


    为了万无一失,我又带她去医院检查,医生说:“恭喜你,有喜了。”


    我长吁了一口气,结婚后的一年里,我爱人老担心这事。


    这个时候,我还在山东老家装修房子,想着老来得子,要谨慎一些,所以就计划去北京备产。


    北京消费高,以我当前的经济实力,得找份工作才行。


    所以我就去boss 上看工作,看到滴滴有WebGL工程师的坑,就投了份简历,然后大大小小的面试了五六场才过。


    我工作的地址是中关村壹号,所以,我就近在大牛坊租了个窗户很大的主卧,孕妈的建档就建在了上地医院。


    上地医院不算三甲,但它的整体服务和环境都挺好的,产科是它的特色,看病的人不少,但也不会特别多,一般不需要排太久的队。


    就这样,我开始了白天给公司当孙子,晚上给爱人当孙……,额,是守护神的生活。


    孕初的几个月,我们会每两周去一次医院孕检。什么时候孕检,医生都会提前告知我们,而且孕检手册也会告诉我们整个流程。


    有的孕检是需要验血的,这时就需要空腹。一般我们会很早起床,把水杯和牛肉干装进背包里,然后去医院。


    孕妇饿久了、抽血多了,很容易心慌头晕,再加上孕期情绪敏感。所以,每次孕检我都会陪着她,陪她早去,避免她饿太久,验完血后,我会先给她吃点牛肉干垫垫,然后再去附近找好吃的。


    在之后的时间里,一切还算顺利,熊宝在妈妈的肚子里慢慢的从一颗种子长成鹌鹑蛋、小鸡蛋、小苹果……


    在他有小木瓜那么大的时候,他学会了玩脐带,并且成功的在自己的脖子上缠了一周。


    那段时间,熊宝的小脑延髓池还长得有点快,都0.9了,规定的最大值是1.0,这让我们很是担心。


    我上网查资料说小脑延髓池的值太大会脑子进水,变成一个傻宝。


    幸亏最后都稳定住了,没有再长。


    在孕期第六个月的时候,也就是2023年年底,我们开始考虑胎儿的生产和月子问题。


    我们是没啥经验的,所以想着找个月嫂,或者找个月子中心,房子还得找个至少两居室的,不能再合租了。


    这一堆算下来并不便宜,北京的金牌月嫂是3万,普通的也得2万。中高级的月子中心是7万到10万。两居室的房子8千/月。


    这对于我在滴滴的薪资来说倒也还好,可问题是年底的时候我不想在滴滴干了。


    原因是我所在的HMI部门的管理出了问题,再干下去会很浪费生命。


    思虑再三,我给卡尔动力的老板提了一些建议后,就离职了。


    卡尔动力是我去滴滴时,刚从滴滴脱离出来的创业公司,我在其中负责Web端的三维可视化。


    在我离开公司前,老板把他的微信给我了,我们加了个好友,现在还保持着联系,希望以后会有合作。


    至于我之前所在的那个HMI部门,它在我离开没多久就被打散重组了,组长也被撤了。很佩服老板的雷厉风行。


    其中太具体的事情我就不再多说了,咱们继续谈生娃的事。


    我离开滴滴后,就把大牛坊的房子退了,东西都寄回了老家。


    接下来,我们计划在山东的潍坊老家生娃。


    之所以如此,有多方面的考虑:


    我们已经在北京完成了孕前和孕中的检查,胎儿很健康,所以没必要再纠结于北京。


    潍坊有多个三甲医院,其医疗技术虽然比不得北京,但生娃也不是那种只有一线城市才能做的、技术难度很高的事。


    北京消费高,没较高的收入就不适合再待在北京了,那时我租的房子也正好到期。而潍坊的消费是很低的,我们每天120元就可以在医院对面短租很精致的小米全屋智能公寓。潍坊中高端的月子中心一个月2万。


    我刚好接到了我上上家公司的一个单子,可以在老家工作,小赚一笔。有很多时候,我离开公司,并不一定是我和公司闹掰了,其原因很多的。


    我爸妈在老家自己种着蔬菜,有鸡鹅牛羊,自己家的东西吃着放心。


    就这样,我们在北京待了四个月后,回到了潍坊老家。


    在老家的日子里,孕妈的饮食都挺好的。我们吃东西前,都会从一个叫“孕育树”的APP上查查孕妈能不能吃。


    其实,查一种食物是否适合孕妈吃是很简单的,但难的是你可能会忘了查,或者自以为不用查。


    以前我爱人就有过几次,比如煮鸽子汤的时候放了某一种中药,喝了两三次后才想起查一下,结果发现那种中药容易让孕妇流产;还有一次,我对象以为自己可以吃桂圆,买了一堆后,我给她一查,发现孕妇不能吃。


    孕妈在怀孕的时候,很容易傻傻的可爱,这需要我们多上点心。


    我家孕妈的精神状态一直是我比较担心的,因为她有点熊,熊孩子的熊,再加上有几个词叫“产前抑郁”和“产后抑郁”,所以我会格外注意和防范她的情绪问题。


    我会时刻告诉自己不能让她生气,学会换位思考,照顾好她的方方面面,她说得都对,不因小事而计较,不轴,不抬杠,努力逗她开心。


    除此之外,赚钱也很重要,因为很多时候钱是可以换来快乐和舒适的。


    在这期间,我给我工作过的上上家公司开发了一个三维机器人的交互展示项目,基本上能够后面的开支。


    我还把去滴滴时遇到的面试题做成了一个低价的付费课-《canvas进阶-面试题》,想着以后多多少少给熊宝赚点奶粉钱。


    每天我也依旧会拿出一点时间去学习,让自己保持一个持续成长的状态。


    与此同时,熊宝在妈妈的肚子里也持续成长,长成了一个小西瓜。


    熊宝玩脐带的能力也进步了,他成功把脐带在自己脖子上又绕了一周,成为了绕颈两周。


    直到熊宝的脑袋入盆的时候,还是两周,医生说:“你们别再想让他绕回来了,当然,也不用担心他再绕更多了。”


    在这期间,我们一感觉熊宝不咋蛄蛹了,就赶紧用自己买的胎心仪测测,生怕他因为绕颈两周而缺氧。


    有的时候熊宝很皮,半天不动,胎心还换了位置,我们常常大半夜的测胎心测好久,都快把她妈吓哭了,直到在一个思维盲点听到强劲有力的小火车声,才放下心来。


    在孕妈离预产期还有20天的时候,我们住在了潍坊阳光融合医院对面的一间环境舒适,干净卫生,可以洗衣做饭的小米全屋智能公寓里。


    在这个时候,孕妈基本上就是随时可以生的了,所以我们需要住在医院旁边,以防突发情况。


    我们住下来的当天,还去潍坊妇幼保健院做了孕检,检查结果并不理想,医生说:“胎心加速不及格,若一直这样,就得刨宫产。”


    我爱人当时就被吓哭了,我们一直都想顺产的,我不想在她的肚子上开一道口子。


    我从网上查了一下,导致胎心加速不行的原因是有很多种的,比如胎儿睡着了,或者妈妈没吃好,胎儿饿着了,不想动。


    这天我爱人确实没吃好,我就跟她说:“我们去吃火锅吧,没有什么是一顿火锅解决不了的。”


    于是我就带着哭得梨花带雨的孕妈吃了顿火锅。


    吃完后,我们没有去潍坊妇幼保健院孕检,而是就近去了我家对面的阳光融合医院。


    在阳光融合医院,医生说胎心加速及格了,虽不是那么理想,但也没有问题。


    至此,我们搬来医院对面的第一天可以睡个好觉了。


    一周后的上午,我们又去潍坊妇幼保健院做了孕检,胎心加速还是不理想,医生让我们下午住院,观察情况,可能要刨宫产。


    我没住,我们去阳光融合医院又做了一次孕检,结果胎心加速还是合格的,所以就没去住院。


    我当时的想法是,阳光融合和潍坊妇幼保健院都是三甲医院,只要有一个测着可以不刨,我们就不刨。


    我们也想过为什么两个同样三甲的医院的测试结果不一样,其原因也不一定是哪个医院不行。


    也可能是因为我去潍坊妇幼保健院都是上午去的,而去阳光融合医院都是下午或晚上去的。


    我在滴滴的时候,回家晚,睡得晚,起得晚,熊他妈也一定要等我回来才睡。这让熊宝在娘胎里面变成了一个小夜游神。


    熊宝在上午的时候总是老老实实的不咋动,等到了晚上就总在妈妈肚子里手舞足蹈。


    所以现在上午去医院测胎心的时候,熊宝可能还没起床,等晚上测胎心的时候,就来了精神了。


    记得离预产期还有10天左右的时候,我们还是上午去潍坊妇幼做孕检,医生直接要让我们下午就做刨宫产,她说:“原因有五:胎心加速不行,绕颈两周,产龄偏高,做过锥切,已经临近预产期。”


    我没有照做,我带熊他妈又去吃了一顿火锅,然后睡了一个午觉,养足了精神,就去了潍坊人民医院。


    潍坊人民医院是综合性医院,属于潍坊医院里的老大。


    我们找到了经验丰富的胡明英医生做检查,胡明英医生是一位很有名望的医生,她本应退休却又被返聘回去了,只因为她当了妇产医生后,就闲不住了。


    胡明英医生给我们做了全面细致的检查,得出以下结论:


    ● 胎心加速是合格的。


    ● 绕颈两周并不算太大的问题,因为小孩在顺的时候,还是可以转出来的,当然这也并非绝对。


    ● 脐带的血液流速和供氧都没问题,并未受绕颈两周的影响。


    ● 孕妇年龄35,并没有超出可以顺产的年龄范畴。


    ● 微小面积锥切,且并非疤痕体质,并不影响顺产。


    两天后,胡明英医生又给孕妈做了骨盆检查,最终结论是:可以顺产,等瓜熟落地即可。


    接下来胡医生就给孕妈开了住院单,当孕妈出现规律宫缩的时候,就可以直接来住院。


    期间,我们又去阳光融合查了一次,得到的结果依旧是可以顺产,如此我们才算放心。


    虽然我不懂医术,但基本逻辑我还是懂的。之前潍坊妇幼保健院在未经全面、细致分析的前提下,让孕妈当天下午就做刨宫产的行为有些武断了。


    2024年4月8日的晚上,孕妈发生规律性宫缩,大约每隔半个小时一次。


    我们立刻去了对面的阳光融合医院的急诊楼做检查,医生说快生了,让我们立刻住院。


    我说我在胡明英医生那里挂了号,我们要去潍坊人民医院。


    阳光融合的医生说她是胡明英医生的学生,让我们放心去潍坊人民医院就行,别再换其它地方了,现在的孕妈快生了,不能再折腾。


    孕妈从规律宫缩到可以顺产,还会经历至少几个小时的开指时间,所以我花个十五分钟去潍坊人民医院的时间还是有的。


    如果这个时候是孕妈的羊水破了,我就会选择直接在阳光融合医院顺产,当然,这个时候的医生也肯定不会再让我们走了。


    我们去了潍坊人民医院后,就拿着住院单住进了候产房。


    这时候的孕妈已经五六分钟宫缩一次了,而且疼感很强烈。


    医生让她等着,等着开指。


    这个过程孕妈很痛苦,她一直疼到第二天晚上,已经有些虚脱了。她虚弱的躺在床上,每次宫缩都会大口喘气,看着很让人揪心。


    我一点点的喂她吃着晚饭,她知道后面的顺产需要体力,强忍着剧痛一点点吃掉我喂她的西蓝花、菠菜和馒头。


    她每次宫缩,我都会给她按摩腰部,这样可以缓解一下她盆骨松动的痛苦。


    在夜间两点的时候,她把吃过的晚饭都吐了,她有气无力跟我说:“我生不动了,我没力气了,你去跟医生说吧,让我刨吧。”


    我紧紧的握着她的手说:“你再坚持一下吧,你想想我们为了顺产,经历了那么多,我们在3个医院间周折往复,你现在刨的话就前功尽弃了。”


    我稳定住她的情绪后,就去了护士站,跟医生说:“11号床有点撑不住了,你可以去看看吗?”


    于是医生就去给她做了检查,跟我说:“她快开到三指了,可以打无痛了。”


    医生把她连床带人一起推进了产房。


    我站在产房门口,不能进去。


    那一夜我没有睡,这是我第一次连着两个晚上没有睡觉,却毫无睡意。


    早晨6点左右,医生跟我说:“她打完无疼后,已经睡了一会,现在醒了,你去给她买点早餐,她预计中午能生。”


    此时我的心算是稍微放下了一些,给她买了几个素包子和小米粥,外加一瓶脉动。医生说能量型饮料可以给她快速补充体力。


    在忐忑等待的时间里,我还跟医院签了一个脐带血的储存协议,医生说以后孩子遇到了白血病、肝硬化等病,可以用脐带血治疗。


    我虽不希望有那么一天,但多一份保障还是好的。


    一直等到上午十点多的时候,医生终于告诉我生了,母子平安!


    从此,我也是有娃的人了。


    接下来,我们在医院的单间住了四天,没什么问题后,就去了月子中心,准备在月子中心住上28天。我因为没什么经验,为确保万无一失,只能花钱解决一些问题了。


    在之后的日子里,我会研究一下育儿之道,看看《好妈妈胜过好老师》,同时努力赚钱养娃。


    后面有啥心得,我会再分享给大家。


    最后给大家总结一下我这一路走来的经验:


    ● 尽量让孕妈规律作息,不要像我似的把熊宝养成了小夜游神。


    ● 孕妈吃的每一种食物都要提前查一下能不能吃。


    ● 孕妈情绪很重要,一定要百般呵护,比如孕检的时候要全程陪伴,不要惹她生气。


    ● 孕妈吃啥和该怎么活动,网上都有,我觉得这是比较简单的。


    ● 当孕妈接近预产期,规律宫缩的时候,一定要立刻去医院,这很重要,千万别拖,即使这是在晚上你睡得正香的时候。


    今年上海就有个宝妈晚上规律宫缩,拖到了早上才去医院,结果堵车了,还没进医院就把宝宝生车里了,但胎盘没出来,这极其危险。还好最后母子平安。


    ● 尽量顺产。如果有医院让你刨,除非紧急情况,不要立刻刨,尽快多换几家更好的、至少三甲的医院看看。


    我尊重医生,但我并不觉得每个医生都是白衣天使。就像曾经魏则西事件,还有今年北京积水潭医院原院长田伟落马,这都说明职业和权利并不会决定人之善恶。


    与此同时,我们也不要拿网上看来的知识来挑战医生的专业,网上知识仅供参考,具体怎么做要听医生的。不过,这与我多找几个更专业的医生问问并不冲突。


    ● 努力赚钱,有很多事都是可以用钱来解决的。


    作者:李伟_Li慢慢
    来源:juejin.cn/post/7367174168599150602
    收起阅读 »

    如何让不同Activity之间共享同一个ViewModel

    问题背景 存在一个场景,在Acitivity1可以跳转到Activity2,但是两个Activty之间希望能共享数据 提出假设的手段 可以定义一个ViewModel,让这两个Activity去共享这个ViewModel 存在的问题 根据不同的Lifecycle...
    继续阅读 »

    问题背景


    存在一个场景,在Acitivity1可以跳转到Activity2,但是两个Activty之间希望能共享数据


    提出假设的手段


    可以定义一个ViewModel,让这两个Activity去共享这个ViewModel


    存在的问题


    根据不同的LifecycleOwner创建出来的ViewModel是不同的实例,所以在两个不同的Activity之间无法创建同一个ViewModel对象


    问题分析


    先来梳理一下一个正常的ViewModel是怎么被构造出来的:



    1. ViewModel是由ViewModelFactoty负责构造出来

    2. 构造出来之后,存储在ViewModelStore里面
      但是问题是ViewModelStore是 和 (宿主Activity或者Fragment)是一一对应的关系
      具体代码如下


    @MainThread  
    public inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline factoryProducer: (() -> Factory)? = null
    ): Lazy<VM> {
    val owner by lazy(LazyThreadSafetyMode.NONE) { ownerProducer() }
    return createViewModelLazy(
    VM::class,
    { owner.viewModelStore },
    {
    (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras
    ?: CreationExtras.Empty
    },
    factoryProducer ?: {
    (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelProviderFactory
    ?: defaultViewModelProviderFactory
    })
    }

    看到上面的代码第9行,viewModelStore和owner是对应关系,所以原则上根据不同LifecycleOwner无法构造出同一个ViewModel对象


    解决思路



    1. 无法在不同的LifecycleOwner之间共享ViewMode对象的原因是:ViewModel的存储方ViewModelStore是和LifecycleOwner绑定,那如果可以解开这一层绑定关系,理论上就可以实现共享;

    2. 另外我们需要定义ViewModel的销毁时机:


      我们来模拟一个场景:由Activty1跳转到Activity2,然后两个Activity共享同一个ViewModel,两个activity都要拿到同一个ViewModel的实例,那这个时候ViewModel的销毁时机应该是和Acitivity1的生命周期走,也就是退出Activity1(等同于Activity1走onDestroy)的时候,去销毁这个ViewModel。



    所以按照这个思路走,ViewModel需要在activity1中被创建出来,并且保存在一个特定的ViewModelStore里面,要保证这个ViewModelStore可以被这两个Activity共享;


    然后等到Activity2取的时候,就直接可以从这个ViewModelStore把这个ViewModel取出来;


    最后在Activity1进到destroy的时候,销毁这个ViewModel


    具体实现


    重写一个ViewModelProvider实现如下功能点:



    1. 把里面的ViewModelStore定义成一个单例供所有的LifecycleOwner共享

    2. 定义ViewModel的销毁时机: LifecycleOwner走到onDestroy的时机


    // 需要放到lifecycle这个包,否则访问不到ViewModelStore
    package androidx.lifecycle

    class GlobalViewModelProvider(factory: Factory = NewInstanceFactory()) :
    ViewModelProvider(globalStore, factory) {
    companion object {
    private val globalStore = ViewModelStore()
    private val globalLifecycleMap = HashMap<String, MutableSet<Lifecycle>>()
    private const val DEFAULT_KEY = "androidx.lifecycle.ViewModelProvider.DefaultKey"
    }

    @MainThread
    fun <T: ViewModel> get(lifecycle: Lifecycle, modelClass: Class<T>): T {
    val canonicalName = modelClass.canonicalName ?: throw IllegalArgumentException("Local and anonymous classes can not be ViewModels")
    return get(lifecycle, "$DEFAULT_KEY:$canonicalName", modelClass)
    }

    @MainThread
    fun <T: ViewModel> get(lifecycle: Lifecycle, key: String, modelClass: Class<T>): T {
    if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
    throw IllegalStateException("Could not get viewmodel when lifecycle was destroyed")
    }
    val viewModel = super.get(key, modelClass)
    val lifecycleList = globalLifecycleMap.getOrElse(key) { mutableSetOf() }
    globalLifecycleMap[key] = lifecycleList
    if (!lifecycleList.contains(lifecycle)) {
    lifecycleList.add(lifecycle)
    lifecycle.addObserver(ClearNegativeVMObserver(lifecycle, key, globalStore, globalLifecycleMap))
    }
    return viewModel
    }

    private class ClearNegativeVMObserver(
    private val lifecycle: Lifecycle,
    private val key: String,
    private val store: ViewModelStore,
    private val map: HashMap<String, MutableSet<Lifecycle>>,
    ): LifecycleEventObserver {
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
    if (event == Lifecycle.Event.ON_DESTROY) {
    val lifecycleList = map.getOrElse(key) { mutableSetOf() }
    lifecycleList.remove(lifecycle)
    if (lifecycleList.isEmpty()) {
    store.put(key, null)
    map.remove(key)
    }
    }
    }
    }
    }

    具体使用


    @MainThread  
    inline fun <reified VM: ViewModel> LifecycleOwner.sharedViewModel(
    viewModelClass: Class<VM> = VM::class.java,
    noinline keyFactory: (() -> String)? = null,
    noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null,
    )
    : Lazy<VM> {
    return SharedViewModelLazy(
    viewModelClass,
    keyFactory,
    { this },
    factoryProducer ?: { ViewModelProvider.NewInstanceFactory() }
    )
    }

    @PublishedApi
    internal class SharedViewModelLazy<VM: ViewModel>(
    private val viewModelClass: Class<VM>,
    private val keyFactory: (() -> String)?,
    private val lifecycleProducer: () -> LifecycleOwner,
    private val factoryProducer: () -> ViewModelProvider.Factory,
    ): Lazy<VM> {
    private var cached: VM? = null
    override val value: VM
    get() {
    return cached ?: kotlin.run {
    val factory = factoryProducer()
    if (keyFactory != null) {
    GlobalViewModelProvider(factory).get(
    lifecycleProducer().lifecycle,
    keyFactory.invoke(),
    viewModelClass
    )
    } else {
    GlobalViewModelProvider(factory).get(
    lifecycleProducer().lifecycle,
    viewModelClass
    )
    }.also {
    cached = it
    }
    }
    }

    override fun isInitialized() = cached != null
    }

    场景使用


    val vm : MainViewModel by sharedViewModel()

    作者:红鲤驴
    来源:juejin.cn/post/7366913974624059427
    收起阅读 »

    用了这么久SpringBoot却还不知道的一个小技巧

    前言 你可能调第三方接口喜欢启动application,修改,再启动,再修改,顺便还有个不喜欢写JUnitTest的习惯。 你可能有一天想要在SpringBoot启动后,立马想要干一些事情,现在没有可能是你还没遇到。 那么SpringBoot本身提供...
    继续阅读 »

    前言



    你可能调第三方接口喜欢启动application,修改,再启动,再修改,顺便还有个不喜欢写JUnitTest的习惯。




    你可能有一天想要在SpringBoot启动后,立马想要干一些事情,现在没有可能是你还没遇到。




    那么SpringBoot本身提供了一个小技巧,很多人估计没用过。



    正文


    1、效果



    废话不多说,先写个service和controller展示个效果最实在。




    来个简单的service



    @Service
    public class TestService {

    public String test() {

    System.err.println("Hello,Java Body ~");
    return "Hello,Java Body ~";
    }
    }


    再来个简单的controller



    @RestController
    @RequestMapping("/api")
    @AllArgsConstructor
    public class TestController {

    private final TestService testService;

    @GetMapping("/test")
    public ResponseEntity test() {
    return ResponseEntity.ok().body(testService.test());
    }
    }


    接下来是不是以为要启动调接口了,No,在SpringBoot的启动类中加这么个玩意儿



    @SpringBootApplication
    public class JavaAboutApplication {

    public static void main(String[] args) {
    SpringApplication.run(JavaAboutApplication.class, args);
    }

    @Bean
    CommandLineRunner lookupTestService(TestService testService) {
    return args -> {

    // 1、test接口
    testService.test();

    };
    }

    }


    启动看下效果



    4.png



    可以发现,SpringBoot启动后,自动加载了service的执行程序。




    这个小案例是想说明什么呢,其实就是CommandLineRunner这么个东西。



    2、它是什么



    CommandLineRunner是一个接口,用于在Spring Boot应用程序启动后执行一些特定的任务或代码块。当应用程序启动完成后,Spring Boot会查找并执行实现了CommandLineRunner接口的Bean。




    说白了,就是SpringBoot启动后,我立马想干的事,都可以往里写。



    3、我用它做过什么



    我的话,和很多厂家对接过接口,在前期不会直接开始写业务,而是先调通接口,再接入业务中。




    比如webservice这种,我曾经使用CommandLineRunner直接调对方接口来测试,还挺舒适,也节省了IDEA资源,但要注意调试完成后注释掉,本地测试的时候再打开就行。



    5.png


    4、它还有哪些用途



    除了可以拿来调试第三方接口,它还有什么用途吗?




    其实开头已经说过,它就是SpringBoot启动后,你立马想干的事,都可以在里面写,所以你完全可以发挥想象去用。




    我这里,提供几个思路作为参考。



    1)、数据库初始化


    你可以使用CommandLineRunner来执行应用程序启动时的数据库初始化操作,例如创建表格、插入初始数据等。



    2)、缓存预热


    CommandLineRunner在应用程序启动后预热缓存,加载常用的数据到缓存中,提高应用程序的响应速度。



    3)、加载外部资源


    加载一些外部资源,例如配置文件、静态文件或其他资源。CommandLineRunner可以帮助你在启动时读取这些资源并进行相应的处理。



    4)、任务初始化


    使用CommandLineRunner来初始化和配置某些定时任务,确保它们在应用程序启动后立即开始运行。



    5)、日志记录


    SpringBoot启动后记录一些必要的日志信息,如应用程序版本、环境配置、甚至启动时间等等,这个看具体需求。



    6)、组件初始化


    你可能需要按照特定的顺序初始化一些组件,CommandLineRunner可以帮助你控制初始化顺序,只需要将它们添加到不同的CommandLineRunner实现类中,并使用@Order注解指定它们的执行顺序即可。



    总结



    其实,能用的地方挺多,我最后再举个例子,netty启动时,往往是绑定了端口并以同步形式启动。




    但如果要和SpringBoot整合,我们不可能还那么做,而是交给SpringBoot来控制netty的启动和关闭,当SpringBoot启动后,netty启动,当SpringBoot关闭时,netty自然也关闭了,这样才比较优雅。




    那么,我们完全可以将netty的启动执行程序放到CommandLineRunner中,这样就可以达到目的了。




    没用过的xdm,今天学会一个新知识点了不,可以自己下去试试哦。


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

    程序员工作七年后的觉醒:不甘平庸,向上成长,突破桎梏

    前言 Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。 昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。 看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,...
    继续阅读 »

    前言


    Hi 你好,我是东东拿铁,一个正在探索个人IP&副业的后端程序员。


    昨天看到雪梅老师公众号的文章,《中国的中年男性,可能是世界上压力最大的一群人》。


    看完文章,深有感触,因为自己有时候,觉着压力挺大的,因为从小到大,一直感觉世俗上的一些条条框框,在约束着你。


    上学时,你要好好读书,争取考上985/211,最起码上个一本。


    工作后,大家都羡慕考公上岸的,上不了岸的话,你需要找一个好公司,拿到一个高工资,最好还能当上管理人员。


    后来有了家庭,你要承担起男人的责任,赚钱养家。


    过去20多年的时间,我都觉着这样的条条框框没有问题,在年少轻狂的时光,这些条条框框决定了你的下限,大家不都是这么过来的吗?


    可是我过去的努力,都是为了符合条条框框的各项要求。我越来越觉着疑惑,我的努力,到底是为了什么啊,是为了这些世俗上的要求吗,我到底为谁而活?


    压力,是自己给的


    说实话,自己也给自己不少压力。


    刚毕业,没有房贷车贷的情况下,我便给了自己很大的压力。压力怎么来的呢?比如一个月五千块钱的工资,买不起一个最新款的iPhone,又比如北京的朋友们,工资相比我在二线城市,竟能高出我一倍。


    后来工作半年决定去北京,也是工作7年来,唯一一次的裸辞。


    初生牛犊不怕虎,裸辞给我带来的毒打,至今历历在目,比如银彳亍卡余额一天天减少的焦虑,比如连面试都没有的焦虑,还有时刻担心着要是留不在北京,被迫得回老家的焦虑。


    记得青旅楼下,有一家串店叫“很久以前羊肉串”,不到五点的时候门口就会有人排队,晚上下楼时看着饭店里熙熙攘攘,吃着烤串喝着扎啤的人时,心里十分羡慕,但却又不会踏进饭店一步。


    毕竟一个目前找不到工作的人,每天一睁眼就是吃饭和住青旅的成本,吃个20块钱一顿的快餐就好了,怎么可能花好几百下馆子呢?


    那时候心里有个愿望,就是我也想每周都可以和朋友来这里吃顿烧烤、喝喝扎啤。


    嗯,我也不知道为什么,那时候对自己就是这么严苛。家庭虽不算富裕,但也绝不可能差这几顿烧烤、住几晚好的宾馆的钱,但我就是这样像苦行僧一样要求着自己,仿佛在向爸妈多要一分钱,就代表着自己输了。


    后来工作稳定了,工资也比毕业时翻了几倍,恰巧又在高位上车了房子,但似乎压力只增不减,同样是不敢花钱。


    现在又有了娃,这次压力也不用自己给了,别管他需要什么,一个小眼神,你只想给他买最好的。因此不敢请假,更不敢裸辞GAP一段时间了,这种感觉就像是在逃避赚钱的责任,不误正业一般。


    一味的向前冲


    带着压力,只能一味的向前冲,为了更高的薪资不断学习,为了更高的职级不断拼搏。


    在“赚钱”这件事上,男人的基因里就像被编写好了一段代码。


        while (true){
    makeMoreMoney();
    }

    过程中遇到困难,压力大,有难过的时候怎么办,身边有谁能去诉说呢?


    中国的传统文化便是“男儿当自强”、“男儿有泪不轻弹”,怎么能去向别人诉说自己的痛苦呢?


    那时候现在的老婆那时候还在上学,学生很难理解职场。结婚后,更没有人愿意在伴侣前展示自己的软弱。


    和家人说?但是不开心的事,不要告诉妈妈,她帮不上忙,她只会睡不着觉。


    和好朋友们一起坐下聚聚,喝几杯啤酒,少聊一些工作,压力埋在心里,让自己短暂的放松一下。



    但现在的行业现状,不允许我们一味的在职场上冲了。


    行业增速放缓,互联网渗透率达到瓶颈,随着而来的就是就业环境变差,裁员潮来袭。


    你可以选择在职场中的高薪与光环,但也要付出相应的代价,比如变成“云老公/老婆”,“云爸爸/妈妈”。


    或许我们都很想在职场中有一番作为,但是外部环境可能会让我们头破血流。


    为了家庭,所以在职场中精进自己,升职加薪。我不禁在想,这看似符合逻辑的背后,我自己到底奋斗的是什么


    不甘平庸,不服输


    从老家裸辞去北京,是不满足于二线城市的工作环境,想接触互联网,获得更快的进步。


    在北京,从小公司跳槽到大厂,是为了获得更高的薪资与大厂的光环。


    再次回到老家,是不满生活只有工作,回来可以更好的平衡工作和生活。


    回想起来,很多时候,自己就像一个异类。


    明明工作还不满一年,技术又差,身边的朋友敢于跳槽到其他公司,涨一两千块钱的工资已经算挺好了,我却非得裸辞去北京撞撞南墙。


    明明可以在中小公司里按部就班,过着按点下班喝酒打游戏的生活,却非得在在悠闲地时候,去刷算法与面经,不去大厂不死心。


    明明可以在大公司有着不错的发展,负责着团队与核心系统,却时刻在思考生活中不能只有工作,还要平衡工作和家庭,最终放弃大厂工作再次回到老家。


    每一阶段,我都不甘心于在当下的环境平庸下去,见识到的优秀的人越多,我便越不服输。


    至此,我上面问自己的两个问题,我到底为谁而活?我自己到底奋斗的是什么,似乎有了些答案。


    我做的努力,短期看是为了能够给自己、给家人更好的物质生活,但长远来看,是为了能让自己有突破桎梏与困境,不断向上的精神


    仰望星空


    古希腊哲学家苏格拉底有一句名言:“未经检视的人生不值得活。”那么我们为什么要检视自己的人生呢?正是因为我们有不断向上的愿望,那么我在想愿望的根源又到底是什么呢?


    既然选择了不断向上,我决定思考,自己想成为什么样的人,或者说,一年后,希望自己变成什么样子,3年呢,5年呢?


    当然,以后的样子,绝不是说,我要去一个什么外企稳定下来,或者说去一个大厂拿多少多少钱。


    而是说,我希望的生活状态是什么,我想去做什么工作/副业,达成什么样的目标。


    昨天刷到了一个抖音,这个朋友在新疆日喀则,拍下了一段延时摄影,我挺受震撼的。



    生活在钢铁丛林太久了,我一直特别想去旅行,比如自驾新疆、西藏,反正越远越好。在北京租的房子,就在京藏高速入口旁,我每天上班都可以看到京藏高速的那块牌子,然后看着发会呆,畅想一下自己开着车在路上的感觉。


    可好多年过去了,除了婚假的时候出去旅行,其余时间都因为工作不敢停歇,始终没有机会走出这一步,没有去看看祖国的大好河山。


    我还发现自己挺喜欢琢磨,无论在做什么事情,我都会大量的学习,然后找到背后运行的规律。因为自己不断的思考,所以现实中,很少有机会和朋友交流,所以我会通过写作的方式,分享自己的思考、经历、感悟。


    我写了不少文章,都是关于工作几年,我认为比较重要的经历的文章,也在持续分享我关于职业生涯的思考。


    从毕业到职场,走过的弯路太多了,小到技术学习、架构方案设计,大到职业规划与公司选择,每当回忆起自己在职场这几年走过的弯路,就特别想把一些经验分享给更多的人,所以我持续的写,希望看到我文章的朋友,都能够对工作、生活有一点点帮助。


    所以,我的短期目标,是希望能够帮助在职场初期、发展期,甚至一些稳定期的朋友们,在职场中少一点困惑,多一点力量


    方式可能有很多,比如大家看我的文章,看我推荐的书籍、课程,甚至约我电话进行1v1沟通,都可以,帮助到一个人,我真的就会感到很满足,假设因为个人能力不足暂时帮不到,我也能根据自己的不足持续学习成长。


    那么一年后,我希望自己变成什么样?
    我希望自己在写作功底上,能够持续进步,写出更具有逻辑性、说服力的内容,就像明白老师、雪梅老师那样。公众号希望写出一篇10w+,当然数量越多越好,当然最希望的是有读者能够告诉我,读完这篇文章很有收获,这样比数据更能让人开心,当然最好还能够有一小部分工作之外的收入。


    那么三年呢?
    3年后,快要32岁了。希望那时候我已经积累了除了写作外,比如管理、销售、沟通、经营能力,能够有自己赚到工资外收入的产品、项目,最好能够和职场收入打平,最差能够和房贷打平,有随时脱离职场的底气。


    五年呢?十年呢?
    太久远了,想起来都很吃力的感觉。我一定还在工作,但一定不是打工,希望自己有了一份自己喜欢的事业,能够买到自己的dream car,然后能够随时带着家人看一看中国的大好河山。


    你是不是想问,为什么一定要想这些?


    因为当我想清楚这个问题的时候,那当下该做什么事情,该做什么选择,就有了一个清晰的标准:这件事情、这个选择,能否帮我们朝「未来的自己」更进一步?


    这时候当再遇到压力、困难,我们就会变的乐观,有毅力、有勇气、自信、耐心,积极主动。


    因为你自己想干成一件事,你就会迸发出120%的能量。


    当然,也希望自己试试放下盔甲,允许自己撤退,允许自己躺平,允许自己怂,允许自己跟别人倾诉痛苦。


    说在最后


    说了很多,感谢你能看到最后。


    感觉整体有点混乱,但还是总结一下:


    起因是感觉自己压力很大,因为持续的大量输入导致自己有点陷入信息爆炸的焦虑,有一天下班到家时感觉头痛无比,九点就和孩子一起睡觉了,因此本来想谈谈中国男性的压力。


    但不由自主的去思考自己的压力是从哪里来的,去发现压力竟都来源于传统文化、社会要求,于是越想越不服气,我为什么非得活成别人认为应该活成的样子?


    于是试着思考自己想成为什么样子,其实也是一直在琢磨的一件事情,因为当开始探索个人IP的时候,我就发现自己需要更高一层的、精神层面的指导,才能让自己坚持下去。


    如果你和我一样,希望你给自己的压力更小一些,环境很差,但总还有事情可以去做,愿你可以想清楚,你想成为的样子。一时想不清楚也没关系,也愿你可以允许自己撤退,允许自己软弱。


    不知道你有没有想过,自己想要成为的样子呢?


    作者:东东拿铁
    来源:juejin.cn/post/7374337202653265961
    收起阅读 »

    如何优雅的将MultipartFile和File互转

    我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。 前言 首先来区别一下MultipartFile和File: MultipartFile是 S...
    继续阅读 »

    我们在开发过程中经常需要接收前端传来的文件,通常需要处理MultipartFile格式的文件。今天来介绍一下MultipartFile和File怎么进行优雅的互转。


    前言


    首先来区别一下MultipartFile和File:



    • MultipartFile是 Spring 框架的一部分,File是 Java 标准库的一部分。

    • MultipartFile主要用于接收上传的文件,File主要用于操作系统文件。


    MultipartFile转换为File


    使用 transferTo


    这是一种最简单的方法,使用MultipartFile自带的transferTo 方法将MultipartFile转换为File,这里通过上传表单文件,将MultipartFile转换为File格式,然后输出到特定的路径,具体写法如下。


    transferto.png


    使用 FileOutputStream


    这是最常用的一种方法,使用 FileOutputStream 可以将字节写入文件。具体写法如下。


    FileOutputStream.png


    使用 Java NIO


    Java NIO 提供了文件复制的方法。具体写法如下。


    copy.png


    File装换为MultipartFile


    从File转换为MultipartFile 通常在测试或模拟场景中使用,生产环境一般不这么用,这里只介绍一种最常用的方法。


    使用 MockMultipartFile


    在转换之前先确保引入了spring-test 依赖(以Maven举例)


    <dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-testartifactId>
    <version>versionversion>
    <scope>testscope>
    dependency>

    通过获得File文件的名称、mime类型以及内容将其转换为MultipartFile格式。具体写法如下。


    multi.png


    作者:程序员老J
    来源:juejin.cn/post/7295559402475667492
    收起阅读 »